Skip to content

docs: document three-auditor panel and fix-loop protocol#1

Merged
edsmkt merged 6 commits into
mainfrom
nm/secrets-possession-boundary
Jul 24, 2026
Merged

docs: document three-auditor panel and fix-loop protocol#1
edsmkt merged 6 commits into
mainfrom
nm/secrets-possession-boundary

Conversation

@edsmkt

@edsmkt edsmkt commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Intent

CHECK: secrets-possession-boundary
HEAD: 17310ae (branch nm/secrets-possession-boundary; secrets implementation landed as 66c692f6145fd7e876f97 on main)
SCOPE:

  • observer_kit/cli.py (load_secret_entries, wrap_command_with_secrets, ensure_secrets_approval,
    record_secrets_injection, resolve_op_bin, snapshot_secrets_env, cmd_run --secrets path)
  • tests/test_secrets.py
  • README.md (--secrets / possession mentions)
  • docs/audit-panel.md (process only)

INTENT:
Opt-in credential injection via KEY=op:// pointer files and op run, so scripts
launched outside the harness do not get vault credentials. Possession boundary,
not a sandbox.

DELIBERATE (do not reverse):

  • Dry-run samples still receive secrets; full runs require pending approve_full_run
    before op is invoked (exit 4)
  • Parent env keys for declared secret names are scrubbed before the child starts
  • OBSERVER_OP_BIN absolute path preferred; PATH fallback remains for local/dev
  • Strict op:// grammar rejects $/{}/templates; plain secrets refused; exit 2
  • Validated pointers snapshotted to a 0600 tempfile before op reads them
  • Durable names-only audit in secrets_audit.jsonl + lane events after run start
  • OBSERVER_SECRETS is an observability hint only, not an attestation
  • Gate/lint remain nudges, not this boundary

NON-GOALS:

  • Network egress allowlists / process sandboxing
  • Making secrets required for every run
  • Hardening the regex PreToolUse gate into a security boundary

REPORT: severity + evidence; report only (no product file edits unless your seat
is no-mistakes after an explicit fix gate).

Also validate the audit-panel process docs commits on this branch. Do not reverse DELIBERATE secrets design. Prefer blocking on correctness/security/test failures over ponytail-style shrinks.

What Changed

  • Added docs/audit-panel.md describing the three-seat audit panel (buggie · no-mistakes · ponytail ultra), run concurrently per check with a single shared prompt, then synthesize and fix agreed majors.
  • Documented the auditor-loop cycle: re-run the triad plus ./auditor-loop/run.sh on the same check until no major issues remain before advancing, and noted the no-mistakes seat needs a feature branch with real commits ahead of main.
  • Framed the buggie/no-mistakes/ponytail seats and the auditor-loop/* references as operator-local/git-excluded rather than live repo links to avoid dead links in the published tree.

Risk Assessment

✅ Low: The only change is a new documentation file whose sole prior finding was fixed per the user's instructions; all referenced local paths are correctly caveated as operator-local/git-excluded, so there is nothing functional or user-visible at risk.

Testing

Ran the secrets suite (tests/test_secrets.py, 21/21 pass) and produced a reviewer-visible CLI transcript from real observer_kit run --secrets invocations that exercises the full possession boundary end-to-end: exit-2 grammar rejections (plain + ${} templates, value not echoed), dry-run samples still injected, exit-4 full-run approval gate with op never invoked, an approved run where op injects vault values and the stale parent literal for a declared key is scrubbed, the names-only OBSERVER_SECRETS hint, and a durable names-only secrets_audit.jsonl confirmed leak-free. The base→target diff itself is docs/audit-panel.md, whose auditor-loop/* references I confirmed are untracked and correctly labeled operator-local/git-excluded — internally consistent, no broken links. This is a CLI/docs change with no rendered UI surface, so a text CLI transcript is the appropriate end-user artifact; all checks pass.

Evidence: Secrets possession-boundary end-to-end CLI transcript
OBSERVER-KIT --secrets POSSESSION BOUNDARY — end-to-end CLI transcript
commit: 545e7a3   (branch nm/secrets-possession-boundary)

op:// pointer file (.observer/secrets.env):
    # op:// pointers only — no literal credentials here
    HUBSPOT_TOKEN=op://vault/hubspot/credential
    CLAY_API_KEY=op://vault/clay/api-key

Parent env exports a STALE literal for a declared key: HUBSPOT_TOKEN=sk-live-STALE-PARENT-LITERAL

================================================================
$ run --secrets plain-secrets.env  # plain (non-op://) value refused, exit 2, no leak of value
================================================================
[observer] secrets: line 1: HUBSPOT_TOKEN must be a strict op://pointer (no $/{}/templates, no plain secrets) so the file stays committable and uninterpolated
[exit code: 2]

================================================================
$ run --secrets templated.env  # op:// with ${...} interpolation refused, exit 2
================================================================
[observer] secrets: line 1: API_KEY must be a strict op://pointer (no $/{}/templates, no plain secrets) so the file stays committable and uninterpolated
[exit code: 2]

================================================================
$ run --secrets secrets.env -- child --dry-run  # dry-run sample still receives secrets
================================================================
[observer] secrets: requesting HUBSPOT_TOKEN, CLAY_API_KEY via op run (source=/private/tmp/secrets-demo2-zyok/project/.observer/secrets.env)
  child sees HUBSPOT_TOKEN = 'vault-secret-hubspot'
  child sees CLAY_API_KEY  = 'vault-secret-clay'
  child sees OBSERVER_SECRETS = 'HUBSPOT_TOKEN,CLAY_API_KEY'
[observer] secrets: child exited 0 after op run (HUBSPOT_TOKEN, CLAY_API_KEY)
[exit code: 0]

================================================================
$ run --secrets secrets.env -- child --full-run  # full run without approval refused, exit 4, op never invoked
================================================================
[observer] secrets: full-run refused — no pending approve_full_run in /private/tmp/secrets-demo2-zyok/project/.observer. Run a --dry-run sample first (secrets allowed for samples), approve on the dashboard, then retry the full run (exit 4). Or pass --run-id runguard:LANE after approval. Opt out: OBSERVER_ALLOW_UNAPPROVED_FULL_RUN=1.
[exit code: 4]

================================================================
$ run --secrets secrets.env -- child  # approved run: op injects vault values, stale parent literal scrubbed
================================================================
[observer] secrets: requesting HUBSPOT_TOKEN, CLAY_API_KEY via op run (source=/private/tmp/secrets-demo2-zyok/project/.observer/secrets.env)
  child sees HUBSPOT_TOKEN = 'vault-secret-hubspot'
  child sees CLAY_API_KEY  = 'vault-secret-clay'
  child sees OBSERVER_SECRETS = 'HUBSPOT_TOKEN,CLAY_API_KEY'
[observer] secrets: child exited 0 after op run (HUBSPOT_TOKEN, CLAY_API_KEY)
[exit code: 0]

================================================================
Durable names-only audit (.observer/secrets_audit.jsonl):
================================================================
{"dry_run": true, "event": "secrets_injected", "keys": ["HUBSPOT_TOKEN", "CLAY_API_KEY"], "pid": 82096, "source": "/private/tmp/secrets-demo2-zyok/project/.observer/secrets.env", "ts": "2026-07-24T12:44:05.538426000Z"}
{"dry_run": false, "event": "secrets_injected", "keys": ["HUBSPOT_TOKEN", "CLAY_API_KEY"], "pid": 82191, "source": "/private/tmp/secrets-demo2-zyok/project/.observer/secrets.env", "ts": "2026-07-24T12:44:07.959771000Z"}

Grep audit ledger for any literal credential value (sk-live / vault-secret):
  (none found — names only, possession boundary holds)
Evidence: test_secrets.py result
21 passed, 0 failed

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed ✅
  • ℹ️ docs/audit-panel.md:4 - The three seat links [buggie](../.claude/skills/buggie/SKILL.md), [no-mistakes](.../no-mistakes/SKILL.md), and [ponytail ultra](.../ponytail/SKILL.md) resolve to repo paths that don't exist — .claude/skills/ in this repo only contains observer-flow and observer-kit, and these skills aren't present at the user skills level either. Anyone reading the doc through the repo gets dead links. Unlike the auditor-loop/* references, these are not caveated as git-excluded/operator-local, so they read as live repo links. Confirm whether these skills are meant to be operator-local (then caveat them like the auditor-loop paths) or vendored into the repo.

🔧 Fix: De-link operator-local audit seats in audit-panel doc
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • /tmp/nmvenv/bin/python tests/test_secrets.py — 21/21 pass (grammar, dedup, exit-2 refusals, parent-env scrub, op-missing fail-closed, wrap shape, names-only audit + lane events, exit-4 approval gate, dry-run injection, ensure_secrets_approval)
  • End-to-end python -m observer_kit run --secrets ... -- <child> CLI transcript with a fake op on OBSERVER_OP_BIN, covering: plain value refused (exit 2), op:// ${} interpolation refused (exit 2), dry-run sample injected (exit 0), full-run without approval refused (exit 4, op not invoked), approved run injecting vault values with stale parent HUBSPOT_TOKEN literal scrubbed, and OBSERVER_SECRETS names-only marker
  • Inspected .observer/secrets_audit.jsonl and grepped for literal credential values (sk-live / vault-secret) — names only, no leak
  • Verified docs/audit-panel.md references via git ls-files auditor-loop/, ls auditor-loop/, git check-ignore — files are untracked and correctly described as operator-local/git-excluded
  • Cross-checked README.md --secrets/op:// documentation against observed CLI behavior (exit 4 on unapproved full run, samples ok, KEY=op:// only)
  • git status --porcelain — worktree clean, no transient artifacts left behind
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

edsmkt added 6 commits July 24, 2026 12:05
One shared prompt, three concurrent seats, sequential waves across a checklist
so agents do not fan out or substitute a single droid run for the full panel.
Document the full per-check cycle: three seats → fix → auditor-loop →
repeat, then advance only when residual majors are gone.
Snapshot cleanup is try/finally; audit uses requested vs injected vs failed;
PATH-resolved op warns to set OBSERVER_OP_BIN; multiple pending approvals
require --run-id; runguard state is rebound via _STATE_DIR not env-only.
Residual re-audits use droid exec; auditor-loop briefs are prompt inputs only.
…n dry-run.

Avoid orphaned 0600 pointer temps when op is missing, and stop treating
grep/head -n as a dry-run sample for the secrets approval carve-out.
@edsmkt
edsmkt merged commit 469d513 into main Jul 24, 2026
2 checks passed
@edsmkt
edsmkt deleted the nm/secrets-possession-boundary branch July 24, 2026 19:53
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