Application Handoff

Bind one authenticated server request to a dedicated Vouch bundle, a fresh Native gate, and an idempotent local would-act record without performing the external action.

Know what the example is

handoff/would-act.mjs is a small informative integration example. It is not an SDK, orchestrator, capability service, authentication system, transaction manager, or external-action adapter. It imports only Node built-ins and runs only the absolute Native executable supplied by the caller.

Its one durable effect is a local would_act record. The record says that this invocation reached an exact fresh gate and that no external action was performed.

Authenticate before constructing server context

The surrounding application must authenticate the caller before it creates the server-context file. The example does not inspect a password, token, cookie, certificate, or session.

After authentication, the application owns all of these facts.

  • the authenticated actor
  • the allowed actor set
  • the allowed action set
  • the trusted current Unix time
  • the request ID and its storage rule
  • the expiry chosen by the server
  • the business facts days and opened

The client cannot make an actor authenticated by writing an actor name into a request. It also cannot choose a trusted current time or extend its own expiry.

Keep request data and trusted context separate

The request record uses the closed schema lispex.example-refund-request/v1.

JSON
{
  "action": "refund",
  "actor": "operator-7",
  "days": 14,
  "expires_at_unix_seconds": 1785600000,
  "opened": false,
  "request_id": "request-123",
  "schema": "lispex.example-refund-request/v1"
}

The independently trusted server context uses the closed schema lispex.example-refund-server-context/v1.

JSON
{
  "allowed_actions": [
    "refund"
  ],
  "allowed_actors": [
    "operator-7"
  ],
  "authenticated_actor": "operator-7",
  "now_unix_seconds": 1785599900,
  "schema": "lispex.example-refund-server-context/v1"
}

Store the first block as ../refund-work/app/request.json and the second as ../refund-work/app/server-context.json. Preserve the exact two-space indentation and one terminal line feed in both files.

The actor must match the authenticated actor byte for byte and must appear in the actor allowlist. The action must appear in the action allowlist. Expiry must be strictly later than the trusted current time.

Both files must already be canonical closed JSON. Unknown or duplicate keys, invalid UTF-8, controls, unsafe numbers, oversized values, and trailing bytes are refused before Native starts. Strings are not normalized or case-folded. The exact admitted scalar bytes are preserved.

Bind all six current-request values

The application reconstructs one exact checked input from the accepted request and server context.

JSON
{
  "input": "csk.checked-input/v1",
  "value": [
    14,
    false,
    "request-123",
    "operator-7",
    "refund",
    1785600000
  ]
}

The values are days, opened, request_id, actor, action, and expires_at_unix_seconds in that order. Store these exact canonical bytes with two-space indentation and one terminal line feed as CURRENT_REQUEST_CHECKED_JSON.

The retained workspace input [14, false] and any bundle issued for it are walkthrough-only material. They cannot be reused here. The handoff requires a dedicated bundle whose signed context binds all six values.

Issue the dedicated bundle separately

The issuer creates the current-request bundle before the recipient application runs the handoff. This command is separate from the handoff and uses an issuer-owned key outside the workspace and application work root.

SH
mkdir -p ../refund-work/app
ISSUER_KEY_URI="pkcs8-file:///absolute/path/outside-workspace/refund-issuer/key/private.pkcs8.der"
lispex vouch issue \
  --source-image ./generated/vouch/refund-window-native.lspx.png \
  --input ../refund-work/app/current-request.checked.json \
  --profile csk.checked-profile/v1 \
  --key-handle "$ISSUER_KEY_URI" \
  --out-dir ../refund-work/app/current-request-issued \
  --emit-bundle

Issuance does not authenticate the application caller and does not grant the requested action. The recipient still supplies its independently reviewed policy, Image, and checked input to a fresh gate.

Prepare the local ledger namespace

Create one caller-owned real directory with two existing real direct-child directories. Keep this namespace stable for the complete invocation.

SH
mkdir -p ../refund-work/app/request-ledger
mkdir -p ../refund-work/app/request-staging

The example creates neither child directory implicitly. It uses the fixed sibling .would-act-ledger-admission.lock as one ledger-wide exclusive lock. An existing lock or a nonempty staging directory requires operator review and causes refusal. The lock is never reclaimed only because it looks old.

Run the exact eight-flag handoff

From the workspace root, invoke the script with every flag exactly once. The Native path must be absolute. The other paths are resolved once from the invocation directory.

SH
node handoff/would-act.mjs \
  --native-bin /absolute/path/to/lispex \
  --bundle ../refund-work/app/current-request-issued/vouch-input-bundle.json \
  --trust-policy ../refund-work/vouch/policy.json \
  --source-image ./generated/vouch/refund-window-native.lspx.png \
  --checked-input ../refund-work/app/current-request.checked.json \
  --request-record ../refund-work/app/request.json \
  --server-context ../refund-work/app/server-context.json \
  --app-work-dir ../refund-work/app

The child call is fixed to lispex vouch gate. It pins the bundle, policy, Image, six-value checked input, exact checked profile, required approve decision, and a fresh caller-owned report path. It uses an argv array with no shell, a closed minimal environment, a 30-second timeout, bounded output, and no PATH discovery. Child stdout and stderr are never forwarded.

Read the exact local record

Only an accepted fresh gate can reach publication. The final filename is the ordinary lowercase SHA-256 of the raw UTF-8 request ID followed by .json. The request ID is not repeated inside the record.

The record has exactly these ten fields in canonical order.

JSON
{
  "action": "refund",
  "actor": "operator-7",
  "authority": "informative-local-example",
  "checked_input_sha256": "sha256:<64-lowercase-hex>",
  "expires_at_unix_seconds": "1785600000",
  "external_action_performed": false,
  "gate_report_sha256": "sha256:<64-lowercase-hex>",
  "required_decision": "approve",
  "schema": "lispex.example-would-act-record/v1",
  "would_act": true
}

checked_input_sha256 is the ordinary SHA-256 of the exact checked-input file bytes. The Native gate report uses a different domain-separated input identity inside its context. gate_report_sha256 is the ordinary SHA-256 of the exact accepted report bytes. Neither digest turns a stored report into reusable authority.

expires_at_unix_seconds is a canonical decimal string in this record. It is a bounded safe integer in the request and server context.

Understand the idempotency boundary

The script locks the whole ledger before scanning capacity, running Native, and publishing the record. It rejects any existing record for the same request without running the gate again. Exclusive hard-link publication lets only one concurrent caller win the final path. Different request IDs also share the ledger-wide admission lock so that they cannot race past total count or byte limits.

This gives the example one no-clobber local record per accepted request ID. It does not give an external payment or refund exactly-once behavior. A crash after local publication still leaves a record whose external_action_performed value is false.

Put the real transaction in an application-owned state machine

A production application keeps the gate and the external transaction as separate steps. A practical integration usually needs all of the following.

  1. Authenticate the caller and load the server-owned request.
  2. Authorize the actor and action and reject expired work.
  3. Bind the exact request to a dedicated bundle and run a fresh local gate.
  4. Atomically reserve the request in application storage under a unique request ID.
  5. Submit the external transaction through an application-owned connector.
  6. Store the external system response and mark the reservation complete.
  7. Reconcile timeout or unknown outcomes before any retry.
  8. Keep the checked-input digest, accepted gate-report digest, transaction identity, and final state in the application audit trail.

The local example intentionally stops before step 4. The application must design reservation, completion, retry, and reconciliation around the actual external system. A provider idempotency key can help only when that provider's contract is independently understood and the application stores the key and outcome durably.

Know the closed refusals

The example fails closed before an external action in every negative case.

  • Missing, duplicate, unknown, or positional arguments cause usage exit 2 before filesystem access.
  • A relative Native path is refused. The request, context, checked-input, work directory, staging, and ledger paths that the handoff reads or writes also refuse unsafe path kinds, symlinks, hardlinks, junctions, and reparse points. Native separately validates the decision bundle, recipient policy, and Image.
  • Noncanonical request or context bytes, wrong schemas, unknown fields, duplicate keys, bad strings, unsafe times, and resource-limit excess are refused.
  • Actor mismatch, disallowed actor, disallowed action, and expired or invalid time are refused before the child starts.
  • Any byte difference between the supplied checked input and the reconstructed six-value input is refused.
  • An existing admission lock, nonempty staging directory, unsafe ledger, existing same-request record, or exhausted ledger capacity is refused.
  • Spawn failure, timeout, signal, output overflow, nonzero child exit, missing report, and unexpected child output are refused.
  • An authentication failure, old or malformed report, wrong profile, wrong input identity, execution disagreement, decision mismatch, or nongranted gate is refused.
  • Record serialization, staging, publication, or cleanup failure returns a refusal. A record already published before cleanup failure remains for operator review.

Application and local refusals exit 3 with empty stdout. A bounded stderr message is diagnostic only. No later success erases an earlier refusal.

Keep the final claim narrow

The handoff record means one exact current invocation would proceed to an application-owned decision point. It does not prove caller identity beyond the server context, policy correctness, request uniqueness outside this ledger, freshness after the checked expiry, external transaction success, or permission to act.

Practical Decision Workspace · Using Lispex Vouch · Vouch receipts