Pack Contract v2
Pack Contract v2 adds a versioned trust and compatibility layer on top of the
manifest shape described in Create Your First
Pack. Contract v1 (an
id/version/tools/policies manifest with no contract_version field)
validates a pack’s shape. Contract v2 additionally validates a pack’s
trust: who published it, which kernel/host versions it works with, what
other packs it depends on, and — the security-critical part — whether its
executable code is allowed to run at all.
Declaring Contract v2
Section titled “Declaring Contract v2”Add four fields to an otherwise-normal manifest:
| Field | Type | Required for v2 | Description |
|---|---|---|---|
contract_version | 2 | Yes | Must be exactly 2. Any other value fails with an upgrade diagnostic. |
publisher.id | string | Yes | Stable publisher identifier (org or account slug). |
publisher.name | string | Yes | Human-readable publisher display name. |
compatibility.kernel | string (semver range) | Yes | Kernel versions this pack works with. |
compatibility.host | string (semver range) | Yes | Host versions this pack works with. |
depends_on | { id, version_range }[] | No | Other installed packs this pack requires, and the version range needed. |
migrations | Migration[] | No | Declarative upgrade steps from an older version. See Migrations. |
depends_on cannot reference the pack’s own id and cannot list the same
dependency twice — both fail manifest parsing immediately. Whether a declared
dependency is actually installed, at a satisfying version, with no dependency
cycle, is checked later by the registration
gate, because that check needs
visibility into every other installed pack, not just this one manifest.
Executable trust tiers
Section titled “Executable trust tiers”Contract v1 dynamically imports and executes whatever capability_factory,
policy_factory, and tool entry fields a manifest declares, in the kernel
host process, with no distinction between “we wrote this” and “a manifest
declared this.” Contract v2 closes that gap with a closed trust
classification — every pack falls into exactly one of three tiers, and an
omitted or malformed declaration always resolves to the least
privileged tier, never the most:
| Tier | adapter_id | capabilities | In-process tools / factories |
|---|---|---|---|
declarative (default) | forbidden | forbidden | forbidden |
trusted-host-adapter | required | forbidden | permitted (sanctioned by the named adapter) |
isolated | required | required | N/A — runs through a capability-confined host |
This closure is enforced at manifest-parse time for every contract_version: 2 manifest — a pack cannot, for example, declare trust_tier: declarative
while still shipping a tools array. Declaring declarative with any
in-process executable entry point (tools, capability_factory,
policy_factory, cli_commands, or mcp_tools) fails validation
immediately, with an actionable message telling you to pick
trusted-host-adapter or isolated instead.
declarative — no executable authority
Section titled “declarative — no executable authority”The default tier. A declarative pack carries data only: tools, policies,
and factories are forbidden by the closure above. This is what an externally
sourced pack gets by default — it cannot trigger arbitrary in-process
dynamic imports no matter what else its manifest says.
trusted-host-adapter — vouched for by name
Section titled “trusted-host-adapter — vouched for by name”A pack whose in-process code a host operator has explicitly agreed to
run. The manifest names an adapter_id; that name only means something once
the host’s own startup code calls
HostAdapterRegistrationPort.register({ adapter_id, tier, pack_id, handle })
for it. There is no path from a manifest field to a registration call — a
pack can never self-register as trusted, and the host’s registration binds
to a specific pack_id, so a different pack copying a known adapter_id
string does not inherit its trust (see Host-adapter
registration below).
isolated — capability-confined execution
Section titled “isolated — capability-confined execution”The most restrictive executable tier. An isolated pack never receives an
adapter_id-registered in-process factory; instead it declares
capabilities — an explicit resource grant, not an ambient default:
memory.limit_mb and cpu.time_limit_ms are required, not defaulted, so an
isolated pack author must make an explicit resource choice.
network.posture deliberately excludes full — the isolated tier is the
least-privileged executable tier, so unrestricted egress is not an option
this schema can express.
Host-adapter registration
Section titled “Host-adapter registration”Trust is never inferred from a manifest field alone. Only the host —
never a pack, never a manifest — can make an adapter_id resolve to
anything:
Registration is:
- Explicit. There is no
register(packId, ...)variant reachable from a manifest. A host operator’s own startup code callsregister()for adapters it chose to vouch for. - Bound to one pack.
pack_idnames the pack this registration vouches for.adapter_idstrings follow a public, predictable naming convention (trusted-host-adapter:<pack-id>), so without this binding a different pack could declare a legitimately-registeredadapter_idin its own manifest and inherit that adapter’s trust. The trust decision checks both theadapter_idand that the registration’spack_idmatches the manifest’s ownid— an adapter registered forsoftware-deliverycan never sanction an impostor pack, no matter whatadapter_idstring it copies. - Opaque.
handleisunknownto the kernel — it never imports, inspects, or invokes it. Only the host’s own dispatch code knows what to do with it. - Idempotent by id. Re-registering an
adapter_idreplaces the prior registration, so a host reload cannot accumulate stale adapters.
The bundled packs use trustedHostAdapterId(packId) →
`trusted-host-adapter:${packId}` as their naming convention (see
@hellmai/lumenflow-host’s host-adapter-registry.ts), registered once at host
startup for every bundled pack that genuinely ships executable entry points.
knowledge ships neither tools nor factories, so it stays declarative and
is not registered here at all — least privilege means a pack that needs no
executable authority gets none.
Dispatch-time enforcement
Section titled “Dispatch-time enforcement”Registration alone only decides whether a pack is allowed to register.
Every actual tool dispatch for a trusted-host-adapter pack is checked
again, per call, against the runtime-sealed ExecutionAuthority the kernel
mints for that call: the dispatch layer reads the authority’s non-forgeable
pack.id, looks up that pack’s trust decision, and refuses to invoke the
handler unless the decision resolves to allowed: true for the
trusted-host-adapter tier. A handler that receives an ExecutionAuthority
must keep its filesystem, network, process, git, and lifecycle effects
provably within allowed_scopes; in-process handlers cannot inherit ambient
cwd, environment, credentials, .git, or .lumenflow authority. This is
the same ExecutionAuthority primitive every other in-process tool handler
already consumes — Pack Contract v2 does not invent a parallel one.
The fail-closed registration gate
Section titled “The fail-closed registration gate”Before any capability factory or policy factory is resolved for any
installed pack, the kernel runs one batch check —
validatePackContractV2Registration() — against every pack the host
resolved. It collects every diagnostic before throwing:
- Contract v1 (missing/wrong
contract_version) — rejected with an actionable upgrade message. - Incompatible
compatibility.kernel/compatibility.hostranges against the running kernel/host versions. - Missing dependencies, unsatisfied dependency version ranges, and dependency cycles.
- Duplicate command/state/telemetry namespaces across the installed set.
- Trust-tier denials — an
adapter_idthat is not registered, registered for the wrong tier, or registered for a differentpack_id.
There is no partial registration: either every diagnostic is collected and the whole batch is refused, or every pack in the batch is valid and registration proceeds. A single unmigrated or misconfigured pack cannot “mostly” install.
Declarative migrations
Section titled “Declarative migrations”A migration describes upgrading a pack’s already-installed state — never arbitrary code:
Every step is one of a closed set of kinds: rename_state_namespace,
rename_event_kind, rename_config_key, deprecate_tool. There is no
run_script or module-entry step kind, and adding one requires an ADR — a
migration step is data a host-side migration runner interprets
mechanically; it is never a pointer to code the kernel or host would
dynamically import. A migration’s to version also cannot exceed the
manifest’s own version — a migration cannot target a future the pack has
not declared.
Authoring, scaffolding, and validating
Section titled “Authoring, scaffolding, and validating”pnpm pack:author, pnpm pack:scaffold, and pnpm pack:validate consume
the same DomainPackManifestSchema and the same
validatePackContractV2Registration()/trust-decision engine described
above — not a separate or weaker copy. A manifest that would fail live
installation fails these commands too.
Both commands generate a contract_version: 2 manifest with publisher and
compatibility already filled in. If you scaffold or author at least one
tool, the manifest also gets trust_tier: trusted-host-adapter and an
adapter_id following the same trusted-host-adapter:<pack-id> naming
convention the bundled packs use — the manifest is schema-valid and ready to
receive a real registration, but no host has vouched for it yet.
pack:validate’s report includes a Contract v2 registration check
alongside manifest schema, import boundaries, tool entries, and security
lint:
That failure is expected and correct for a pack no host has vouched for yet
— Host-adapter registration is a separate,
later, host-side act; no pack:author/pack:scaffold invocation can grant
it to its own output. pack:author still writes the generated files and
reports this check honestly rather than either silently passing it or
refusing to generate the pack at all — it blocks only on checks the author
actually controls (manifest shape, import boundaries, tool entries,
security lint, integrity).