Skip to content

feat(cli): hcs policy status + ADR 0080 (D-087) - #100

Merged
verlyn13 merged 1 commit into
mainfrom
feat/cli-policy-status
Jul 25, 2026
Merged

feat(cli): hcs policy status + ADR 0080 (D-087)#100
verlyn13 merged 1 commit into
mainfrom
feat/cli-policy-status

Conversation

@verlyn13

Copy link
Copy Markdown
Member

What

The first invocable surface this project has produced.

$ just cli policy status
snapshot  .../policies/generated-snapshot/tiers.yaml
status    loaded
digest    sha256:7e30b768700a479464d1fb3af363764ca42f6d1249f5f4f7100cc745dae10a9d  (observed, not verified — see ADR 0079)
schema    policy_rule_schema_version 0.1.0
rules     8

  OPERATION CLASS                 TIER               APPROVAL  PATH   CEILING
  read_only_diagnostic            read-safe          false     false  not_applicable
  agent_internal_state            write-local        false     false  not_applicable
  workspace_verify                read-safe          false     false  not_applicable
  cleanup_plan                    read-safe          false     false  not_applicable
  worktree_mutation               write-project      true      true   PT24H
  destructive_git                 write-destructive  true      true   PT1H
  merge_or_push                   write-destructive  true      true   PT1H
  external_control_plane_mutation write-destructive  true      true   PT1H

Change class: E — adapter read path. Ledger row D-087. ADR 0080 ships in this PR.

The citation rule worked prospectively

No ADR authorized a CLI surface. adr/0003 covers stdio + Streamable HTTP transports. adr/0079 §Out of scope excludes "any consumer of the returned rules" — which a verb rendering those rules is.

Citing either would have been trap #61's third occurrence. Checking first caught it before a false citation was written — the first time that rule has prevented rather than diagnosed.

ADR 0079's precondition, discharged

"before an adapter forwards a path argument, the public form must become kernel-resolved"

@hcs/kernel/api now exports loadBoundPolicyRules(), which takes no path and cannot be pointed at an arbitrary file. resolveBoundSnapshotPath() reads HCS_ROOT and falls back to the kernel module's own location — charter inv. 15 warns GUI apps, launchd jobs and IDE extensions do not inherit shell env, so a resolver that only read the variable would fail in exactly those contexts. The parameterized form stays for tests, which must stage mutated snapshots.

Two gate bugs, found by finally having a subject

boundary-check rule 2 is [ -d packages/adapters ]-guarded and scans adapters only. Until this package existed it had no subject. Its first real one exposed two false positives in the repair that landed in #93:

  1. The subtraction required /api/ with a trailing slash — matching @hcs/kernel/api/policy but not @hcs/kernel/api, the exact path charter:84 declares legal. The rule rejected the correct import.
  2. Stage 1 matched any occurrence of the token, so it then flagged the adapter's own documentation, where the path appears in backticks.

Fixed by anchoring the subtraction on quote/slash/end, and requiring stage 1 to match a quoted specifier — every import form quotes it; prose does not. Negative-controlled both ways:

planted  import ... from "@hcs/kernel/src/policy/rule-loader"   → RED
planted  require("../../../kernel/src/policy/rule-loader")      → RED
clean tree                                                       → ✓ ring boundaries intact

Separately, the justfile's scoped-test discovery globbed packages/*/tests and missed nested packages, so just test cli couldn't find packages/adapters/cli/tests. Now covers packages/*/*/tests; the unknown-target error lists kernel, schemas, cli.

Read-only by construction

Registers no Capability, emits no OperationShape, mints and consumes no ApprovalGrant, spawns nothing, writes nothing.

Dispatch is an exhaustive match over a closed verb list, not a lookup with a default handler — unknown verb exits 2, rejected snapshot exits 1. A test asserts policy alone is not treated as a prefix of policy status.

run(argv) returns {exitCode, lines}, so the verb is testable without spawning a process or capturing streams.

The adapter decides nothing (inv. 1). It formats the loader's result. A guard derived from policyRuleTierSchema.options asserts no tier literal appears in adapter source; a second asserts every @hcs/kernel import is exactly @hcs/kernel/api.

The digest is labelled "observed, not verified" in the output, because ADR 0079 cut provenance verification from the loader's scope and the verb must not imply a guarantee the kernel does not make. A test asserts that label is present.

The kernel surface lock fired again when the barrel widened — third time it has caught an unannounced API change.

Boundary checks

  • No policy duplicated into adapter/hook — guard-enforced against adapter source
  • No universal shell execution added — the adapter spawns nothing
  • No audit-write agent endpoint added
  • OperationShape remains upstream of CommandShape — none emitted
  • Registers no Capability; mints and consumes no ApprovalGrant
  • Ring 2 imports Ring 1 only through @hcs/kernel/api — enforced by the exports map and by test
  • Citation discipline — ADR 0080 authorizes this code and ships with it. adr/0079 and adr/0003 are cited only for what they exclude; adr/0079 §Out of scope is quoted verbatim.
  • Precedence — amends no ADR, relieves no ADR-stated rule

Validation

just verify green. just test cli 8/8, just test kernel 29/29, full suite 556.

The first invocable surface this project has produced.

  $ just cli policy status
  snapshot  .../policies/generated-snapshot/tiers.yaml
  status    loaded
  digest    sha256:7e30b768...  (observed, not verified — see ADR 0079)
  schema    policy_rule_schema_version 0.1.0
  rules     8

    OPERATION CLASS                 TIER               APPROVAL  PATH   CEILING
    read_only_diagnostic            read-safe          false     false  not_applicable
    worktree_mutation               write-project      true      true   PT24H
    destructive_git                 write-destructive  true      true   PT1H
    ...

THE CITATION RULE WORKED PROSPECTIVELY

No ADR authorized a CLI surface. ADR 0003 covers stdio + Streamable HTTP
transports. ADR 0079 §Out of scope excludes "any consumer of the returned
rules" — which a verb rendering those rules is.

Citing either would have been trap #61's third occurrence. Checking first
caught it before a false citation was written, which is the first time that
rule has prevented rather than diagnosed. ADR 0080 ships here with the code.

ADR 0079's PRECONDITION, DISCHARGED

ADR 0079 §Out of scope: "before an adapter forwards a path argument, the public
form must become kernel-resolved." So `@hcs/kernel/api` now exports
`loadBoundPolicyRules()`, which takes no path and cannot be pointed at an
arbitrary file. `resolveBoundSnapshotPath()` reads HCS_ROOT and falls back to
the kernel module's own location — charter inv. 15 warns GUI apps, launchd
jobs, and IDE extensions do not inherit shell env, so a resolver that only read
the variable would fail in exactly those contexts. The parameterized form stays
for tests, which must stage mutated snapshots.

TWO GATE BUGS, FOUND BY HAVING A SUBJECT

boundary-check rule 2 is `[ -d packages/adapters ]`-guarded and scans adapters
only. Until this package existed it had no subject. Its first real one exposed
two false positives in the repair that landed in #93:

  1. The subtraction required `/api/` with a trailing slash, so it matched
     `@hcs/kernel/api/policy` but NOT `@hcs/kernel/api` — the exact path
     charter:84 declares legal. The rule rejected the correct import.
  2. Stage 1 matched any occurrence of the token, so it then flagged the
     adapter's own documentation, where the path appears in backticks.

Fixed by anchoring the subtraction on quote/slash/end and requiring stage 1 to
match a QUOTED specifier — every import form quotes it; prose does not.
Negative-controlled both ways: a planted `@hcs/kernel/src/policy/...` import and
a planted `../../../kernel/src/policy/...` traversal both still go red.

Separately, the justfile's scoped-test discovery globbed `packages/*/tests` and
missed nested packages, so `just test cli` could not find
`packages/adapters/cli/tests`. Now covers `packages/*/*/tests` too; the
unknown-target error lists `kernel`, `schemas`, `cli`.

READ-ONLY BY CONSTRUCTION

Registers no capability, emits no OperationShape, mints and consumes no
ApprovalGrant, spawns nothing, writes nothing. Dispatch is an exhaustive match
over a closed verb list, not a lookup with a default handler: unknown verb exits
2, rejected snapshot exits 1. `run(argv)` returns {exitCode, lines} so the verb
is testable without spawning a process or capturing streams.

The adapter decides nothing (inv. 1). It formats the loader's result and does
nothing else. A guard derived from policyRuleTierSchema.options asserts no tier
literal appears in adapter source, and a second asserts every @hcs/kernel import
is exactly `@hcs/kernel/api`.

The digest is labelled "observed, not verified" in the output, because ADR 0079
cut provenance verification from the loader's scope and the verb must not imply
a guarantee the kernel does not make. A test asserts that label is present.

The kernel surface lock fired again when the barrel widened — third time it has
caught an unannounced API change.

Class E — adapter read path.

Validation: `just verify` green. `just test cli` 8/8, `just test kernel` 29/29,
full suite 556.
@verlyn13
verlyn13 requested a review from a team as a code owner July 25, 2026 20:50
@verlyn13
verlyn13 merged commit 0924599 into main Jul 25, 2026
1 check passed
@verlyn13
verlyn13 deleted the feat/cli-policy-status branch July 25, 2026 20:52
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