Handler Supply Chain
Include installed packages
In a project using packages, touba supply-chain generate adds
package_installation to the execution lock. This field pins the installation
lock's digest; validation then checks each package's files, including host
modules and files unused by the current entry point. Changes require another
validation and lock review. Keep the execution lock at the project root to
include the touba.packages.lock.json inventory.
One lock for multiple modules
A shared project manifest can bind tools from several modules. Explicitly
supply all required assembled AIR files with repeated --air options:
touba supply-chain generate \
--air .touba/build/prod/air/app/expenses.air.json \
--air .touba/build/prod/air/app/support.air.json \
--manifest .touba/build/prod/touba.manifest.json \
--out touba.lock.json
The first AIR is recorded in air; subsequent files appear in
additional_air, each with a lock-relative path and digest.
Validation from any listed AIR verifies every declared AIR, then checks
bindings against their combined tool declarations. Changed, missing,
duplicate or unconfined files are refused. There is no implicit project scan,
new grant or automatic approval. A binding absent from all AIRs remains an
error. After genuine review, the same lock can be required for each
prod entrypoint.
A Touba handler is host code. It can call a network, SDK, API, or external system when the manifest authorizes it. This power creates a risk: the executed file can drift from the expected file.
The handler supply-chain lock (SC-1) adds a coherence check between:
- compiled AIR;
- runtime manifest;
- handler files;
- declared SDK metadata;
- declared handler package identity.
The dependency artifact extension (SC-2) adds verification of handler package files and explicitly declared dependency artifacts. It does not scan the project and does not perform vulnerability auditing.
The provenance and review extension (SC-3) adds declared provenance and review for the lock. It can require an approved review before accepting a lock; cryptographic signing and code review remain separate controls.
What the Lock Verifies
Handler supply-chain verification checks explicitly supplied artifacts.
| Element | Verification |
|---|---|
| AIR | sha256 digest |
| Manifest | sha256 digest |
| Handler | sha256 digest |
| Handler command | command, cwd, args |
| SDK | declarative name, version, handler protocol, and status |
| Handler package | declared identity, root, declared files |
| Dependency artifacts | digests of declared files |
| Provenance | declared preparer, source, timestamp |
| Review | review status, reviewers, declared evidence |
| Drift | ok, missing, changed, unknown |
This verification does not execute the handler. The dependency artifact extension (SC-2) does not resolve transitive dependencies. The provenance and review extension (SC-3) does not sign artifacts.
Generate a Lock from Real Files
After compiling AIR and reviewing the runtime manifest:
touba supply-chain generate \
--air program.air.json \
--manifest touba.manifest.json \
--out touba.lock.json
Generation hashes the exact bytes of AIR, manifest, handler entrypoints,
package identity and dependency lockfiles. Static local JavaScript and Python
imports are followed transitively without executing the code. Add other assets
with repeated --file <package-relative-file> options. Declared dependencies
require a package-manager lock; Touba hashes it without resolving or installing
its packages. Pure programs need no manifest or handler package.
Python handlers
Place a pyproject.toml at the package root (the lock directory by default):
[project]
name = "business-handlers"
version = "0.1.0"
Identity must be static. Declare application dependencies there and retain
the lock generated by your manager: pylock.toml, uv.lock or poetry.lock.
Touba inventories these files; it does not verify that the installed venv
matches their resolution. The interpreter, bytecode caches and native modules
remain the host's responsibility. The bundled SDK wheel stays in vendor/touba-sdk;
include it with --file and supply inspected SDK metadata with
--sdk-metadata-json when needed.
Script analysis uses isolated Python 3.11+, with no site, PYTHONPATH,
handler import or project venv loading. --python <interpreter> selects
this trusted tool, never a binding command. Adjacent modules, packages,
internal relative imports and cycles are inventoried. Files must remain
within --package-root, including through symlinks. Detected dynamic loading
and import-path changes are refused. This static check is not a Python sandbox;
python -m package.module bindings resolve from their explicit cwd; local
packages have __init__.py files. Custom loaders and modified import paths
remain refused. See the Python tutorial.
The output remains review.status: pending. Generation neither approves a
review nor overwrites an existing lock. Use a new filename for an updated lock,
review its changes, then select it explicitly. Strict validation refuses a
pending review. SDK metadata is declarative: after inspecting the installed
dependency, provide its toubaSdkMetadata() result with --sdk-metadata-json.
Without it, SDK identity remains unknown and strict validation refuses.
The generator never imports or executes handler/package code.
Path rules:
handler.cwdis manifest-relative;argsare relative to that cwd.- Lock artifact paths,
handler.pathandpackage_rootare lock-directory-relative. entrypoint,filesanddependency_artifactsare package-root-relative.- The inventory must include its entrypoint. Files and symlinks must remain confined.
The default entrypoint argument is index zero. Use --entrypoint-arg <index>
for interpreter flags, or command for a directly executed local binary.
Validation checks that the declared handler is an executable or explicit
argument path; it does not infer arbitrary command semantics. Missing commands,
contradictory paths and entrypoints omitted from the inventory are refused.
Relocate the whole lock root, keeping relative paths and file bytes unchanged. Absolute interpreter paths remain machine-specific and must exist at the new location. Changing the manifest or command requires a new lock and review, never automatic approval.
Validate the Lock
touba supply-chain validate \
--lock touba.lock.json \
--air program.air.json \
--manifest touba.manifest.json
Expected text output:
Touba Supply Chain touba-supply-chain-validation-v1
Status ok
Lock touba-lock-v1
AIR ok
Manifest ok
Provenance ok
Review approved evidence=ok required=no
Handlers 1
- tool:examples.search/SearchDatasets drift=ok sdk=ok package=ok package_files=ok dependencies=ok
Diagnostics ok
To retrieve a JSON report:
touba supply-chain validate \
--lock touba.lock.json \
--air program.air.json \
--manifest touba.manifest.json \
--output json
Strict mode also fails on warnings:
touba supply-chain validate \
--lock touba.lock.json \
--air program.air.json \
--manifest touba.manifest.json \
--strict
To require an approved review:
touba supply-chain validate \
--lock touba.lock.json \
--air program.air.json \
--manifest touba.manifest.json \
--require-review
This command refuses a lock without review, with status: "pending", or with status: "rejected". It also verifies declared review evidence by digest when review.evidence is present.
Run with the Lock
touba run can use the lock as an explicit gate before handler dispatch:
touba run program.air.json \
--manifest touba.manifest.json \
--supply-chain-lock touba.lock.json \
--require-supply-chain-lock
In this mode, the runtime refuses before execution if the lock is absent, if lock validation produces an error, or if it produces a warning. Without --require-supply-chain-lock, a warning remains visible but non-blocking.
This gate grants no runtime capability and does not replace the manifest. It verifies alignment between AIR, manifest, handlers, SDK metadata, handler_package, package files, and declared dependency artifacts before launching host code.
Project prod Profile
A project initialized by touba init also contains a prod run profile in touba.project.json:
{
"run_profiles": {
"prod": {
"require_supply_chain_lock": true,
"supply_chain_lock": "touba.lock.json"
}
}
}
You can then run:
touba run program.air.json \
--profile prod \
--manifest touba.manifest.json
The touba facade turns this profile into explicit runtime options. It does not generate the lock and does not automatically search for a manifest or handler. The dev profile does not force the lock.
Inspect the Supply-Chain Lock
To read a lock:
touba inspect --supply-chain touba.lock.json
To read a validation report:
touba inspect --supply-chain supply-chain.report.json
inspect displays paths, digests, commands, SDK status, handler_package identity, provenance, review, and diagnostics. It does not display handler content or review evidence content.
Drift Statuses
| Status | Meaning |
|---|---|
ok | observed artifact matches the lock |
missing | expected artifact is not found |
changed | digest or metadata differs |
unknown | validation cannot conclude |
unknown is not a hidden success. It is a signal to examine.
The same applies to sdk.status = missing, unknown, or unsupported: in simple validation, this is a warning; in strict mode, the run is refused before the handler.
The same rule applies to handler_package_status = unknown: simple validation remains readable, but a strict profile refuses execution because the handler package is not explicitly identified.
handler_package_files_status and handler_dependency_artifacts_status can be unknown when no file is declared, ok when digests match, missing when a file is absent, changed when a digest differs, or invalid when the shape or path is refused.
Relation with the Manifest
The manifest remains the runtime authority for:
- handler;
- grants;
- secrets;
- memories;
- limits;
- payload constraints;
- triggers.
The lock grants no additional right. It only verifies that the supplied artifacts match what is expected.
Bounded constraint example:
{
"bindings": {
"tool:examples.fetch_source/FetchSource": {
"payload_constraints": {
"input": {
"request.query": {
"non_empty": true,
"max_length": 120
},
"request.mode": {
"allowed_values": ["dry_run", "review", "execute"]
}
}
}
}
}
}
This constraint is verified by the runtime before handler launch. It does not replace the AIR schema and does not become a host expression.
Assurance Scope
Use the supply-chain lock as a coherence guard: it makes declared artifacts, digests, command metadata, package identity, dependency artifacts, provenance, and review state inspectable before host code is launched.
Cryptographic signing, registry distribution, package installation, vulnerability auditing, dependency graph resolution, system sandboxing, proof that a handler is safe, prevention of exfiltration by an authorized handler, and Verified / Unverified status are separate supply-chain and platform layers.
Share a Python module
Organize handlers as a local Python package with __init__.py files:
pyproject.toml
handlers/
__init__.py
shared.py
invoices/
__init__.py
apply.py
payments/
__init__.py
apply.py
Both apply.py files can use from handlers.shared import …. Each binding
selects the project interpreter, cwd: "." and, for example,
args: ["-m", "handlers.invoices.apply"]. The generator resolves the module
from that cwd, inventories the __init__.py files and follows local imports.
The lock includes shared.py; changing it invalidates the lock.
The package root must contain all these files. Use --package-root . when
generating the lock if this root is the project directory. This organization
keeps one copy of shared helpers and works with both run and serve.