@orpc-agent/cli
v5.0.0
Published
Capability inventory and CI drift gate for oRPC Agent applications. Independent community project, not affiliated with or endorsed by the oRPC maintainers.
Maintainers
Readme
@orpc-agent/cli
Capability inventory and CI drift gate for oRPC Agent applications.
Answers two questions about a repository: what can an agent reach from here, and did that change in this pull request.
pnpm add -D @orpc-agent/clinpx orpc-agent initinit is interactive: it finds the module that assembles your application, loads it through the same child-process loader every other command uses, shows what it found — capabilities, exposure, and whether runtime-level policies are in scope — and writes the orpcAgent config, the baseline snapshot, and the CI script once you confirm. It writes nothing before the summary.
Found in src/app.ts
capabilities 2
exposed 2
approval-gated 0 declared in meta.approval
runtime policies gate-model-writes
Write this to package.json? (Y/n)Then:
npx orpc-agent inspect10 capabilities · 10 exposed
risk 6 low · 3 medium · 1 high · declared gates: 1 approval · 4 policy-scoped capabilities · 1 runtime policy
CAPABILITY EFFECT RISK EXPOSE APPROVAL POLICIES
cases.escalate write medium aiSdk, direct, test — —
customers.get read high aiSdk, direct, mcp, test — —
messages.send external high aiSdk, direct, test required —
orders.refund write high aiSdk, direct, test — runtime:gate-model-writes, refund-limit
Runtime policy scope — evaluated before capability policies when scope matches
POLICY PHASES SCOPE MATCHES
gate-model-writes invocation effect(destructive|write) · surface(aiSdk) 3Point --entry at a defineGovernance value — it names the registry and the runtime-level policies, and it is safe at module scope, so it does not matter that the runtimes serving traffic are built inside a factory the CLI will not call:
export const governance = defineGovernance({
registry: capabilities,
policies: [gateModelWrites],
});
const runtime = createAgentRuntime({ governance, approvals: { coordinator } });An AgentRuntime works too — it carries the governance it was built from. A bare registry also works, and names no policies at all; when that is what the entry resolves, the output says so in place of the count:
10 capabilities · 10 exposed
risk 6 low · 3 medium · 1 high · declared gates: 1 approval · 1 policy-scoped capability · runtime policies not observedThe gate
Commit the snapshot, then let CI fail on drift:
npx orpc-agent snapshot # writes capabilities.snapshot.json
npx orpc-agent check # exit 1 if the app no longer matches itThe snapshot is deterministic — no timestamps, no tool version, no absolute paths — so a clean run is byte-identical and any diff is a real change. When one shows up, check sorts it by what it means:
Capability drift — 3 changes, 2 widening
WIDENING — the agent gained reach, or a control weakened
orders.refund expose now exposed on mcp
billing.charge approval approval no longer required
NEUTRAL — contract changes worth reading
orders.refund description description changed — the model reads thisTwo of those classifications are deliberately counter-intuitive:
sideEffectchanges count as widening in both directions. Declaring less than before (write→read) silently stops every policy keyed onsideEffectfrom matching. That weakens governance exactly like a new exposure does.idempotent: false → trueis widening. It is the flag that lets the runtime retry a write.
Also widening: a lowered risk, a removed policy, removed redaction, a new capability that arrives already exposed, and a procedure that gains meta.agent.
Removing a runtime-level policy is widening too, and it is the reason the snapshot records them at all. Deleting one entry from defineGovernance({ policies: [...] }) can strip a conditional approval gate from every capability at once while leaving every per-capability field byte-identical — the largest possible change to what a model can reach, in one line, with nothing else to see:
WIDENING — the agent gained reach, or a control weakened
(runtime) runtime.policies runtime policy removed: gate-model-writes — any approval,
denial or hiding it added within its declared scope is goneLosing sight of them is widening as well: if a snapshot recorded runtime policies and a later run does not observe any (because --entry was repointed at a bare registry), check fails rather than quietly reverting to a weaker check.
Commands
| | |
|---|---|
| orpc-agent init | interactive setup: entry, export, baseline, CI script |
| orpc-agent inspect | print the inventory (--json for the raw snapshot) |
| orpc-agent snapshot | write the snapshot file — also how you update it |
| orpc-agent check | compare the app against the snapshot file |
Exit codes are part of the contract: 0 clean · 1 drift · 2 could not run. CI has to tell "the inventory changed" apart from "the tool never loaded the app", because the second one passing silently is how a gate rots.
Useful flags: --verbosity min|normal|detail (min stops at the headline; detail adds descriptions and declared metadata per row, and makes check print the inventory it gated; --detail is the shorthand), --fail-on widening (let narrowing changes through), --format github (annotations) or --format md (PR comment), --export <name> when a module exports several registries, --no-descriptions, --timeout, --import <module>, --plain.
Terminal output
inspect renders a richer view — colour-coded side effects and risk, and the runtime-policy state as a panel rather than a trailing line — when it is attached to a terminal. It falls back to the plain renderer, byte for byte, whenever output is piped or redirected, CI is set, NO_COLOR is set, --plain is passed, or the optional UI dependencies are absent.
check is always plain. Its output is a report that gets pasted into pull requests and read out of CI logs, so it stays stable text with no rendering framework in its path at all.
The interactive layer is Ink, declared in optionalDependencies and loaded with a dynamic import(). An install that skips optional dependencies keeps the gate to this package plus core, and everything except init still works:
pnpm add -D @orpc-agent/cli --no-optionalDefaults can live in package.json, which makes the CI step just orpc-agent check:
{ "orpcAgent": { "entry": "src/app.ts", "export": "governance" } }GitHub Actions
- run: pnpm build
- run: npx orpc-agent check --format githubHow it loads your application
The entry module is imported in a child process, because importing an app runs its top-level code: it may open pools, start servers, or keep the event loop alive forever. Isolating it means the CLI can time out and hard-exit without inheriting any of that. The child gets ORPC_AGENT_INSPECT=1 so an application can guard its own import-time work:
if (process.env.ORPC_AGENT_INSPECT !== "1") await connect();TypeScript entries load natively on Node ≥ 22.18. On older Node the CLI uses tsx or jiti if the project already has one — neither is a dependency of this package — and otherwise says so instead of guessing. --import <module> overrides the choice; pointing --entry at compiled JavaScript needs nothing at all.
Exports are found by shape: a value from defineGovernance, an AgentRuntime, or a createCapabilityRegistry registry. A module exporting all three is not ambiguous — whichever carries the most governance wins, governance first. Exports resolving to genuinely different registries still ask for --export. A function export is refused rather than called — calling it could connect, migrate, or charge a card. The inventory is read from a value, never produced by invoking your code.
Snapshot versions
Snapshots are written at version 3, which adds runtime-policy scope and resolved candidate capabilities. Version 2 files remain readable and gain neutral scope-observability drift until refreshed. Version 1 files still mean "runtime policies were never observed". Upgrading does not break a committed snapshot or a --fail-on widening gate.
What this does not see
This is a static inventory. It reports what the registry, the metadata and the runtime configuration declare, and deliberately does not pretend to more:
It does not evaluate policies, and never will.
evaluateneeds a real actor, surface, input and context, and may do I/O. The tool reports authoritative scope and current candidate capabilities, never the decision returned for a real invocation. A capability a policy would hide from everyone still appears here, and theAPPROVALcolumn shows only whatmeta.approvaldeclares.This is why the headline says
declared gatesand runtime candidates carry aruntime:prefix.checkis a diff of declarations and applicability, not a proof of runtime verdicts.Runtime policies are only in scope when a runtime is.
--entryresolving a bare registry means they were never observed — reported asruntime policies not observed, which is unknown, not none. A snapshot taken that way cannot detect a runtime policy being deleted.Adapter-level
toolNamingis invisible.toolNamesis derived from metadata; an adapter configured with its own naming function overrides it.Composite capability-scoped policies appear under the composite's name, not their members'. Runtime-level composites are flattened, matching what audit events record.
Guarding against new code that is wrong from the start is a different job from guarding against change — a rules engine, not a snapshot. That is not in this version.
Programmatic use
buildSnapshot, diffSnapshots, loadSnapshot, and the renderers are exported for building your own gate or report.
Docs: CLI reference · a drift gate in CI · repository
MIT · Independent community project, not affiliated with or endorsed by the oRPC maintainers.
