Skip to content
Projects

Dicklesworthstone/franken_node has 31 GitHub stars and 3 forks. GitHot measured 0 stars in 24 hours and +1 in 7 days. Last checked 2026-11-12 06:12 UTC. Last push 2026-11-04.

How these counts are measured

Trust-native JavaScript/TypeScript runtime platform built on franken_engine with deterministic compatibility, migration autopilot, extension trust controls, and incident replay.

What you can do

  • Trust cards - Per-extension provenance, risk score, audit history, camouflage assessment, revocation state
  • Revocation freshness gates - Risky and dangerous actions fail closed when trust state is stale
  • Deterministic incident replay - Signed bundles with timeline, evidence, policy decisions; default replay verifies the recorded sequence. Opt-in native capture supports guest re-execution and certified uncaught-exception prefixes, failing closed on mismatch.
  • Counterfactual simulator - Re-evaluate the recorded incident decision trace under a different policy mode and inspect the diff

Measured star growth

in 24h
0
in 7 days
+1
in 15 days
Update delayed
in 1 month
Update delayed

Net star changes over overlapping periods, not daily totals. Missing measurements are left blank. Stars measure attention, not software quality.

README.md

Original

franken_node

franken_node - trust-native JavaScript and TypeScript runtime platform

Status README smoke Verifier SDK standalone Security Unsafe Compatibility Tests License Rust

franken-node is a trust-native JavaScript/TypeScript runtime platform for extension-heavy systems. It pairs Node/Bun ecosystem velocity with deterministic security controls, cryptographically-grounded trust operations, and replayable incident forensics.

# One-line installer (Linux / macOS)
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/franken_node/main/install.sh | bash

[!IMPORTANT] Status: pre-1.0. The CLI surface and the on-the-wire JSON shapes (decision receipts, trust cards, replay verdicts, counterfactual reports, incident bundles) are stable and covered by golden tests. Internal Rust APIs and feature-gated modules may still break between versions. See Stability for the full breakdown.


A concrete scenario

It's Tuesday. A transitive npm dependency in your build was published 14 days ago by a brand-new publisher whose username is 2 characters off a popular library. The package's behavior has slowly drifted in the last three minor releases.

Under your current stack: the typosquat scanner flags it tomorrow; your package-lock pinned the new version yesterday; the egress check runs at deploy time and sees nothing wrong because the malicious payload activates after a 48-hour delay. By Friday you're paging the security team.

Under franken-node:

  • trust scan --deep reads the package's npm registry record and writes onto its trust card that the package was first published 14 days ago (new packages are raised to at least medium risk), plus a DGIS maintainer-fragility score (sole maintainer, no maintainers, no publish in over a year). A package name one edit away from a pinned popular package is raised to high risk. This happens before the package is admitted. The scan does not see publisher account age (npm does not publish it) or a look-alike publisher username.
  • Behavior drift across releases is not yet on the card: the BPET camouflage detector (security::bpet::camouflage_detector) can flag GradualCreep in an observed-vs-declared capability series, but no runtime path feeds it yet, so camouflage_hints stays empty.
  • Under strict, run refuses a trusted dependency, and so the dependency never reaches the network, once the revocation frontier is more than 5 minutes old. Under balanced the same staleness is a preflight warning (1-hour window).
  • If the malicious behavior had already executed, incident bundle + incident replay integrity-verify the recorded bundle. Runs recorded with --capture-replay also support incident replay --execute for native guest execution, a certified uncaught-exception prefix, or a finalized typed engine-stop prefix, using captured host-I/O and admitted process results. incident counterfactual --policy strict scores an alternative policy against the recorded evidence.

Every gate above is a runtime default, not an external scanner. Each run (including a preflight refusal) and each trust revoke/quarantine/release and fleet release decision appends a signed, hash-chained entry to the evidence ledger; other decisions (a trust-scan risk raise, for example) are recorded on the trust card, not in the ledger.

franken-node operator workflow: audit, trust scan, lockstep, quarantine, incident bundle, counterfactual

Who this README is for

If you're a... Start at
Developer evaluating for a new project TL;DR → Quick Example → Comparison
Operator deploying or running the platform End-to-End Operator Workflow → Command Reference → Operational Runbooks → Troubleshooting
Security auditor verifying claims Trust-Native Primitives → Threat Model → Cryptographic Primitives → Reproduction Playbook → Verifier SDK
Architect making a build-vs-buy decision Design Philosophy → The Trust Gradient → The Engine-Split Contract → Limitations
CI/SRE wiring it into pipelines Integration Patterns → Structured Logging and Observability → Doctor Walkthrough

TL;DR

The Problem

Node and Bun made rapid extension development frictionless, but the security and operations layer never caught up. In production, teams stitch together:

  • ad-hoc supply-chain scanners that scream and then get ignored
  • revocation queries that run after a risky action ships
  • incident review sessions that can never reproduce the original execution
  • "permission" models that gate file access but not extension behavior
  • migration spreadsheets between Node, Bun, and whatever-comes-next

Every layer is best-effort, weakly coupled, and indistinguishable from theater.

The Solution

franken-node makes trust, migration, compatibility, and incident replay part of the runtime contract, so JS/TS velocity comes with:

  • Revocation-first execution: risky actions consult fresh trust state before they run, not after
  • Trust cards: every extension carries provenance, behavior risk, audit history, and a camouflage assessment
  • Deterministic incident replay: a high-severity incident exports as a signed bundle whose recorded evidence any operator (or the standalone verifier SDK) can integrity-verify and re-derive, and run policy counterfactuals against. Replay re-derives the recording; it does not re-execute the program.
  • Migration autopilot: audit → rewrite → validate → rollout, with unsigned JSON rollback plans (not Ed25519-signed). migrate validate is static+smoke; behavioral comparison is a separate verify lockstep (Bun+franken dyad by default; real Node.js can be added when available)
  • Fleet quarantine control plane: quarantine, release, and reconcile with signed decision receipts. By default over the local durable store (transport=file, live_control_plane=false); with fleet serve on a coordinator and [fleet] control_plane_url on every node, the same commands run as a live multi-node control plane over authenticated HTTP (transport=http, live_control_plane=true)
  • Verifier SDK: independent third parties can validate receipts, bundles, and the honesty manifest without trusting the producing runtime. bench run reports are SHA256 provenance_hash documents, not Ed25519-signed SDK attestations.

Why use franken-node?

Capability What you get
Trust cards Per-extension provenance, risk score, audit history, camouflage assessment, revocation state
Revocation freshness gates Risky and dangerous actions fail closed when trust state is stale
Deterministic incident replay Signed bundles with timeline, evidence, policy decisions; default replay verifies the recorded sequence. Opt-in native capture supports guest re-execution and certified uncaught-exception prefixes, failing closed on mismatch.
Counterfactual simulator Re-evaluate the recorded incident decision trace under a different policy mode and inspect the diff
Migration autopilot Audit, rewrite, validate, and rollout. --emit-rollback is unsigned JSON (not Ed25519-signed); migrate validate is static+smoke, not verify lockstep
Compatibility oracle Lockstep vs Node (spec) plus Bun as a second reference; matching only Bun stays fail
Fleet quarantine control plane Quarantine, reconcile, release with signed decision receipts; local file transport by default, or a live HTTP coordinator (fleet serve) shared by every node
Signed extension registry Ed25519-signed artifacts, provenance enforcement, assurance levels
Remote capability tokens Scope-bound, single-use-optional Ed25519 capability tokens with audience binding
Verifier SDK Independent verification of receipts, bundles, and the honesty manifest (not Ed25519-signed bench run reports)
Operator doctor Workspace-pressure analyzer, close-condition receipts, evidence-readiness from --input snapshots (not a live broker)
Proof pipeline VEF receipts; proofs queue status and proofs workers restart inspect snapshots / emit restart-request artifacts (not a live broker)
Safe-mode lifecycle Persisted directory-scoped state blocks new application runs; repeated native engine panics enter safe mode automatically, with explicit recovery and retained crash identities (unsigned operator JSON)
No unsafe code #![forbid(unsafe_code)] in both lib.rs and main.rs

Quick Example

# 1) Install current main (GitHub Latest can lag; see Installation)
curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/franken_node/main/install.sh | bash -s -- --method source --node-ref main --engine-ref main

# 2) Bootstrap policy and workspace metadata
franken-node init --profile balanced --scan

# 3) Audit a Node/Bun project for migration risk
franken-node migrate audit ./my-app --format json --out migration-audit.json

# 4) Apply transforms with rollback artifact
franken-node migrate rewrite ./my-app --apply --emit-rollback ./rollback-plan.json

# 5) Compare default Bun+franken dyad (not L1 Node spec)
franken-node verify lockstep ./my-app --runtimes bun,franken-node --emit-fixtures

# 6) Seed trust cards from package.json + lockfile
franken-node trust scan ./my-app --deep --audit

# 7) Run under policy-governed runtime controls
franken-node run ./my-app --policy strict

# 8) Diagnose environment and policy setup
franken-node doctor --verbose --json

# 9) Export and replay a high-severity incident, then run a counterfactual
franken-node incident bundle \
    --id INC-2026-0007 \
    --evidence-path ./incidents/INC-2026-0007/evidence.v1.json \
    --verify
franken-node incident replay   --bundle ./INC-2026-0007.fnbundle --trusted-public-key ./keys/replay-trust-anchor.pub
franken-node incident counterfactual \
    --bundle ./INC-2026-0007.fnbundle \
    --trusted-public-key ./keys/replay-trust-anchor.pub \
    --policy strict

# 10) Inspect fleet posture across a zone
franken-node fleet status --zone prod-us-east --verbose --json

First safe workload (current source)

The one-line installer without --method source can install a stale GitHub Latest tag that cannot run JS. After a source install (or cargo run -p frankenengine-node):

mkdir -p /tmp/fn-first-run && cd /tmp/fn-first-run
franken-node init --profile balanced --json --out-dir .
printf '%s\n' 'console.log("hello-from-engine");' > hello.js
franken-node run ./hello.js --policy balanced

That path is what default_run_executes_fixture_js_through_embedded_engine_without_degraded_fallback proves. Do not set FRANKEN_NODE_ALLOW_DEGRADED_RUNTIME_FALLBACK. Do not wrap Node as run.


Charter

See the Product Charter for scope boundaries, governance model, and decision rules. The charter is the authoritative reference for what franken_node is, what it is not, and how direction changes are authorized. The engine-side counterpart lives in /dp/franken_engine; this repository is the product layer on top of that substrate.


Design Philosophy

  1. Compatibility is a wedge, not the destination. franken-node chases practical Node/Bun migration first, then pushes beyond the baseline runtimes with trust-native behavior that incumbent runtimes cannot retrofit.
  2. Security controls are operational, not decorative. Policy gates, revocation checks, and quarantine paths are runtime defaults with measurable behavior, not advisory dashboards.
  3. Claims require evidence. Benchmark, resilience, and security statements map to reproducible artifacts and a verifier SDK that runs outside the runtime that produced them.
  4. Determinism drives incident quality. Replay and forensics depend on stable event ordering, stable schemas, and explicit control contracts. Schema versions are pinned and registered.
  5. Performance optimization preserves semantics. Hot-path work is accepted only with conformance evidence and bounded tail-latency impact. Benchmarks live alongside the code they validate.

Comparison

Area franken-node Node.js Bun Deno
Per-extension trust cards Built-in External tooling External tooling External tooling
Revocation-aware execution gates Built-in Not native Not native Not native
Deterministic incident replay bundles Built-in Not native Not native Not native
Counterfactual policy simulation Built-in N/A N/A N/A
Compatibility divergence receipts Built-in N/A N/A N/A
Migration autopilot pipeline Built-in External scripts External scripts External scripts
Fleet quarantine log (file transport) Built-in local External platform External platform External platform
Signed extension registry Built-in npm (signature optional) bunx (no enforcement) deno.land (URL trust only)
Threshold-signature verification Built-in Not native Not native Not native
Verifier SDK for public claims Built-in N/A N/A N/A
Operator doctor + workspace pressure Built-in N/A N/A N/A
Permission model Policy profiles + trust gates None None Permission flags
Runtime memory safety forbid(unsafe_code) Rust C++ engine Zig/JSC Rust + V8

Installation

[!IMPORTANT] This repository depends on sibling engine crates from franken_engine via the Engine Split Contract. Source builds require both repositories checked out side-by-side. The one-line installer side-steps this by shipping prebuilt binaries.

Option 1: One-line installer (recommended)

Linux / macOS:

curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/franken_node/main/install.sh | bash

Windows (PowerShell):

irm https://raw.githubusercontent.com/Dicklesworthstone/franken_node/main/install.ps1 | iex

The installer tries the latest GitHub release tag first. As of 2026-10-21, Latest is v0.2.0, published 2026-10-04 with Linux x86_64, macOS arm64 and Windows x86_64 assets. Its macOS binary executes native guest JavaScript, but it predates newer commands such as run --capture-replay and fleet serve. A release tag can lag main substantially. For current source behavior:

curl -fsSL https://raw.githubusercontent.com/Dicklesworthstone/franken_node/main/install.sh | bash -s -- --method source --node-ref main --engine-ref main

A checksum-verified release asset is used when you pass --method release or --version vX.Y.Z. Asset availability does not establish that the release implements every current README surface. Check the installed --version and command help before relying on newer behavior. Pass --help (bash) / -EasyMode (PowerShell adds the dir to PATH) for options.

Child-process spawning is not enabled by any runtime profile. Profile-governed run also rejects external Node/Bun runtimes because they cannot enforce the engine capability contract. Profile-governed execution requires a build with the embedded engine feature; an arbitrary external --engine-bin is never trusted by name alone. Use the explicit lockstep or compatibility-corpus tools for comparative execution. On Linux, an operator who intends to opt in may pass --enable-process-spawn; the installer then requires the installed binary's bounded Bubblewrap readiness probe to pass. The installer does not install or configure Bubblewrap. Install the distribution package first (for example, sudo apt install bubblewrap), then verify it explicitly:

franken-node doctor process-spawn-readiness --json

macOS and Windows fail closed for this opt-in because the current containment contract requires Linux PID and user namespaces. Normal installations do not probe Bubblewrap and are unaffected.

[!NOTE] Homebrew is not currently published for franken-node; the public Dicklesworthstone/homebrew-tap repository does not yet ship a franken-node formula. Use the installer above or build from source.

Option 2: Build from source (split-repo layout required)

# Start in a fresh parent directory; the bootstrap preserves existing checkouts.
git clone https://github.com/Dicklesworthstone/franken_node.git

cd franken_node
bash scripts/bootstrap_source.sh --workspace "$PWD" --engine-ref main \
  --receipt ../franken-node-source-inputs.tsv
cargo build --release -p frankenengine-node
./target/release/franken-node --version

Expected local layout:

<parent>/
  franken_engine/
  franken_node/
  frankentui/
  fastapi_rust/
  sqlmodel_rust/

The bootstrap materializes all four sibling path dependencies. Supporting revisions come from packaging/source-siblings.tsv; --engine-ref selects the engine and the new receipt records its resolved commit. Supply an immutable engine commit for a reproducible source selection. Existing siblings must be clean and match the requested commits; the bootstrap refuses to reset or replace them. The receipt records source inputs, not a successful build or qualification.

The Cargo workspace package is named frankenengine-node; the produced binary is named franken-node. rust-toolchain.toml selects nightly-2026-09-21 with clippy and rustfmt. The current published fsqlite-pager 0.4.1 dependency requires nightly intrinsics on x86_64; Rust 1.99 stable fails with E0554. This is a compiler requirement, not a completed source-build qualification. The workspace uses Rust 2024 and forbids unsafe code.

Option 3: Verify a release archive

franken-node verify release ./release-dir --key-dir ./trusted-public-keys

[!WARNING] verify release is fail-closed and accepts no built-in trust roots. You must point --key-dir at a directory of public keys you trust; without it, verification refuses to proceed.


Quick Start

  1. Create a default config:
    franken-node init --profile balanced --scan
    
  2. Audit an existing project:
    franken-node migrate audit ./my-app --format json --out migration-audit.json
    
  3. Seed trust cards from your dependency graph, then record a fresh revocation frontier:
    franken-node trust scan ./my-app --deep --audit
    franken-node trust sync --force
    
    --deep/--audit and trust sync fetch registry and OSV data through a scoped remote capability. init provisions it: a signing key at .franken-node/keys/remotecap-signing.key (mode 0600) and a 90-day token at .franken-node/remotecap/trust-scan-token.json whose only scope is network egress to registry.npmjs.org, api.deps.dev and the OSV query endpoint. Re-running init renews an expired token; remotecap revoke --token-file revokes it. FRANKEN_NODE_REMOTECAP_KEY and FRANKEN_NODE_TRUST_SCAN_REMOTECAP_TOKEN override both. Without a token the scan fails closed. Under strict, run admits a tracked dependency only if the recorded frontier is under 5 minutes old; balanced warns once it is over an hour old or missing.
  4. Compare the default Bun+franken dyad (not the L1 Node spec):
    franken-node verify lockstep ./my-app --runtimes bun,franken-node
    
    Matching only Bun stays fail. Corpus L1 requires real Node as spec.
  5. Run in policy-governed mode:
    franken-node run ./my-app --policy strict
    
  6. Inspect trust and fleet state:
    franken-node trust list --risk high
    franken-node fleet status --json   # local store, or the live coordinator when configured
    franken-node incident list --severity high
    
  7. Diagnose environment health:
    franken-node doctor --verbose
    franken-node doctor workspace-pressure
    

Use Cases

Scenario Why franken-node fits
Regulated SaaS deploying third-party extensions Trust cards + revocation freshness gates + signed registry give a continuous audit trail of which artifacts ran at which version with what attestations. The verifier SDK lets external auditors validate claims without trusting the operator.
Multi-region operator with quarantine SLA One node runs franken-node fleet serve; every other node points [fleet] control_plane_url at it. trust quarantine on any node reaches every agent over authenticated HTTP, heartbeats are stamped on the coordinator's clock, and fleet reconcile waits on real multi-node convergence. Without a coordinator the same commands use the local file transport.
Migration from legacy Node to a hardened runtime migrate audit → migrate rewrite --apply --emit-rollback → verify lockstep is an end-to-end pipeline. --emit-rollback writes unsigned JSON (not Ed25519-signed); migrate validate is static+smoke, not lockstep. verify lockstep is the behavioral oracle.
Post-incident counterfactual analysis incident bundle --verify exports a signed, deterministic snapshot of the incident window. incident counterfactual --policy strict answers "would tighter policy have blocked this?" with a reproducible diff.
Public claim verification frankenengine-verifier-sdk runs outside the producing runtime. Third parties verify receipts, capsules, and counterfactuals without trusting franken-node's own logs.
Operator triage under disk / build pressure doctor workspace-pressure analyzes disk, memory, RCH queue depth, and build-fleet state, routes through balanced/conservative/permissive policy, and emits recommended actions.
Long-running fleet agent on edge nodes fleet agent --zone <z> --poll-interval-secs <n> pulls actions from the coordinator (or the local file-transport log when none is configured), applies them to the node's own trust-card registry with bounded retries, and pushes a heartbeat each cycle.
Compliance archival The evidence ledger is append-only, signature-chained, and witness-traced. Incident bundles plus the registry's signed publish records produce a defensible chain of custody.

End-to-End Operator Workflow

Concrete narrative: a platform team taking a moderately-sized Node application through adoption, rollout, and a real incident.

Day 0 — Adoption

# Bootstrap policy + workspace metadata + dependency trust scan
franken-node init --profile balanced --scan

This writes franken_node.toml, creates .franken-node/state/ for durable state, and runs a baseline trust scan against package.json. The scan populates trust cards keyed by npm:<scope>/<name>@<version> with publisher identity, publication date, dependent counts, and any OSV vulnerability hits.

Day 1 — Migration assessment

franken-node migrate audit ./app --format json --out audit.json
franken-node migrate-report ./app --format html --output report.html

migrate audit produces a SARIF-compatible findings file plus a deterministic JSON inventory. migrate-report produces a single-document operator-facing assessment suitable for review by stakeholders outside the runtime team.

Day 2 — Apply transforms, validate

franken-node migrate rewrite ./app --apply --emit-rollback rollback.json
franken-node verify lockstep ./app --runtimes bun,franken-node --emit-fixtures

--emit-rollback writes an unsigned JSON MigrationRollbackPlan (original/rewritten file contents; not Ed25519-signed). Restore from that plan and/or .migrate-backup/ snapshots. verify lockstep defaults to the Bun+franken dyad and records divergence receipts; that dyad is not the L1 spec. Matching only Bun stays fail. On hosts with real Node.js, pass --runtimes node,bun,franken-node — Node is then the spec. Material behavioral deltas fail closed.

Day 5 — Pre-production validation

franken-node verify recovery-runbook --readiness-input broker-snapshot.json --json
franken-node doctor --verbose --json
franken-node ops validation-readiness --input broker-snapshot.json --json

ops validation-readiness and verify recovery-runbook inspect --input / --readiness-input snapshots (live_broker=false); they do not start a live broker. If any recorded precondition is stale, the recovery runbook names which gates need to be re-run.

Day 7 — Production rollout

franken-node run ./app --policy balanced

Run franken-node verify lockstep ./app --runtimes bun,franken-node as a separate comparative dyad (not the L1 Node spec; matching only Bun stays fail) before profile-governed execution. run never launches an external reference runtime.

Day 21 — Incident

A high-severity alert fires; a transitive npm dependency was revoked upstream due to a typosquatting attack. The revocation freshness gate catches it on the next risky action and quarantines the node.

# Containment
franken-node fleet status --json --verbose
franken-node trust quarantine --artifact sha256:abc...

# Forensics
franken-node incident list --severity high
franken-node incident bundle --id INC-2026-0042 --verify
franken-node incident replay --bundle INC-2026-0042.fnbundle \
    --trusted-public-key keys/replay-trust-anchor.pub

# Counterfactual: would strict mode have blocked this?
franken-node incident counterfactual --bundle INC-2026-0042.fnbundle \
    --trusted-public-key keys/replay-trust-anchor.pub --policy strict

The incident bundle is a self-contained, deterministic snapshot: timeline, environment, policy decisions, evidence-ledger entries, and the trust artifacts referenced during the incident. The counterfactual report includes a diff of decisions, actions taken vs. blocked, and any newly emitted evidence.

Re-execute captured native JavaScript

Opt in when recording a run to retain its entry source, execution settings, arguments, and raw host-I/O and admitted process results in the signed run record. The capture is limited to 4 MiB and may contain application secrets.

franken-node init --profile balanced --out-dir .
franken-node run app.js --capture-replay --json > run.json

# Read receipt.receipt_id from run.json, including when the guest threw.
franken-node incident capture --from-run RUN_RECEIPT_ID --json
# Use incident_id from that capture result.
franken-node incident bundle --id INCIDENT_ID --verify
franken-node incident replay --bundle INCIDENT_ID.fnbundle --execute \
    --trusted-public-key .franken-node/keys/receipt-signing.pub --json

Replay authenticates the bundle before executing its captured source through the native engine. It consumes the exact recorded host requests and responses; it does not repeat filesystem writes, network requests, or child processes. The original source and input files may be missing or changed. An unused transcript suffix, an unexpected request, or a different result fails verification.

Run-derived exports also carry the exact original signed run record. An exporter can sign the incident with a different key without replacing that original authority. To require both signatures and bind the incident's receipt, effect ledger, native capture and outcome to the original statement:

franken-node incident replay --bundle INCIDENT_ID.fnbundle --execute \
    --trusted-public-key keys/bundle-exporter.pub \
    --trusted-run-public-key keys/original-run-authority.pub --json

The original run key is supplied independently; a key embedded in the bundle cannot establish trust. A missing proof, wrong original signer or inconsistent run binding fails before guest execution. Successful JSON reports run_origin_verified: true and the authenticated receipt identity. The same portable verification is available in the verifier SDK and its Python, JavaScript and TypeScript bindings, without the original project or private keys. This verifies the original receipt authority's statement; independent engine-session certification and guest re-execution remain separate checks. Historical managed captures require their original authenticated run record when first exported with this proof.

Replay mode Verified result
Default, replay_kind: recorded_trace Signature, bundle integrity, and the recorded decision sequence.
--execute, verification_scope: guest_execution Fresh guest output, value, IR3/IR4 witness, instruction count, nondeterminism trace, lane, exit code, host-I/O transcript, and the global effect journal when process authority was admitted.
--execute, verification_scope: guest_failure_prefix The same uncaught exception, console output, execution trace identity, finalized host-I/O prefix, and any captured global process/I/O journal. Completed-witness, instruction-count, nondeterminism, lane, exit-code, and policy comparisons remain null.

A captured exception leaves the original run failed with exit status 1 and its original error and output. A later replay exits successfully when it reproduces that failure. Runs without host effects receive a completion or failure timeline event, without inventing an effect receipt.

Strict and balanced runs with a valid signed child-process admission can now capture process replay. The worker retains bounded exact request preparation, including policy-owned executable and shell aliases, plus the global ordering of filesystem, network, and process outcomes. Replay uses those captured inputs with an inert process provider and an expired process authority; it does not reopen executables, consult the current process policy, or launch a child. An unknown preparation, conflicting capture, reordered effect, or unconsumed journal suffix cannot produce a successful replay verdict, including when the guest catches the immediate error. A request refused before journal admission may lack replayable preparation and consequently fail replay verification.

Process capture uses the engine's conservative Unknown exception provenance in the original execution as well as replay. This can restrict flows that an ordinary run would allow; it keeps the actual original and replay witnesses comparable without making replay data more trusted or waiving any comparison. Preparation is limited to 4,096 unique entries and 2 MiB within the overall 4 MiB capture limit.

Entry-only captures disable runtime module loading during replay. Source now implements --frozen-modules --capture-replay: it retains the existing bounded source capsule, graph pins and original prepared entry binding. Replay restores only that captured resolver, retaining the original capability restrictions without reading or extracting the original project. Native qualification of this new multi-file path is still pending; see the execution TODO. The capture envelope is now franken-node/native-replay-capture/v3; historical v2 captures are refused rather than treated as authenticating newly added fields. Ambient environment authority, timeouts, interrupted sessions, and uncertified engine failures remain outside this replay scope. For completed runs, decisions_match remains informational. Without frozen sources, withholding module authority changes the capability population used by Bayesian policy evaluation; neither path's guest verdict certifies original policy decisions. --execute requires a build with the engine feature.

Day 22 — Release

franken-node fleet release --incident INC-2026-0042 --json

fleet release lifts quarantine with a signed receipt and waits for convergence of the configured fleet store: the local file transport (live_control_plane=false), or every node heartbeating into a fleet serve coordinator (live_control_plane=true). franken-node fleet reconcile confirms that catch-up; franken-node fleet status --verbose shows the per-node application of the release.

Quarterly — Public claim verification

# Independent third party, with their own trust anchors
franken-node verify release ./release-dir --key-dir ./auditor-trust-roots
franken-node bench run --scenario secure-extension-heavy --output bench.json

Auditors use frankenengine-verifier-sdk to recompute receipts and verify capsule integrity without trusting the runtime that produced them. A bench run report is a SHA256 provenance_hash document, not an Ed25519-signed SDK attestation; the SDK does not independently attest benchmark claims.


Stability

franken-node is pre-1.0. The contract surface is split into three stability bands:

Surface Stability Notes
CLI command shape (franken-node … subcommands, flags, exit codes) Stable Removals and breaking flag changes land as a deliberate deprecation with a CHANGELOG entry. Downstream tests assert this surface in cli_subcommand_goldens.rs.
On-the-wire JSON shapes (decision receipts, trust cards, replay verdicts, counterfactual reports, incident bundles, structured-log events) Stable Every shape has a schema_version and goldens under tests/golden/. Schema bumps are explicit and registered in schema_versions.rs.
Internal Rust APIs (everything under crates/franken-node/src/) In flux Module names, struct fields, and feature-gated surfaces may change between versions. Pin a specific commit when depending.
External verifier SDK (frankenengine-verifier-sdk at sdk/verifier/) Stabilizing Smaller, intentionally narrower than the product crate; intended to be the long-lived audit interface.

Anything not yet documented here, expect movement. The headline claims ("trust-native runtime", "deterministic replay", "fail-closed gates") are backed by code today; the goal of pre-1.0 is to finalize ergonomics and dependency hygiene, not to retract the security commitments.


Runtime Profiles

--profile <name> (or FRANKEN_NODE_PROFILE) selects the runtime policy profile, not the packaging profile:

Profile When to use Behavior
strict High-assurance environments, regulated workloads, post-incident lockdowns Maximum trust-card freshness requirements, revocation gates on all risky actions, all divergence emits receipts, migration.require_lockstep_validation=true (doctor WARN when false; migrate validate still does not run verify lockstep)
balanced Default for most teams Reasonable freshness windows, revocation gates on risky and dangerous classes, lockstep recommended as a separate verify lockstep stage
legacy-risky Constrained migration windows on legacy codebases Permits insecure compatibility behaviors when explicitly enabled by policy. Not a long-term mode.

Native execution also applies the following independent resource limits:

Profile Instructions per run Source bytes per module Tokens per module Parser recursion depth
strict 200,000,000 256,000 32,768 128
balanced 1,000,000,000 1,048,576 65,536 256
legacy-risky 5,000,000,000 2,097,152 131,072 384

runtime.max_instructions replaces the instruction budget. The optional runtime.max_parse_source_bytes and runtime.max_parse_tokens settings replace their respective parser limits for the entry file and every required/imported JavaScript module. Source bytes and tokens are separate limits: a file can fit within the byte limit and still exceed the token limit. Recursion depth remains profile-governed, and the wall-clock timeout still applies.

For a large npm dependency such as lodash, an operator can keep balanced and explicitly set max_parse_tokens = 131_072. This admits additional parser work without changing the profile's trust requirements or other resource limits. The allowed override ranges are 1–2,097,152 source bytes and 1–131,072 tokens; zero, malformed values, and values above these caps fail configuration validation. Overrides never silently clamp. The effective source, token, and recursion limits are included in native run receipts and engine decisions.

The optional runtime.max_heap_objects, runtime.max_total_memory_bytes, and runtime.max_console_entries settings also apply to both native lanes. Each must be a positive integer; environment values override TOML and invalid values fail before guest execution. Leaving an option unset preserves the engine's existing lane default:

Native lane Heap-object allocations Accounted memory Console entries Console bytes
Deterministic 100,000 64 MiB 100,000 8 MiB
Throughput 1,000,000 512 MiB 1,000,000 8 MiB

Heap objects count allocations until the engine supports live-object reclamation. Accounted memory excludes console text and is not a process-RSS ceiling. The console entry and byte ceilings apply independently; overflow fails the run while retaining earlier output, and the byte ceiling remains fixed. Completed native run receipts and engine decisions include execution_limits with the effective limits for both lanes and selected_lane identifying the lane actually used.

Function-frame capacity can be increased without selecting a different trust profile. Set runtime.max_registers to a value in 1–65,536 to replace the register window on both native lanes, or runtime.max_call_depth in 1–10,000 to replace the guest call-depth ceiling. These are independent of the instruction, parser, memory, and console budgets; exceeding any active limit still fails execution. Invalid overrides are rejected before execution. Register backing storage is bounded separately by the engine and is outside the reported heap-memory ceiling; a larger register window can substantially increase process memory. The engine's independent native-stack guard also continues to apply. Leaving them unset preserves the profile defaults:

Profile Deterministic registers per frame Throughput registers per frame Guest call depth
strict 128 256 32
balanced 256 4,096 Engine default
legacy-risky 8,192 16,384 128

For a function that needs more than balanced's 256 registers, an explicit max_registers = 2_048 gives it a larger frame while retaining balanced's capability and trust rules. Effective register and call-depth ceilings are recorded for each lane under execution_limits. Historical receipts that did not record those two fields leave them absent; replay does not invent values.

No profile—including legacy-risky—grants process_spawn. That capability is impossible by default and requires an expiring Ed25519-signed ChildProcessSpawn token bound to the complete resolved policy/config plus a validated Linux Bubblewrap backend. Missing, malformed, expired, or policy-mismatched grants fail before guest execution. Profile-governed run always rejects external Node/Bun paths because they cannot claim this containment contract. The signer is anchored outside project configuration at the fixed, root-owned /etc/franken-node/process-spawn-trust-anchor.pub as exactly 64 lowercase hexadecimal Ed25519 public-key characters; the file and every path ancestor must be non-writable by group/other, so a project cannot declare its own trusted signer.

Packaging profiles (local, dev, enterprise) live in packaging/profiles.toml and govern installer metadata, not runtime behavior.


Runtime Lane Scheduler

The runtime separates concurrent work into scheduler lanes so that hot-path work cannot starve control-critical work and so that bulk maintenance cannot push real-time deadlines past their SLO. There are two related layers:

The four logical lanes (runtime::lane_scheduler::SchedulerLane):

Lane Carries Stable label
ControlCritical Epoch transitions, barrier coordination, marker writes control_critical
RemoteEffect Remote computation invocations, artifact uploads remote_effect
Maintenance Garbage collection, compaction, cleanup tasks maintenance
Background Telemetry export, log rotation, low-priority housekeeping background

Each lane has a built-in priority order (top to bottom above) so that ControlCritical work always wins arbitration against Background work when both are runnable.

Named lane configs ([runtime.lanes.<name>] in franken_node.toml) parameterize the concrete bulkhead each task class is routed through. Common configurations:

[runtime.lanes.cancel]
max_concurrent = 12
priority_weight = 100
queue_limit = 24
enqueue_timeout_ms = 25
overflow_policy = "reject"

[runtime.lanes.realtime]
max_concurrent = 24
priority_weight = 60
queue_limit = 48
enqueue_timeout_ms = 75
overflow_policy = "enqueue-with-timeout"

Task classes are stable identifiers (runtime::lane_scheduler::TaskClass) that the runtime maps to lanes via mapping rules. Built-in classes include epoch_transition, barrier_coordination, marker_write, remote_computation, artifact_upload, artifact_eviction, garbage_collection, compaction, telemetry_export, and log_rotation. Operators can inspect or exercise lane behavior with:

franken-node runtime lane status --json
franken-node runtime lane assign epoch_transition --trace-id ops-poke

The lane assignment path is deterministic: the same task class with the same timestamp routes to the same lane. The lockstep oracle and replay engine depend on that property to work across runs.


Network Egress and SSRF Policy

security::ssrf_policy and security::network_guard together enforce egress policy as a runtime default. Every guest network call on the run path transits the SSRF policy gate before any socket opens. run executes guests only in the native engine; external node/bun runtimes are refused under the governed profiles.

The policy is configured in franken_node.toml:

[security.network_policy]
ssrf_enforcement = "block"     # none | monitor | block

Mechanics:

  • CIDR awareness. CidrRange represents IPv4 CIDR blocks; the gate rejects connections to any IP that falls inside a deny CIDR before the socket opens. is_private_ip blocks the standard private ranges (10/8, 172.16/12, 192.168/16) and loopback by default unless an explicit allowlist entry permits them.
  • Allowlist entries. AllowlistEntry items pin a hostname (or CIDR), optional port, and the connector ID that may use it. The allowlist is scoped per connector so a misbehaving extension cannot piggyback on another extension's permissions.
  • DNS rebinding defense. check_ssrf_resolved_ips re-validates every resolved IP from a hostname after DNS resolution; a hostname that allow-listed against 1.2.3.4 but suddenly resolves to 127.0.0.1 is rejected.
  • Policy receipts. Each enforcement decision can emit a PolicyReceipt and a SsrfAuditRecord that lands in the evidence ledger so post-mortems can reconstruct exactly which endpoint was reached, denied, or rebound.
  • Capability binding. franken-node remotecap issue --endpoint <e> pins capability tokens to specific endpoints; the gate refuses use of a token at any other endpoint regardless of the operation requested.
  • Enforcement modes. ssrf_enforcement is block (default), monitor or none. On the native run path monitor currently enforces exactly like block; an allow-and-record mode is not implemented yet. Only none disables the gate.
  • TLS for guest https egress. A guest https.*/fetch(https://…) call performs a real TLS handshake in the engine's network mechanism (rustls; certificate verification against the built-in webpki roots). tls_extra_roots_pem_path under [security.network_policy] ADDS operator trust anchors (private CAs) to the webpki roots — it never replaces them, and a missing or unparseable bundle fails closed (certificate verification simply fails for hosts signed by those anchors; verification is never weakened or skipped).

Network allowlist parsing is fuzz-covered (fuzz_targets/network_allowlist_entry_*) and the rebind path is exercised by the connector interop suites.


Command Reference

franken-node --help prints the full surface. The following tables list every leaf command available in the current build.

Core lifecycle

Command Purpose
franken-node init Bootstrap config, policy profile, and .franken-node/state/ workspace metadata. Flags: --profile, --config, --out-dir, --overwrite, --backup-existing, --scan, --state-dir, --no-state, --json (franken-node/init-cli/v1; failures franken-node/init-error-cli/v1).
franken-node run <app_path> Run app under policy-governed runtime controls. A directory runs its entry file as node <dir> does: package.json main (as given, with .js, or its index.js), else index.js; a main outside the directory is refused. The app path is handler-required so --json failures emit franken-node/run-error-cli/v1 instead of a human clap error. Flags: --policy, --config, --runtime (auto|node|bun|franken-engine), --engine-bin, --compat-preflight, --json (early failures franken-node/run-error-cli/v1; blocked preflight prints JSON then exits 1). External Node/Bun selections fail closed; use verify lockstep for comparison.
franken-node doctor Local diagnostics: config/profile, close-condition, evidence snapshot, workspace pressure, bwrap. Exits 1 when the overall verdict is fail (after printing the full report) and 0 for pass/warn. Workspace pressure (host load, builds, disk) is advisory in this command and never fails it; doctor workspace-pressure gives the full signal. --policy-activation-input is a snapshot JSON file (not a live guardrail probe). Flags: --config, --profile, --policy-activation-input, --verbose, --json (early failures franken-node/doctor-error-cli/v1), --structured-logs-jsonl.

Migration

Command Purpose
franken-node migrate audit <path> Inventory migration risk. The project path is handler-required so --json failures emit franken-node/migrate-error-cli/v1 instead of a human clap error. Flags: --format (json|text|sarif), --json (alias for --format json), --out.
franken-node migrate rewrite <path> Apply migration transforms. --emit-rollback writes an unsigned JSON MigrationRollbackPlan (not Ed25519-signed) plus .migrate-backup/ snapshots. The project path is handler-required so --json failures emit franken-node/migrate-error-cli/v1 instead of a human clap error. Flags: --apply, --emit-rollback, --json.
franken-node migrate rollback <path> Inspect retained native rewrite transactions or restore one by its explicit identity. --transaction <ID> selects the transaction; without --apply the command is preview-only, and there is no latest-ID inference. The project path is handler-required so --json failures emit franken-node/migrate-error-cli/v1 instead of a human clap error. Flags: --transaction, --apply, --json.
franken-node migrate validate <path> Rerun static migration audit checks and, unless --static-only, a transformed-runtime smoke test. Does not run verify lockstep. The project path is handler-required so --json failures emit franken-node/migrate-error-cli/v1 instead of a human clap error. Flags: --format (json|text), --json, --static-only.
franken-node migrate rollout <path> Drive the progressive rollout state machine (shadow → canary → ramp → default) with fail-closed gates. --action is promote (default), rollback, or status; --stage overrides the target stage; --ramp-pct applies in ramp stage. The state machine records stages and evidence; it does not start or route traffic to instances. Leaving Shadow requires --lockstep-report <path>, the JSON printed by franken-node verify lockstep <project> --json. The report must have verdict Pass, include a franken leg and a reference leg, and match the project's current input. Without it the promotion fails closed. --force bypasses the confidence and evidence checks but never records lockstep_verified. --no-auto-rollback disables automatic rollback on verification failure. The project path is handler-required so --json failures emit franken-node/migrate-error-cli/v1 instead of a human clap error. Flags: --migration-id, --action, --stage, --ramp-pct, --lockstep-report, --force, --no-auto-rollback, --json.
franken-node migrate-report <path> Export one-command migration assessment. The project path is handler-required so --json failures emit franken-node/migrate-error-cli/v1 instead of a human clap error. Flags: --format (json|html), --json (alias for --format json), --output.

migrate rewrite <path> --apply --verify checks captured dependency metadata and installed package identities before differential execution. Its npm 11 compatibility path gives npm-shrinkwrap.json precedence over package-lock.json, including when the ignored lockfile is stale or malformed. Root scalar overrides can select supported registry ranges and replacement forks such as "vulnerable": "npm:@maintained/fork@^2", including $dependency references to aliased direct dependencies. The replacement's name and range are checked at the original dependency's installation location in both the lockfile and actual installed tree; raw declarations remain unchanged. Direct dependency conflicts retain npm's exact-spec EOVERRIDE behavior. Nested or version-qualified rules, nonregistry sources, omitted alias ranges, dist-tags, and bundled or nested-shrinkwrap contexts require review. Workspace and installed-package manifests cannot redefine the root's overrides.

On Linux, trust dependency queries use that same captured override authority, including the replacement fork's identity and its locked version. Reports keep the original dependency request alongside the effective override. The topology commitment binds the complete selected root rules, including unused entries and resolved $dependency references; later queries do not reopen package metadata. Unsupported override contexts and conflicting captured identities fail closed.

Bun text-lock projects also support root scalar registry overrides and resolutions, checked against both the lock graph and actual hoisted or isolated installations. The presence of overrides, including an empty object, takes precedence over resolutions. Every selected rule must match bun.lock.overrides, including unused rules; original dependency declarations and input hashes remain unchanged. Bun permits direct dependency replacement and treats literal * as an active replacement. Versioned npm: aliases keep their original requirements. Nested or version-qualified rules, reference/catalog replacements, replacement sources and contextual workspace replacements still require review. Lifecycle, native-addon, patch and trusted-dependency checks remain in force.

The shared Unix workspace selector accepts bounded brace alternatives such as {apps,packages}/*, packages/{core,{cli,api}} and packages/{,group/}core, alongside literal paths, * and standalone **. Alternatives and overlapping selectors share one descriptor-relative directory walk, and each manifest is captured once. Checked migration requires a match for each authored selector, rather than for every alternative. Expanded alternatives count against the existing pattern limit. Numeric brace ranges, negation and other unsupported glob dialects still require review; symlink and reserved-directory restrictions remain in force.

Verification

Command Purpose
franken-node verify module <id> Verify a franken_node crate-src module contract (api, cli, …). Not guest JS conformance. The module id is handler-required so --json failures emit verifier-cli-contract-v1 instead of a human clap error. Flags: --json.
franken-node verify source-graph <project> <entrypoint> Capture the existing bounded static module graph without executing guest code. Emits its content commitment for later pinned execution; does not authenticate a publisher or grant authority. Flags: --json (existing franken-node/module-source-graph/v2 report), --allow-contained-symlinks. Exit 0 means the declared static scope is complete; incomplete capture or errors exit 1. Missing/unsafe input errors use franken-node/verify-source-graph-error-cli/v1. Unix only.
franken-node verify migration <id> Check a local migration evidence record and its post-conditions; does not re-run migrate rewrite. The migration id is handler-required so --json failures emit verifier-cli-contract-v1 instead of a human clap error. Flags: --json (live_rewrite=false, source=local_evidence_record).
franken-node verify compatibility <target> Probe a concrete runtime (node, bun, franken-node). Profile names (strict, balanced, legacy-risky) fail closed and do not auto-PASS. The target is handler-required so --json failures emit verifier-cli-contract-v1 instead of a human clap error. node/bun smoke is console.log; franken-node smoke is fixture JS through the embedded engine (run --console-only), not --version alone. Flags: --json.
franken-node verify corpus <path> Verify corpus schema and coverage. The corpus path is handler-required so --json failures emit verifier-cli-contract-v1 instead of a human clap error. Flags: --json.
franken-node verify lockstep <path> Compare runtimes in lockstep. Default --runtimes bun,franken-node. Use --runtimes node,bun,franken-node only when node is real Node.js; Node is then the spec and matching only Bun stays fail. The project path is handler-required so --json failures emit franken-node/verify-lockstep-error-cli/v1 instead of a human clap error. --emit-fixtures writes divergence fixtures. Flags: --json (stderr banner off; early failures franken-node/verify-lockstep-error-cli/v1).
franken-node verify release <path> Verify release artifact signatures. The release path and --key-dir are handler-required so --json failures emit franken-node/verify-release-error-cli/v1 instead of a human clap error. Fails closed without --key-dir. Flags: --json (franken-node/verify-release-cli/v1; early failures franken-node/verify-release-error-cli/v1).
franken-node verify transparency-log <path> Verify a complete retained chain from a JSONL export or .franken-node/state/evidence-ledger.db. --public-key verifies contents and predecessor-bound signatures; legacy standalone signatures leave chain history unproven with nonzero exit. --expected-head-hash <64 lowercase hex> additionally checks a trusted, separately retained checkpoint and requires --public-key. Empty or oversized logs fail closed. JSON: franken-node/verify-transparency-log-cli/v1; handler failures: franken-node/verify-transparency-log-error-cli/v1.
franken-node verify recovery-runbook Generate a recovery runbook from a --readiness-input snapshot (not a live broker). Flags: --json (early failures franken-node/verify-recovery-runbook-error-cli/v1).

Trust and supply chain

Command Purpose
franken-node trust card <id> Show trust profile for one extension. The extension id is handler-required so --json failures emit franken-node/trust-error-cli/v1 instead of a human clap error. Flags: --json.
franken-node trust list List extensions; filter by --risk, --revoked. --json emits the card list as JSON.
franken-node trust scan [path] Populate baseline trust cards from package.json. A plain rescan skips existing cards; with --deep/--audit it refreshes them from the fetched evidence (same version only; lowering risk needs an OSV answer from the default endpoint) and keeps certification, revocation, quarantine and camouflage marks. Flags: --deep, --audit, --json.
franken-node trust observe <observation.json> --collector-key <public-key-file> Verify collector-attested measurements for an exact package artifact, append a durable observation stream, and apply BPET camouflage findings to its signed trust card. Repeated observations are idempotent. --json emits franken-node/behavioral-ingestion/v1. See the measurement and signing contract.
franken-node trust sync Refresh trust-card cache and npm vulnerability state from OSV; --force to ignore caches. --json emits franken-node/trust-sync-cli/v1.
franken-node trust revoke <id> Revoke an artifact or publisher in the local trust-card registry (not a live fleet). The extension id is handler-required so --json failures emit franken-node/trust-error-cli/v1 instead of a human clap error. Optional --receipt-signing-key, --receipt-out. --json emits the revoked trust card; failures franken-node/trust-error-cli/v1.
franken-node trust quarantine Quarantine a suspicious artifact in the local trust-card registry and publish the quarantine to the fleet store: the local file transport, or the live coordinator when [fleet] control_plane_url is set (every agent polling it applies the quarantine). --artifact is handler-required so --json failures emit franken-node/trust-error-cli/v1 instead of a human clap error. --json emits franken-node/trust-quarantine-cli/v1.
franken-node trust graph Ecosystem reputation graph from the verified trust-card registry and the project's package-lock.json: publisher→extension and lockfile depends_on edges, effective trust after named propagation rules (R1 revoked → 0; R2 quarantined ≤ 100; R3 at most 200 above the weakest dependency; R4 publishers with a revoked/quarantined extension capped at 400 and their extensions at publisher + 300), an explanation per lowered score, untracked dependencies reported as coverage gaps, and card-version transitions with the audit records that caused them. --extension <id> adds that extension's blast radius (every extension whose trust rests on it). --json emits franken-node/trust-graph-cli/v1.
franken-node trust release Release a local trust-card hold with --artifact <extension-id or sha256:prefix>, preserving fleet quarantine owners and permanent revocation, or release a sentinel run-target record with --app. Exactly one target, --operator-id, and --reason are required. Artifact releases bind operator/rationale into signed card history and report remaining containment. --json emits franken-node/trust-artifact-release-cli/v1 for artifacts or franken-node/trust-release-cli/v1 for apps; failures emit franken-node/trust-release-error/v1.
franken-node trust-card show <id> Show a card from the local trust-card registry JSON (not a live HTTP API). The extension id is handler-required so --json failures emit franken-node/trust-card-error-cli/v1 instead of a human clap error. Flags: --json.
franken-node trust-card export <id> --json Export trust card as canonical JSON. The extension id is handler-required so --json failures emit franken-node/trust-card-error-cli/v1 instead of a human clap error.
franken-node trust-card list List with filters: --publisher, --query, --page, --per-page. Flags: --json.
franken-node trust-card compare <left> <right> Compare two trust postures. Left and right ids are handler-required so --json failures emit franken-node/trust-card-error-cli/v1 instead of a human clap error. Flags: --json.
franken-node trust-card diff <id> <left_version> <right_version> Diff one card across versions. Id and versions are handler-required so --json failures emit franken-node/trust-card-error-cli/v1 instead of a human clap error. Flags: --json.

Remote capabilities

Command Purpose
franken-node remotecap issue Issue a local Ed25519 capability token. Does not perform the scoped network operation. --scope and --endpoint are handler-required so --json failures emit franken-node/remotecap-error-cli/v1 instead of a human clap error. Optional: --ttl, --issuer, --operator-approved, --single-use. Flags: --json (franken-node/remotecap-issue-cli/v1).
franken-node remotecap verify Verify a capability token without using it. --token-file, --operation, and --endpoint are handler-required so --json failures emit franken-node/remotecap-error-cli/v1 instead of a human clap error. Flags: --json (franken-node/remotecap-verify-cli/v1).
franken-node remotecap use Authorize a scoped network operation against a token (dry-run). Does not perform the HTTP request. --token-file, --operation, and --endpoint are handler-required so --json failures emit franken-node/remotecap-error-cli/v1 instead of a human clap error. Flags: --json (franken-node/remotecap-use-cli/v1).
franken-node remotecap revoke Revoke a capability token. Required: --token-file. Flags: --json (franken-node/remotecap-revoke-cli/v1).

Fleet quarantine control plane (file transport or live HTTP coordinator)

Every fleet command, and trust quarantine, talks to one of two stores:

  • File transport (default). The node's own WAL-durable fleet store; JSON reports transport=file, live_control_plane=false, activated_source=file_transport_not_live.
  • Live coordinator. When [fleet] control_plane_url (or FRANKEN_NODE_FLEET_CONTROL_PLANE_URL) names a node running fleet serve, commands use its HTTP API with the bearer token from [fleet] control_plane_token_path (or FRANKEN_NODE_FLEET_CONTROL_PLANE_TOKEN_PATH). Reports carry transport=http, live_control_plane=true, activated_source=http_control_plane_live and control_plane_url. A missing token, a refused token or an unreachable coordinator is an error; there is no silent fallback to the local store. Plaintext http:// is accepted only for loopback; reach a remote coordinator over https://.
Command Purpose
franken-node fleet serve Run the live coordinator: serves this node's durable fleet store over HTTP (fastapi_rust listener on the asupersync runtime). Routes: GET /v1/fleet/health (open), GET/POST /v1/fleet/actions, GET/POST /v1/fleet/nodes, GET /v1/fleet/state (bearer token, constant-time compare). Validates every record, caps bodies at 64 KiB, stamps heartbeats with its own clock. Flags: --bind (default 127.0.0.1:9440; port 0 picks a free port, printed first on stdout), --token-file (default .franken-node/keys/fleet-control-plane.token), --generate-token (create a 0600 256-bit token), --allow-non-loopback (plaintext listener; only behind TLS termination or an encrypted overlay), --max-requests, --json (JSONL FLEET_HTTP_LISTENING / FLEET_HTTP_REQUEST / FLEET_HTTP_SHUTDOWN events).
franken-node fleet status Show fleet/quarantine state from the configured store. activated is true only when a live coordinator answered. Flags: --zone, --verbose, --json.
franken-node fleet describe <node> Describe one fleet node with zone context and incident state. Flags: --zone, --json.
franken-node fleet release Retire one fleet quarantine incident with signed receipts and application convergence. Preserves local quarantine, other scoped incidents, and permanent revocation. --zone disambiguates a shared incident ID; --zone all selects its global scope. --incident is handler-required so --json failures emit franken-node/fleet-error-cli/v1.
franken-node fleet reconcile Reconcile the fleet-action log and wait for convergence. Times out fail-closed (same as fleet release). Flags: --json.
franken-node fleet agent Pull fleet actions, apply them to this node's trust-card registry, and push a heartbeat each cycle. --zone is handler-required so --json failures emit franken-node/fleet-error-cli/v1 instead of a human clap error. Flags: --node-id, --poll-interval-secs, --max-cycles, --once, --json.
# coordinator
franken-node fleet serve --bind 127.0.0.1:9440 --generate-token
# every other node (token copied from the coordinator)
export FRANKEN_NODE_FLEET_CONTROL_PLANE_URL=http://127.0.0.1:9440
export FRANKEN_NODE_FLEET_CONTROL_PLANE_TOKEN_PATH=/etc/franken-node/fleet.token
franken-node fleet agent --node-id edge-1 --zone us-east --poll-interval-secs 10
franken-node trust quarantine --artifact npm:@acme/auth-guard --json
franken-node fleet status --json   # transport=http, live_control_plane=true

Incident replay and forensics

Command Purpose
franken-node incident bundle Export a deterministic incident bundle. --id is handler-required so --json failures emit franken-node/incident-error-cli/v1. Reads --evidence-path or <project-root>/.franken-node/state/incidents/<slug>/evidence.v1.json. run automatically signs captured source evidence when SSRF checks, a denied host effect, or Sentinel trips; the incident id is INC-RUN-<host-effect chain head>. Captured sources must authenticate against the configured receipt authority before export. An export signing-key override does not replace that authority. Explicit operator-authored inputs remain supported; JSON reports source_authenticated. --verify checks the generated bundle. Flags: --json.
franken-node incident replay Verifies recorded bundle integrity by default; --execute also re-executes captured native guest inputs. --bundle is handler-required so --json failures emit franken-node/incident-error-cli/v1 instead of a human clap error. Also fails closed without --trusted-public-key or --key-dir. Flags: --execute, --json.
franken-node incident counterfactual Simulate alternative policy actions. Same trust-anchor requirement as replay. --bundle and --policy are handler-required so --json failures emit franken-node/incident-error-cli/v1 instead of a human clap error. Optional: --promote, --promotion-signing-key, --operator-id. --model synthetic (default) is the sandboxed risk-score stand-in. --model production re-decides every host effect a run-captured incident recorded (its signed effect-receipt chain entry) under the runtime profile named by --policy (strict, balanced, legacy-risky), using the run path's own capability table: an effect whose capability the profile does not grant is refused at the capability gate; one it grants gets the recorded outcome of the profile-independent SSRF / flow / sandbox gates; spawns keep their admission-bound outcome. Decisions after the first divergence are labelled determination=path_assumed. Bundles without recorded effects, and non-profile policies, are refused. The model is bound into the counterfactual digest. Flags: --json.
franken-node incident list List recorded incidents. Filter: --severity. Flags: --json (failures franken-node/incident-error-cli/v1).
franken-node incident capture Capture a recorded native run as incident evidence. --from-run <receipt-id|receipt-path> verifies the product-signed run record and complete receipt snapshot, then the engine ledger's counts, hash chain, detached signature, and product-root-signed session identity before sealing the captured source. An existing source is returned as already_captured only after its signature and run bindings authenticate. Legacy unsigned records require a new run. Flags: --severity, --json (franken-node/incident-capture-cli/v1).

Long-term verifiability (LTV)

Command Purpose
franken-node ltv attest Build self-contained long-term verification evidence from a signature-verified incident bundle: the bundle (plus, with --run-report, its run's host-effect chain hashes) becomes Merkle leaves, the root is re-attested and threshold-cosigned by operator witness keys. Required: --bundle, --out, at least one --witness-key, and a trust anchor (--trusted-public-key or --key-dir). --bundle, --out, and --witness-key are handler-required so --json failures emit franken-node/ltv-error-cli/v1 instead of a human clap error. Flags: --json.
franken-node ltv verify-as-of Re-verify LTV evidence offline through the verifier SDK (verify_as_of_ltv): inclusion, re-attestation chain, witness threshold, and anteriority against --as-of. Fails closed (non-zero exit) on any failed assertion. Required: --evidence and --witness-anchor. Both are handler-required so --json failures emit franken-node/ltv-error-cli/v1 instead of a human clap error. Flags: --json.

Runtime, safe-mode, and proofs

Command Purpose
franken-node runtime lane status Emit the local default lane policy and an empty telemetry snapshot (not a running node). Flags: --json (failures franken-node/runtime-error-cli/v1).
franken-node runtime lane assign <task_class> Assign one task class through a fresh default lane scheduler; the assignment is not persisted to a live node. Flags: --json (failures franken-node/runtime-error-cli/v1).
franken-node runtime epoch Compare two caller-supplied epoch integers. Does not inspect a live ControlEpoch. --local-epoch is handler-required so --json failures emit franken-node/runtime-error-cli/v1 instead of a human clap error. Flags: --peer-epoch, --json.
franken-node safe-mode enter Enter safe mode and block new runs within this directory and its descendants using persisted unsigned JSON operator state (not Ed25519-signed). --reason, --operator-id, and --trust-state-hash are handler-required so --json failures emit franken-node/safe-mode-cli/v1 instead of a human clap error. Reasons: explicit-flag, environment-variable, config-field, trust-corruption, crash-loop, epoch-mismatch. Flags: --json.
franken-node safe-mode status Inspect persisted unsigned JSON safe-mode state. Flags: --json (franken-node/safe-mode-cli/v1).
franken-node safe-mode exit Exit safe mode after explicit operator confirmation. --operator-id is handler-required so --json failures emit franken-node/safe-mode-cli/v1 instead of a human clap error. Required: --confirm. --trust-state-consistent, --no-unresolved-incidents, and --evidence-ledger-intact are operator attestations, not independently verified checks. Flags: --json.
franken-node proofs queue status Inspect proof queue, proof status, and worker readiness from --input/--receipt snapshots (live_queue=false, queue_source=validation_readiness_snapshot). Not a live broker. Flags: --json.
franken-node proofs workers restart Validate and emit a restart-request artifact. Does not restart a process unless --execute is set and FRANKEN_NODE_PROOF_WORKER_RESTART_EXECUTOR is bound. --operator-id, --operator-role, and --reason are handler-required so --json failures emit franken-node/proofs-error-cli/v1 instead of a human clap error. Required: --confirm. Flags: --json.

Ops and diagnostics

Command Purpose
franken-node ops health-check Process health from compile-time git SHA, evidence-ledger flush mtime, and persisted session files under .franken-node/state/sessions (not a live SessionManager). Flags: --json (franken-node/ops-health-check-cli/v1; session_count_source=persisted_session_files).
franken-node ops resource-governor Advise whether validation should run / defer / deduplicate. Default observation is a live procfs sample; --process-snapshot is a fixture. Human output names source=. Flags: --json.
franken-node ops validation-readiness Report validation evidence freshness from --input/--receipt snapshots, or an empty default snapshot when omitted. Human and --json name input_source and live_broker=false. Not a live broker.
franken-node ops validation-closeout Render closeout summary from a --receipt snapshot (not a live broker). --bead-id and --receipt are handler-required so --json failures emit franken-node/ops-error-cli/v1 instead of a human clap error. Flags: --json.
franken-node ops config-audit Audit active config across profiles. Flags: --json (franken-node/ops-config-audit-cli/v1).
franken-node ops metrics Emit operator metrics (Prometheus by default). Flags: --json (franken-node/ops-metrics-cli/v1).
franken-node ops incident-coverage Replay availability over retained durable run decisions and discoverable run records. Authenticated security outcomes determine the population; each counted capture must authenticate and each replayable bundle must match its source and trusted signature. JSON includes population_scope, authentication status, and source_errors. --min-coverage <0..1> fails when the population is empty, incomplete, unauthenticated, or below the floor. This local inventory does not establish deletion-resistant historical completeness. Flags: --json (franken-node/ops-incident-coverage-cli/v1).
franken-node ops compat-corpus-run Run the committed compatibility corpus through bun + native franken-engine lockstep and write the digest-bound results artifact. --corpus-root and --out are handler-required so --json failures emit franken-node/ops-error-cli/v1 instead of a human clap error. --policy selects the profile the franken leg runs under (default legacy-risky, the artifact of record). The artifact's run_provenance block records the binary's sha256, the embedded git revision, the policy and the host's CPU count and load. Flags: --json (franken-node/ops-compat-corpus-run-cli/v1), --require-node-reference, --policy, --case-timeout-secs.
franken-node ops proof-carrying-evidence Produce a 3-effect host-IO evidence chain from a real native-engine run. This is an additional L1 conjunct, not a substitute for corpus ≥95%. L1 GREEN still requires the compatibility corpus floor. Flags: --json, --out, --merge-corpus, --merge-l1-verdict.
franken-node doctor workspace-pressure Probe local disk, memory, RCH slots, and build counts; apply balanced/conservative/permissive pressure policy. Default human report is stdout. --human-output <path> writes that report to a file (not a boolean flag). Flags: --json, --output, --human-output, --conservative, --permissive (failures franken-node/doctor-error-cli/v1).
franken-node doctor close-condition Emit dual-oracle close-condition receipt. Human output names declared L1 pass_rate, node_canonical_observation_passes, node_canonical_unscored_fail_ids, and child_process_native_eval_aborts (those remain fail; not recategorized as pass). Flags: --json (failures franken-node/doctor-error-cli/v1).
franken-node doctor evidence-readiness Report evidence readiness from an --input snapshot (not a live broker). --input is handler-required so --json failures emit franken-node/doctor-error-cli/v1 instead of a human clap error. Flags: --json.
franken-node doctor process-spawn-readiness Securely resolve (or accept --bubblewrap-path for) a root-owned, non-setuid, non-writable Bubblewrap binary and run a bounded functional user/PID/cgroup/IPC/UTS namespace probe. Returns non-zero with unavailable or unsupported; readiness by itself does not grant process_spawn or replace the signed opt-in. Flags: --bubblewrap-path, --json (failures franken-node/doctor-error-cli/v1).

Registry, bench, debug

Command Purpose
franken-node registry publish <package> Admit a signed artifact into the local on-disk registry (not npm or a live control-plane publish). The package path and --version are handler-required so --json failures emit franken-node/registry-publish-error/v1 instead of a human clap error. Required: --version and --signing-key (raw Ed25519 32-byte key; hex, base64, or supported JSON wrapper). Optional: --max-active-artifacts. --json emits franken-node/registry-publish-cli/v1.
franken-node registry search <query> Query locally stored registry artifacts with trust filters (not a live npm/control-plane service). The query is handler-required so --json failures emit franken-node/registry-error-cli/v1 instead of a human clap error. Filter: --min-assurance. --json emits franken-node/registry-search-cli/v1.
franken-node registry verify <id> Verify a locally stored registry artifact. The extension id is handler-required so --json failures emit franken-node/registry-error-cli/v1 instead of a human clap error. --json emits franken-node/registry-verify-cli/v1.
franken-node registry gc Archive older registry artifacts. Optional: --keep. --json emits franken-node/registry-gc-cli/v1.
franken-node bench run Run benchmark suite and emit a SHA256 provenance_hash report (not Ed25519-signed). Flags: --scenario, --fixture-mode, --output, --json. The report is always JSON on stdout; --json suppresses the human summary on stderr.
franken-node debug explain Walk a signed decision-receipt through verification steps. --receipt is handler-required so --json failures emit franken-node/debug-error-cli/v1 instead of a human clap error. --json emits machine-readable per-step results (franken-node/debug-explain-cli/v1).
franken-node debug evidence Inspect verifier evidence artifacts. --artifact is handler-required so --json failures emit franken-node/debug-error-cli/v1 instead of a human clap error. --kind accepts auto, node-replay-capsule, provenance-attestation, vef-evidence-capsule. --json emits machine-readable diagnostics.
franken-node debug trace Trace a policy-activation fixture through doctor_policy_activation (not live guest execution). --policy and --input are handler-required so --json failures emit franken-node/debug-error-cli/v1 instead of a human clap error. --json emits machine-readable trace (franken-node/debug-trace-cli/v1).

[!NOTE] Commands that declare --json emit machine-readable output. That flag is not on every subcommand. bench run always writes the JSON report (provenance_hash SHA256 content hash, not Ed25519-signed) to stdout; --json is accepted for operator/agent wrappers and suppresses the human summary on stderr. Many JSON-capable commands also accept --structured-logs-jsonl for event-coded diagnostics on stderr (see Structured Logging and Observability), and --trace-id <id> to correlate log events across processes.

[!WARNING] Fail-closed commands. incident replay and incident counterfactual refuse to run without --trusted-public-key or --key-dir. registry publish refuses without --version and --signing-key. verify release refuses without --key-dir. safe-mode enter requires --reason, --operator-id, and --trust-state-hash. There are no built-in trust roots and no implicit defaults; this is intentional, not an ergonomics gap.


Configuration

franken_node.toml (discovered from the working directory, the project root, or the path passed to --config):

# Runtime profile: strict | balanced | legacy-risky
profile = "balanced"

[compatibility]
# API compatibility mode for migration and runtime dispatch
mode = "balanced"
# Divergence receipts are always recorded in production profiles
emit_divergence_receipts = true
# TTL for signed compatibility receipts
default_receipt_ttl_secs = 3600

[migration]
# Enable automatic rewrite suggestions
autofix = true
# Doctor WARN when false. `migrate validate` does not invoke `verify lockstep`.
require_lockstep_validation = true

[trust]
# Revocation freshness requirements by action class
risky_requires_fresh_revocation = true
dangerous_requires_fresh_revocation = true
# Quarantine defaults
quarantine_on_high_risk = true

[replay]
# Persist high-severity replay artifacts
persist_high_severity = true
# Deterministic bundle export format version
bundle_version = "v1"
# Maximum permitted replay capsule freshness window
max_replay_capsule_freshness_secs = 3600

[registry]
# Enforce signature and provenance gates
require_signatures = true
require_provenance = true
minimum_assurance_level = 3

[fleet]
# Optional override for the persisted fleet transport state root
state_dir = ".franken-node/state/fleet"
# Fleet convergence timeout for quarantine/release operations
convergence_timeout_seconds = 120

[observability]
# Stable metrics namespace for automation
namespace = "franken_node"
emit_structured_audit_events = true

[remote]
# Default TTL for remote idempotency entries
idempotency_ttl_secs = 604800

[security]
# Maximum degraded-mode duration before suspension
max_degraded_duration_secs = 3600

# Child-process spawning has no profile default. An operator-issued opt-in is
# represented by [security.child_process_spawn] plus its nested .token table.
# The token subject must equal the policy subject reported by trusted issuance
# tooling after all non-recursive fields below are populated, and the signature
# must cover the CapabilityToken content hash. The operator trust anchor is a
# separate keyring file; it is never accepted from this project config.
# backend = "bubblewrap"
# binary_path = "/usr/bin/bwrap"
#
# Process execution authority is also explicit and token-bound: each command
# alias resolves without PATH lookup to one exact canonical executable and
# SHA-256 digest. The cwd jail, environment vocabulary, shell choice, and
# resource ceilings below are all part of the signed policy subject.
#
# [security.child_process_spawn.execution_policy]
# jailed_cwd_root = "/srv/franken-node/apps/example"
# allow_shell = false
# allowed_env_keys = ["LANG"]
# fixed_env = { LANG = "C.UTF-8" }
#
# [security.child_process_spawn.execution_policy.allowed_executables.true]
# path = "/usr/bin/true"
# sha256 = "<64 lowercase hexadecimal characters>"
#
# [security.child_process_spawn.execution_policy.limits]
# max_children = 4
# max_executable_path_bytes = 4096
# max_executable_bytes = 134217728
# max_argv_count = 128
# max_argv_bytes = 65536
# max_env_count = 128
# max_env_bytes = 65536
# max_cwd_bytes = 4096
# max_prelaunch_bytes = 131072
# max_request_bytes = 16777216
# max_stdin_bytes = 1048576
# max_output_bytes = 4194304
# max_runtime_millis = 30000

[security.network_policy]
# SSRF enforcement mode: none | monitor | block
ssrf_enforcement = "block"
# Keep the legacy boolean guard enabled while migrating older configs.
ssrf_protection_enabled = true
block_cloud_metadata = true
audit_blocked_requests = true

[engine]
# Optional path override for the franken_engine binary
# binary_path = "/usr/local/bin/franken-engine"

[runtime]
# Preferred runtime: auto | node | bun | franken-engine
preferred = "auto"
# Global cap on in-flight network-bound operations
remote_max_in_flight = 50
# Retry hint when bulkhead is saturated
bulkhead_retry_after_ms = 50
# Optional static process metadata reads in native entrypoint code, including
# supplied process.argv values and process.platform. Omitted: denied in
# strict/balanced, allowed in legacy-risky. Explicit false denies every profile.
# allow_process_shape = true
# Also FRANKEN_NODE_RUNTIME_ALLOW_PROCESS_SHAPE; environment overrides TOML.
# Replay capture retains supplied arguments and this bounded grant. It does
# not grant environment values, filesystem, network, or child-process access.
# Instruction budget for one `run` in the native engine (optional; default
# strict 200M, balanced 1B, legacy-risky 5B). Also
# FRANKEN_NODE_RUNTIME_MAX_INSTRUCTIONS. The wall-clock timeout still applies.
# max_instructions = 1_000_000_000
# Optional per-frame register and guest call-depth ceilings on both lanes.
# Accepted ranges: registers 1..=65_536; call depth 1..=10_000.
# max_registers = 2_048
# max_call_depth = 128
# Also FRANKEN_NODE_RUNTIME_MAX_REGISTERS and
# FRANKEN_NODE_RUNTIME_MAX_CALL_DEPTH; environment overrides TOML.
# Optional positive execution ceilings on both native lanes; omitted values
# preserve the engine defaults listed under Runtime Profiles.
# max_heap_objects = 200_000
# max_total_memory_bytes = 100_663_296
# max_console_entries = 2_000
# Accounted memory excludes console and is not process RSS. Console bytes
# retain the separate fixed engine ceiling; overflow fails without eviction.
# Independent per-module parser limits, including required/imported modules.
# Defaults are listed under Runtime Profiles. Accepted override ranges:
# source bytes 1..=2_097_152; tokens 1..=131_072. Invalid values fail closed.
# A balanced-profile opt-in for larger npm modules, without changing profile:
# max_parse_tokens = 131_072
# max_parse_source_bytes = 1_048_576
# Also FRANKEN_NODE_RUNTIME_MAX_PARSE_TOKENS and
# FRANKEN_NODE_RUNTIME_MAX_PARSE_SOURCE_BYTES; environment overrides TOML.

[runtime.lanes.cancel]
max_concurrent = 12
priority_weight = 100
queue_limit = 24
enqueue_timeout_ms = 25
overflow_policy = "reject"

[runtime.lanes.realtime]
max_concurrent = 24
priority_weight = 60
queue_limit = 48
enqueue_timeout_ms = 75
overflow_policy = "enqueue-with-timeout"

[thresholds]
# Algorithmic/statistical thresholds (all optional, with safe defaults)
max_failure_rate = 0.05
min_quality_score = 0.8
max_variance_pct = 5.0
regression_threshold_pct = 10.0

franken-node init writes starter configuration files with the active profile and, unless --no-state is supplied, bootstraps the default state layout under --state-dir, --out-dir, or the current directory, in that order. The config is written to --out-dir, or else beside that state: state bootstrap signs the trust-card registry with the config's registry key, so the two must travel together. Only init --no-state without --out-dir prints the resolved starter TOML to stdout instead of writing it. franken-node ops config-audit --json prints the resolved configuration with profile overlays applied.


Crate Features

The frankenengine-node crate ships a granular feature surface. Defaults include native execution, HTTP/process helpers and the fleet HTTP server; additional product surfaces opt in explicitly.

Feature Default Purpose
engine ✅ Links the sibling frankenengine-engine and frankenengine-extension-host crates.
http-client ✅ Outbound HTTP client support (ureq, url).
external-commands ✅ External command/process helpers (which, ctrlc).
fleet-control-plane-server ✅ Live fleet serve HTTP coordinator on fastapi_rust/asupersync over the durable fleet store; implies http-client.
extended-surfaces opt-in Legacy umbrella over the granular product features below.
control-plane opt-in API middleware, fleet operations, control-plane functionality.
policy-engine opt-in Security policies, guardrail monitors, hardening state machines.
remote-ops opt-in Remote operations, distributed coordination, eviction sagas.
admin-tools opt-in Enterprise governance, migration tools, administrative functionality.
verifier-tools opt-in Verifier-specific tooling, economy, and SDK surfaces.
advanced-features opt-in Claims, conformance, encoding, extensions, federation, performance, and repair surfaces.
test-support opt-in Shared test helpers (enables control-plane and admin-tools).
loom-models opt-in Library-level #[cfg(loom)] helpers for explicit Loom invocations.
asupersync-transport opt-in Direct asupersync transport integration.
compression opt-in Gzip/deflate support via flate2.
cbor-serialization opt-in CBOR encoding support via ciborium.
blake3 opt-in Optional BLAKE3 hashing support.
profiling opt-in Per-hot-path latency histograms via hdrhistogram; absent from default builds.
# Enable everything except packaging extras
cargo build --release -p frankenengine-node --features extended-surfaces

# Build a minimal node binary with no product surfaces
cargo build --release -p frankenengine-node --no-default-features --features engine

Architecture

flowchart TB
    fn["<b>franken_node</b><br/>compatibility, migration, trust UX, ops<br/>verifier SDK, fleet control,<br/>recorded incident bundles"]
    asu["<b>asupersync</b><br/>runtime for the default fleet server<br/>additional transport integration is opt-in"]
    ftui["<b>frankentui</b><br/>Buffer echo of<br/>preformatted doctor/fleet lines"]
    fapi["<b>fastapi_rust</b><br/>default fleet HTTP listener<br/>authenticated durable-store routes"]
    feng["<b>franken_engine</b><br/>native JS / TS runtime<br/>+ extension host"]
    fsql["<b>frankensqlite</b><br/>published fsqlite dependency<br/>durable trust, fleet and evidence stores"]

    fn --> ftui
    fn --> fapi
    fn --> feng
    fn --> fsql
    fapi --> asu

    classDef product fill:#1f6feb,color:#fff,stroke:#0a3b91,stroke-width:1px;
    classDef plane fill:#5b3cc4,color:#fff,stroke:#3a2780,stroke-width:1px;
    classDef substrate fill:#27c93f,color:#0b1020,stroke:#1d8e2c,stroke-width:1px;
    classDef store fill:#f0c674,color:#0b1020,stroke:#a8884c,stroke-width:1px;
    class fn product;
    class asu,ftui,fapi plane;
    class feng substrate;
    class fsql store;

Repository layout

Path Purpose
Cargo.toml Workspace manifest (crates/franken-node, crates/franken-security-macros, sdk/verifier)
crates/franken-node/src/main.rs Binary entrypoint and CLI dispatch
crates/franken-node/src/lib.rs Library exports, bounded I/O helpers, lock utilities
crates/franken-node/src/cli.rs Stable CLI argument shapes (the contract downstream tests assert)
crates/franken-node/src/config.rs Config discovery, profile resolution, CLI override merging
crates/franken-security-macros/ Internal proc-macro helpers
sdk/verifier/src/lib.rs Public verifier-facing SDK (frankenengine-verifier-sdk)
docs/specs/** Product contracts (gates, schemas, runbooks)
tests/**, crates/franken-node/tests/** Integration, conformance, contract, e2e, golden, security, perf tests
scripts/check_*.py 430+ Python validators wired into CI gates
.github/workflows/ Gate-oriented CI: claim gates, conformance gates, closer-discipline, mutants, coverage, security-golden-artifacts, etc.
packaging/profiles.toml Packaging profile metadata (local, dev, enterprise)

Core source domains

Domain What lives there
cli, config, main CLI surface, profile resolution, structured logging
runtime/ Epoch transitions, lane scheduler/router, safe-mode controller, bulkhead, resource governor, lockstep harness, hardware planner, speculation
control_plane/ Control epoch barriers, fork detection, audience tokens, MMR proofs, marker stream, divergence gate, evidence replay gate, key-role separation
security/ Constant-time comparisons, revocation freshness, threshold signatures, remote capabilities, sandbox policy compiler, SSRF policy, sybil defense, zk attestation, challenge flow, DGIS, BPET
supply_chain/ Trust cards, certification, camouflage assessment, attestation manifest
migration/ Migration audit, rewrite, validate, BPET migration gate, deterministic audit logs
replay/ Time-travel engine, replay verdicts, divergence detection
tools/ Replay bundle builder with fsync-backed durability
vef/ Verifiable Execution Fingerprint: proof service, generator, scheduler, verifier, receipt chain
dgis/ Dependency Graph Immune System: contagion simulator, fragility model, SPOF detection
atc/ Autonomous Trust Control surface contract and fingerprinting
federation/ Privacy-preserving cross-deployment threat learning, ATC↔BPET↔DGIS bridges
observability/ Evidence ledger, durability violation detection, witness ref tracking
ops/ Validation broker / planner / proof cache / debt ledger, doctor, evidence index, cleanup executor
extensions/, registry/ Signed extension publication, schema enforcement, GC, search
connector/ Adapter layer (CRDT, lease coordinator, durability, ecosystem compliance, rollout state, verifier SDK glue)
remote/, api/ Remote capability lifecycle, fleet quarantine API surface, session auth
storage/ frankensqlite adapter, retrievability gate, cleanup receipts
verifier_economy/, claims/, encoding/, repair/ Reputation/staking, claim compiler, canonical serialization, recovery orchestration
sdk/verifier/ External verifier SDK (bundle, capsule, counterfactual, top-level lib.rs)

Trust-State Lifecycle

A trust card moves through a small set of operationally meaningful phases. These labels are descriptive, not enum variants in the source. What the source models concretely is the card's user_facing_risk_assessment, revocation_status, audit_history, camouflage_hints, and reputation_trend; the diagram below shows how those fields evolve over the lifecycle of a single card. The registry separately validates the card snapshot under a SnapshotSourceContext (TrustedFile for local loads, UntrustedNetwork for remote refreshes), which picks the validation strategy without being stored on the card itself.

stateDiagram-v2
    [*] --> Seeded: franken-node trust scan
    Seeded --> Active: trust sync (refresh from OSV)
    Active --> Consumed: revocation gate passes,<br/>action executes
    Active --> Refused: revocation frontier stale,<br/>signed refusal receipt
    Consumed --> RiskBumped: CamouflageHint severity<br/>crosses threshold
    Active --> RiskBumped: same
    RiskBumped --> RevokedQuarantined: trust quarantine /<br/>trust revoke
    Active --> RevokedQuarantined: same
    Consumed --> RevokedQuarantined: same
    RevokedQuarantined --> Active: fleet release<br/>(after remediation)
    Refused --> Active: trust sync --force

Every transition is recorded in the card's audit_history and (for fleet-visible transitions) in the evidence ledger. A card's full history is replayable: an auditor can reconstruct exactly which decisions ran under which snapshot.


Trust-Native Primitives

Every primitive below is implemented in crates/franken-node/src/ and has integration or conformance tests under tests/ and crates/franken-node/tests/. Being implemented is not the same as being used: the last column says whether a command of the default binary (default features) actually calls it, from a static trace of the command handlers on 2026-09-25 (bd-reality-20260923-26n9r.14). "Library only" means compiled but reached by no command; feature: means compiled only with that opt-in feature.

Primitive Module Properties Reached by the default CLI
Trust cards supply_chain::trust_card Publisher identity, risk assessment, audit history, version tracking, camouflage hint, HMAC-signed snapshots, trusted-vs-untrusted source context Yes: trust, trust-card, run preflight. Camouflage marking is library only
Revocation freshness gates security::revocation_freshness{_gate} SafetyTier-based (Standard/Risky/Dangerous) age policies; now >= expires_at fail-closed semantics; bound to capability issuance revocation_freshness: yes (run preflight, remotecap issue). revocation_freshness_gate: library only
Fleet quarantine state machine api::fleet_quarantine, control_plane::fleet_transport, control_plane::fleet_http_server, control_plane::fleet_transport_http QuarantineScope, FleetAction (quarantine/revoke/release), signed DecisionReceipt, ConvergencePhase tracking, durable transport, live HTTP coordinator Partly: fleet serve/agent/status/release/reconcile and trust quarantine run over the durable store or a live coordinator with signed decision receipts; FleetControlManager and the release-quorum logic are library only
Ecosystem reputation graph supply_chain::trust_graph Publisher / extension / lockfile-dependency graph; rule-based trust propagation with per-node explanations; card-version transitions with audit causes; blast radius Yes: trust graph
Deterministic incident replay replay::time_travel_engine, tools::replay_bundle WorkflowTrace capture, environment snapshot, schema versioning, ReplayVerdict, divergence detection, fsync-backed durable serialization tools::replay_bundle: yes (incident bundle/replay, run incident capture). time_travel_engine: library only
Counterfactual simulator tools::counterfactual_replay, replay::time_travel_engine + sdk/verifier Re-decide a recorded incident under an alternative --policy; emit diff of decisions, blocked actions, and evidence incident counterfactual: --model production re-decides run-captured host effects with the run path's per-profile capability table; --model synthetic scores recorded evidence. time_travel_engine is library only
Compatibility lockstep oracle runtime::lockstep_harness, api::compat_gate verify lockstep defaults to Bun+franken dyad; corpus L1 treats Node as spec (matching only Bun stays fail; child_process aborts remain fail) lockstep_harness: yes (verify lockstep). compat_gate: feature:control-plane
Migration autopilot migration::*, BPET migration gate Audit → rewrite → validate → rollout; unsigned JSON MigrationRollbackPlan (not Ed25519-signed); migrate validate is static+smoke, not verify lockstep migration::*: yes (migrate). BPET migration gate: feature:admin-tools
Signed extension registry supply_chain::extension_registry; registry::*, extensions::artifact_contract Ed25519-signed artifacts, schema enforcement, assurance levels, GC, search Yes: the registry commands use supply_chain::extension_registry. registry::*: feature:admin-tools; artifact_contract: feature:advanced-features
Threshold signature verification security::threshold_sig k-of-n quorum, cached configurations, domain-separated and constant-time verification Library only
MMR proofs control_plane::mmr_proofs Merkle-Mountain-Range inclusion/prefix proofs, raw-hash internals, rebuild/sync from marker streams Library only
Audience tokens control_plane::audience_token Expiry, attenuation, domain separation, token chains with depth/root/leaf accessors, replay-resistant nonce window Library only
Fork detection control_plane::fork_detection State-vector hashing, rollback proofs, marker-proof verifier, DetectionResult (Converged/Forked/GapDetected/RollbackDetected) Library only
Control epoch barriers control_plane::control_epoch, epoch_transition_barrier Validity-window policy, EpochRejectionReason enum, fail-closed artifact rejection Library only (runtime epoch compares integers you pass it)
Evidence ledger observability::evidence_ledger Append-only Ed25519-signed decision log, hash-chain prev-entry linkage, replay-attack detection, bounded capacity with eviction, optional spill-to-disk Yes: run (including preflight refusals), trust revoke, trust quarantine, trust release and fleet release append signed, hash-chained entries to .franken-node/state/evidence-ledger.db (fleet decisions go beside the fleet state directory); verify transparency-log checks it
Remote capability tokens security::remote_cap, remote::* Scope-bound, single-use-optional Ed25519 tokens with endpoint binding Yes: remotecap, trust scan --deep/--audit, trust sync, init
DGIS adversarial topology security::dgis, dgis::* Dependency contagion simulator, fragility model, SPOF detection, immunization planner Partly: the fragility model scores npm maintainer data in trust scan --deep; the contagion simulator, SPOF detection and immunization planner are library only
BPET evolution risk scorer security::bpet, migration::bpet_migration_gate Phenotype feature extraction, topology risk delta during rollout Library only (bpet_migration_gate: feature:admin-tools)
ATC adversarial trajectory checker security::trajectory_gaming + federation::atc_* Camouflage detection severity, ATC participation weighting, reciprocity tracking Partly: trust observe authenticates collector-attributed package measurements and persists BPET camouflage findings on trust cards; federation weighting/reciprocity remains library only (feature:advanced-features)
VEF execution receipts vef::* Proof service / generator / scheduler / verifier, linked receipt chain, fail-closed verification Partly: debug evidence verifies vef::evidence_capsule; the proof service, scheduler, verifier and receipt chain are library only
Verifier SDK sdk/verifier (frankenengine-verifier-sdk) Independent bundle replay, capsule verification, counterfactual reasoning outside the producing runtime Yes: ltv attest/verify-as-of call it; incident bundles the CLI writes verify with its verify_incident_bundle; it also builds standalone

The Trust Gradient

Trust in franken-node is graduated rather than binary. The diagram is the full model for a signed extension; not every command passes every gate. run checks the dependency's trust card, the revocation frontier and the runtime profile. Registry admission, provenance and capability scope apply to the registry and remotecap commands, not to npm dependencies at run. The gates layer:

flowchart TD
    A["Publisher identity<br/>Ed25519 signing key"] --> B["Signed artifact in registry<br/><code>--signing-key</code>, <code>--version</code>"]
    B --> C["Provenance + assurance check<br/><code>require_provenance</code>, <code>minimum_assurance_level</code>"]
    C --> D["Trust card snapshot<br/>publisher, version, audit history, camouflage"]
    D --> E["Revocation freshness gate<br/><code>SafetyTier</code> vs. frontier age"]
    E --> F["Capability scope<br/>audience token + endpoint binding"]
    F --> G["Runtime policy profile<br/>strict | balanced | legacy-risky"]
    G --> H["Decision receipt<br/>run outcomes appended to the evidence ledger"]

    classDef identity fill:#1f6feb,color:#fff,stroke:#0a3b91;
    classDef storage fill:#5b3cc4,color:#fff,stroke:#3a2780;
    classDef policy fill:#f0c674,color:#0b1020,stroke:#a8884c;
    classDef audit fill:#27c93f,color:#0b1020,stroke:#1d8e2c;
    class A,B identity;
    class C,D storage;
    class E,F,G policy;
    class H audit;

Each layer can fail closed independently. The gate-by-gate decomposition is what enables the counterfactual simulator to answer "would tightening one gate have prevented this?" rather than just "was this incident preventable?".

The same model in words:

  1. Identity. Every publish path requires an Ed25519 signing key and an explicit --version. There is no anonymous publish path.
  2. Storage integrity. The registry recomputes the artifact hash on read and verifies the signature in constant time.
  3. Provenance. registry.require_provenance = true rejects artifacts that did not arrive with attestation metadata.
  4. Reputation. Trust cards aggregate what scans and syncs observe (vulnerabilities, typosquats, package age, maintainer fragility). Camouflage assessment can raise a card's risk, but no command feeds it trajectory data yet.
  5. Currency. Revocation freshness gates refuse risky and dangerous actions when the local frontier is older than policy allows.
  6. Audience. Capability tokens are bound to specific endpoints and operations; a token issued for one endpoint cannot be used at another.
  7. Profile. The operator-selected runtime profile sets thresholds for every gate above, all at once.
  8. Audit. Decisions leave receipts: every run writes a SHA-256 execution receipt, trust revoke/trust quarantine/incident bundle export signed decision receipts with --receipt-out, fleet release/reconcile sign their decision and convergence receipts, and sentinel escalations are signed. Each run also appends a signed, hash-chained entry (receipt id and hash, exit code, containment verdict, host-effect chain head) to the durable evidence ledger, as do preflight refusals, trust revoke/quarantine/release and fleet release; verify transparency-log .franken-node/state/evidence-ledger.db --public-key .franken-node/keys/receipt-signing.pub checks the chain.

Remote Capability Operations

A capability token is bound to a specific remote operation plus an endpoint. The operation vocabulary is enumerated in security::remote_cap::RemoteOperation:

Operation Canonical name Used for
NetworkEgress network_egress Generic outbound network calls
FederationSync federation_sync Pulling federated state from a peer deployment
RevocationFetch revocation_fetch Pulling fresh revocation frontiers
RemoteAttestationVerify remote_attestation_verify Verifying an attestation served by a remote service
TelemetryExport telemetry_export Shipping telemetry to an external sink
RemoteComputation remote_computation Off-host compute (e.g., remote proof generation)
ArtifactUpload artifact_upload Publishing artifacts to a remote registry

The capability gate also tracks a ConnectivityMode:

  • Connected: outbound network is permitted under policy.
  • LocalOnly: every remote operation fails closed regardless of the capability token; useful in safe mode or for offline replays.

RemoteScope aggregates the set of allowed operations on a token. Single-use tokens (remotecap issue --single-use) burn their scope after one accepted use; reuse fails closed. Token use also re-validates the resolved IP set so a token issued for one endpoint cannot be silently redirected by DNS changes (see Network Egress and SSRF Policy).


Capability Artifact Format

Extensions that want to declare exactly what capabilities they expect ship a capability artifact alongside their package. The format is governed by extensions::artifact_contract with SCHEMA_VERSION = "capability-artifact-v1.0". The shape:

Type Purpose
CapabilityEntry One requested capability with bounded inputs (ARTIFACT_TOKEN_INPUT_POLICY)
CapabilityContract The full list of CapabilityEntry items the extension declares; subject to ARTIFACT_CAPABILITY_LIST_POLICY length bounds
ExtensionArtifact The wire-format artifact: contract + signature + provenance metadata
AdmissionDenialReason Typed refusal reason emitted when the registry refuses admission

The module exports stable event_codes, error_codes, and invariants sub-modules so a SIEM filter or auditor can pin on the codes without parsing message text. Admission is fail-closed: every refusal produces a typed AdmissionDenialReason and (where appropriate) a signed decision receipt.

franken-node registry publish accepts the artifact alongside the package; franken-node registry verify <id> recomputes the contract hash and validates the signature in constant time.


How the Primitives Work

Trust cards

A trust card is a per-extension record carrying publisher identity, audit history, version chain, vulnerability state, camouflage assessment, and revocation status. Cards are HMAC-signed snapshots; the registry loads them under a SnapshotSourceContext of either TrustedFile (operator-managed local loads, lazy validation) or UntrustedNetwork (remote refreshes during trust sync, eager pre-parse signature check and a hard JSON size cap). The card itself carries card_hash and registry_signature so its integrity is verifiable regardless of which context loaded it.

Camouflage assessment is produced by security::bpet::camouflage_detector and surfaced through security::trajectory_gaming::CamouflageHint, whose kind is one of PhaseShift, Dropout, DistributionMismatch, or GradualCreep. Each hint carries a numeric severity and an evidence map. supply_chain::trust_card::apply_camouflage_assessment raises the card's risk class when severity crosses the configured threshold. Hints accumulate across snapshots so future evaluations see the trajectory, not just the latest sample.

Revocation freshness gates

security::revocation_freshness defines a SafetyTier enum (Standard, Risky, Dangerous) and a per-tier maximum age policy. Before a risky or dangerous action runs, the runtime asks: is the local revocation frontier fresher than policy.max_age_for(tier)?

  • The revocation frontier is a value recorded in the durable trust registry: the time of the last trust sync --force in which every trust signal was fetched from the network without error. It is never inferred from file timestamps, which the database rewrites even on read-only access. A missing frontier counts as stale.
  • run applies the gate when it admits a dependency on the strength of its trust card, both at preflight and again at execution time (TOCTOU recheck). strict maps to Dangerous (5 min) and refuses a stale or missing frontier (revocation_stale). balanced maps to Risky (1 h) and admits with a preflight warning instead. legacy-risky is Standard, with no age floor.
  • Declared dependencies with no trust registry at all block run under strict/balanced (registry_missing); legacy-risky warns.
  • A dependency whose trust card is High or Critical risk (known advisories from trust sync/trust scan --audit, or a suspected typosquat) blocks run under strict (high_risk). balanced admits it with a preflight warning.
  • remotecap issue is always Dangerous: it needs a frontier recorded within the last 5 minutes in the working directory's registry. The local revoked-token list (remotecap revoke) is authoritative and is not treated as a freshness signal.
  • If the gate fails the action does not execute; the operator gets an explicit revocation freshness gate denied … error and a trust sync --force recommendation.

Threshold signatures (k-of-n)

security::threshold_sig verifies signatures where any k of n named signers must produce a valid Ed25519 signature over the canonical message form. The verification path:

  1. Canonicalize the input with a domain separator (b"threshold_sig_verify_v2:").
  2. Length-prefix each variable-length field individually.
  3. Decode each presented signature and run the Ed25519 verify in constant time against the corresponding cached public key.
  4. Tally successes; require at least k. Tally is bounded by n and uses saturating_add to defeat overflow-based bypass.

The verification path is the verify_threshold / verify_threshold_cached public API in security::threshold_sig; the cached form pre-decodes the signer set once and re-uses it across many verifications of the same quorum.

MMR proofs (Merkle Mountain Range)

control_plane::mmr_proofs maintains an append-only log shaped as an MMR. Each leaf is the canonical hash of an evidence entry; each peak is a binary tree over the leaves under it. Two proofs are supported:

  • Inclusion: Given a leaf and the current peaks, prove the leaf is at position i. Verifier reconstructs the peak hashes from sibling hashes along the path and compares against the published root.
  • Prefix: Given a prior root at size s1 and the current root at size s2 >= s1, prove the log only ever grew (the prefix at s1 is unchanged). Verifier reconstructs old peaks from new ones and bagged intermediate hashes.

Hash inputs are domain-separated (b"mmr_proofs_leaf_v1:", b"mmr_proofs_node_v1:", b"mmr_proofs_v1:") and length-prefixed. Internal storage uses raw hashes to avoid per-step allocation in proof generation.

Audience tokens

control_plane::audience_token::AudienceBoundToken binds a credential to one or more audiences (the intended recipient service identifiers). The canonical fields:

  • token_id, issuer: bounded-length identifiers.
  • audience: Vec<String>: every recipient that may legitimately accept this token. A relying party whose identity is not in the list rejects it.
  • capabilities: BTreeSet<ActionScope>: granted action scopes, strictly attenuated on each delegation.
  • issued_at, expires_at: UTC millis; fail-closed when now >= expires_at.
  • nonce: replay-detection nonce. The runtime consults a bounded recently-seen window so replays are caught even if the issuer reuses an ID across epochs.
  • parent_token_hash: Option<String>: the hash of the parent token (None for root tokens). A sub-token inherits and may only attenuate.
  • max_delegation_depth: u8: caps how many further delegations are allowed; 0 means the token cannot be further delegated.

The signature is verified in constant time against the issuer's public key. The canonical preimage begins with the domain separator b"audience_bound_token_signature_v1:", then length-prefixes every variable-length field (token_id, issuer, each audience entry, each capability label, nonce, parent hash) and little-endian-encodes the numeric fields.

Fork detection

control_plane::fork_detection maintains a state vector (a hash commitment over each replica's observable state at a given epoch) plus a parent-hash chain that pins the rollback boundary. On each tick, the runtime compares its state vector against peers received via the control plane and emits one of four DetectionResult outcomes:

  • Converged: both replicas share identical state at the same epoch.
  • Forked: same epoch but different state hashes. The runtime emits a divergence receipt for replay.
  • GapDetected: epoch difference exceeds one. Reconciliation is required before further decisions can be issued.
  • RollbackDetected: the parent-hash chain is broken, indicating an unauthorized rollback. The runtime refuses to accept the offending state and the operator must investigate.

Each detection result carries a stable label (CONVERGED, FORKED, GAP_DETECTED, ROLLBACK_DETECTED) suitable for logs and metrics. Only Converged is treated as safe; the other three trigger evidence emission and gate subsequent decisions.

Control epoch barriers

control_plane::control_epoch defines a monotonic epoch counter that advances on every control-plane transition (policy change, fleet action, schema migration). Each artifact carries an epoch field. Before applying an artifact:

  1. The validity window check confirms the artifact's epoch is acceptable relative to the current epoch.
  2. Counter arithmetic uses saturating_add to defeat overflow-based bypass.
  3. Rejected artifacts emit a typed EpochRejectionReason, one of InvalidArtifactId, FutureEpoch, or ExpiredEpoch, and are recorded as EpochArtifactEvent entries so post-mortems can trace why the artifact was refused. Each variant maps to a stable label (EPOCH_REJECT_INVALID_ARTIFACT_ID, EPOCH_REJECT_FUTURE, EPOCH_REJECT_EXPIRED) for downstream consumers.

DGIS contagion simulator

dgis::contagion_simulator::simulate takes a ContagionGraph (defined in the sibling module dgis::contagion_graph with ContagionEdge and EdgeKind types) plus an initial_infected: &[NodeId] slice and a SimulatorConfig, and simulates adversarial spread against a configured ContagionProfile. Each tick advances an InfectionState tracking per-node exposure (exposure_of(node)), the set of currently infected nodes, and a saturating step counter. The simulator is deterministic: a fixed seed and profile yield an identical SimulationTrace, so a snapshot can be replayed bit-exactly.

Termination is classified by TerminationReason and reported alongside the trajectory. The simulator sits next to two complementary DGIS modules in the same crate: dgis::fragility_model (whose FragilityFactor, FragilityScore, MaintainerProfile, and PublisherProfile types classify per-node brittleness against maintainer / publisher signals) and dgis::spof_detection (which consumes the fragility model to pinpoint single points of failure). A separate security::dgis::update_copilot module produces TopologyRiskMetrics that the BPET subsystem can ingest via security::bpet::dgis_fusion; the BPET evolution risk scorer described below operates on those topology metrics rather than on the contagion simulator's InfectionState directly.

BPET evolution risk scorer

trust observe connects signed, collector-attributed package measurements to the BPET camouflage detector and durable trust cards. Four comparable samples enable trajectory analysis; observed camouflage can raise the package's risk. The command checks an independently pinned collector key, exact version and artifact bindings, measurement continuity, and observation replay protection. See Signed behavioral observations for the CLI, collector signing API, units, and storage limits. Native application effect ledgers still lack per-dependency attribution, so this surface consumes isolated-package collector observations.

security::bpet::evolution_risk_scorer::compute_risk_score takes a FeatureVector of four normalized features (each in [0.0, 1.0]):

  • drift — distributional drift of the observed behavior
  • regime_shift — probability that behavior has crossed a regime boundary
  • hazard — current hazard rate from the extension's reliability baseline
  • provenance — confidence in the publisher / supply-chain provenance inputs

It applies a WeightingPolicy (also four-dimensional, validated to be finite, non-negative, and sum-to-one within SUM_TOLERANCE) and emits an ExplanationVector plus a ConfidenceInterval. Companion modules extend the surface: phenotype_extractor (lifts evidence into ExtractedFeature::{Known, Partial, Unknown} annotations), dgis_fusion (folds security::dgis::update_copilot::TopologyRiskMetrics into the scorer's inputs), trust_surface_integration (mutates trust cards via trust_card_mutation_from_guidance and produces adversary-posterior updates), and federation::bpet_atc_bridge (carries the scorer output into the federation layer).

migration::bpet_migration_gate is an independent admission gate that operates on TrajectorySnapshot/TrajectoryDelta inputs via evaluate_admission and evaluate_rollout_health; a sufficient delta versus the prior version raises the migration's required assurance level before the gate will admit it.

VEF execution receipts

vef::* produces Verifiable Execution Fingerprints: a hash-linked chain of signed receipts over execution segments. The chain is built from ReceiptChainEntry items, each containing:

  • index: monotonic position in the chain.
  • prev_chain_hash: the prior entry's chain hash, so any tampering breaks every downstream entry.
  • receipt_hash: canonical hash over the entry's ExecutionReceipt (which carries the segment's inputs, outputs, and worker identity).
  • chain_hash: H(prev_chain_hash || receipt_hash) for this entry.
  • appended_at_millis, trace_id: for correlation.

ReceiptCheckpoint entries periodically commit the chain head plus an inclusive index range so partial verification is cheap. Verification (vef::receipt_chain::verify_integrity, verify_entries_and_checkpoints) recomputes each chain_hash and matches it against the recorded value; vef::proof_verifier then checks the per-receipt signatures. sdk/verifier consumes these so an external party can validate a sequence of segments without trusting the producer.

Time-travel replay engine

replay::time_travel_engine is a library API that no CLI command calls yet. incident replay re-derives recorded bundle integrity by default; its --execute path uses ops::native_replay and the native execution orchestrator. The separate library engine captures a WorkflowTrace: the sequence of steps, their inputs and outputs, the environment snapshot at the start of the workflow, schema version metadata, and the side-effect declarations required to reproduce them. Its replay re-executes the trace against the same schema version and produces a ReplayResult whose verdict is one of:

  • ReplayVerdict::Identical: every step's output and side-effects matched the captured values.
  • ReplayVerdict::Diverged(count): count steps diverged; details appear as Divergence entries indexed in the result.

Each Divergence carries a typed DivergenceKind:

  • OutputMismatch: output bytes differ.
  • SideEffectMismatch: declared/observed side-effects differ.
  • FullMismatch: both output and side-effects differ.
  • ClockDrift { expected_ns, actual_ns, drift_ns, tolerance_ns }: timestamp drift exceeded the tolerance window.

Bundle export is fsync-backed and uses an atomic-rename TempFileGuard, so crash recovery leaves either the prior bundle or the new one; never a torn write.


Incident Bundle Anatomy

A .fnbundle produced by franken-node incident bundle is a single canonical JSON artifact (keys sorted, no floats) built from an incident evidence package (.franken-node/state/incidents/<id>/evidence.v1.json).

Captured source packages retain their evidence fields at the top level and add source_signature. This Ed25519 seal covers the complete canonical source: events, metadata, detector, severity, trace, evidence references, and run receipt and host-effect chain bindings. Its signature domain is separate from bundle signatures. The configured receipt-signing authority authenticates the source before export; --receipt-signing-key selects the exported bundle signer and does not replace that source authority. Missing or invalid source signatures, duplicate JSON keys, and unknown source-schema fields are rejected.

Recorded native runs also bind a complete receipt_snapshot to their run metadata and engine ledger under a product record signature. Capture verifies this record and the engine's session identity, signature, and effect chain. Editing both advertised receipt hashes or substituting receipt metadata cannot satisfy that binding. Existing unsigned run records must be recreated by a fresh run before they can supply authenticated runtime capture.

Native execution failures with a recovered dispatch report pass through the same SSRF quarantine, Sentinel enforcement, receipt persistence, and automatic incident capture as successful executions. Throwing after a denied effect cannot skip those controls. The receipt commits the original execution_failure before its identity and hash are derived, and the durable run decision records the failed attempt as Deny. A recovered ledger retains its independently verified engine identity and can be captured and bundled later; an unavailable finalized ledger remains absent.

For these failures, run --json preserves the franken-node/run-failure-effect-evidence/v2 envelope and its error, captured_output, and host_effect_ledger, adding receipt, receipt_path, and dispatch. The command still exits nonzero. If receipt persistence itself fails, the envelope retains the original failure and console and reports persistence_error, without inventing a persisted receipt. --console-only continues to emit the guest streams without receipt or incident notices.

Native timeouts also retain an execution receipt. Its interruption_evidence binds the parent-observed write-ahead journal before receipt identity and hashing. The signed durable Deny decision includes that journal, the complete run_receipt_snapshot, and the authenticated session identity context, so the observation remains inspectable if the separate receipt file becomes unavailable. The decision signs the parent's observation; session identity alone does not authenticate a finalized engine outcome. An unmatched admission remains interrupted_indeterminate, a provider-return marker remains provider_returned, and replay_certified stays false, including when worker cleanup is unproven.

Timeout JSON retains the franken-node/native-effect-interruption-evidence/v1 fields and adds error, receipt, receipt_path, and dispatch. guest_output_available: false makes clear that the interrupted worker did not supply its final console response. Receipt-storage failure retains the journal and original timeout error with a separate persistence_error. Interrupted runs remain nonzero failures, produce no fabricated finalized host-effect ledger or automatic replay capture, and remain visible as source errors in incident coverage. incident capture --from-run explicitly refuses to certify them.

An explicit --evidence-path can still import operator-authored evidence; the export reports source_authenticated: false. Captured-store files and inputs that contain source signatures or runtime-run claims still require source authentication, including when an explicit path is supplied. Listing a corrupted captured source reports it as invalid, and repeated capture authenticates an existing source before returning already_captured.

ops incident-coverage measures retained durable run decisions and discoverable run records, derives incident eligibility from authenticated security outcomes, and matches captured sources to their signed bundles. Missing files associated with retained decisions remain visible. Clean successful Node/Bun runs are excluded from native-ledger checks only by their authenticated durable decisions, never by an editable receipt runtime label; JSON counts those exclusions separately. A requested coverage floor fails when the measured population is empty, unreadable, or unauthenticated. Its reported population_scope does not establish historical completeness after deletion or rollback of the local inventory and trust material.

Durable decisions use chain-v1:<hex> Ed25519 signatures. A distinct signing domain binds the entry contents and its exact predecessor before the append transaction commits. Deleting a middle entry and editing the next link therefore invalidates the signature, even when the remaining hash links match. The in-memory ledger refuses to fill in or normalize a predecessor covered by this signature. Standalone content signatures retain their existing contract.

verify transparency-log --public-key requires an empty predecessor at genesis and authenticated links throughout the retained chain. JSON reports chain_links_authenticated, unbound_signature_entries, and head_hash. An older log with valid standalone signatures has signatures_verified: true but status: "unproven" and exits nonzero; coverage refuses to treat that log as an authenticated inventory. Existing rows are preserved and never silently re-signed into proof of a history that was not authenticated at append time.

A genuine older prefix still has valid signatures. To detect that rollback, retain an authenticated head_hash separately and supply it later with --expected-head-hash. A matching authenticated chain reports checkpoint_verified: true; a different or truncated head reports a checkpoint error and exits nonzero. The checkpoint establishes the expected retained head, not freshness beyond the operator's trusted observation. Without a checkpoint, valid establishes the retained chain only. The verifier rejects logs exceeding its 10,000-entry limit instead of silently checking a truncated window.

The exported bundle's fields (tools::replay_bundle::ReplayBundle):

Field Purpose
bundle_id, incident_id, created_at Stable identifiers; bundle_id and created_at are derived deterministically from the timeline.
policy_version The policy version recorded with the evidence.
timeline Ordered TimelineEvents: sequence_number, timestamp, event_type, payload, causal_parent.
initial_state_snapshot The captured state at the start of the window.
manifest Event count, time span, chunk count, compressed size, and decision_sequence_hash over timeline + initial state + policy version.
chunks Gzip-sized BundleChunk segments of the timeline.
evidence_refs, trust_artifact_refs Relative pointers to the evidence behind each event (omitted when empty).
integrity_hash SHA-256("replay_bundle_hash_v1:" ‖ canonical JSON of every field above).
signature Ed25519 over "replay_bundle_sig_v1:" ‖ u64_le(len) ‖ integrity_hash, with the signer's public key, key id, source and trust scope.

incident replay recomputes the integrity hash, verifies the signature against the operator-supplied trust anchor, and re-derives the decision-sequence hash from the recorded timeline. This default mode reports replay_kind: recorded_trace. With --execute, a bundle carrying authenticated native replay inputs also re-executes the captured guest and reports its separate execution_result; the supported comparison scopes are described in Re-execute captured native JavaScript.

Independent verification: frankenengine_verifier_sdk::incident_bundle::verify_incident_bundle re-derives the integrity hash and the decision-sequence hash from the raw JSON, rejects unknown (unsigned) fields, and verifies the Ed25519 signature strictly under a public key the verifier supplies. The key embedded in the bundle is accepted only if it equals that anchor. Conformance: sdk/verifier/tests/cli_incident_bundle.rs (a real CLI-produced bundle) and incident_bundle_integrity_conformance_sdk_independently_verifies_product_bundle.

Counterfactual mode (incident counterfactual --policy <p>) re-scores the recorded decision trace under a different policy's thresholds with the labelled synthetic executor. It does not execute the program again, and the bundle itself is unchanged.


Epoch Transition Barrier

The epoch transition barrier primitive (control_plane::epoch_transition_barrier) drains in-flight work when a policy change, key rotation, schema bump, or fleet release advances. The default CLI fleet path does not drive this as a live multi-node plane (live_control_plane=false).

The barrier moves through ordered BarrierPhase states; each participant acknowledges the phase with a DrainAck. The barrier carries:

  • BarrierId: opaque identifier for the in-flight barrier.
  • ParticipantId: per-node identity expected to drain and acknowledge.
  • SCHEMA_VERSION = "eb-v1.0": pinned schema for the barrier payload.
  • DEFAULT_BARRIER_TIMEOUT_MS and DEFAULT_DRAIN_TIMEOUT_MS: hard upper bounds on how long the barrier waits before aborting.
  • An event_codes module exporting stable codes for every phase transition.
  • An error_codes module exporting stable codes for every refusal or abort condition.

If the timeout fires before all participants acknowledge, the barrier emits an AbortReason and refuses to advance the epoch. The runtime falls back to the prior epoch and the operator gets a structured event naming the participant that failed to drain. The barrier fails closed: advancement only happens when every participant acknowledges in time.

This is the mechanism that makes "policy rollout" a transactional operation across the fleet rather than a best-effort broadcast.


Anti-Entropy Reconciliation

Replicas in a fleet zone may temporarily disagree on observable state (network partition, slow follower, missed marker). runtime::anti_entropy runs a deterministic reconciliation cycle that re-converges replicas without violating the linearizability of their decision logs.

Each cycle emits stable event codes from runtime::anti_entropy::event_codes so a SIEM filter can pin them without parsing message text:

Event Code Meaning
Cycle started FN-AE-001 A new reconciliation cycle began
Delta computed FN-AE-002 The deltas between local and peer state vectors were computed
Record accepted FN-AE-003 A peer record was accepted into the local state
Record rejected FN-AE-004 A peer record was refused (schema, signature, or epoch violation)
Cycle completed FN-AE-005 The cycle reached a stable terminating state
Fork detected FN-AE-006 Reconciliation surfaced a fork; emits a divergence receipt
Cycle cancelled FN-AE-007 The cycle was cancelled by an operator or supervisor
Replay idempotent FN-AE-008 A replayed cycle had no observable effect (idempotency proof)

Refusal conditions have matching stable error codes (ERR_AE_INVALID_CONFIG, ERR_AE_EPOCH_VIOLATION, ERR_AE_PROOF_INVALID, ERR_AE_FORK_DETECTED, ERR_AE_CANCELLED, ERR_AE_BATCH_EXCEEDED). The reconciler is benchmarked under anti_entropy_insert_bench so regressions surface in CI.


Key-Role Separation

control_plane::key_role_separation enforces that the same Ed25519 key cannot serve two distinct roles in the protocol. The KeyRole enum has four variants — Signing (authenticate control-plane messages and attestations), Encryption (protect confidential payloads in transit and at rest), Issuance (create delegation tokens and authority certificates), and Attestation (sign operator attestation payloads), each carrying a fixed 2-byte role tag. A KeyRoleBinding records which key fingerprint maps to which role; a KeyRoleRegistry rejects any operation that attempts to mix roles.

Operational consequences:

  • A key compromise in one role does not silently compromise another. Stealing an Encryption key cannot be used to issue Signing artifacts like decision receipts.
  • Role binding is enforced in constant time so a probe cannot leak whether a given key is "almost" the wrong role.
  • The registry emits a typed KeyRoleSeparationError on every refused binding and a KeyRoleEvent for every accepted one, so the audit trail is complete.

End-to-end coverage lives in tests/e2e_key_role_separation_registry.rs and tests/security/threshold_signature_verification.rs.


Decision Receipt Anatomy

A signed decision receipt is the smallest atomic unit of accountability in franken-node. Anything an operator does (release a quarantine, quarantine an artifact, revoke a publisher, promote a counterfactual, restart a proof worker) produces one. The unsigned payload is the Receipt struct (security::decision_receipt::Receipt); the on-the- wire artifact is SignedReceipt, which flattens Receipt and adds the signer / chain / signature triple.

Receipt fields:

Field Purpose
receipt_id Unique stable identifier for this receipt
action_name The action that was decided (e.g. fleet.release, trust.revoke)
actor_identity Identity that authorized the action
timestamp RFC 3339 issuance time; monotonicity validated against the prior receipt
signature_version Pinned signature scheme/version committed into the canonical payload
nonce Per-receipt nonce; defeats receipt-reuse attacks
audience Audience binding; defeats cross-context receipt abuse
input_hash Canonical hash over the decision's inputs
output_hash Canonical hash over the decision's outputs
decision Outcome enum: Approved, Denied, or Escalated
rationale Human-readable explanation for the outcome
evidence_refs List of opaque string pointers (path-like or registry-defined IDs) backing this decision
policy_rule_chain Ordered list of policy rule IDs that produced this outcome
confidence f64 confidence score (canonicalized so NaN/Inf never appear)
rollback_command The exact command an operator would run to reverse the decision
previous_receipt_hash Hash of the prior receipt in the chain (None for the chain root)

SignedReceipt adds:

Field Purpose
signer_key_id Stable identifier for the key that signed this receipt
chain_hash `H(previous_chain_hash
signature Ed25519 signature over the canonical preimage, verified in constant time

The canonical preimage is domain-separated and length-prefixed, so two receipts can never share a signature input by accident. Receipts are written atomically (TempFileGuard + fsync of the containing directory), so a crash leaves either the prior receipt or the new one; never a torn write. The hash chain (previous_receipt_hash + chain_hash) binds the receipt log into a tamper-evident sequence; an auditor only needs the chain head and the per-receipt signatures to verify the whole history.


Determinism and Replay Architecture

Deterministic replay only works if every input is captured and every non-deterministic source is intercepted. franken-node enforces this through four overlapping layers:

  1. Schema version registry. schema_versions.rs enumerates every serialized artifact (trust card snapshot, decision receipt, evidence entry, replay capsule, migration bundle, audience token, MMR proof, threshold-sig payload, fleet decision receipt) with an integer schema version. Schemas are immutable once shipped; migrations are explicit version bumps with upgrade and downgrade helpers.
  2. Canonical serialization. Every artifact has a single canonical byte form. Field order is fixed, integers are little-endian, variable-length fields are length-prefixed, and a domain separator prefixes the entire payload. Signatures are computed over the canonical bytes; verification recomputes them from the parsed structure.
  3. Captured environment. WorkflowTrace::environment_snapshot includes the policy profile, active feature flags, deterministic clock discipline policy, and the resolved configuration hash. Replay refuses to run if any of these has drifted (or, in counterfactual mode, the policy delta is recorded explicitly as the experimental change).
  4. Side-effect declarations. Each step declares the set of side-effects it intends to perform (file write, network call, registry query). The runtime records the actual side-effects observed. Replay compares declared vs. observed and refuses to run a step whose side-effect set has drifted.

The four layers compose: a replay bundle that includes a captured trace, an environment snapshot, schema versions, and side-effect declarations can be replayed years later, on a different machine, with bit-exact results, provided the schema versions are still registered.


Threat Model

What franken-node defends against, mapped to the primitive that enforces the defense:

Threat class Example Defense
Supply-chain forgery Adversary publishes a same-named artifact with a tampered binary Registry requires Ed25519 signature; registry verify recomputes the artifact hash and verifies the signature in constant time
Dependency poisoning Compromised transitive npm dep ships a malicious payload OSV refresh on trust sync; revocation freshness gates fail closed for risky actions when the frontier is stale
Typosquatting Adversary publishes lodahs, @evil/express trust scan compares each dependency against a pinned list of popular npm names (one edit away, scope or separator confusion, look-alike characters) and raises a match to High risk with the reason on the card. run under strict refuses High/Critical dependencies (high_risk); balanced admits them with a preflight warning; legacy-risky ignores risk. The seed list is small and curated, so a typosquat of an unlisted package is not caught.
Revocation race Adversary issues a sensitive action between revocation publication and local cache refresh risky_requires_fresh_revocation = true + dangerous_requires_fresh_revocation = true block actions whose tier requires fresher state
State divergence / fork attack Adversary feeds two nodes inconsistent control-plane state Fork detection state vectors; rollback proofs; RollbackDetected and Forked outcomes gate subsequent decisions
Replay attack on tokens Adversary re-uses a captured audience token Bounded nonce window + now >= expires_at fail-closed + constant-time signature verification
Timing side-channel Adversary extracts a signing key by measuring verification time security::constant_time::ct_eq{,_bytes} (backed by subtle) on every signature, hash, MAC, content-hash, trace-id, action-id check
Hash-collision / canonicalization mismatch Adversary crafts two inputs that pipe-collide under a delimiter-based hash Domain separators (b"<module>_<function>_vN:") + length-prefixed variable-length fields throughout
Counter overflow Adversary forces a u64 counter to wrap and bypass a check saturating_add / saturating_sub on every counter, sequence, epoch, and timestamp
Parser-bomb DoS Adversary submits an artifact that explodes on parse bounded_read{,_to_string} enforce per-file size caps before parse; capacity-critical Vec paths use push_bounded
Auth-failure memory DoS Adversary triggers per-source-IP tracking growth AuthFailureLimiter caps source-IP cardinality
Torn writes on crash Power loss between rename and fsync corrupts durable state TempFileGuard atomic-rename + directory fsync on every durable write
Trajectory gaming Adversary slowly drifts a benign extension into a malicious one Camouflage assessment scores trajectory deltas; ATC reciprocity catches asymmetric contribution; BPET evolution risk gates rollouts
Schema downgrade Adversary submits an old-schema artifact to bypass a new check Schema version pinning in schema_versions.rs; epoch barriers reject artifacts outside the validity window
Verifier collusion Producer signs its own claims and asks you to trust them frankenengine-verifier-sdk runs outside the producing runtime; receipts are linked, signed, and independently verifiable
Memory unsafety Use-after-free, buffer overflow #![forbid(unsafe_code)] lint forbidance + Rust borrow checker
CAS poisoning / EffectReceipt splice Adversary rewrites content-addressed bytes, back-dates an effect, or splices a forged receipt into a host-effect chain CAS read-time hash verification plus domain-separated, length-prefixed EffectReceipt hashes and chain-link recomputation; regression anchors: crates/franken-node/tests/cas_effect_receipt_conformance.rs and crates/franken-node/src/runtime/effect_receipt.rs
Declassification-authority abuse Adversary forges, over-scopes, expires, or replays a declassification receipt at the wrong sink or epoch Declassification receipts bind schema, sink policy, forbidden label set, actor, purpose, epoch, revocation freshness, and canonical receipt id; regression anchors: crates/franken-node/src/security/lineage_tracker.rs and tests/security/exfiltration_sentinel_scenarios.rs
MCP mutation abuse Agent or sub-agent escalates scope, delegates beyond the parent token, omits rollback evidence, or replays a stale mutation session Mutating MCP tools require audience-bound capability chains, attenuated delegation, rollback commands, signed action receipts, and session digest verification; regression anchors: crates/franken-node/src/api/mcp.rs and crates/franken-node/tests/mcp_control_surface_contract.rs
LTV witness collusion / crypto-suite downgrade Witnesses collude to re-attest stale evidence or submit an artifact under a weaker crypto_suite discriminator TNR release gates must require suite-discriminator binding, witness independence checks, and fail-closed re-attestation evidence before LTV claims ship; current related regression anchors: crates/franken-node/tests/threshold_sig_quorum_metamorphic.rs and docs/specs/crypto_trait_abstraction.md
Conformal calibration and Sentinel signal poisoning Adversary contaminates the Phase-0 corpus or correlated Sentinel signals to widen or narrow BPET coverage Signed calibration artifacts, canonical corpus records, verifier recomputation, poisoning/Sybil policy thresholds, and Sentinel quarantine interplay; regression anchors: tests/security/bpet_calibration_benchmark.rs, docs/policy/signal_poisoning_sybil_defense.md, and docs/policy/risk_signal_poisoning_sybil.md
Fleet-log equivocation / quorum replay Partitioned validators race catch-up, replay quorum certificates, or double-count duplicate attestations Canonical action hashing, distinct-validator quorum accounting, equivocation fault receipts, decision-receipt tamper rejection, and threshold-signature replay tests; regression anchors: crates/franken-node/src/api/fleet_quarantine.rs, tests/security/threshold_signature_verification.rs, and crates/franken-node/tests/threshold_sig_quorum_metamorphic.rs

Out of scope: explicit non-goals so operators know what to layer on top:

  • Network-level DoS protection. franken-node has internal bulkheads and bounded resource governance, but it is not a load balancer or WAF.
  • Filesystem-level mandatory access control. SELinux, AppArmor, or equivalent should remain in place.
  • Hardware-attested execution. SGX/TDX integration is a separate layer; the verifier SDK is designed to compose with hardware attestation but does not require it.
  • Source-code provenance for the application itself. franken-node validates extension provenance; application source provenance is the caller's responsibility.

Schema Versioning Policy

schema_versions.rs is the single authoritative registry of every serialized artifact's schema version and the upgrade/downgrade helpers between versions. The policy:

  • Immutability after release. Once a schema version ships, its canonical byte form is frozen. Bug fixes that change byte output are schema bumps, not patches.
  • Explicit migrations. Every bump requires registering an upgrade helper from vN → vN+1 and (where feasible) a downgrade helper from vN+1 → vN. Verifiers that pin an older version can still consume newer artifacts when the downgrade path is non-lossy; when it is lossy, the verifier explicitly refuses.
  • Refusal on unknown. A bundle or capsule whose schema version is not registered fails verification. There is no "best effort" deserialization.
  • Golden coverage. Every shipped version has a golden test in tests/golden/ whose bytes lock the canonical encoding. Changing the bytes without a version bump fails CI.
  • Cross-artifact consistency. When two artifacts must agree (e.g. a decision receipt references a trust card snapshot), both must use schema versions that the runtime can reconcile. The validity window check on epochs enforces this for control-plane artifacts.

Pinned versions, immutability, golden bytes, and refusal on unknown together make replay deterministic across time and across machines.


Cryptographic Primitives

Purpose Primitive Source crate
Asymmetric signing / verification Ed25519 ed25519-dalek
Constant-time comparison subtle::ConstantTimeEq subtle (via security::constant_time)
General hashing SHA-256 sha2
Optional fast hashing BLAKE3 blake3 (feature-gated)
Keyed hashing / snapshots HMAC-SHA-256 hmac
Key derivation HKDF-SHA-256 hkdf
Random nonces / IDs OS RNG rand (cryptographic backend)
Secret zeroization Zeroize derive zeroize
Canonical CBOR (optional) ciborium feature-gated
Replay-bundle compression Gzip via DEFLATE flate2 (feature-gated)

Every signature and every keyed-hash input begins with a domain separator: a literal byte string of the form b"<module>_<operation>_v<version>:", so a valid signature for one protocol context can never be reused in another.


State Layout

.franken-node/ is created by franken-node init (unless --no-state is supplied). Override the state location with --state-dir, or FRANKEN_NODE_FLEET_STATE_DIR for the fleet subtree specifically. The bootstrap layout is:

Path Purpose
.franken-node/state/ State root
.franken-node/state/incidents/<incident-id>/evidence.v1.json Signed captured incident source, authenticated before listing or bundle export
.franken-node/state/run-ledgers/<day>/<receipt-id>.json Product-signed run metadata, complete execution receipt snapshot, and engine-signed host-effect ledger
.franken-node/state/execution-receipts/ Transient execution receipts; excluded from version control by the generated .gitignore
.franken-node/state/fleet/ Durable fleet transport state (signed decision receipts, per-node convergence)
.franken-node/state/registry/ Local registry artifact store root
.franken-node/state/registry/artifacts/ Active signed extension artifacts
.franken-node/state/registry/archive/ Archived artifacts retained after registry gc
.franken-node/state/migrations/ Migration audit, rewrite, and validate outputs
.franken-node/state/trust-card-registry.v1.db Durable trust-card registry store (WAL frankensqlite; the legacy .v1.json pair is a one-time import source only)
.franken-node/safe-mode/state.json Unsigned JSON safe-mode controller; an active scope blocks new runs and interrupts running native sessions with admitted entrypoints in this directory or its descendants. --state-dir can address a standalone controller; only the conventional ancestor paths govern run.
.franken-node/keys/ Signing key material; excluded from version control by the generated .gitignore
.franken-node/keys/receipt-signing.key Default Ed25519 seed (mode 0600) for decision receipts, captured incident sources, run-ledger records, incident bundles, close-condition receipts and evidence-ledger entries, used when neither --receipt-signing-key, FRANKEN_NODE_SECURITY_DECISION_RECEIPT_SIGNING_KEY_PATH nor security.decision_receipt_signing_key_path is set
.franken-node/keys/receipt-signing.pub Matching public key (hex), for verify transparency-log --public-key and incident replay --trusted-public-key
.franken-node/state/evidence-ledger.db Durable evidence ledger: one signed, hash-chained entry per run (WAL frankensqlite)
.franken-node/keys/remotecap-signing.key RemoteCap signing key (mode 0600), used when FRANKEN_NODE_REMOTECAP_KEY is unset
.franken-node/remotecap/trust-scan-token.json Default trust-scan egress token (mode 0600, git-ignored), used when FRANKEN_NODE_TRUST_SCAN_REMOTECAP_TOKEN is unset

Additional subtrees are materialized lazily by individual subsystems (.franken-node/safe-mode/state.json unsigned JSON, proof pipeline snapshots, evidence ledger, validation-broker receipt files, cleanup audit) as those subsystems are exercised; the bootstrap above is the minimum layout that franken-node init creates up-front.

Native-runtime evidence authority is deliberately outside this project tree, because the project directory is also the guest filesystem sandbox root. <user-state> is XDG_STATE_HOME when set, otherwise $HOME/.local/state (LOCALAPPDATA is also recognized on Windows):

Path Purpose
<user-state>/franken-node/runtime-evidence/keys/product-root.key Owner-only persistent product root; never serialized into the execution child
<user-state>/franken-node/runtime-evidence/identity-captures/<verification-key>.json Product-root-signed public identity capture for one short-lived native-engine evidence key

These directories are created lazily with owner-only permissions. Native startup fails closed if the resolved user-state directory is inside the guest project root. A process-spawn-enabled Bubblewrap worker additionally masks the complete runtime-evidence directory with an empty tmpfs, so an allowed child utility cannot read the parent-owned root through the read-only host-root bind.

Every durable write goes through an atomic-rename + directory-fsync TempFileGuard; readers take an fs2 advisory lock when consistency across multiple writers is required (see lib.rs::lock_utils).


Environment Variables

franken-node reads configuration in this precedence: CLI flags → environment variables → the selected profile block in franken_node.toml → base values in that file (resolved by --config or default discovery) → built-in safe defaults. Environment variables follow the FRANKEN_NODE_<SECTION>_<KEY> convention. The most common:

Variable Equivalent config Notes
FRANKEN_NODE_PROFILE profile strict, balanced, or legacy-risky
FRANKEN_NODE_RUNTIME_PREFERRED runtime.preferred auto, node, bun, or franken-engine
FRANKEN_NODE_RUNTIME_ALLOW_PROCESS_SHAPE runtime.allow_process_shape Explicit true/false for static process metadata reads in the native entrypoint; omitted values retain the profile default; replay capture retains the bounded grant and supplied arguments
FRANKEN_NODE_RUNTIME_MAX_INSTRUCTIONS runtime.max_instructions Instruction budget for one run (> 0); replaces the profile default
FRANKEN_NODE_RUNTIME_MAX_REGISTERS runtime.max_registers Registers per call frame on both native lanes (1–65,536); replaces profile defaults
FRANKEN_NODE_RUNTIME_MAX_CALL_DEPTH runtime.max_call_depth Guest call depth on both native lanes (1–10,000); replaces the profile default
FRANKEN_NODE_RUNTIME_MAX_HEAP_OBJECTS runtime.max_heap_objects Positive allocation ceiling on both native lanes; not a live-object count
FRANKEN_NODE_RUNTIME_MAX_TOTAL_MEMORY_BYTES runtime.max_total_memory_bytes Positive engine-accounted memory ceiling in bytes; excludes console text and is not RSS
FRANKEN_NODE_RUNTIME_MAX_CONSOLE_ENTRIES runtime.max_console_entries Positive console-entry ceiling on both lanes; overflow fails and the fixed byte ceiling still applies
FRANKEN_NODE_RUNTIME_MAX_PARSE_SOURCE_BYTES runtime.max_parse_source_bytes Source bytes per entry/required/imported JavaScript module (1–2,097,152); replaces the profile default
FRANKEN_NODE_RUNTIME_MAX_PARSE_TOKENS runtime.max_parse_tokens Tokens per entry/required/imported JavaScript module (1–131,072); replaces the profile default
FRANKEN_NODE_RUNTIME_MODULE_CONDITIONS runtime.module_conditions JSON string array, for example ["development","react-server"]; additional package exports/imports conditions. [] clears configured conditions.
FRANKEN_NODE_ENGINE_BINARY_PATH engine.binary_path Override the resolved franken_engine binary path
FRANKEN_NODE_COMPATIBILITY_MODE compatibility.mode API compatibility mode
FRANKEN_NODE_COMPATIBILITY_EMIT_DIVERGENCE_RECEIPTS compatibility.emit_divergence_receipts true/false
FRANKEN_NODE_REPLAY_BUNDLE_VERSION replay.bundle_version Pin replay bundle format
FRANKEN_NODE_REPLAY_MAX_REPLAY_CAPSULE_FRESHNESS_SECS replay.max_replay_capsule_freshness_secs Freshness window for replay capsules
FRANKEN_NODE_FLEET_STATE_DIR fleet.state_dir Override the fleet transport state root
FRANKEN_NODE_FLEET_NODE_ID fleet.node_id Default fleet agent node ID
FRANKEN_NODE_FLEET_POLL_INTERVAL_SECONDS (none) Default fleet agent poll interval
FRANKEN_NODE_FLEET_CONVERGENCE_TIMEOUT_SECONDS fleet.convergence_timeout_seconds Fleet release / reconcile timeout
FRANKEN_NODE_DOCTOR_POLICY_ACTIVATION_INPUT doctor --policy-activation-input Fallback path to policy-activation snapshot JSON (not a live probe); the CLI flag takes precedence
FRANKEN_NODE_REGISTRY_REQUIRE_SIGNATURES registry.require_signatures Fail-closed if unset
FRANKEN_NODE_REGISTRY_REQUIRE_PROVENANCE registry.require_provenance Fail-closed if unset
FRANKEN_NODE_REGISTRY_MINIMUM_ASSURANCE_LEVEL registry.minimum_assurance_level 1-5
FRANKEN_NODE_REMOTE_IDEMPOTENCY_TTL_SECS remote.idempotency_ttl_secs Remote-cap idempotency window
FRANKEN_NODE_OBSERVABILITY_NAMESPACE observability.namespace Metrics namespace
FRANKEN_NODE_OBSERVABILITY_EMIT_STRUCTURED_AUDIT_EVENTS observability.emit_structured_audit_events true/false
FRANKEN_NODE_MIGRATION_AUTOFIX migration.autofix Autofix toggle
FRANKEN_NODE_MIGRATION_REQUIRE_LOCKSTEP_VALIDATION migration.require_lockstep_validation Doctor WARN when false; migrate validate still does not run verify lockstep
FRANKEN_NODE_SECURITY_MAX_MERGE_DECISIONS (security cap) Bounded decision history
FRANKEN_NODE_BENCHMARK_MIN_THROUGHPUT_OPS / ..._MAX_LATENCY_MS / ..._MIN_AGGREGATE_SCORE bench thresholds Bench gate parameters
RUST_LOG (none) tracing-subscriber filter expression for diagnostic logging

Run franken-node ops config-audit --json to see the resolved configuration with all environment-variable overlays applied.


Structured Logging and Observability

franken-node has two distinct logging surfaces. tracing-subscriber diagnostics are filtered with the standard RUST_LOG syntax for internal debugging; --structured-logs-jsonl is a stable, command-specific event stream intended for SIEM ingestion.

RUST_LOG=info,frankenengine_node=debug \
    franken-node init --profile balanced --scan \
    --structured-logs-jsonl --trace-id rollout-2026-05-16-canary

Each --structured-logs-jsonl line is a single canonical JSON object with a fixed shape per command, carrying a stable event_code:

{
  "timestamp": "2026-05-16T14:22:31.118Z",
  "level": "info",
  "event_code": "INIT-001",
  "message": "init command started",
  "trace_id": "rollout-2026-05-16-canary",
  "span_id": "init-bootstrap",
  "surface": "CLI-INIT"
}

Event codes are command-prefixed (INIT-001/INIT-002/INIT-003, RUN-001/RUN-002/RUN-003/RUN-004, DR-* for doctor, FLEET-005, DR-TRUST-004, DR-MIGRATE-005, …) so a downstream filter can pin the codes it cares about without parsing the message text. The surface field identifies the command family (e.g. CLI-INIT, CLI-RUN, CLI-DOCTOR).

franken-node ops metrics --format prometheus exports operational signals under the namespace franken_node_* (configurable via [observability].namespace). The metric surface is stable; consumers can pin directly against the names. Current names emitted by the CLI:

Metric Type Labels Surface
franken_node_health_pass gauge — 1 when this CLI ops health-check passed (compiled git SHA plus a local ledger/receipt file); not per-surface daemon liveness
franken_node_process_uptime_seconds gauge — Uptime of this CLI process (ops metrics / ops health-check); not a long-running node daemon
franken_node_active_session_count gauge — Count of files under <project>/.franken-node/state/sessions (same source as ops health-check; not a live SessionManager)
franken_node_build_info gauge version, git_sha Always 1; labels carry version info
franken_node_last_successful_evidence_ledger_flush_timestamp_seconds gauge — Last successful evidence ledger flush (epoch seconds)
franken_node_evidence_ledger_spill_entries counter — Entries spilled from the evidence ledger to durable storage
franken_node_incident_evidence_files gauge — Incident evidence files on disk
franken_node_execution_receipts counter — Execution receipts emitted
franken_node_fleet_active_quarantines counter zone Active quarantines per fleet zone
franken_node_fleet_node_records gauge — Tracked fleet node records
franken_node_revocation_filter_entries gauge — Current entries in the cuckoo revocation filter

The evidence ledger is a stronger consistency surface than metrics: every signed decision lives there with witness traces and is queryable through ops/evidence_index. Metrics are for dashboards; the ledger is for audits.


Output Contracts

Every command that accepts --json emits a stable, schema-versioned shape. Downstream automation can rely on these shapes; CI integration tests assert them against goldens.

Decision receipt (fleet release, trust revoke, trust quarantine, incident counterfactual --promote). The on-the-wire shape is SignedReceipt, which flattens Receipt:

{
  "receipt_id": "rec-2026-0042",
  "action_name": "fleet.release",
  "actor_identity": "ops-1",
  "timestamp": "2026-05-16T14:22:31.118Z",
  "signature_version": "ed25519-v1",
  "nonce": "9d4f3a…",
  "audience": "fleet:prod-us-east",
  "input_hash": "sha256:abc…",
  "output_hash": "sha256:def…",
  "decision": "approved",
  "rationale": "incident INC-2026-0007 remediated and verified",
  "evidence_refs": ["INC-2026-0007/timeline.jsonl", "INC-2026-0007/quarantine-receipt.json"],
  "policy_rule_chain": ["fleet.release.requires_remediation", "fleet.release.requires_operator"],
  "confidence": 0.99,
  "rollback_command": "franken-node trust quarantine --artifact …",
  "previous_receipt_hash": "sha256:1234…",
  "signer_key_id": "operator-2026-05",
  "chain_hash": "sha256:5678…",
  "signature": "<base64-ed25519>"
}

Trust card (trust card, trust-card export). The on-the-wire shape is the canonical-key-order serialization of supply_chain::trust_card::TrustCard (trust-card export requires --json and pipes through to_canonical_json):

{
  "schema_version": "trust-card-v1.0",
  "trust_card_version": 14,
  "previous_version_hash": "sha256:1234…",
  "extension": { "extension_id": "npm:@example/plugin", "version": "2.4.1" },
  "publisher": { "publisher_id": "pub-example", "display_name": "Example Org" },
  "certification_level": "silver",
  "capability_declarations": [],
  "behavioral_profile": {
    "network_access": true,
    "filesystem_access": false,
    "subprocess_access": false,
    "profile_summary": "documented"
  },
  "revocation_status": { "status": "active" },
  "provenance_summary": {
    "attestation_level": "verified",
    "source_uri": "https://registry.example/@example/plugin/2.4.1",
    "artifact_hashes": ["sha256:abc…"],
    "verified_at": "2026-05-15T09:11:02Z"
  },
  "reputation_score_basis_points": 7350,
  "reputation_trend": "stable",
  "active_quarantine": false,
  "dependency_trust_summary": [],
  "last_verified_timestamp": "2026-05-15T09:11:02Z",
  "user_facing_risk_assessment": { "level": "medium", "summary": "…" },
  "audit_history": [
    {
      "timestamp": "2026-05-15T09:11:02Z",
      "event_code": "TRUST_CARD_UPDATED",
      "detail": "scan completed",
      "trace_id": "trust-scan-2026-05-15"
    }
  ],
  "derivation_evidence": null,
  "camouflage_hints": [],
  "card_hash": "sha256:5678…",
  "registry_signature": "<HMAC-SHA-256>"
}

revocation_status is a serde-tagged enum: {"status": "active"} for active cards, or {"status": "revoked", "reason": "...", "revoked_at": "..."} for revoked ones. certification_level is one of unknown, bronze, silver, gold, platinum; reputation_trend is one of improving, stable, declining; user_facing_risk_assessment.level is one of low, medium, high, critical.

Lockstep verdict (verify lockstep) — illustrative shape; the authoritative contract lives in docs/L1_LOCKSTEP_RUNNER.md and the corresponding tests under tests/conformance/:

{
  "schema_version": "v1",
  "runtimes": ["bun", "franken-node"],
  "excluded_runtimes": [
    {
      "name": "node",
      "reason": "excluded because this host provides Bun's node wrapper rather than a real Node.js binary"
    }
  ],
  "status": "divergent",
  "divergences": [
    {
      "fixture": "tests/cases/async-iter-001",
      "node_hash": "sha256:…",
      "bun_hash": "sha256:…",
      "franken_node_hash": "sha256:…",
      "first_diverging_step": 12
    }
  ]
}

WorkflowTrace library verdict (replay::time_travel_engine). Its ReplayResult serializes to this shape, with verdict rendered as "identical" or "diverged" (the Diverged variant carries a usize count):

{
  "schema_version": "ttr-v1.0",
  "trace_id": "INC-2026-0042",
  "verdict": "identical",
  "steps_replayed": 4711,
  "replay_duration_ns": 8521033,
  "divergences": []
}

For a divergent replay, the verdict field serializes as {"diverged": <n>} (the Diverged(usize) variant; external tagging under #[serde(rename_all = "snake_case")]) and the divergences array carries one or more entries, each with a typed DivergenceKind: output_mismatch, side_effect_mismatch, full_mismatch, or clock_drift with expected_ns, actual_ns, drift_ns, tolerance_ns.

Counterfactual diff (incident counterfactual). The structured report is built by incident_counterfactual_report_json in crates/franken-node/src/main.rs and pins the schema to "franken-node/incident-counterfactual-report/v1". Promotion fields are null unless --promote was supplied with a signing key:

{
  "schema_version": "franken-node/incident-counterfactual-report/v1",
  "timestamp": "2026-05-16T14:22:31.118Z",
  "bundle_id": "INC-2026-0042.fnbundle",
  "incident_id": "INC-2026-0042",
  "bundle_created_at": "2026-05-16T14:22:31.118Z",
  "bundle_integrity_hash": "sha256:abc…",
  "policy": "strict",
  "evidence_refs": ["INC-2026-0042/timeline.jsonl"],
  "total_decisions": 4711,
  "changed_decisions": 17,
  "severity_delta": -3,
  "counterfactual_digest": "sha256:def…",
  "counterfactual": { },
  "promotion_contract": null,
  "promotion_contract_digest": null,
  "promotion_signature": null
}

JSON shapes are versioned; bumping a schema_version is a deliberate change and must land alongside a golden update under tests/golden/.


Operator Glossary

Term Meaning
Trust card Per-extension record with provenance, risk, audit history, camouflage assessment, revocation state. HMAC-signed.
Decision receipt Signed record of a policy decision (quarantine, release, revoke, counterfactual promotion). Includes actor, action, subject, evidence refs.
Evidence ledger Append-only signed log of decisions and observations, with witness traces.
Marker stream Ordered stream of control-plane markers used to anchor MMR proofs and detect forks.
Audience token Signed credential bound to a specific relying party (audience), with attenuation chain.
Audience binding Property that a token's signature is only valid for the audience named in the canonical payload.
Validity window Range of epochs (centered on the current epoch) within which an artifact is acceptable. Outside the window → fail closed.
Lane Runtime execution lane (e.g., cancel, realtime) with its own concurrency cap, priority weight, and overflow policy.
Epoch Monotonic control-plane counter advanced on every policy change, fleet action, or schema migration.
Capsule A bounded, versioned, signed structure carrying an artifact and its provenance (for example, a replay capsule).
Bundle A self-contained, deterministic snapshot exported for offline use (for example, an incident bundle).
Convergence The state in which all nodes in a fleet zone have applied a control action. Tracked per-node.
Safe mode Operator-authorized degraded mode that suspends capability issuance and refuses new decisions pending investigation.
Divergence receipt Signed record of behavioral disagreement across runtimes during lockstep validation.
Camouflage assessment Trajectory-based risk score detecting an extension that drifts from benign to malicious.
MMR proof Inclusion or prefix proof in a Merkle Mountain Range; cheaper than a full Merkle tree on append-only logs.
VEF receipt Verifiable Execution Fingerprint: a linked, signed receipt over an execution segment.
Fragility DGIS classification of a dependency-graph node based on maintainer/publisher patterns and SPOF properties.
ATC reciprocity Federation property: a participant's access tier should be proportional to its contribution.
Revocation frontier The set of currently-revoked artifacts at a given timestamp; checked for freshness on risky actions.
Idempotency window Time-bounded de-duplication window for remote operations.

Doctor Walkthrough

franken-node doctor is the primary diagnostic command. It exercises policy activation, evidence readiness, environment health, and policy decision routing in one pass.

What it checks

Behind doctor is the WorkspacePressureDoctor (ops::doctor), which classifies the workspace into a DoctorStatus:

Status Label Meaning
Healthy HEALTHY No pressure detected.
Warning WARNING Minor issues; monitor but continue.
Degraded DEGRADED Significant pressure; action recommended.
Critical CRITICAL Immediate action required.

The doctor consumes a WorkspacePressureInputs snapshot (free disk bytes, target-dir bytes, active builds, memory pressure ratio, RCH status, file-reservation count, agent-mail coordination health) and produces a DoctorOutput carrying:

  • schema_version, timestamp: pinned schema version and report generation time.
  • status: one of the four DoctorStatus levels above.
  • summary: one-line human-readable state.
  • resources: a ResourceSummary with the inspected values plus human-readable strings (free disk bytes/human, target-dir bytes/human, active builds, memory pressure ratio, RCH status, active reservations, coordination health).
  • policy_decisions: a BTreeMap<String, PolicyDecisionSummary> keyed by work-class, each summary carrying work_class, admission, and reason_code for the routing decision.
  • recommended_actions: a Vec<RecommendedAction> keyed by priority ("high", "medium", "low") with a short description per action.
  • diagnostics: operator-readable detail messages.
  • metadata: machine-readable annotations.

Reading a doctor report

franken-node doctor --verbose --json | jq '.status, .resources.free_disk_human, .recommendations[].description'

For workspace-pressure specifically:

franken-node doctor workspace-pressure
franken-node doctor workspace-pressure --human-output pressure.txt
franken-node doctor workspace-pressure --json --conservative > pressure.json

--conservative keeps the threshold tight (recommend earlier); --permissive only relaxes thresholds in known-clean environments.

Other doctor subcommands

  • franken-node doctor close-condition --json [--receipt-signing-key …] emits a dual-oracle close-condition receipt.
  • franken-node doctor evidence-readiness --input broker-snapshot.json --json reports evidence freshness from that --input snapshot (not a live broker).
  • franken-node doctor --policy-activation-input <fixture> exercises the policy activation contract against a checked-in snapshot JSON (not a live guardrail probe), useful for catching drift between the policy code and the spec.
  • franken-node doctor process-spawn-readiness --json securely resolves (or accepts --bubblewrap-path for) a root-owned, non-setuid, non-writable Bubblewrap binary and runs a bounded functional user/PID/cgroup/IPC/UTS namespace probe. It returns non-zero with unavailable or unsupported; readiness by itself does not grant process_spawn or replace the signed opt-in.

doctor is the single best command to run before opening a support ticket: its output (or --json artifact) is everything an investigator needs.


Validation Broker Architecture

Library modules (ops::validation_broker, ops::validation_planner, ops::validation_proof_cache, ops::validation_proof_coalescer, ops::validation_proof_debt_ledger) model the evidence pipeline between RCH workers, the proof cache, and operator close-out flows. The default CLI does not start a live broker daemon: ops validation-readiness, ops validation-closeout, and proofs queue status inspect --input / --receipt snapshot files (live_broker=false).

Components

Component Purpose
Validation broker Front door for evidence: accepts proof submissions, deduplicates, and emits receipts.
Validation planner Plans the proof-class lanes a piece of work needs to traverse (e.g. cargo check, source-only audit, full conformance).
Proof cache Memoizes proof results by canonical input hash; subject to GC and freshness gates.
Proof coalescer Merges concurrent in-flight proofs targeting the same hash so the cluster never duplicates expensive work.
Proof debt ledger Tracks proof obligations that have not yet been satisfied, with priority and deadline metadata.
Evidence index Searchable surface over the evidence ledger for ops queries.

Flow

operator ──► validation-planner ──► proof lanes (RCH workers + local)
                  │                          │
                  v                          v
            debt ledger              proof coalescer
                                              │
                                              v
                                       proof cache
                                              │
                                              v
                                  validation broker (receipt)
                                              │
                                              v
                                      evidence ledger

Operator surface

Command What it does
franken-node ops validation-readiness --input broker-snapshot.json --json Reports per-lane freshness vs. policy from an --input snapshot (live_broker=false). --json emits the schema-versioned readiness report.
franken-node ops validation-closeout --bead-id <id> --receipt <r> --json Renders the closeout summary from a --receipt snapshot (not a live broker). --json emits the schema-versioned closeout report.
franken-node ops resource-governor --process-snapshot <p> --requested-proof-class <c> --json Advises whether the proof lane should run, defer, or dedupe. --json emits the schema-versioned governor decision.
franken-node verify recovery-runbook --readiness-input <input> --json Generates a runbook from that readiness snapshot (not a live broker). --json emits the schema-versioned runbook report.

Snapshot receipts carry the canonical input hash, the proof-class lane that produced them, the worker identity, the policy profile in force, and an issuance timestamp. Inspecting those files is not proof that a live broker is running.


Building a franken-node Extension

A franken-node-compatible extension is a Node/Bun-style package plus a signed publication manifest. The publish path is fail-closed: an unsigned, unversioned package cannot be admitted to the registry under the default profile.

Minimum requirements:

  1. Package layout. Build your Node package, then archive its package.json and entrypoint into a single artifact file such as dist/plugin.tgz. registry publish reads file bytes; it does not package a directory for you.
  2. Ed25519 signing key. A 32-byte raw private key. franken-node accepts the key file in hex, base64, or a supported JSON wrapper. Generate a random 32-byte seed in an accepted encoding; store it under .franken-node/keys/ (the bootstrap layout .gitignores this directory).
  3. Explicit version. --version <semver> on registry publish; there is no implicit "latest".
  4. Source metadata. Run from your Git checkout with a committed source revision and an origin URL. The local publisher collects that metadata and signs its publication manifest and provenance with the supplied key. This is local operator publication, not independent build attestation or external publisher authorization.

End-to-end publish + verify:

# From the extension's Git checkout, with package.json and index.js built:
mkdir -p dist
tar -czf dist/plugin.tgz package.json index.js

# One-time: create a publisher seed without overwriting an existing key.
# Keep the key private; OpenSSL's Ed25519 PEM output is not accepted here.
umask 077
mkdir -p .franken-node/keys
(set -o noclobber; openssl rand -hex 32 > .franken-node/keys/publisher.ed25519)

# Publish a file into the local on-disk registry.
franken-node registry publish ./dist/plugin.tgz \
    --version 1.2.3 \
    --signing-key .franken-node/keys/publisher.ed25519 \
    --json > publish.json

# Use the actual extension_id returned by publish (not an npm package name).
extension_id=$(python3 -c 'import json; print(json.load(open("publish.json"))["extension_id"])')
franken-node registry verify "$extension_id" --json

# Find the local publication with the CLI's level-2 assurance floor.
franken-node registry search plugin --min-assurance 2

# Periodic GC of older lineage entries
franken-node registry gc --keep 5

For long-lived publisher identities, rotate the signing key on a regular cadence and update the corresponding trust card snapshot via trust scan --audit so dependents see the rotation in their next trust sync.

What the registry does on publish

  1. Reads the package bytes; rejects above bounded_read policy limit.
  2. Computes the canonical artifact hash.
  3. Validates the signature against the publisher's public key in constant time.
  4. Checks minimum_assurance_level against the supplied attestation metadata.
  5. Writes the active artifact to .franken-node/state/registry/artifacts/.
  6. Updates the per-lineage active set and archives anything beyond --max-active-artifacts.

franken-node registry gc moves stale artifacts to .franken-node/state/registry/archive/ rather than deleting them, so an operator can always recover a prior version.


Integration Patterns

franken-node is designed to drop into existing operations stacks. Common integrations:

CI/CD pipelines

# GitHub Actions: audit + default Bun+franken dyad (not L1 Node spec)
- name: franken-node migration audit
  run: |
    franken-node migrate audit . --format sarif --out audit.sarif

- name: franken-node lockstep dyad
  run: |
    # Default dyad is not the L1 spec. Matching only Bun stays fail.
    # On runners with real Node.js, use --runtimes node,bun,franken-node
    # (Node is then the spec).
    franken-node verify lockstep . --runtimes bun,franken-node \
        --emit-fixtures

- name: Upload SARIF for code scanning
  uses: github/codeql-action/upload-sarif@v3
  with:
    sarif_file: audit.sarif

The exit code of migrate audit, verify lockstep, and verify release is 0 on pass and non-zero on fail; CI integration is straightforward.

Container orchestration

In a Kubernetes-style deployment, run franken-node fleet agent --zone <zone> --poll-interval-secs 30 as a long-lived sidecar or daemonset. The agent polls the local file-transport action log (not a live control plane) and applies records with bounded retries. Liveness can be tied to franken-node ops health-check --json (compiled git SHA plus local ledger/receipt files, not per-surface daemon liveness). Readiness can be tied to franken-node ops validation-readiness --input <snapshot> --json; omitting --input uses an empty default snapshot (live_broker=false), not a live broker.

Monitoring stack

Scrape franken-node ops metrics --format prometheus from your Prometheus deployment under the franken_node_* namespace. Useful alerts against the currently-emitted surface:

  • franken_node_health_pass == 0: this CLI ops health-check failed (missing compiled git SHA or no local ledger/receipt file), not per-surface daemon liveness.
  • time() - franken_node_last_successful_evidence_ledger_flush_timestamp_seconds > 600: evidence ledger has not flushed in the last 10 minutes.
  • rate(franken_node_evidence_ledger_spill_entries[5m]) > 0: the ledger is spilling at a non-zero rate (capacity pressure).
  • sum(franken_node_fleet_active_quarantines) > 0: any quarantine is currently active across any zone.
  • absent(franken_node_process_uptime_seconds): the ops metrics scrape is down (this is a CLI scrape, not a node daemon heartbeat).

SIEM / audit pipelines

Ship the JSONL emitted by --structured-logs-jsonl to Splunk, Elastic, or your SIEM of choice. The trace_id field is intended for cross- process correlation. For long-term audit, copy .franken-node/state/evidence-ledger/ (when materialized) and signed decision receipts into immutable storage.

Package registries

franken-node trust scan inventories declared dependencies across the root and captured workspaces, including development dependencies and reachable transitive requirements. It uses npm-shrinkwrap.json when present, otherwise package-lock.json, and follows the nearest captured installation at each dependency location. Versions and integrity hashes come from that same capture; an unrelated hoisted package cannot supply a nested package's evidence. An npm alias is checked under its actual target identity (npm:package-name).

run uses the same traversal for production, optional, and peer requirements from its selected project authority, excluding development-only branches. Known revoked or quarantined transitive identities therefore reach the existing profile-specific admission checks. These are bounded metadata checks: a lockfile does not authenticate installed source bytes or enumerate undeclared dynamic imports. Missing optional or peer targets remain visible as declared identities.

The native worker receives every identity considered by preflight over its authenticated control channel and rereads current signed trust state before loading the entrypoint. A card that becomes revoked, quarantined, or High/Critical risk during worker startup is evaluated under the selected profile again; required cards and registries cannot silently disappear. Previously untracked identities are rechecked too. A legacy-risky run whose registry was unavailable still checks any valid registry that appears before execution, and revocations block every profile. The native supervisor continues rechecking this admitted dependency set while the guest runs and interrupts it when the same profile rules deny current trust state. This does not intercept each module import or attribute undeclared dynamic code to a package identity.

Trust cards are keyed by package identity. When reachable installations contain multiple versions, or mix a pinned version with an unresolved requirement, a new card reports multiple-or-unresolved, has at least Medium risk, and carries a warning. Single-version --deep/--audit refresh is unavailable for that card; the scanner does not use one selected version to clear all the others.

--deep fetches package metadata from the public npm registry (registry.npmjs.org) and dependent counts from api.deps.dev; those two hosts, over HTTPS, are the only ones the scan will contact, so private registries and npm registry mirrors are not supported. --audit and trust sync query OSV (api.osv.dev, or FRANKEN_NODE_OSV_QUERY_URL; during trust sync a clean answer from a non-default endpoint is refused as a reason to lower a card's risk). The [registry] section in franken_node.toml governs the local extension registry's signature and provenance requirements, not npm.


The Engine-Split Contract

franken-node is a product layer: The JavaScript/TypeScript runtime internals live in the sibling repository franken_engine. The boundary is enforced by the Engine Split Contract and a duplicate- implementation CI gate.

franken_engine owns franken_node owns
Native runtime internals (VM, parser, AST, interpreter, GC) Compatibility capture from Node/Bun ecosystems
Policy semantics and trust primitives at the VM level Migration and operator experience (audit, rewrite, validate, rollout)
Typed runtime-evidence authority and verification-identity contracts Persistent product-root custody, per-session authority provisioning, signed identity capture, and worker-response reconciliation
Bayesian sentinel inference and containment actions Extension ecosystem and trust distribution (registry, trust cards, reputation)
Native Rust execution (no V8/QuickJS bindings) Packaging, rollout, and file-transport fleet log
Product-layer policy controls and verification surfaces
L1 Product Oracle (Node is the compatibility spec; matching only Bun stays fail; child_process native-eval aborts remain fail)
Fleet file-transport quarantine log (not a live multi-node plane)

The split keeps the verifier SDK auditable: a third party can verify product-layer claims (trust, migration, replay, fleet) without having to audit the engine internals, and vice versa. The engine is a Cargo.toml path dependency on a sibling checkout (../franken_engine). A path dependency pins nothing: a build uses whatever that checkout contains, so reproducing a binary needs the engine commit recorded alongside it.

For native runs, the product parent retains a persistent evidence root outside the execution child, provisions a fresh short-lived engine signing authority per session, and persists a root-signed public identity capture. The child receives only the session authority; the parent rejects any returned evidence identity that does not match its signed capture. Verifiers must obtain the product root independently—the root embedded in a capture is not a trust anchor. See the runtime-evidence authority handoff.


Security Posture

The frankenengine-node crate enforces these properties at the source level and across the test surface:

  • #![forbid(unsafe_code)] in both lib.rs and main.rs; no unsafe blocks are permitted.
  • Constant-time comparison for every signature, hash, MAC, content-hash, trace-id, and action-id check via security::constant_time::{ct_eq, ct_eq_bytes} (backed by the subtle crate).
  • Saturating arithmetic on every counter, sequence, epoch, and timestamp computation that could meaningfully overflow.
  • Bounded Vec growth via push_bounded (defined in lib.rs); raw Vec::push is gated out of capacity-critical contexts. Zero-capacity paths clear rather than panic.
  • Domain-separated, length-prefixed hashing with b"<module>_<function>_vN:" domain prefixes and explicit length-prefix framing on each variable-length input.
  • Atomic rename + directory fsync for all durable writes (decision receipts, replay bundles, fleet transport state, control-epoch state).
  • Bounded file reads through bounded_read{,_to_string} (defaults reject parser-bomb inputs above policy limits).
  • Bounded source-IP cardinality in AuthFailureLimiter so brute-force and connection-exhaustion vectors cannot grow unbounded source tracking.
  • Fail-closed expiry (now >= expires_at), fail-closed signature verification, fail-closed trust-anchor requirements on incident replay and verify release.
  • Schema version pinning in schema_versions.rs with upgrade and downgrade helpers for every serialized artifact.
  • CI gates at .github/workflows/ enforce closer discipline, golden artifact integrity, no-contract-no-merge, claim gates (ATC, BPET, DGIS, VEF), and execution-normalization; ~436 scripts/check_*.py validators back individual contracts.

Testing and Verification

  • ~4,100 #[test] cases in the workspace test trees (tests/integration, tests/conformance, tests/contract, tests/e2e, tests/golden, tests/security, tests/perf) are registered on the default cargo test -p frankenengine-node target, but the suites are feature-gated: the flagship CLI e2e families (fleet_cli_e2e, trust_cli_e2e, migrate_cli_e2e) require --features test-support and most bd_* conformance families require --features advanced-features, so a truly default-feature run executes only the ungated subset. CI and the verification lanes run with those features enabled. Another ~22,000 inline #[cfg(test)] unit tests are compiled out of a default cargo test by the crate-level #![cfg(any(not(test), franken_node_inline_tests))] gate and only run on the dedicated inline lane (see below). The inline library-test portion is guarded by .github/workflows/inline-lib-tests-gate.yml. Pull requests run a cheap preflight that asserts the dedicated inline-test override remains wired; the expensive full lane is invoked manually with run_full_inline_tests=true, which runs the extended-surfaces/test-support cargo test --lib harness under --cfg franken_node_inline_tests.
  • Loom-based concurrency interleaving tests for the auth failure limiter, operator process-start initialization, evidence-ledger append ordering, and the remote-cap replay token set.
  • Property and metamorphic suites for storage, control-lane policy, control-epoch, trust card, replay window, replay bundle event reorder, time-travel, artifact signing, decision-receipt round trip, activation pipeline, epoch-key derivation, threshold-signature quorum, evidence-ledger chain order, canonical serializer, and observability witness.
  • Adversarial trust-card forgery, supply-chain poisoning, and BPET adversarial evolution suites under tests/security/.
  • Mock-free end-to-end pipelines for transparency inclusion, migration, revocation registry, incident bundle lifecycle, trust-card lifecycle, control-epoch lifecycle, quarantine-registry lifecycle, evidence-ledger lifecycle, fork-detection lifecycle, marker-stream lifecycle, audience token chain, MMR proofs, evidence-replay gate, divergence gate, key-role separation, control-lane scheduler, cancellation protocol, epoch-transition barrier, and provenance gate.
  • Criterion benchmarks under [[bench]] for Cuckoo revocation, BLAKE3, replay-bundle gzip, trust-card canonical encoding, proof-verifier gate, anti-entropy insert, threshold-signature verify, and a consolidated perf_wins suite.

Run focused suites:

cargo test -p frankenengine-node                       # ~4.1k integration tests registered; ungated subset runs on default features (CLI e2e need --features test-support; inline unit tests need the inline lane)
cargo test -p frankenengine-node fleet_cli_e2e         # CLI/integration
cargo test -p frankenengine-node verify_release_cli_e2e
cargo test -p frankenengine-node doctor_policy_activation_e2e

For the verifier SDK in isolation:

cargo test -p frankenengine-verifier-sdk

For the complete multi-file compatibility project collection:

python3 -m unittest discover -s tests -p test_project_compat_corpus.py
python3 scripts/project_compat_runner.py --corpus-dir fixtures/compatibility/projects \
  --bin ./target/release/franken-node --node /path/to/node --bun /path/to/bun \
  --artifacts-dir /tmp/franken-node-project-results

The artifact directory must be new. Omitting both --corpus-dir and --corpus selects only projects/corpus.json; it does not run the other project manifests. The full collection currently contains 170 cases in 26 manifests. The first command checks real Node against every frozen expectation. The runner executes distinct Node, Bun and native binaries with separate workspaces, retaining their hashes, exact streams, statuses and expected file observations. Reference-version differences remain non-pass outcomes. In particular, Node 24/26 differ from the pinned Node 22.14.0 on some Blob, dotenv and Web stream cases; no one version is currently qualified for the whole collection. These project results are separate from the historical 560-case compatibility score. A test double or a reference-only run supplies no native compatibility proof.


Honesty Manifest

The headline claims in this README obey the same rule as decision receipts and benchmark reports: claims require evidence, and the evidence is re-verifiable outside the producing runtime. Two committed artifacts back them:

  • docs/honesty_manifest_evidence.json — the census: a per-source snapshot of the committed tree from which each claim is derived (one {source, count} entry per .rs file that contributes #[test] attributes, one per registered fuzz target, one per validator script, etc.). This is the "committed tree snapshot" any third party can regenerate from source.
  • docs/honesty_manifest.json — the signed manifest: for each claim it binds the recomputed value, the README's pinned value, a drift tolerance, and a sha256 evidence_digest over the matching census entry, plus a single corpus_digest over all claims and a detached Ed25519 signature over the canonical unsigned payload.

The verifier SDK recomputes the whole chain with zero trust in the producer:

use frankenengine_verifier_sdk::honesty_manifest::{
    verify_honesty_manifest, HonestyTrustAnchor,
};

let manifest = std::fs::read("docs/honesty_manifest.json")?;
let evidence = std::fs::read("docs/honesty_manifest_evidence.json")?;
let verified = verify_honesty_manifest(
    &manifest, &evidence, &HonestyTrustAnchor::HarnessDefault,
)?;
println!("verified {} headline claims", verified.claim_count);

verify_honesty_manifest (1) verifies the Ed25519 signature against the pinned reproducible harness key — or an operator-supplied trust anchor for a re-signed production manifest; (2) recomputes every claim value from the census and checks each evidence_digest; (3) enforces the README-claim tolerance band; and (4) checks the corpus_digest commits to exactly the claim set presented. Flipping a single census count, recorded value, or signature byte makes it fail closed.

Two CI layers keep the manifest honest. scripts/check_claims_manifest.py --check-honesty continuously checks the committed census against the live tree (no crypto required), so headline numbers cannot silently drift; the verifier-SDK conformance test (sdk/verifier/tests/honesty_manifest_recompute.rs) performs the load-bearing Ed25519 + recompute verification on every cargo test run. Regenerate after a deliberate change with:

python scripts/check_claims_manifest.py --update-honesty

Conformance Harnesses

Conformance harnesses are stronger than tests: they encode protocol contracts as runnable code that produces signed evidence. The major categories shipped in tests/conformance/:

Harness Asserts
cancellation_protocol_conformance Cooperative cancellation completes within bounded time and produces a typed termination receipt
connector_lifecycle_transitions The connector state machine respects the published transition diagram for every pair of states
connector_protocol_harness Connectors implement the protocol contract on the wire (feature advanced-features)
verifier_sdk_capsule_replay The verifier SDK's replay path produces the same verdict as the runtime for every fixture capsule
verifier_session_monotonic Verifier sessions are monotonic: a sealed session cannot be reopened or rewritten
idempotency_key_derivation Idempotency keys are deterministic functions of canonical inputs (feature extended-surfaces)
control_policy_evidence_required Every control-plane decision is accompanied by evidence references the verifier can recompute
replay_bundle_integrity_conformance Replay bundles round-trip through serialize/sign/verify without semantic loss
replay_trace_canonical_conformance Replay traces serialize to the canonical form independent of insertion order
replay_coverage_gate_conformance Replay coverage artifacts conform to the documented gate schema
incident_bundle_integrity_conformance .fnbundle artifacts pass the same integrity gauntlet on read as on write
frankensqlite_adapter_conformance The frankensqlite storage adapter honors the documented isolation and durability semantics
fleet_decision_contract_harness Fleet decision receipts conform to the contract shape, including evidence refs
canonical_serializer_conformance All serialized artifacts produce the documented canonical bytes

Conformance results are emitted as signed artifacts, so an external verifier can recompute the same verdict on the same input.


Adversarial Test Suites

tests/security/ is a focused adversarial-corpus surface. Each suite is deliberately constructed to violate an invariant; the runtime must refuse cleanly.

Suite Adversary's goal
adversarial_trust_card_forgery Forge a trust card snapshot that bypasses HMAC verification
adversarial_supply_chain_poisoning Smuggle a malicious dependency past the registry gates
bpet_adversarial_evolution_suite Drift a benign extension into malicious behavior without tripping camouflage detection
dgis_quarantine_containment Find a contagion path that escapes DGIS quarantine
dgis_fragility_spof Cause an SPOF cascade that the fragility model fails to catch
dgis_graph_ingestion Inject a malformed dependency graph that confuses the topology audit
dgis_contagion_simulator Find a contagion trajectory the simulator misses
dgis_adversarial_suite Bundle of adversarial graph-topology attacks
threshold_signature_verification Defeat k-of-n verification by replay, malleability, or partial signature
obligation_tracked_channels Leak obligations across cancellation boundaries
anti_amplification_harness Use the protocol as an amplification vector
camouflage_detector Defeat camouflage detection with a structured trajectory
exfiltration_sentinel_scenarios Exfiltrate state past the runtime's sentinels
signed_lineage_graph_builder Construct a lineage graph that passes the builder but fails downstream verification
dgis_immunization_planner Defeat immunization planning by exploiting brittleness of the recommendation

Each suite is paired with a fixture corpus under fixtures/ and runs as part of the normal cargo test invocation. New suites land alongside new threat models so the test surface keeps pace with the threat model.


Operational Runbooks

The runbook set under docs/runbooks/ covers the incident classes that recur in operation. Each is a short, action- ordered playbook.

Runbook When to consult
epoch_transition_failure.md Epoch advancement aborted or stalled at the barrier
evidence_ledger_divergence.md Two replicas disagree on the evidence ledger contents
fleet_quarantine_activation.md A fleet-wide quarantine has been issued and must converge
mass_revocation_event.md Bulk revocation hit a sensitive number of extensions at once
obligation_leak_incident.md Obligations were not released within the SLA window
cancel_timeout_incident.md A cooperative cancel did not complete within the timeout
proof_pipeline_outage.md The VEF proof pipeline became unavailable
proof_lane_readiness_blockers.md Specific proof lanes are blocked from readiness
region_quiescence_breach.md A region failed to quiesce within the quiescence window
telemetry_bridge_operator_guide.md Detailed reference for the telemetry-bridge integration
trust_state_corruption.md The local trust-card registry has been corrupted

The runbook index lives at docs/runbooks/README.md.


Migration Pipeline Stages

franken-node migrate is a four-stage pipeline with explicit outputs between stages. Each output is a deterministic artifact: rerunning the same stage on the same input produces the same bytes.

flowchart LR
    src["source code<br/>Node / Bun project"] --> audit["AUDIT<br/><code>migrate audit</code>"]
    audit --> auditOut["migration-audit.json<br/>(SARIF / JSON / text)"]
    auditOut --> rewrite["REWRITE<br/><code>migrate rewrite --apply</code>"]
    rewrite --> rewriteOut["transformed source<br/>+ rollback-plan.json"]
    rewriteOut --> validate["VALIDATE<br/><code>migrate validate</code>"]
    validate --> validateOut["validation receipt<br/>(conformance + lockstep)"]
    validateOut --> rollout["ROLLOUT<br/><code>fleet release</code>"]
    rollout --> rolloutOut["fleet release receipts<br/>(signed, per-zone convergence)"]

    classDef stage fill:#1f6feb,color:#fff,stroke:#0a3b91;
    classDef artifact fill:#f0c674,color:#0b1020,stroke:#a8884c;
    class src,audit,rewrite,validate,rollout stage;
    class auditOut,rewriteOut,validateOut,rolloutOut artifact;

Stage details:

  1. Audit (migrate audit <path> --format json|text|sarif --out <p>) walks the source tree, classifies each finding, and emits a deterministic inventory. SARIF output is the recommended format for CI integration because it's natively understood by GitHub Code Scanning and most IDEs.
  2. Rewrite (migrate rewrite <path> --apply --emit-rollback <p>) applies the planned transforms. Without --apply the command is a dry-run. --emit-rollback writes an unsigned JSON MigrationRollbackPlan (not Ed25519-signed) plus .migrate-backup/ snapshots so the transformation can be unwound from file contents.
  3. Validate (migrate validate <path>) reruns static migration audit checks and, unless --static-only, a transformed-runtime smoke test (franken-node, node, or bun). It does not run verify lockstep. Doctor WARNs when migration.require_lockstep_validation is false; that flag does not make migrate validate invoke lockstep.
  4. Rollout uses the configured fleet store (the local file transport, or a fleet serve coordinator): franken-node fleet release --incident <migration-id> emits the signed release receipt and waits for fleet convergence.

franken-node migrate-report produces a single-document operator assessment that summarizes the audit and (if run) the validation result in HTML or JSON.


Operator Quick Reference

A one-table cheatsheet for common operator flows. Each entry expands into the corresponding section above.

Goal Command
Adopt franken-node init --profile balanced --scan
Audit a project franken-node migrate audit . --format sarif --out audit.sarif
Compare Bun+franken dyad (not L1 Node spec) franken-node verify lockstep . --runtimes bun,franken-node
Compare with real Node as spec franken-node verify lockstep . --runtimes node,bun,franken-node (only when node is real Node.js; matching only Bun stays fail)
Seed trust cards franken-node trust scan . --deep --audit
Refresh trust franken-node trust sync --force
Run in strict mode franken-node run ./app --policy strict
Inspect a card franken-node trust card npm:@example/plugin --json
List high-risk franken-node trust list --risk high --json
Quarantine an artifact franken-node trust quarantine --artifact sha256:…
Lift quarantine franken-node fleet release --incident INC-… --json
Reconcile fleet franken-node fleet reconcile --json
Snapshot incident franken-node incident bundle --id INC-… --verify
Verify recorded incident franken-node incident replay --bundle …fnbundle --trusted-public-key …pub
Re-execute captured native guest franken-node incident replay --bundle …fnbundle --trusted-public-key …pub --execute
Run a counterfactual franken-node incident counterfactual --bundle … --trusted-public-key … --policy strict
Diagnose environment franken-node doctor --verbose --json
Workspace pressure franken-node doctor workspace-pressure (human stdout; --human-output <path> writes a file)
Publish an extension franken-node registry publish ./dist/plugin.tgz --version 1.2.3 --signing-key …
Verify a release franken-node verify release ./release-dir --key-dir ./trusted-keys
Issue capability franken-node remotecap issue --scope … --endpoint … --ttl 300
Audit config franken-node ops config-audit --json
Export metrics franken-node ops metrics --format prometheus
Enter safe mode franken-node safe-mode enter --reason … --operator-id … --trust-state-hash …

Fuzzing and Differential Testing

fuzz/fuzz_targets/ ships 146 registered cargo-fuzz harnesses targeting the adversarially-exposed surfaces: parsers, deserializers, signature verifiers, lifecycle inputs, canonical encoders, transcript readers. A subset is undergoing API-drift remediation; the verification-target compile census (.github/workflows/verification-target-compile-gate.yml) builds them with --keep-going and tracks the compiling ratio. Representative harnesses:

  • fuzz_canonical_serializer_roundtrip: every canonical encoder must satisfy decode(encode(x)) == x for arbitrary inputs.
  • fuzz_canonical_serializer_deserialize: every deserializer must refuse malformed bytes without panicking.
  • fuzz_constant_time_comparison: ct_eq is exhaustively probed for observable timing or branch leakage on adversarial inputs.
  • fuzz_capability_token_parser: adversarial inputs to the capability token parser cannot crash, allocate without bound, or accept invalid tokens.
  • fuzz_connector_lifecycle_inputs: the connector state machine refuses every malformed transition input.
  • fuzz_compromise_reduction_report_json: adversarial JSON cannot exploit the compromise-reduction report parser.
  • fuzz_api_request_deserialization: control-plane API request parsing is closed under adversarial bytes.
  • fuzz_config_toml_parse: adversarial TOML cannot crash or trigger unbounded allocation in the config loader.

fuzz/regression/ is meant to keep discovered failure inputs as permanent regression cases, but today it holds 5 inputs for 2 of the 146 targets (shim, migration, dated 2026-02). The legacy category adapter reads these seeds through simulated functions under fuzz/targets/; that does not establish regression replay across the 146 registered cargo-fuzz targets (bd-fuzz-regression-corpus-f28ac). Coverage data lives in fuzz/coverage/; fuzz artifacts live in fuzz/artifacts/. The mutants-gate.yml workflow is configured to run cargo-mutants against the test surface as a separate effective-coverage signal: a mutant that survives indicates a gap in the test surface, not a bug per se. The gate requires a registered mutation-adequacy floor (MUTATION_SCORE_FLOOR_BP) on pull requests that touch the focused modules. It also requires nonzero inline-test discovery for every focused module and readable, nonempty mutant outcomes. Hosted execution and an actual mutation score remain unverified; workflow configuration and gate-control tests do not supply that measurement.

Effective coverage, not a raw count

A raw test count is Goodhart-bait, so the honest "tests" signal is effective coverage, expressed as three recomputable parts rather than a single token count:

  1. Registered test surfaces for cargo test -p frankenengine-node (thousands registered; the flagship CLI e2e suites require --features test-support, most conformance families --features advanced-features, and the ~22k inline unit tests run only on the dedicated inline lane). The Honesty Manifest recomputes source counts from the committed tree (Honesty Manifest); those counts do not establish discovery, execution, or success. Retain actual test-run results separately.
  2. Mutation adequacy — the fraction of seeded mutants the suite catches, to be measured by mutants-gate.yml against the registered floor (hosted execution remains unverified; see above). Mutation score measures whether the tests would catch a regression, which a count cannot.
  3. A pass-rate confidence interval — the verifier SDK exposes the Wilson score interval (honesty_manifest::wilson_score_interval_bp) so an auditor can bound the executed-test pass rate from a measured (passed, trials) pair (e.g. 95/100 → a 95% lower bound of ~88.8%), rather than trusting a bare "all green."

The configured gate computes its score from run outcomes rather than a score baked into the committed manifest. A measured score requires a completed run; the source count and interval calculation remain separately recomputable.


Profile Differences in Detail

Profile selection changes concrete behavior. The salient differences:

Behavior strict balanced legacy-risky
Stale or missing revocation frontier at run (trusted deps) refuse (5 min) warn (1 h) no floor
High/Critical-risk dependency at run refuse (high_risk) warn ignored
Auto-quarantine of dependencies after an SSRF violation or sentinel escalation same warn-only
Lockstep evidence to leave rollout Shadow required required required (all profiles; --force bypasses and is recorded)
Divergence receipts always emitted emitted in production optional
registry.require_signatures required required may be relaxed
registry.require_provenance required required may be relaxed
Compatibility mode tightest balanced permissive
Guest file writes (fs.writeFileSync and friends; confined to the project root) denied denied allowed
Allowlisted process metadata reads in native entrypoint code (process.argv, process.platform) denied unless runtime.allow_process_shape=true denied unless runtime.allow_process_shape=true allowed unless runtime.allow_process_shape=false
process.env reads denied denied denied (no profile grants them yet)
Pure builtins (JSON, Error, Number, path string operations) allowed allowed allowed
Compatibility corpus pass rate (560 cases, 2026-09-26, same binary, back to back) 70.71%: the same as balanced except http 3/50, since strict grants no network egress 78.93% (fs 2/50) 86.61%
SSRF default block block block
Safe-mode auto-entry on crash-loop not implemented (the crash-loop detector has no caller) same same
Camouflage severity threshold for risk bump constant 0.50, not yet per profile same same

The profile string is canonicalized at load time and recorded in every decision receipt; replays under a different profile produce a counterfactual diff, not a bug.

For entrypoint scripts that need their arguments while retaining balanced policy, set [runtime] allow_process_shape = true or run:

FRANKEN_NODE_RUNTIME_ALLOW_PROCESS_SHAPE=true franken-node run app.js -- alpha beta

The grant permits the engine's statically allowlisted process metadata, including the actual supplied argument strings. Raw or aliased process, computed property access, and environment values remain refused. It does not add filesystem, network, or spawn capabilities. The current engine provides argv, platform, and a synthetic pid; other allowlisted field names may still be unpopulated. Required/imported modules use their independent engine lowering policy and do not inherit this entrypoint grant.

Native dispatch reports expose invocation_settings: the parser budgets, execution ceilings for both lanes, and process_shape_read_allowed grant actually supplied to the engine. These settings survive typed invocation failures, including parsing/lowering failures and uncaught guest exceptions, and are bound into the persisted receipt and any signed run record. Failed attempts leave the selected lane and completed containment decision absent. The snapshot proves which settings were supplied, not that guest instructions ran. Startup failures, interrupted workers, panics, and telemetry failures may still have no reported settings; historical absence also remains unknown.

Native replay capture supports this bounded process-shape grant in strict and balanced profiles. It retains the supplied process.argv strings and restores them during offline execution, together with the original grant. The engine's fixed platform and synthetic pid do not depend on the replay host. Captured arguments may contain secrets, just like captured source and I/O. This does not grant environment reads, child processes or module loading. Those require their separate explicit capability and capture paths described in native replay. Statically lowered builtin facades can still use their recorded I/O transcript.


The Close-Condition Oracle

franken-node doctor close-condition emits a dual-oracle close-condition receipt: the gate that determines whether the platform is complete enough to ship. The contract is documented in docs/DUAL_ORACLE_CLOSE_CONDITION.md.

Three oracle dimensions must all be GREEN:

Dimension Owner Validates Artifact
L1 Product Oracle Track 10.2 Targeted compatibility corpus vs Node (spec). Bun is a second reference; matching only Bun stays fail. child_process native-eval aborts remain fail (not recategorized as pass). artifacts/oracle/l1_product_verdict.json
L2 Engine-Boundary Oracle Track 10.17 franken_engine integration points conform to the engine-split contract artifacts/oracle/l2_engine_verdict.json
Release Policy Linkage Track 10.2 Release gates consume both verdicts and enforce pass-through artifacts/oracle/release_policy_verdict.json

The gate is conjunctive:

PASS if and only if
    L1.verdict == "GREEN"
    AND L2.verdict == "GREEN"
    AND release_policy.verdict == "GREEN"

No partial success is accepted; an AMBER or RED verdict on any dimension blocks the close condition for shipping. CI section 10.N runs check_oracle_close_condition.py --consistency-mode so an honest L1 RED (corpus below 95%) can merge while the ship gate stays fail-closed. Declaring L1 GREEN below the corpus floor is still FAIL in both modes. See docs/DUAL_ORACLE_CLOSE_CONDITION.md (Consistency mode vs release/ship mode).

Receipts emitted by the close-condition oracle are signed and chained into the evidence ledger so a later audit can verify exactly which inputs produced the verdict. Live L1 is currently RED (corpus 69.82%, 391/560, per artifacts/13/compatibility_corpus_results.json); do not read a GREEN L1 from backfill artifacts.


Performance Characteristics

Benchmarks live under crates/franken-node/benches/ and run through Criterion. The current [[bench]] set:

Benchmark What it measures
cuckoo_revocation_bench Insert / lookup throughput against the Cuckoo-filter revocation index
blake3_performance_bench BLAKE3 throughput on the franken-node hashing call sites (feature blake3)
replay_bundle_gzip_bench Compression and integrity-check round-trip on replay bundles
trust_card_canonical_bench Canonical encode/decode of a trust card snapshot
proof_verifier_gate_bench VEF receipt verification under the gate path
anti_entropy_insert_bench Anti-entropy log insert (feature advanced-features)
threshold_sig_verify_bench k-of-n Ed25519 verify with cached public keys
perf_wins Consolidated suite tracking historical wins to defend against regressions
cargo bench -p frankenengine-node
cargo bench -p frankenengine-node --bench threshold_sig_verify_bench

Hot-path discipline is enforced by tests/hot_path_perf_budget_contract.rs (feature policy-engine): performance budgets for the most-traveled paths are encoded as conformance assertions, and a regression fails CI.

The bench run CLI surface produces a SHA256 provenance_hash report (not Ed25519-signed). A verifier can recompute the hash over the report body; that is tamper-evident content hashing, not an independently signed attestation.


Reproduction Playbook

The claims in this README are reproducible. Substitute your own project paths and trust anchors where indicated.

# Default Bun+franken dyad (not the L1 Node spec; matching only Bun stays fail)
franken-node verify lockstep ./my-app \
    --runtimes bun,franken-node \
    --emit-fixtures
# When node is real Node.js, Node is the spec:
# franken-node verify lockstep ./my-app --runtimes node,bun,franken-node --emit-fixtures

# Reproduce a benchmark report
franken-node bench run --scenario secure-extension-heavy --output bench.json
# Recompute the SHA256 provenance_hash over the report body (tamper-evident
# content hash, not an Ed25519-signed SDK attestation).

# Reproduce an incident bundle and replay verdict
franken-node incident bundle --id INC-2026-0007 \
    --evidence-path ./incidents/INC-2026-0007/evidence.v1.json \
    --verify
franken-node incident replay --bundle ./INC-2026-0007.fnbundle \
    --trusted-public-key ./keys/replay-trust-anchor.pub

# Reproduce a registry publish + verify cycle
franken-node registry publish ./dist/plugin.tgz \
    --version 1.2.3 \
    --signing-key .franken-node/keys/publisher.ed25519 --json > publish.json
extension_id=$(python3 -c 'import json; print(json.load(open("publish.json"))["extension_id"])')
franken-node registry verify "$extension_id" --json

# Reproduce the doctor activation telemetry (checked-in fixture)
franken-node doctor --verbose --json \
    --policy-activation-input ./fixtures/policy_activation/doctor_policy_activation_pass.json

# Reproduce a counterfactual diff
franken-node incident counterfactual --bundle ./INC-2026-0007.fnbundle \
    --trusted-public-key ./keys/replay-trust-anchor.pub \
    --policy strict --json

The full reproduction playbook for headline claims is at docs/reproduction_playbook.md, and checked-in fixture inputs live under fixtures/ at the repo root.


Reference Documentation Index

Beyond the README, the most-used reference documents under docs/ are:

Document Purpose
PRODUCT_CHARTER.md What franken_node is, what it isn't, governance, and decision rules
ENGINE_SPLIT_CONTRACT.md Hard boundary between franken_node and franken_engine
ARCHITECTURE_OVERVIEW.md Diagrammatic and prose architecture
CAPABILITY_OWNERSHIP_REGISTRY.md Which crate owns which capability
DUAL_ORACLE_CLOSE_CONDITION.md Three-oracle ship gate
COMPATIBILITY_BANDS.md Behavior bands across compatibility modes
L1_LOCKSTEP_RUNNER.md Reference for the lockstep oracle
L2_ENGINE_BOUNDARY_ORACLE.md Reference for the engine-boundary oracle
COMPATIBILITY_MODE_POLICY.md How modes map to runtime behavior
reproduction_playbook.md How to reproduce headline claims
COMMIT_STANDARDS.md Commit message standards for this repo
runbooks/README.md Index of operational runbooks
specs/ 100+ contract specifications, one per gate/protocol
adr/ Architecture decision records
planning/ Relocated MODE_OUTPUT reports under docs/planning/modes/
progress/ Relocated BD implementation memos (BD_IWA3Z, BD_P9MPD5, BD_P9MPD7, BD_SH95A)

Spec documents are part of the product contract: changes there must land alongside the corresponding scripts/check_<gate>.py and .github/workflows/<gate>.yml changes.


Common Pitfalls

Things operators wish they had known on day one:

  1. The engine-split contract is enforced. Building from source without the sibling franken_engine checkout will fail at link time. Use the one-line installer unless you specifically need source builds.
  2. legacy-risky is a migration aid, not a long-term mode. It relaxes revocation freshness, provenance, and signature gates so a legacy codebase can adopt incrementally. Leaving it in production defeats the trust-native premise.
  3. incident replay will refuse without a trust anchor. There are no built-in trust roots. --trusted-public-key <path> or --key-dir <dir> is mandatory; if you've never set one, that is itself the answer to "why won't replay run?"
  4. registry publish requires both --version and --signing-key. There is no implicit version (it would defeat reproducibility), and there is no anonymous publish path.
  5. master is for legacy URL compatibility only. Real work happens on main. The two branches are kept synchronized; if you push to main, also git push origin main:master so install URLs stay fresh.
  6. br sync --flush-only never runs git. Beads stays non-invasive about VCS by design. After br sync --flush-only you must git add .beads/ and commit manually.
  7. Bare bv launches an interactive TUI. In automated agent sessions or CI, use the robot flags (bv --robot-plan, bv --robot-priority, bv --robot-insights) instead.
  8. Cargo work goes through rch exec. Local builds storm a shared machine when many agents run simultaneously. rch fails open if workers are unavailable, so you can always fall back to local cargo when alone.
  9. The doctor's output is the diagnostic. Before opening a ticket, run franken-node doctor --verbose --json and attach the result.
  10. Signed artifacts trump local caches. When trust state and a signed artifact disagree, the runtime trusts the artifact and refreshes the cache, not the other way around.

Verifier SDK

The crate frankenengine-verifier-sdk (at sdk/verifier/) is a small, deliberately stable Rust API for independent third-party verification. It re-implements the verification side of the protocol so that a verifier does not need to trust, or even depend on, the main frankenengine-node crate. It builds and tests on stable Rust from sdk/verifier/ alone, with no workspace and no franken-node crates: cargo test --manifest-path sdk/verifier/Cargo.toml. The standalone workflow checks this (verifier-sdk-standalone.yml); a configured workflow is not evidence that hosted Actions are enabled or green.

What it exposes:

  • incident_bundle: verify_incident_bundle(bytes, &trusted_signer) independently verifies a .fnbundle produced by franken-node incident bundle. It re-derives the integrity and decision-sequence hashes, rejects unsigned fields, and checks the Ed25519 signature under the verifier's own anchor.
  • bundle: the SDK's own ReplayBundle byte-stream format (serialize, deserialize, hash, integrity_hash, seal, sign_bundle, verify_ed25519_signature, verify_signed_bundle, verify). This is a separate format from the CLI's .fnbundle; use incident_bundle for CLI output. bundle::verify checks integrity only (integrity_hash, an unkeyed digest; schema vsdk-replay-bundle-v2.0 carries no "signature" field). Authenticity requires verify_signed_bundle with a trusted key.
  • capsule: verify a replay capsule, including schema-version checks and side-effect declaration matching.
  • counterfactual: verify signed counterfactual receipts (it does not re-run incidents).
  • honesty_manifest: independently re-verify the README's headline claims. verify_honesty_manifest recomputes every claim from the committed per-source census, checks each evidence_digest and the corpus_digest, and verifies the manifest's Ed25519 signature against the pinned harness key (or an operator-supplied trust anchor) — failing closed on any tampered count, value, or signature byte. See Honesty Manifest.
  • A top-level VerifierSdk plus the create_verifier_sdk(identity) helper. VerifierSdk exposes verify_claim, verify_migration_artifact, verify_trust_state, verify_signed_migration_artifact, verify_signed_trust_state, verify_workflow_execution, validate_bundle, append_transparency_log, execute_workflow, create_session, record_session_step, and seal_session.
  • Stable result types: VerificationVerdict, VerificationOperation, ValidationWorkflow, TransparencyLogEntry, AssertionResult, VerificationResult, SessionStep, VerificationSession, VerifierSdkError.

The SDK is what a regulator, an auditor, or a research team uses to validate franken-node's public claims. Its conformance is asserted by tests/conformance/verifier_sdk_capsule_replay.rs and tests/conformance/verifier_session_monotonic.rs.

Python and JS/TypeScript auditors can use the same independent Rust verifier through its offline executable. Install it with Cargo from the SDK directory in a standalone distribution (or use --path sdk/verifier in this checkout):

cargo install --path . --bin franken-verifier
franken-verifier verify --format incident-bundle \
  --trusted-key-hex "$AUDITOR_PUBLIC_KEY_HEX" < incident.fnbundle

Distribute the executable together with bindings/python/franken_verifier.py, bindings/javascript/franken_verifier.mjs and its adjacent franken_verifier.d.mts. The executable and public anchor must come from sources the auditor trusts independently of the artifact. No producer service, product/engine checkout, private signing key, Python package or JS package installation is required. Python requires 3.10 or later; JavaScript requires Node 18 or later. Running .mts directly additionally requires a Node version with built-in TypeScript stripping; the consumer test uses Node 26. Both interfaces share the Rust cryptographic implementation; they are separate language interfaces, not independent cryptographic implementations.

from pathlib import Path
from franken_verifier import verify_artifact, VerifierError

result = verify_artifact(
    Path("incident.fnbundle").read_bytes(),
    auditor_public_key_hex,
    verifier="/trusted/bin/franken-verifier",
)
print(result["scope"], result["facts"]["incident_id"])
import { readFileSync } from "node:fs";
import { verifyArtifact, VerifierError } from "./franken_verifier.mjs";

const result = await verifyArtifact(
  readFileSync("incident.fnbundle"), auditorPublicKeyHex,
  { verifier: "/trusted/bin/franken-verifier", timeoutMs: 10_000 },
);
console.log(result.scope, result.facts.incident_id);

Pass original bytes, without decoding/re-encoding the artifact in Python or JavaScript. Results bind artifact_sha256 to those exact input bytes. The versioned response is franken-verifier-cli/v1; errors throw VerifierError with a stable code and optional process exit/status diagnostics. The CLI exits 0 only for authenticated evidence, 1 for refusal and 2 for usage/I/O errors. Inputs are limited to 64 MiB, JSON nesting uses the Rust decoder's 128-level limit, responses are bounded to 64 KiB, and callers can set a timeout up to 120 seconds (default 10 seconds). The executable is trusted code, not a sandbox for running arbitrary verifier programs.

Explicit format Authenticated scope Signature input
incident-bundle (default in bindings) authenticated_recorded_incident: signed recorded events, hashes and bounded native-capture metadata Embedded Ed25519 signature checked under the caller's anchor
capsule authenticated_capsule_content: independently re-derived signed payload/input hash Signed SDK capsule JSON with strict fields
signed-bundle authenticated_sdk_replay_bundle: the distinct canonical SDK bundle format Required detached signature_hex (Python), signatureHex (JS) or --signature-hex (CLI)

These scopes do not execute a captured guest or establish original-policy equivalence. Integrity-only SDK bundles are never returned as authenticated without their detached signature. Unknown/duplicate fields, invalid UTF-8, unsupported formats, tampering and foreign anchors fail closed. A native capture result exposes only digest/size/terminal-state/exchange-count metadata, not captured source, arguments, exception text or host-I/O values.

sdk/verifier/tests/language_clients.rs runs real Python, JavaScript and TypeScript consumers of a copied distribution. On Linux, bwrap hides the product and engine checkouts; missing runtimes/isolation tools fail the test. Independent Node-crypto vectors and planted broken-child controls check signature, canonical-byte, protocol and failure propagation boundaries.

This function runs as the doc-test of verify_incident_bundle (sdk/verifier/src/incident_bundle.rs), against a bundle the real CLI wrote:

use ed25519_dalek::VerifyingKey;
use frankenengine_verifier_sdk::incident_bundle::verify_incident_bundle;

/// Verify a `franken-node incident bundle` output against a signer key the
/// auditor obtained independently (never the key embedded in the bundle).
fn audit(
    bundle_path: &std::path::Path,
    trusted_signer: &VerifyingKey,
) -> Result<(), Box<dyn std::error::Error>> {
    let bytes = std::fs::read(bundle_path)?;
    let verified = verify_incident_bundle(&bytes, trusted_signer)?;
    println!(
        "verified incident {} ({} events, integrity {})",
        verified.incident_id, verified.event_count, verified.integrity_hash
    );
    Ok(())
}

Troubleshooting

Symptom Cause Fix
lockstep validation failed Behavior delta across runtimes franken-node verify lockstep ./my-app --emit-fixtures and inspect generated divergence fixtures.
revocation freshness gate denied … Under strict, a dependency is being admitted on the strength of its trust card, or a capability is being issued (remotecap issue, always 5 minutes), but the registry's recorded revocation frontier is more than 5 minutes old or was never recorded. Under balanced the same text appears as a preflight warning (1-hour window) and the run proceeds franken-node trust sync --force. Only a forced refresh in which every trust signal was fetched without network errors records a new frontier; cached answers do not. The frontier is stored in the durable trust registry, not inferred from file timestamps.
artifact rejected: missing attestation Registry policy requires provenance proofs Rebuild artifact with provenance metadata and re-sign before registry publish.
quarantine not converged One or more nodes did not apply the control action in time franken-node fleet status --verbose, then franken-node fleet reconcile. Re-check the convergence timeout in [fleet].
incident replay refused: no trust anchor --trusted-public-key / --key-dir missing on incident replay or incident counterfactual Supply an Ed25519 trust anchor; no built-in trust roots exist.
incident replay nondeterministic Missing, corrupted, or stale evidence/bundle components Re-export with franken-node incident bundle --id <ID> --verify or ensure <root>/.franken-node/state/incidents/<slug>/evidence.v1.json exists.
registry publish refused: missing version / missing signing-key Fail-closed publish path Pass --version <semver> and --signing-key <path> (raw Ed25519 32-byte key; hex, base64, or supported JSON wrapper).
safe-mode entry refused: missing trust-state-hash Fail-closed safe-mode entry path Supply --reason, --operator-id, and --trust-state-hash together.
workspace pressure: defer from doctor workspace-pressure Disk / memory / build pressure crossed the active policy threshold Run franken-node doctor workspace-pressure (human stdout) for the recommended action set; --human-output <path> writes that report to a file. Consider --conservative to keep validation gating tight, or --permissive only for known-clean machines.

franken-node doctor --verbose --json is the single best diagnostic; it exercises policy activation, evidence readiness, and environment health in one pass.


Limitations

  • Legacy profile permits insecure behaviors. legacy-risky exists for constrained migration windows. It is not a long-term mode, and it does not ambiently permit child-process spawning.

  • Child-process spawning is Linux/Bubblewrap-only and impossible by default. A signed, expiring, resolved-policy-bound ChildProcessSpawn opt-in and a successful functional backend probe are both mandatory. Unsupported operating systems and missing or insecure Bubblewrap binaries fail closed. Current runtime-of-record work still withholds the capability until the authenticated native worker can prove it was launched inside that namespace.

  • Strict profile blocks weak-provenance extensions. This is intentional but can surprise teams whose registries are not yet provenance-clean.

  • Migration rewrites target high-value patterns first. Niche framework macros may still need manual edits; the audit will tell you.

  • Fleet CLI is file-transport by default (live_control_plane=false). A partitioned node will not converge until fleet reconcile.

  • Single-node mode stores fleet state locally; multi-node coordination runs through one fleet serve coordinator. Its listener is plaintext HTTP (loopback by default): expose it beyond the host only behind TLS termination or an encrypted overlay.

  • Counterfactual simulations depend on the completeness of the telemetry captured during the incident window. Sparse evidence produces sparse counterfactuals. The default counterfactual executor is a policy-threshold model that re-evaluates the recorded decision trace; the report labels which executor produced the diff so a synthetic re-evaluation is never mistaken for a live re-run.

  • franken-node run executes through the in-process franken-engine and surfaces the program's real console output (stdout/stderr) plus an exit code derived from the runtime's containment verdict (91 Challenge, 92 Sandbox, 93 Suspend, 94 Terminate, 95 Quarantine). run --json carries the engine's reasoning in dispatch.engine_decision: the MAP risk state and posterior, the expected-loss selector's action, the stopping rule that crossed, and the instruction count; human output prints the same as a why: line under the containment note. A program that throws still prints what it printed before the throw, then exits non-zero, as under Node (run --json does not yet carry that output). An entry runs as an ES module when it is .mjs/.mts, or its nearest package scope declares "type": "module". Untyped .js, .ts, and extensionless entries use the engine's syntax detection after the worker admits and reads their source; for example, an export declaration selects an ES module without requiring a package type. Explicit .cjs/.cts and "type": "commonjs" keep CommonJS semantics. Detection honors the configured parser budget and keeps the original TypeScript/hashbang ingestion evidence. Explicit formats avoid the additional detection parse. A CommonJS entry has require, module, exports, __filename and __dirname and can require('./lib/x') files inside the project directory, while require('fs'), require('path') and the other supported builtins keep routing through the capability-gated host calls. require('pkg') resolves installed packages the way Node does (the node_modules walk-up inside the project directory, package.json exports with the require condition, main, and .json modules). Retained probes of six larger packages (lodash, ms/semver, dayjs, uuid, commander, minimist) did not match Node. Three pinned tiny packages (is-number, isarray, escape-string-regexp) now have offline upstream-source specimens and match published v0.2.0, Node and Bun in all three profiles; that is release evidence, not current- source or representative ecosystem qualification. Large files can exceed the per-file parse budget, some syntax still fails to parse, string literals mentioning "token" or "password" are refused by the information-flow check, and some builtins still exist only as recognized call patterns such as fs.readFileSync(...). The native providers listed below also expose runtime objects and functions that packages can pass around (bd-reality-20260923-26n9r.4). With --frozen-modules, captured packages can use literal require, ESM import, reexports and import() for the engine's native buffer, events, os, querystring, url, timers, timers/promises, perf_hooks, path, path/posix, path/win32, util, util/types, assert, assert/strict, string_decoder, stream, stream/promises, module, crypto and zlib providers, including node: aliases. ESM default exports share the CommonJS objects, and named exports come from the same native providers. Each route retains its policy and information-flow checks, including cached loads. This does not provide the complete Node/Bun builtin API inventory. TypeScript enums and prefix assertions such as <number>value in frozen .ts, .mts and .cts modules use the engine's existing normalizer. Assertions erase types without converting runtime values; their type-only imports never grant filesystem access. Initializer and asserted-expression dependencies remain in the pinned graph, and prepared JavaScript must preserve those routes and obey the configured parser budget. JSX/TSX syntax, namespace, parameter-property and import-equals transforms remain outside frozen-module admission. Frozen capture also supports immutable top-level request strings such as const target = './adapter.cjs'; require(target), earlier constant aliases, string concatenation, templates, and bounded conditional string choices. The same captured targets work with require.resolve, import() and supported createRequire bindings. Capture includes every possible string branch; it does not execute conditions or predict the branch that will run. Mutable, shadowed, forward-referenced and otherwise unproved bindings remain refused, and these expressions grant no access to uncaptured modules. ESM packages can construct a captured loader with createRequire(fileURLToPath(import.meta.url)). Named, renamed, default and namespace imports from url or node:url are supported, as are earlier top-level const filename aliases. Capture proves the converter and origin bindings; runtime still requires the exact current captured filename and authorizes every dependency request. Arbitrary URL inputs, converter mutation, shadowing and unproved filename aliases remain refused. CommonJS packages can also use require.main === module and the equivalent strict equality/inequality checks through proven loader aliases to distinguish CLI execution from a dependency load. Capture retains dependencies from both branches; this support does not expose arbitrary loader properties or allow the loader object to escape. The crypto provider exposes the engine's existing hash/HMAC, key-derivation, cipher, key-pair/signature and random functions as ordinary callable exports; fresh entropy still requires random_read at invocation. The zlib provider exposes its existing bounded gzip/deflate/Brotli operations, including the supported callback APIs. Extracting these functions or passing them through another package retains their capability, information-flow and resource checks. Frozen module execution can admit contained workspace links with run app.cjs --frozen-modules --frozen-module-symlinks, or with both runtime.frozen_modules = true and runtime.frozen_module_symlinks = true in configuration. Capture rejects links that escape the project, absolute targets, and cycles; the default continues to reject module symlinks.

    Select custom package exports/imports branches with repeatable conditions: franken-node run app.mjs --conditions development --conditions react-server (short form -C). This works for both ordinary and frozen native loading. Each load retains node plus its own import or require condition; package declaration order still decides which matching branch wins. The equivalent TOML setting is runtime.module_conditions = ["development", "react-server"]. Explicit CLI conditions replace the configured list; the environment override accepts a JSON array. At most 62 nonempty names of 128 bytes each are admitted, with control characters rejected. Conditions select code without granting additional capabilities. Frozen captures and offline replay retain the condition set, so changing it changes the source-graph commitment.

    Source builds also accept an independently retained source-graph pin: run app.cjs --frozen-modules --frozen-module-graph-hash "$REVIEWED_SOURCE_GRAPH_HASH". The equivalent configuration is runtime.frozen_module_graph_hash = "sha256:..." with runtime.frozen_modules = true; an explicit CLI pin takes precedence. Obtain the full sha256: plus 64 lowercase hexadecimal digits without running the guest through the product CLI:

    franken-node verify source-graph ./project app.cjs --json
    # Review and retain input_hash as REVIEWED_SOURCE_GRAPH_HASH, then:
    franken-node run ./project/app.cjs --frozen-modules \
      --frozen-module-graph-hash "$REVIEWED_SOURCE_GRAPH_HASH"
    

    The inspection uses the same bounded source capture as native frozen execution and emits its existing franken-node/module-source-graph/v2 metadata report, without source bodies, dependency installation, network access or guest evaluation. Exit 0 means the declared static request scope is complete; incomplete captures retain diagnostics and exit 1. Runtime completeness and release certification remain false. Use the same project root and entrypoint for inspection and execution. Native file execution chooses its root from the selected path: an absolute entrypoint uses its parent; a relative entrypoint searches for the nearest package.json or franken_node.toml between its parent and the invocation directory, falling back to its parent. For a nested packaged entrypoint, inspect the package root and run the relative entrypoint from that root. Switching that nested path to an absolute path changes the admitted root and can invalidate the reviewed pin. If inspection needs --allow-contained-symlinks, execution also needs --frozen-module-symlinks. The development franken-module-graph tool exposes the same capture through --capture-source-graph. Pass the same repeatable --conditions values to verify source-graph and run when reviewing a custom-condition graph. The development inspection tool calls this additive option --additional-condition; its existing --condition option continues to replace the complete condition set. Keep that approval outside guest-writable source; a configured pin needs an operator-controlled TOML file. The native worker checks the pin before preparing or evaluating modules, then executes that same captured graph. Changed captured source, observed manifests, links or resolution probes require a newly approved pin, even when console output would be identical. A graph pin is operator-selected source integrity; it supplies neither publisher provenance nor additional capabilities. Files the graph never observes are outside this commitment: for example, a .cjs entry with explicit relative .cjs loads need not read an unused root package.json. Current installed-release qualification remains a separate requirement.

    Publisher-approved frozen source. The product can also bind this graph to an existing signed local registry artifact. Export the exact captured inputs as a new private capsule, then publish those bytes with the publisher's signing key. Source export never executes the program or overwrites an existing file:

    franken-node verify source-graph /projects/example app.cjs \
      --write-source-capsule /review/example.capsule --json
    franken-node registry publish /review/example.capsule \
      --version 1.2.3 --signing-key /operator/publisher.ed25519 --json
    # Retain the absolute manifest_path from the publish result, then from the project:
    franken-node run app.cjs --frozen-modules \
      --registry-artifact /registry/path/artifact.manifest.json \
      --publisher-public-key /operator/publisher.pub
    

    Supply a publisher public key independently retained outside the guest project; an embedded key cannot approve itself. The registry artifact must be a complete FNSGCAP1 source capsule within the registry's 5 MiB artifact limit. Both the dispatcher and its authenticated native worker reverify the exact retained manifest and capsule using this external key. The worker then checks the live graph against that signed content and executes its single immutable capture. Changed entry/dependency bytes, observed manifests or probes are refused before evaluation. The entrypoint, additional conditions and contained-symlink policy must match the capsule, and an explicit --frozen-module-graph-hash must agree with it. Manifest, artifact and key reads are bounded and reject symlinks in every path component.

    Native-enabled builds also export and admit capsules containing supported Node builtins and captured failures for optional require() or import() requests. These source-free terminal routes use the same admission rules as the native loader. The source report retains fully_resolved: false; successful explicit export adds capsule_closure_verified: true. Missing static imports, unknown builtin providers and unbounded computed loads remain refused. A dependency that appears after capture changes the graph pin and requires new approval. Capsule admission grants no capabilities and does not replace the worker's source preparation or current policy checks.

    The frozen graph report and run receipt retain the authenticated publisher key ID, package name/version and artifact hash. The native_module_graph receipt field participates in both the receipt hash and its deterministic identity; the original signed run record authenticates the same receipt snapshot. Publisher-approved runs require that signed record to be persisted, even without --capture-replay. Missing or misconfigured receipt signing authority, or failed record persistence, fails the command. This check occurs after execution and does not undo guest effects.

    This establishes publisher approval of source identity and a durable statement from the run receipt authority. It does not authenticate unsigned registry lifecycle/provenance fields, attest registry-service approval, grant capabilities or increase resource limits. Existing dependency trust-card revocation and quarantine, fleet policy and runtime controls still govern execution. The broader signed capability and resource contract remains separate work.

    The broader host-effect "runtime-of-record" — the full Node/Bun host-API surface with capability-metered effects — is still in active development under the engine-split program; do not yet rely on it as a complete drop-in for arbitrary host-effect workloads.

  • incident replay verifies the recorded incident by default. Explicit --execute re-executes source and host-I/O captured with run --capture-replay. Completed guests and certified uncaught-exception prefixes have distinct verification scopes; a failed prefix has no completed IR4 witness or final instruction-count/nondeterminism comparison. Explicit frozen-module source capture is implemented but awaiting current-source native qualification; entry-only captures still disable runtime module loading. Ambient environment replay and interrupted execution remain unsupported. Admitted process replay preserves captured preparation and global effect order while refusing live process dispatch. Original policy decisions are not certified by the guest replay verdict.

  • Source builds require the sibling franken_engine repository to be checked out next to this one (engine-split contract). The one-line installer side-steps this by shipping prebuilt binaries.

  • Homebrew is not currently published. Use the installer or build from source.


FAQ

Is this a drop-in replacement for Node or Bun?

No. The measured compatibility corpus is the L1 spec. Its artifact of record says 69.82% (391/560, artifacts/13/compatibility_corpus_results.json); a 2026-09-25 re-measurement on one host measured 78.93% under the default balanced profile, which refuses file writes and process.platform by policy, and 86.61% under legacy-risky (see CLAIM-001 for provenance limits). child_process is 0/30 in every profile: native-eval aborts remain fail, not pass. verify lockstep defaults to a Bun+franken dyad for hosts without real Node.js; that dyad is not the spec. When --runtimes includes node as real Node.js, Node is the spec and matching only Bun stays fail. Inspect divergence receipts before any production rollout.

Does franken-node require a full rewrite of existing projects?

No. The migration autopilot audits and transforms incrementally, emits unsigned JSON rollback plans (--emit-rollback, not Ed25519-signed), and runs static+smoke migrate validate. Behavioral comparison is a separate verify lockstep stage.

Can I run franken-node without a centralized fleet control plane?

Yes. Default fleet state is the local durable store under .franken-node/state/ (live_control_plane=false). Multi-node coordination is opt-in: run franken-node fleet serve on one node and set [fleet] control_plane_url (plus control_plane_token_path) on the others.

What does deterministic replay include?

Replay bundles include the workflow timeline, environment snapshot, policy decisions, trust artifacts referenced by those decisions, the evidence ledger entries for the incident window, and signed schema-version metadata so the bundle can be reproduced exactly under the same runtime version.

How does franken-node help with supply-chain risk?

It combines Ed25519-signed artifacts, provenance checks, revocation freshness gates, trust cards (with camouflage and fragility signals), and local file-transport quarantine (live_control_plane=false) as runtime defaults rather than external scanners. Risky and dangerous actions consult fresh trust state before they execute.

Do I have to use strict mode?

No. balanced is the default for most teams. strict is intended for high-assurance environments and post-incident lockdowns; legacy-risky is intended only for constrained migration windows where insecure compatibility behaviors must be explicitly enabled by policy.

What is a "trust card"?

A per-extension record carrying publisher identity, risk assessment, audit history, version chain, vulnerability state, camouflage assessment, and revocation status. Cards are HMAC-signed snapshots loaded under a SnapshotSourceContext (trusted-file vs. untrusted-network, which picks the validation strategy), and consulted by the runtime before risky and dangerous actions.

What is the "counterfactual simulator"?

After an incident, franken-node incident counterfactual --bundle <b> --policy strict replays the same captured trace under a different policy profile and reports the diff of decisions, blocked actions, and evidence emissions. It answers "would strict mode have caught this?" with a reproducible artifact.

Where do I find the public verifier SDK?

The crate frankenengine-verifier-sdk lives at sdk/verifier/ and is deliberately smaller and more stable than the main product crate. It exposes bundle, capsule, and counterfactual surfaces for third parties verifying claims independently of the runtime that produced them.

How are CI gates organized?

Gate-oriented and path-scoped. Each contract has a docs/specs/<gate>.md, a scripts/check_<gate>.py validator, and one or more .github/workflows/*-gate.yml workflows that run the validator on changed paths. Closer-discipline, no-contract-no-merge, claim gates (ATC, BPET, DGIS, VEF), connector conformance, replay coverage, canonical vectors, mutation testing, and security golden artifacts are all enforced.

How does franken-node prevent SSRF and DNS rebinding?

Every outbound network call guest code makes on the native engine path (the default for run) passes through the SSRF policy gate (security::ssrf_policy). The node/bun fallback runtimes, which need the explicit FRANKEN_NODE_ALLOW_DEGRADED_RUNTIME_FALLBACK opt-in, get policy hints only and are not gated. The gate enforces CIDR-aware deny rules (including private ranges and loopback by default), checks per-connector allowlist entries with optional port pinning, and re-validates resolved IPs after DNS resolution to defeat rebinding. [security.network_policy].ssrf_enforcement is block (default), monitor or none. monitor currently enforces like block on the native run path (no allow-and-record mode yet); none disables the gate. Capability tokens are endpoint-bound, so a token issued for https://api.example.com cannot be silently redirected to http://127.0.0.1:8080.

What happens when the runtime enters safe mode?

franken-node safe-mode enter --reason <reason> --operator-id <id> --trust-state-hash <hash> persists unsigned operator state under .franken-node/safe-mode/state.json and blocks new application runs whose canonical entrypoint is in this directory or any descendant. Admission checks run in the CLI, the public dispatcher, and again in the authenticated native worker before source execution. An inactive child scope cannot override an active ancestor; malformed, oversized, unreadable, and nonregular state also refuses admission. The error identifies the controlling scope, where the operator should inspect and recover state.

Already running native sessions retain their admitted entrypoint scopes and recheck local safe mode, dependency trust, and fleet policy every 250 ms between completed checks. A disallowed state interrupts the worker and its containment unit. A fleet update that changes effective instruction, parser, or network restrictions requires restarting the guest under that policy; equivalent newer revisions continue while advancing the observed revision floor. Applications are not automatically restarted. Renaming an already loaded entrypoint does not change its retained safe-mode scope.

Control reads run on one owned observer per session, independently of the worker's absolute deadline. If a check cannot complete for 10 seconds, execution fails closed. The worker is terminated before the supervisor joins an observer still finishing a store read, so returning the result can take longer than stopping the guest. run --json reports control_trigger and control_indeterminate (or control_cleanup_unproven if cleanup fails), with the actual interrupted-effect WAL and a persisted unsuccessful-run receipt. In-flight provider outcomes remain unknown; containment does not undo effects already committed. Remote fleet propagation still depends on agent polling, and a fleet action acknowledgement proves persistence rather than session quiescence. Live-control interruptions do not increment the panic counter.

The native supervisor automatically enters safe mode after three validated native engine panic responses for the same entrypoint within 60 seconds. It records the immutable session nonce and original observation time in the resolved project root's controller; the controller's crash_loop_threshold and crash_loop_window_secs configure those defaults. Guest exceptions, timeouts, and generic worker exits do not contribute to this panic count. These are parent-observed panic records; the separately signed session-key capture does not sign the panic outcome itself. This performs containment, not automatic rollback to an earlier application artifact.

Concurrent crash and operator updates share a stable sibling lock and replace the controller atomically after syncing a temporary file. Explicit recovery retires recorded crash contributions while retaining recent nonce identities, so replaying an old observation cannot immediately reactivate safe mode. Expired observations keep their original times and remain uncounted. If the clock moves backward, the tracking configuration is invalid, or retaining recent identities would exceed the bounded history, a distinct CrashTrackingFailure reason activates safe mode. A storage failure is attached to the original panic error; it is never reported as a successful counter update. Panic errors also expose this result through run --json.

trust-corruption and epoch-mismatch still require an operator-supplied reason, and inspection commands remain available. A different in-flight panic first recorded after recovery can start the fresh window. Exiting requires franken-node safe-mode exit --confirm --operator-id <id> plus the operator attestations (--trust-state-consistent, --no-unresolved-incidents, --evidence-ledger-intact), which are not independently verified by the CLI. A custom --state-dir is standalone unless it addresses one of these conventional ancestor state locations. This local operator control is unsigned and does not protect against an actor permitted to replace the operator's state files.

How do I scale up to many fleet nodes?

Default fleet state is the local durable store, not a live cluster. To span nodes, run franken-node fleet serve --generate-token on a coordinator, copy the token file to every node, and set [fleet] control_plane_url / control_plane_token_path (or FRANKEN_NODE_FLEET_CONTROL_PLANE_URL / FRANKEN_NODE_FLEET_CONTROL_PLANE_TOKEN_PATH). franken-node fleet agent --zone <zone> then polls the coordinator and heartbeats into it. convergence_timeout_seconds governs how long fleet release and fleet reconcile wait for convergence before failing closed.

What is the difference between runtime.lanes and SchedulerLane?

SchedulerLane is the four-lane logical priority structure built into the runtime (ControlCritical, RemoteEffect, Maintenance, Background). runtime.lanes is the user-facing TOML table of named lane configs (e.g. cancel, realtime) that parameterize concrete bulkheads. Task classes are mapped to named lanes via mapping rules, and named lanes inherit the priority order of their containing SchedulerLane. Most operators only configure the named lanes; the four logical lanes are an internal invariant.

What's vectors/CHANGELOG.md for?

Per-vector test data (golden inputs, canonical fixtures, conformance seeds) has its own changelog at vectors/CHANGELOG.md. Vector bumps are governance events because they cross-reference signed conformance artifacts; keeping their history separate from the main CHANGELOG.md lets operators audit golden drift independently of code drift.

Where do I find runbooks for specific failure modes?

Operator runbooks live under docs/runbooks/. The proof-lane readiness blocker runbook (docs/runbooks/proof_lane_readiness_blockers.md) and the recovery runbook generated by franken-node verify recovery-runbook are the two most-used entry points.


About Contributions

About Contributions: Please don't take this the wrong way, but I do not accept outside contributions for any of my projects. I simply don't have the mental bandwidth to review anything, and it's my name on the thing, so I'm responsible for any problems it causes; thus, the risk-reward is highly asymmetric from my perspective. I'd also have to worry about other "stakeholders," which seems unwise for tools I mostly make for myself for free. Feel free to submit issues, and even PRs if you want to illustrate a proposed fix, but know I won't merge them directly. Instead, I'll have Claude or Codex review submissions via gh and independently decide whether and how to address them. Bug reports in particular are welcome. Sorry if this offends, but I want to avoid wasted time and hurt feelings. I understand this isn't in sync with the prevailing open-source ethos that seeks community contributions, but it's the only way I can move at this velocity and keep my sanity.

License

MIT License with OpenAI/Anthropic Rider. This is not permissive MIT: the rider grants no rights to OpenAI, Anthropic, their affiliates, or anyone acting for them, and prohibits incorporating the software (or any derivative work) into any dataset, training corpus, evaluation harness, or ML pipeline. See the full terms in LICENSE.

Explore related projects