Skip to main content
Version: 1.0.0

Approval before an external effect

Require human approval before a real file write, verify rejection before the handler, and resume without duplicating an acquired effect. The demonstration records a payment in an isolated local ledger; it never sends money or contacts a payment provider. Amounts are integer cents, not floating-point currency.

Prerequisites: Touba distribution, Node.js 24 and npm. In a macOS/Linux terminal:

Terminal
mkdir -p approval-effect-demo
cd approval-effect-demo
mkdir -p handlers vendor
npm init -y
node --input-type=module -e 'import {execFileSync} from "node:child_process"; import {copyFileSync} from "node:fs"; const v=JSON.parse(execFileSync("touba",["version","--json"],{encoding:"utf8"})); copyFileSync(v.distribution.sdk_archive,"vendor/touba-sdk.tgz");'
npm install --offline --ignore-scripts --save-exact ./vendor/touba-sdk.tgz

1. Place the barrier before the tool​

payment.tb
module examples.scenario_external_approval

type PaymentRequest
reference is Text
amount_cents is Int
destination is Text

type PaymentResult
receipt is Text

tool RecordPayment
description "Append an approved demonstration payment to the local ledger. This does not transfer money."
takes request as PaymentRequest
gives PaymentResult
requires filesystem_write, secret_read
secrets LEDGER_TOKEN

strategy RunRegulatedPayment
takes request as PaymentRequest
gives PaymentResult
uses RecordPayment
do
show
with message as "Payment review requested: " ++ request.reference
human.approve
with context as request
into approval
RecordPayment
with request as request
into stored
show
with message as "Ledger receipt: " ++ stored.receipt
give stored

Approval releases the continuation, not a new grant. The tool still needs its declared authority at dispatch. human.approve is executable source, not a prompt instruction the model can disregard.

This recipe keeps approval in the entry strategy for clarity. The human checkpoint also supports approvals inside directly called source skills and strategies, preserving the caller stack without replaying earlier calls. See human validation for the supported scope.

2. Implement the actual effect​

The handler writes and syncs one ledger entry, then returns an applied receipt. An unconfirmed attempted write is unknown, never a fabricated success. The runtime journal and retry_mode: none prevent an automatic unsafe redispatch.

handlers/ledger.mjs
import { open } from "node:fs/promises";
import { randomUUID } from "node:crypto";
import { defineTool, createToolResult, ToubaHandlerError } from "@touba/sdk";
await defineTool({
toolRef: "tool:examples.scenario_external_approval/RecordPayment",
inputSignature: [{ name: "request", type_ref: "type:examples.scenario_external_approval/PaymentRequest" }],
outputType: "type:examples.scenario_external_approval/PaymentResult",
requiredGrants: ["filesystem_write", "secret_read"], requiredSecretHandles: ["LEDGER_TOKEN"],
run: async ({ request, context }) => {
if (!context.effect) throw new ToubaHandlerError("EffectContextRequired", "A ledger write requires an effect context.");
let attempted = false;
try {
if (!context.env.LEDGER_TOKEN) throw new Error("Credential unavailable");
if (request.amount_cents <= 0) throw new Error("Amount must be positive");
const receipt = randomUUID();
const file = await open("payment-ledger.ndjson", "a", 0o600);
try {
attempted = true;
await file.writeFile(JSON.stringify({ ...request, receipt }) + "\n");
await file.sync();
} finally { await file.close(); }
return createToolResult({ receipt }, { effectReceipt: context.createEffectReceipt({ outcome: "applied" }) });
} catch {
throw new ToubaHandlerError("LedgerWriteUnconfirmed", "The ledger write did not return a confirmed result.", {
effect_receipt: context.createEffectReceipt({ outcome: attempted ? "unknown" : "not_applied" }),
});
}
},
});
configure.mjs
import { writeFileSync } from "node:fs";
const manifest = { manifest_version: "touba-manifest-v1", bindings: {
"tool:examples.scenario_external_approval/RecordPayment": {
handler: { kind: "process", command: process.execPath, args: ["handlers/ledger.mjs"], cwd: "." },
grants: ["filesystem_write", "secret_read"], secrets: ["LEDGER_TOKEN"],
effect_contract: { kind: "external_write", retry_mode: "none" }, timeout_ms: 10000, max_payload_bytes: 65536,
} },
};
writeFileSync("touba.manifest.json", JSON.stringify(manifest, null, 2) + "\n");
manifest.bindings["tool:examples.scenario_external_approval/RecordPayment"].grants = ["secret_read"];
writeFileSync("denied.manifest.json", JSON.stringify(manifest, null, 2) + "\n");
touba.secrets.json
{
"secrets_version": "touba-secrets-v1",
"secrets": {
"LEDGER_TOKEN": {
"source": "env",
"name": "LEDGER_TOKEN"
}
}
}

The token used below is a synthetic local example, not a payment credential. No real key is needed. The complete host manifest explicitly declares the write contract; a generated draft must not infer it for the author.

3. Check approval, refusal and resume​

The script obtains the current approval reference from the actual checkpoint. Its decisions are isolated test inputs, not approvals for a real transaction.

exercise.mjs
import assert from "node:assert/strict";
import { existsSync, readFileSync } from "node:fs";
import { ToubaEngine, ToubaEngineError } from "@touba/sdk";
const engine = new ToubaEngine({ projectRoot: ".", env: { LEDGER_TOKEN: "isolated-ledger-token" } });
const options = { source: "payment.tb", entryRef: "strategy:examples.scenario_external_approval/RunRegulatedPayment",
manifest: "touba.manifest.json", secrets: "touba.secrets.json", effectJournal: "payment.effect-journal.sqlite",
inputs: { request: { reference: "DEMO-42", amount_cents: 12500, destination: "isolated-demo-ledger" } } };
const paused = await engine.run({ ...options, humanCheckpointOut: "payment.checkpoint.json" });
assert.equal(paused.reason, "human_approval_pending");
assert.equal(existsSync("payment-ledger.ndjson"), false);
const checkpoint = JSON.parse(readFileSync("payment.checkpoint.json", "utf8"));
const approvalRef = checkpoint.approval_ref;
const rejected = await engine.run({ ...options, humanDecisions: { [approvalRef]: "rejected" } });
assert.equal(rejected.reason, "human_approval_rejected");
assert.equal(existsSync("payment-ledger.ndjson"), false);
const approved = { [approvalRef]: "approved" };
await assert.rejects(engine.run({ ...options, manifest: "denied.manifest.json", humanDecisions: approved }), error => {
assert.ok(error instanceof ToubaEngineError);
assert.equal(error.details.reason, "ToolCapabilityNotGranted");
return true;
});
assert.equal(existsSync("payment-ledger.ndjson"), false);
const resume = { ...options, inputs: undefined, humanResumeCheckpoint: "payment.checkpoint.json", humanDecisions: approved };
const completed = await engine.run(resume);
assert.equal(completed.status, "success");
assert.equal(completed.reason, "completed");
const replayed = await engine.run(resume);
assert.deepEqual(replayed.result, completed.result);
const lines = readFileSync("payment-ledger.ndjson", "utf8").trim().split("\n").map(JSON.parse);
assert.equal(lines.length, 1);
assert.equal(lines[0].receipt, completed.result.receipt);
assert.equal(lines[0].amount_cents, 12500);
assert.ok(!JSON.stringify(completed.trace).includes("isolated-ledger-token"));
console.log(JSON.stringify(completed.result));
console.log("APPROVAL_GRANTS_AND_SINGLE_ACQUIRED_WRITE_OK");
Terminal
node configure.mjs
node exercise.mjs

Expected: APPROVAL_GRANTS_AND_SINGLE_ACQUIRED_WRITE_OK and one line in payment-ledger.ndjson. Pending approval, rejection and a missing write grant produce no ledger entry. Resuming the same checkpoint with the same journal restores the applied result without a second append. A new run is a new invocation.

The checkpoint contains business data in readable JSON with restrictive local permissions, not encryption. Keep it and the effect journal private. Never reuse approval data after changing the program; resume validates full AIR identity. Each iteration and branch has its own request occurrence; a consumed decision does not release the next one. See approval scope.

This example does not promise exactly-once behavior from arbitrary payment APIs. Real payment adapters need their own idempotency/reconciliation contract and operational controls. See effect contracts.