README.md (7507B)
Inspectable workflow laboratory
One pipeline is an ordinary tricu value. Two tricu interpreters consume it:
explain produces text; execute produces a pure request/reply interaction.
A third function structurally inserts approval before a selected stage. Python
validates the declared plan, grants capabilities, executes local artifact effects,
and supplies replies. The pipeline never calls the built-in native IO driver.
This is a bounded experiment with a sequential, static declaration—not a CI
service. Its “deployment” creates a local release.txt, with no shell, network,
credentials or remote target.
Try it
nix build .#tricu-workflow-demo --no-link
APP=$(nix path-info .#tricu-workflow-demo)
"$APP/bin/tricu-workflow-demo" explain
"$APP/bin/tricu-workflow-demo" explain --approval
The pipeline in pipeline.tri composes library functions:
pipeline text =
[(W.writeArtifact "build" "candidate.txt" text)
(W.checkArtifact "test" "candidate.txt" text)
(W.publishArtifact "deploy" "candidate.txt" "release.txt")]
--approval applies requireApproval "deploy" to that value. The explanation is:
build: write candidate.txt
test: check candidate.txt
approval required: deploy
deploy: publish candidate.txt -> release.txt
JSON output also includes every action argument, its approval annotation, the normalized definition identity, and the executable bundle hash. The text summary omits artifact contents; those remain visible in the JSON stages.
Execution requires an existing workspace and explicitly granted operations:
WORK=$(mktemp -d)
"$APP/bin/tricu-workflow-demo" execute --approval --workspace "$WORK" \
--allow write --allow check --allow publish
# Exit 1: build/test succeed, approval is denied, and no release is created.
For an approved run, use a fresh workspace and the exact definition_id from the
protected explanation:
WORK=$(mktemp -d)
PLAN_ID=PASTE_THE_REVIEWED_DEFINITION_ID
"$APP/bin/tricu-workflow-demo" execute --approval --workspace "$WORK" \
--allow write --allow check --allow publish --approve "$PLAN_ID:deploy"
cat "$WORK/release.txt"
Replace the PLAN_ID assignment with the inspected hash. A changed --message
alters the definition identity; use the same message when explaining/executing.
Existing artifacts are never overwritten, so each attempt should use a fresh
workspace. Exit 0 means completed, 1 means a requested action failed/was denied,
and 2 means an invalid package, plan, request protocol, or command. Execution
reports include ordered action outcomes and a failure stage/reason when relevant.
Argparse usage errors print to stderr; other reports are JSON.
What the representation enables
workflow.tri defines stage constructors, interpreters and the
approval transformation. Plans are lists of:
["stage" name action]
["approval" name stage]
Actions are ["write" basename text], ["check" basename text], or
["publish" source-basename destination-basename]. An approval wraps exactly one
stage with the same name. Repeating the transformation preserves an existing
wrapper; transforming a missing target is rejected by the host adapter.
The execution interpreter yields:
["request" name action continuation]
["done"]
["failed" name reason]
An approval is requested as ["approve"]. Python applies each continuation to
["ok"] or ["error" reason]. A refusal terminates the tricu interpretation;
later actions are not requested. Continuations stay as callable tree values in
the Haskell runtime; no serialization of closures or native callbacks is needed.
Temporary arguments, observations and continuations use Python handle scopes.
This representation exposes declared workflow structure. It does not analyze raw executable trees or infer arbitrary program behavior. This grammar has no dynamic expansion, parallel scheduling, dependency DAGs, retries or resumable execution. A more expressive planner would need to distinguish its currently known structure from future work, rather than promising a complete static preview.
Identity and authority are separate
The definition ID is SHA-256 over the domain tricu.workflow-demo.plan.v1, a NUL
separator, and compact sorted-key UTF-8 JSON for the validated stage projection.
Every name, action argument and approval flag contributes. It identifies this
normalized declaration—not the raw executable tree, an execution, or a snapshot
of workspace contents. The executable bundle hash is reported separately.
An explanation grants no authority. The executor checks:
- The complete declaration before any effect: 1..16 distinct stages, known action shapes, simple basenames and UTF-8 artifact text of at most 64 KiB.
- Every execution request against the inspected order, stage name and complete action arguments. Extra, reordered or undeclared requests fail before dispatch.
- An independent capability allowlist for write/check/publish. Default is empty.
- A grant matching this definition ID and stage at each approval request.
- For every publication, a successfully completed approval boundary in this run. A grant alone, a missing wrapper, or a previous run's approval is insufficient.
Thus the transform changes the visible control structure, while executor policy
remains authoritative. --approve is an explicit local caller decision, not a
signed approval, authenticated reviewer, durable ticket or interactive review UI.
The Linux executor anchors operations to an opened directory descriptor, accepts only simple basenames, refuses symlink reads, rejects nonregular input artifacts, limits reads/text to 64 KiB, and creates outputs exclusively. “Publish” copies an artifact into a new local filename; it is not an atomic deployment or transaction. Errors can leave partial output files, and earlier successful effects are retained. Protocol errors after effects likewise do not undo them.
Use a trusted workspace. A file can change between checking and publication; approval binds the declaration, not observed artifact bytes or environment state. Definition identity is neither a cache key for arbitrary effects nor an execution provenance record. Process/memory exhaustion and nontermination require external supervision. Pure requests with restricted dispatch are useful authority boundaries, but this demo is not a hardened untrusted-plugin sandbox.
Reproduce the checks
nix build .#tricu-workflow-demo-test --no-link
CHECK=$(nix path-info .#tricu-workflow-demo-test)
cat "$CHECK/report.json"
Nix compiles the tricu modules at build time. The Python wheel contains the portable bundle and a generated checksum; startup verifies both its checksum and the native bundle format. The runtime closure excludes the compiler executable and checkout source snapshot. These source-compiling checks are opt-in.
Independent expectations check stages, explanation, definition hashes and action traces. Negative controls cover missing/stale/wrong-stage grants, changed payloads, missing capabilities/boundaries, invalid declarations, forged requests and early completion, symlinks/nonregular files, closed executors, and 32 repeated runs with fresh approvals and stable descriptor counts. CLI checks run outside the checkout with a read-only working directory/home and no runtime tools on PATH. Tests also prohibit the native IO entrypoint during explanation and successful execution.