Skip to content

Source workflow definitions from a remote npm registry, end to end - #126

Open
alexanderguy wants to merge 52 commits into
mainfrom
intr-416-source-a-workflow-definition-from-a-remote-npm-registry-end
Open

Source workflow definitions from a remote npm registry, end to end#126
alexanderguy wants to merge 52 commits into
mainfrom
intr-416-source-a-workflow-definition-from-a-remote-npm-registry-end

Conversation

@alexanderguy

@alexanderguy alexanderguy commented Aug 7, 2026

Copy link
Copy Markdown
Member

Closes INTR-416.

The walking skeleton for code-sourced workflow definitions — it builds every new seam once and proves it with a passing e2e that installs a fixture workflow package from a test registry, probes it, approves and freezes a content hash, deploys by source-ref, and runs it to completion.

The path

  1. Install + resolve — the hub resolves a code-sourced workflow's dependency closure from a registry (packument / package.json reads, no code execution).
  2. Probe — the closure is evaluated in an airlocked one-shot child on the sidecar; the child projects the live definition to an inert wire form and returns it. No ambient inputs; never the hub, never the sidecar host.
  3. Gate + freeze — the hub recomputes the wire hash, gates the advertised grant surface against the operator's ApprovalSet, fails closed on mismatch, and records the approved hash + frozen closure on the version row. Identity moves off the one-def-per-asset projection to an (assetId, wireHash) content hash.
  4. Deploy by source-ref — the deploy frame carries the source ref + frozen closure + the frozen inert projection and its approved hash verbatim (no recompute).
  5. Re-materialize + evaluate — the sidecar applies the frozen closure to a local package dir, evaluates the pinned code to a live definition, and re-verifies by project-then-hash against the approved hash (fail closed) before running the live definition.
  6. Re-verify at every load boundary — top-level run (fresh / resumed / restored) and onTrigger bodies, each against its own out-of-band-pinned hash.

Security model

Approval binds to the wire-projection hash (computeWireDefinitionHash over the inert projection), not hashDefinition / projectForHash, which drop grant-bearing fields.

The re-verify barrier is load-bearing only where the approved hash arrives out-of-band from the bytes being checked (documented in docs/AUTH.md): the source-ref closure eval, the top-level inert read, and onTrigger bodies (whose hash is intrinsic to the parent's approval). A childWorkflow references a separately-approved asset the parent holds no hash for, so that spawn reads and envelope-validates without a gate — its integrity is the workflow-kind repo's hub-writes / sidecar-reads authorization plus push-time validation. Primary runtime grant enforcement remains the deploy-time credentials snapshot.

Follow-up disposition

  • Source-ref restart-restore — implemented on this branch. A sidecar restart now re-materializes the pinned closure (a cache hit against the surviving content-addressed cache, SRI-verified) and re-spawns the deployment as source-ref, instead of refusing it. The deployment record became a discriminated union that requires source + closure + approvedWireHash for a source-ref record (so a restored child re-verifies against the hub-approved pin, not a recompute), which also lets the hand-rolled restore guard be deleted. Orphaned materialized closures are reclaimed before re-apply and on undeploy.
  • childWorkflow-asset TOCTOU re-verify → INTR-443. The terminal childWorkflow spawn is deliberately ungated (documented in spawn-child.ts): the disk-tamper threat is dominated by larger equivalent loose reads on the same data dir (sources.json, grants.json), substantially covered by the workflow-kind repo's hub-writes/sidecar-reads authorization + push-time validation, and owned by host isolation. Whether a childWorkflow should instead be an owned, parent-namespaced sub-workflow (making a barrier coherent) is a product decision filed as INTR-443.
  • Runtime frozen-grant ceiling → already enforced. The unconsumed approvedDeployGrants wire field was removed rather than shipped unenforced. The actual guarantee ("a workflow can't acquire run-time grants beyond its approved walk") is already delivered by the per-deployment frozen capability walk (which pins run grants to the content-addressed approved definition) plus the content-hash identity — an explicit cap on top would be a no-op in the honest case.
  • Rotation record read-modify-write → INTR-444. The sources-rotation handler rebuilds the deployment record from the live spec, which re-arms a "restore-critical field silently dropped on rotation" bug class; converting it to a read-modify-write of the persisted record is filed as INTR-444.

Testing

make all green: 6060 tests, 0 failures, including the walking-skeleton e2e and the full integration suite. Rebased onto main (regenerated the two workflow-definition migrations as 0075 / 0076 after main's credentials migrations).

@linear

linear Bot commented Aug 7, 2026

Copy link
Copy Markdown

INTR-416

A code-sourced workflow package names the module whose evaluation
produces its definition. Accept that entry alongside interchange.tools,
rejecting non-string values at the validation boundary.
Approval records the operator-approved wire hash for a version so a
later definition load can re-verify against it. Null is the legitimate
pre-approval state, so the column takes no NOT NULL constraint.
The deploy gate, the install-time probe, and re-verify must derive a
deployment's content handle from the identical canonical form. Housing
the hash and its canonical serializer in one shared module makes that
the single source of truth every party imports.
A code-sourced workflow declares where its definition is fetched from.
Model the source as a discriminated union so further source kinds can be
added without a breaking change.
The install-time probe ships a workflow's needs-surface -- its tool
grants, model identity, and structure -- to the hub as plain data, and
identity, approval, and re-verify all bind to a content hash of that
form. A naive serialization drops the function-valued tool factories and
silently loses the grant surface, so the projection reads each factory's
declared grants into plain data and fails loud on anything it cannot
reify.
Before a workflow's code can be probed it must be pinned. Resolve the
full dependency closure to concrete versions and integrity hashes on the
hub, reusing the tool-package resolver, reading only registry metadata
and never executing package code.
A code-sourced workflow is defined by evaluating its module. Import the
declared entry from a materialized package, take the single export that
validates as a definition, and reject anything ambiguous or malformed.
The loader consumes an already-materialized package so the host layer
need not depend on the packaging layer.
The hub needs a connected sidecar to evaluate a workflow and report its
needs-surface before any deployment exists. Carry that request and its
result on a dedicated frame pair with its own timeout and disconnect
cleanup, correlated by request id and kept off the agent-address routing
a deployed instance uses.
One asset can hold many workflow definitions, so identity cannot be one
definition per asset. Key a definition by its asset paired with the
content hash of its wire projection; callers compute that hash and
resolve by selector. This removes the one-definition-per-asset index in
the same change that teaches every caller to pass a hash, so no window
exists where the two disagree.
Discovering a code-sourced workflow's needs-surface means evaluating
author code, which must run neither on the hub nor on the sidecar host.
Spawn a one-shot child behind the IPC airlock that evaluates the pinned
code and returns only the inert projection, grant set, and content hash;
reap it on every exit path so a wedged evaluation cannot leak a process.
The needs-surface a probe reports is advisory. Before trusting it, the
hub recomputes the content hash over the received projection and rejects
a mismatch, and holds every advertised grant to the operator's approval.
On approval it freezes the recomputed hash and the advertised grant set
as the single source of truth the deploy path stays a subset of, so a
workflow can never gain at deploy or run time a grant it did not have at
approval.
The deploy hand-off carries the pinned source and the frozen dependency
closure instead of an inline definition, along with the hub-approved
content hash. The child's identity hash is that approved value rather
than a recompute on the sidecar, so what the operator approved and what
the deployment runs are bound to the same content. Deploy grants are
materialized as a subset of the frozen approved set.
Re-reading and re-walking the definition on every mail-triggered run let
a mutated asset blob under a stable id change the grants a run receives.
Walk once per deployment and reuse the frozen result, keyed by the
content-addressed definition identity, so run grants follow the approved
content and nothing downstream re-derives them per message.
Laying a resolved dependency closure out on disk and evaluating its
entry module were fused in one method. Split the eval-free layout into
its own callable step so a caller can materialize a closure into a
package directory without importing any of its code; the loader now
composes that step with its unchanged evaluation phase.
The sidecar materializes exactly the closure the hub pinned at approval,
verifying each entry's integrity before extraction and resolving nothing
anew, then loads the pinned entry. What the deployment runs is the same
content the operator approved, down to every transitive dependency.
A definition is read from disk every time a workflow loads -- fresh run,
resume, and child-body spawn. Recompute its content hash there and fail
closed when it disagrees with the hub-approved hash, so a definition
swapped after approval halts the load. Placing the check at the load
boundary rather than at run start is what covers resumed runs, whose
run-start events never re-fire.
The probe executor was built but never connected, so a live sidecar
answered every probe with a rejecting placeholder. Construct the
production closure materializer and inject the executor through the
orchestrator, so the sidecar materializes a probed workflow's pinned
closure, evaluates it in the airlocked child, and returns its
needs-surface. The request's single-package cardinality and its entry
are validated before the child spawns.
The live-inert projector's test suite uses arktype, declared as a
workflow-deploy dev dependency. Record its resolution in the lockfile so
a frozen install matches the manifest.
The deploy frame builder recomputed a content hash from the live
definition on every path. A source-ref deployment is approved and later
re-verified against the hash of its inert projection, and the live and
inert forms hash differently, so the child rejected the deployment
closed. Carry the frozen hash and the inert projection through unchanged
on the source-ref path, and let the live-authored path keep recomputing
its own.
The pieces to deploy a code-sourced workflow existed but could not be
driven together: approve discarded the inert projection and frozen
closure the source-ref deploy needs, and neither it nor the frame
builder was reachable outside the package. Return the projection and
closure from approve and add one public entrypoint that carries them
into the deploy frame, so a caller cannot recompute the hash, re-resolve
the closure, or otherwise wire the hand-off wrong.
The tarball reader consumed the response body through the web
ReadableStream API, but the registry fetcher returns a Node stream with
no such method, so every real registry tarball fetch threw before a byte
was read. Read whichever stream shape the body actually is, keeping the
streamed size cap and the abort deadline intact on both paths.
The workflow child must project a definition to its inert form to
re-verify it on load, but the child's package must not depend on the
deploy package where that projector lived. Move it into the workflow
package, which the child already depends on, so the projection is
reachable without crossing that boundary.
A source-ref deployment materialized the inert projection and tried to
run it, but the projection is an approval surface the runtime cannot
execute: its agents carry no live inference and its tools are data, not
functions. At the load boundary, evaluate the pinned closure to the live
definition, re-verify that it projects to the approved hash, and run
that. Live-authored deployments keep reading their inert definition
unchanged.
One test installs a workflow package from a test npm registry, probes it
in the airlocked child, approves and freezes its content hash, deploys
it by source reference, has the sidecar re-materialize the pinned
closure and evaluate the pinned code, and asserts the deployed workflow
runs to completion -- exercising closure resolution, the probe, the
gate, source-ref deploy, sidecar apply, load-boundary re-verify, and
per-run grant materialization in a single flow.
Evaluating a pinned closure and driving a run to a terminal event is
real work that the default per-test timeout does not accommodate once
the full suite is competing for the machine. Give the closure-evaluating
source-ref tests an explicit timeout in line with the other integration
tests so they do not flake under load.
The load-boundary re-verify recomputes the wire hash of the
definition a respawn reads and fails closed unless it matches
DEFINITION_HASH. The single-step conversation-durability test spawned
its warm child with a placeholder DEFINITION_HASH, so the re-verify
rejected the respawn and the run stalled. Compute the true wire hash
of the seeded workflow.json and feed that, matching how a production
restore carries the persisted approvedWireHash.
The workflow runtime's commit-chain and pending-event buffers are
module-scoped maps keyed by runId, shared across every run in the
process and dropped only when a run drains to terminal. A sibling
test fires a 'run-1' run and shuts down without draining it, leaving
a buffered RunStarted under that key. The source-ref fresh-trigger
test reused the same literal, inherited the stale buffer, read as
already-started, and stalled with no schedulable primitives. Give it
a per-test unique runId so its chain stays isolated regardless of
sibling ordering.
The referenced-definition resolver forced both spawn types through
one mandatory approved-hash gate sourced from a map only one type
can populate, so every childWorkflow and onTrigger-body spawn failed
closed. Split it into two explicit contracts over one shared
read+validate helper: an onTrigger body is part of the parent's
approval, so its hash rides the signed frame and the body path
re-verifies against it; a childWorkflow references a
separately-approved asset the parent holds no hash for, so it reads
and envelope-validates without a gate -- a gate there could only
fail-closed-always, and the asset's integrity is the workflow-kind
repo's hub-writes/sidecar-reads authorization plus push-time
validation.

Thread the parent's frame-carried body hashes from the substrate
factory into the suspendable adapter, and persist them on the
deployment record so the body re-verify survives a sidecar restart,
matching how the top-level approvedWireHash is carried across
restart. Document the out-of-band-pin rationale in AUTH.md.
A source-ref deployment's runnable definition is the evaluated pinned
closure, not the on-disk inert workflow.json (a non-executable
approval surface). The deployment record carried no lineage, so a
boot-time restore re-read the inert projection, resolved lineage to
live-authored, and would have run the non-executable shape. Persist a
lineage discriminator on the record and have restore refuse a
source-ref record loudly, keeping the record for a later boot that
re-materializes the closure. Full source-ref restart-restore (persist
source + closure, re-evaluate on boot) is a separate follow-up.
The source-ref deploy frame computed and carried approvedDeployGrants
(a subset of the gate's frozen approved set), but no sidecar code
consumed it: the enforced per-step authorization boundary is the
deploy-time credentials snapshot, sourced from the operator's
config.grants. A security field that is computed, shipped, and
ignored claims a guarantee it does not deliver, so remove it -- the
wire field, its session-service population, and the now-dead
materializeDeployGrantsFromFrozen helper. The deploy-time gate/freeze
(the approval decision, and the per-run grant-walk freeze) is
unaffected. Enforcing the frozen advertised set as a runtime ceiling
is a separate follow-up.
Rebasing onto main renumbered this branch's two workflow-definition
migrations after main's credentials migrations, regenerated via
drizzle-kit as 0075 (approved_wire_hash) and 0076
(workflow_definition_content_hash). Format the drizzle-emitted meta
JSON to satisfy the lint gate, and add the wire_hash column to the
workflow-definition parse-row test fixture that main's row shape did
not carry.
A sidecar restart refused to bring a source-ref deployment back --
its runnable definition is the evaluated pinned closure, not the
on-disk inert workflow.json, so restoring it as live-authored would
run the non-executable shape. Persist the source + frozen closure on
the deployment record (behind a discriminated union that requires
them, plus approvedWireHash, for a source-ref record -- so a restored
child re-verifies against the hub-approved pin, not a recompute) and
have restore re-run applyFrozenWorkflowClosure and re-spawn as
source-ref. The tarball fetch is a cache hit against the surviving
content-addressed closure cache, SRI-verified, and time-bounded.

Reclaim the deployment's closure instance dir before the re-apply and
in undeploy so repeated restarts do not accumulate orphaned
materialized closures.
The terminal childWorkflow resolver reads a separately-approved asset
with no out-of-band pin to verify against. Spell out why a re-verify
gate there is deliberately absent: it would only defend local-disk
tamper of SIDECAR_DATA_DIR (out of the sidecar's threat model, one of
several equivalent-or-worse loose reads), the hub-writes/sidecar-reads
authorization plus push-time validation is the real boundary, and a
per-ref frame-delivered pin would regress the reference from late- to
early-bound. Whether a childWorkflow should instead be an owned,
parent-namespaced sub-workflow is a product decision tracked
separately.
The hub's deploy-frame builder spelled the non-source-ref lineage
"live" while the sidecar (env, deployment record, spec, run child)
spelled it "live-authored" -- one concept, two literals across the
wire boundary. Standardize the hub side on "live-authored" so a
lineage-equality check reads the same on both sides.
The source pin and its frozen dependency closure are meaningless
apart: a source with no closure names bytes the sidecar cannot
materialize, a closure with no source is a dependency tree for
nothing. Yet they rode the deploy frame, the deploy-frame args, the
deployment record, and the deploy spec as two independently-optional
fields, so every consumer re-encoded "both present or both absent" as
a paired presence check and every fixture could express the illegal
half-populated state.

Introduce a `SourceRefPin` (co-required `source` + `closure`) in
@intx/types/sidecar -- mirroring the probe frame that already
co-requires the same two -- and thread it through as one field.
`AgentDeployWorkflow` now carries `sourceRef?: SourceRefPin`; the
frame-args source-ref arm, the deployment record's source-ref union
arm, and `WorkflowDeploySpec` all carry the pin as a unit. Each
consumer collapses to a single presence check on the pin, and
`buildDeploymentRecord`'s guard drops from three required fields to
two (the pin plus the approved hash). The record test now also
asserts a half-populated pin is rejected by the pin's own
co-requirement, closing the gap the two-field shape left open.
The deploy path and the boot-time restore path each built an identical
`applyFrozenWorkflowClosure` call by hand: the deterministic instance
dir, the cache root, the two substrate byte caps, and the registry
table. Two copies of the same plumbing meant a cap or path change had
to land in both, and the deploy copy silently lacked the restore
copy's reclaim-first step.

Pull the shared apply into one factory-scoped helper,
`materializeDeploymentClosure`, a sibling of `buildDeploymentRecord`.
Each call site collapses to a single call plus its own result
consumption, which stays divergent on purpose: deploy validates the
evaluated definition and takes `packageDir`; restore takes
`packageDir` and re-carries the pin on its spec.

The helper always force-reclaims the instance dir before applying.
`deploymentId` is deterministic per agent address, so a redeploy or a
restore reuses the same dir and a prior soft-failed deploy or dead
process can leave it half-materialized; the rm makes the apply write
into a clean dir either way (a no-op on a never-deployed address).
This closes a latent gap on the deploy path, which previously did not
reclaim. The rm is safe only because no live reader holds the dir when
it runs -- a precondition each caller still establishes and states at
its own site (single-flight-guarded pre-spawn on deploy; dead prior
process, serial pre-connect on restore).
Origin/main added migrations 0075-0078 (workflow-run launch spec,
sidecar allocation, run dispatch), colliding with this branch's
hand-numbered 0075/0076. The branch's two schema additions -- the
`(asset_id, wire_hash)` content-hash identity on `workflow_definition`
and the `approved_wire_hash` column on `workflow_definition_version` --
are carried by their schema source changes, so their generated
migration artifacts were dropped during the rebase and regenerated once
here as a single 0079 off main's 0078 snapshot.
The rebase surfaced four points where main's new code met this branch's
changed contracts:

- deployWorkflowDefinition dropped `definition` from its params
  destructure while still hashing it for the content-hash selector.
- main's exclusive-allocation path (workflow-allocation-service) called
  ensureWorkflowDefinitionForAsset with a bare asset id; it now passes
  the (assetId, wireHash) selector, computing the same
  computeLiveDefinitionHash as the normal deploy path so both key a
  definition identically.
- the probe tests' accept-any authenticator returned the old
  `kind: "sidecar"` identity; main renamed the discriminant to
  `"shared"`.
- the orchestrator-probe test omitted main's now-required
  applyWorkflowRunPack config field; a no-op applier satisfies it since
  the test exercises only the probe path.
@alexanderguy
alexanderguy force-pushed the intr-416-source-a-workflow-definition-from-a-remote-npm-registry-end branch from 41dca4b to 5af4b87 Compare August 7, 2026 18:16
Code-sourced workflow code is evaluated on the sidecar, so it matters
that the sidecar is not a trust anchor for authorization. Add a section
making the model explicit and audited: credentials are decrypted and
delivered hub-side (the sidecar holds no cipher), the running code is
closure-pinned and re-verified against the hub-approved hash, and the
sidecar's capability walk is therefore advisory. The operator's
ApprovalSet gates it, and a compromised sidecar can only over-report
(declined), under-report (under-provisioned, fails closed), or lie
about the projection (re-verify fails closed) -- none of which escalate.

State the honest caveats rather than overclaim: the child-side grant
gate is defense-in-depth (the sidecar writes grants.json, so it is not
a boundary against a compromised sidecar -- but forging a grant yields
no new credential material and no new code); the grant list is not a
compute/network sandbox (host isolation bounds that); and the advisory
list's accuracy is informed-consent integrity, not an escalation
barrier.
The child re-verifies its own recompute of the wire projection against
the HUB-approved hash. The sidecar sourced that hash as
`spec.approvedWireHash ?? computeWireDefinitionHash(spec.definition)`,
so an absent hash silently made the sidecar its own hash authority and
collapsed the re-verify to a self-check -- the exact integrity
downgrade the field exists to prevent. Both feeds into the spawn core
always carry it (the hub deploy builder stamps it; a restore
re-attaches it from the persisted record), so an absent hash is a
wiring bug. Replace the recompute with a fail-loud guard.

Every deploy path that fed the spawn core was free-riding on that
recompute:
- The sidecar unit tests built raw frames with no hash. The shared
  frame helper now defaults a placeholder (production always stamps
  one) with an `omitApprovedWireHash` opt-out for the new fail-loud
  test; the four files that build frames inline stamp a placeholder.
- The integration suite's 35 per-test deploy callbacks sent the frame
  straight through the hub router with no hash. Rather than duplicate
  the computation in each, the deploy-flow harness -- standing in for
  the hub -- wraps its `sendAgentDeploy` to stamp the real hash when a
  workflow frame arrives without one, mirroring what production's
  `sendMultiStepDeployFrame` does.
`runOneShotProbeChild` raced the result line, `handle.exited`, and the
deadline. A probe child writes its result line and then exits promptly,
so the buffered-line read and `handle.exited` both become ready at once;
when the exit arm won that race a valid, already-written result was
discarded and the probe failed spuriously.

Exit is not a distinct outcome the line read misses: when the child
exits its stdout write end closes, so `readResultLine` settles either
with the trailing line or with null (the existing "closed its output"
branch). Race only the line against the deadline -- a child that neither
writes nor exits is still caught by the deadline -- so all three terminal
cases stay covered with the race hazard removed.
`createDbFrozenApprovalWriter` ran the ensure-definition writes and the
approved-hash stamp as two separate statements. A crash between them
persists a version row with a NULL `approvedWireHash`, which the schema
treats as the legitimate "not yet approved" state -- so a half-failed
freeze is indistinguishable from an un-approved definition. Wrap both in
one `db.transaction` so the freeze is all-or-nothing.

The stamp's `WHERE version = FROZEN_VERSION` is hand-coupled to the
version the ensure helper projects; a future drift would silently update
zero rows and persist no hash. Add `.returning()` and throw unless
exactly one row was stamped, so a drift fails loud rather than open.
The wire canonicalizer preserved `undefined`-valued object keys (emitting
an invalid `{"k":undefined}` token) and did not normalize `undefined`
array elements. Its output was therefore not invariant to a JSON
round-trip: the hub hashes a projection parsed off the JSON wire (where
such keys are already gone) while a child hashes the in-memory
projection, so a projection carrying an `undefined`-valued field would
hash differently on the two sides and re-verify would fail on a
legitimately-approved definition.

Mirror `JSON.stringify` for the values JSON cannot represent -- drop
`undefined`/function/symbol object keys, render such array elements as
`null` -- keeping only the deterministic key ordering as the intended
difference. The prior header comment claimed the preservation was
intentional; correct it.
`parseLineage` already guarantees the correlation -- a source-ref env
carries `closurePackageDir`, a live-authored one does not -- but the
`SpawnTimeEnv` interface declared the two as independent fields
(`lineage` plus `closurePackageDir: string | undefined`), so the run
child had to re-check "source-ref but no closurePackageDir" with a throw
that could never fire.

Split the interface into a `SpawnTimeEnvBase` plus a discriminated union
on `lineage`, and give `parseLineage` that union as its return type so
the correlation is expressed once at the boundary. The run child's dead
presence guard is deleted; narrowing on `lineage` now types
`closurePackageDir` as a plain string on the source-ref arm.
`materializeClosure` filtered the manifest entries to the host, laid
those out, then returned only the store dir -- so `loadManifest`
re-derived the same host filter with a second predicate
(`entryMatchesHost`) purely to avoid re-emitting the platform-filter
debug logs. Two filters that must agree, or the phase-3 load diverges
from what was materialized.

Return the filtered entries alongside the store dir and load exactly
those. The second filter pass and its drift risk are gone;
`entryMatchesHost` stays only as the shared helper inside
`passesPlatformFilter`.
`tool-materialization` re-exported `readRegistries` solely so the
deploy-apply wiring's import path did not have to change; the definition
lives in `sidecar-materialization-config`, which `index.ts` already
imports it from directly. Delete the pass-through and import
`readRegistries` from its home module in `workflow-host-wiring`.
`readWorkflowDefinitionEnvelope` prefixed its read/parse/validate errors
"workflow-host verified-definition loader", but it is shared by the
gated loaders AND the deliberately-ungated childWorkflow spawn, so an
operator debugging an ungated read saw a "verified" label the path does
not carry. Relabel those errors "workflow-host definition read"; the
re-verify errors that DO gate keep their "verified" wording in the
loaders that own them.
The projector enumerates each `AgentDefinition` field rather than
spreading it -- deliberately, so it can drop credential-adjacent fields
and reify function-valued tool factories. The cost is that a
grant-bearing field added to `AgentDefinition` but not added to
`projectAgent` would be silently dropped from the inert projection, and
thus from the approval hash and the operator's gated view. Record that
invariant at `projectAgent` and point to the grant-surface reification
tests that lock the current coverage.
Three coupled cleanups to the deploy-frame contracts in @intx/types:

- The `WorkflowStep` schema was ten near-identical arms whose per-variant
  fields were all optional passthrough, so the union validated almost
  nothing beyond the `kind` discriminant while adding ~110 lines of
  ceremony. Collapse it to one schema over the ten-kind enum plus the
  common fields. The load-bearing membership check is preserved; the
  handful of per-arm scalar validations that were enforced on some arms
  are intentionally dropped (the wire defers primitive-field validation
  to @intx/workflow).

- `AgentDeployWorkflow` restated the `definition` + `sources` +
  `approvedWireHash` field set and the byte-identical
  stepOrder-covered-by-sources narrow that `WorkflowProjectionWithSources`
  already carried -- a duplication this branch had widened. Express it as
  `WorkflowProjectionWithSources.and(...extras)` so the shared base and
  its narrow are defined once.

- With the union collapsed, sidecar.ts was still over 1000 lines. Extract
  the wire-step schema and the projection contracts to a new
  `wire-workflow.ts`, re-exporting the public names from sidecar.ts so
  `@intx/types/sidecar` consumers are unaffected. sidecar.ts drops to 918
  lines.
loader.ts had grown to ~1930 lines spanning four concerns. Decompose it,
leaving loader.ts as the author-code loader (createToolLoader + the
interchange-entry import/eval helpers) at ~940 lines:

- loader-internal.ts -- the shared failure type (ToolLoaderError), the
  fetch byte/time caps, and the small cross-cutting helpers. Split out so
  the two concern modules and loader.ts can all depend on them without an
  import cycle.
- registry-fetch.ts -- building the npm-registry-fetch options, deriving
  a default tarball URL, and reading a response body under a byte cap.
- store-layout.ts -- materializeClosure plus the eval-free closure
  materialization and per-instance store layout it drives (cache extract,
  range resolution, hardlink layout, direct-dependency symlinks).

loader.ts re-exports every moved public member (ToolLoaderError, the
DEFAULT_* caps, buildRegistryFetchOpts, readResponseWithLimit,
materializeClosure, storeEntryDir) so `./loader` and package-root
consumers are unaffected. The only cross-module cycle is a type-only
import of the materializeClosure arg/result shapes back from loader.ts,
which has no runtime effect.
createDbFrozenApprovalWriter had no coverage -- its gate-level callers
substitute a mock persist -- so the transaction that makes the freeze
all-or-nothing was untested. Add a tests/db suite over the real writer:
one test asserts a single freeze projects the definition AND stamps the
approved hash on its version row (both writes present => committed
together, not left NULL-hash), and one asserts a repeat freeze is
idempotent. Export createDbFrozenApprovalWriter from @intx/hub-sessions
so the harness can reach it.

The rowcount guard is documented as not exercised: the ensure step
always guarantees a FROZEN_VERSION row before the stamp, so the guard
defends a code-level constant drift rather than a reachable data state.
Add a regression guard for the dropped exit race arm: a mock probe child
handle whose `exited` is already resolved AND whose stdout carries a
valid HMAC-signed result reproduces the write-then-exit race
deterministically. With the former exit arm the resolved exit would win
the race and discard the buffered result; racing only the line, the
executor must return the projection, grants, and hash. The signed line is
built from the same envelope/HMAC primitives the child uses, so the test
exercises the real parse path.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant