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:
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
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.
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" }),
});
}
},
});
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");
{
"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.
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");
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.