Repository navigation
Guard: a numbered pointer such as PAPER_ENV_FILE_2 is a credential pointer; alpaca-paper-2 inventory row - #481
Merged
seathatflowsinourveins merged 4 commits intoSep 29, 2026
Conversation
seathatflowsinourveins
force-pushed
the
claude/guard-pointer-coverage-20260929
branch
from
September 29, 2026 02:16
f1198bc to
1a505bf
Compare
…inter; alpaca-paper-2 inventory row At 94894f5 every reader, redirect, source and dump on "$PAPER_ENV_FILE_2" passed secret_path_guard.py: the word boundary after PAPER_ENV_FILE failed before "_2", so only the account-1 pointer was guarded although the second account's pointer already exists in the operator's shell startup file. The fix gives every pointer name an optional numeric suffix. Failing first: with only the new inventory row (alpaca-paper-2, pointing at the existing file; no tool writes it), tests.test_secret_path_guard's existing test_inventory_pointer_variables_are_guarded failed for PAPER_ENV_FILE_2 (check() returned None, not credential_file_read); it passes with the guard change. New rows: BLOCKED forms for the numbered pointer (cat, head, a read loop, tracing while sourcing, an environment dump after sourcing, SEC_CONTACT_ENV_2), ALLOWED forms (--env-file, wc, stat), and a status test that mirrors the account-1 row. adoption/hooks/claude/SHA256SUMS carries the new guard hash; a host reinstalls the guard with tools/adoption/install_claude_profile.py --only guard after merge (tests.test_secret_path_guard.test_host_profile_copy_is_verbatim compares the host copy and skips in CI). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…non-numeric-suffix gap Cross-family review note: one new BLOCKED row (a `set -a` source of the numbered pointer followed by an env dump) was already blocked by the environment_dump rule before the change, so it tested a reason change, against the comment above it. Checked every candidate row against the previous guard (the origin/main version, inert strings): replaced it with three rows that pass the old guard and are blocked by the new one (a declare -p after sourcing, an inline os.environ dump after sourcing, xtrace with source), and recorded that a pointer with a non-numeric suffix (PAPER_ENV_FILE_B) is not recognised, as an asserted known gap. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…; record the systemd-run launcher gap The trading lane asked for a row proving the loader path a unit uses (systemd-run --user, a bash -ic that hands the pointer to a loader as --env-file) still passes for both accounts, so the numbered-pointer fix never blocks a unit. Added three ALLOWED rows (account 1, account 2, a double-quoted form). Checked against the previous guard and the new one: all pass on both. While checking, found that systemd-run is not a modelled launcher: a reader it starts is not inspected, for either account, and with --pipe --wait its output returns to the caller. Recorded as two asserted known gaps (EXPECTED_PASS_THROUGH) so the change that closes it is noticed; the guard work in the key-management change models it. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…idence.json (hot-file commit, last) Hashes of the six changed files re-registered on top of main's manifest (docs/lanes.md protocol: checkout main's copy, register_file for each, component_matrix and new_host_grand_list --write, validate.py passes: 69 components, 7990 hashed files, 165 receipts). Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
seathatflowsinourveins
force-pushed
the
claude/guard-pointer-coverage-20260929
branch
from
September 29, 2026 02:49
1a505bf to
b40b359
Compare
seathatflowsinourveins
deleted the
claude/guard-pointer-coverage-20260929
branch
September 29, 2026 03:42
seathatflowsinourveins
added a commit
that referenced
this pull request
Sep 29, 2026
…, memory-only status rows, coverage warnings, decision record started (#490) * Tavily moves to a 0600 store file; the checker reports memory-only rows and undeclared keys The inventory's tavily row changes from kernel_keyring (key tavily_api_key) to private_env_file <store>/tavily.env, status still optional, rotation through open_credential_terminal.sh tavily, and notes that carry the dated reading of the 2026-09-26, 09-28 and 09-29 instructions (D1 design, C21). scripts/credential_status.py (C06 part 1): - a kernel_keyring row keeps state unchecked and gains persistence memory_only and the warning memory_only_lost_on_restart; - a required kernel_keyring row is an inventory error, so validate.py and set_credential.py refuse such an inventory too; - two coverage lists, names only, each a warning with exit 0: undeclared_store_file (one scandir of the store root, directories skipped, a symlinked root never listed through) and undeclared_keyring_key (this uid's live native-agent-stack:<name> keys in /proc/keys that no kernel_keyring row claims; revoked, dead, negative, invalidated and expired keys skipped). The line format follows linux v6.18 security/keys/proc.c proc_keys_show() and user_describe() in security/keys/user_defined.c. - --proc-keys (default /proc/keys) lets the tests pass a fixture, as --root and --inventory already do; inspect() scans only when given a path. Tests: the tavily row test and the keyring-row test now plant a memory-only row (the real inventory has none), and every CLI run in the file passes a fixture key list, so no test reads this host's /proc/keys or store. set_credential's entry test now expects tavily to load as a file entry and keeps the keyring refusal with a planted row. Failing first (run by name before the implementation, 7 tests): test_tavily_row_is_a_stored_file_entry FAIL (store kernel_keyring != private_env_file); test_required_keyring_row_is_an_inventory_error FAIL (0 != 1 errors); test_keyring_row_reports_memory_only_lost_on_restart, test_undeclared_store_file_is_reported_by_name_only, test_undeclared_keyring_key_is_reported_from_a_proc_keys_fixture and test_kernel_keyring_row_is_validated_and_never_inspected ERROR (inspect() had no proc_keys); test_only_operator_supplied_stored_entries ERROR (Refused: tavily not an operator-supplied stored entry). After: TMPDIR=/var/tmp PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_credential_tools tests.test_credential_status tests.test_secret_path_guard tests.test_install_claude_profile tests.test_adoption_docs_consistency: 174 tests, the one failure is the tolerated test_host_profile_copy_is_verbatim (host copy of the guard); tests.test_codex_worker_lane tests.test_host_requests: 107 OK (9 skipped). validate.py reports only the manifest hash mismatches of the four changed pinned files, which the coordinator registers. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * set_credential.py --from-env: create-only, isolated, one-variable writer for a keyring key The keyring-to-file chain of the D1 design (C05; amendments 2 and 3): python3 scripts/kernel_keyring.py exec tavily_api_key TAVILY_API_KEY -- python3 -I tools/credentials/set_credential.py tavily --from-env - It runs only in an interpreter started with -I (sys.flags.isolated). A start without -I is refused at module load and never re-executed: that interpreter has already honoured PYTHONPATH and the user site directory beside the value, which a re-run cannot undo. - It stores only an entry that declares exactly one variable, required or optional, so the Alpaca pairs and sec-contact are refused. - It pops the variable from its environment first, then refuses an absent, empty or out-of-grammar value with the existing encode() rules. - It is create-only: an existing name (a symlink too) is refused before the temporary file is written, and the finished, fsynced temporary file gets its final name with os.link, which fails with EEXIST instead of replacing; the temporary name is then unlinked. There is no replace option. - The only output is "<id>: stored"; refusals name the variable, never the value, and an unexpected error prints its type only, without a traceback. - The parser takes no abbreviations: argparse would otherwise read --from as --from-env, which the start-up check does not look for. - The store-directory checks are the existing open_store(). Also KEY_PREFIX_HINT gains alpaca-paper-2: PK, from the review of #481. (And one long line of commit 1's render_text is reflowed.) Failing first (tests.test_credential_tools.StoreFromEnvTests and test_second_paper_account_gets_the_same_prefix_hint, run by name before the implementation): 9 of 10 failed. Seven errored with "module 'set_credential' has no attribute 'run_from_env'" (or 'create_exclusively'); the two CLI tests failed because argparse rejected --from-env (exit 2, no "python3 -I" hint); the prefix test failed (no "does not start with PK" warning for an AK id). test_cli_takes_no_abbreviation_of_from_env passed vacuously before the option existed, so it is shown by mutation below. Mutation check, each in a scratch copy of the tree, StoreFromEnvTests only: dropping allow_abbrev=False fails the abbreviation test (both subtests); os.replace instead of os.link fails the create-only link test and the store test; re-executing instead of refusing fails the non-isolated CLI test; env.get instead of env.pop fails the store test; skipping the isolation check fails the isolation test. The non-isolated CLI test carries its own control: a sitecustomize module on PYTHONPATH records that it ran and saw TAVILY_API_KEY in the start without -I, and does not run under -I. After: the five acceptance modules ran 184 tests, the one failure being the tolerated test_host_profile_copy_is_verbatim. All values are synthetic, in a temporary XDG_CONFIG_HOME; no test touches the kernel keyring or the store. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * --from-env needs python3 -I -S, refuses before any write, and never re-executes From the coordinator's independent review of the first version: -I skips PYTHONPATH, PYTHON* variables and the user site directory but still imports site, which runs the .pth lines of the interpreter's own site-packages beside the value (on this host a uv-managed Python under the home directory). The chain is now python3 scripts/kernel_keyring.py exec tavily_api_key TAVILY_API_KEY -- python3 -I -S tools/credentials/set_credential.py tavily --from-env - Before the module-level re-exec block: with --from-env in argv the tool never re-executes, and refuses (exit 2, naming the required invocation, never a value) unless sys.flags.isolated and sys.flags.no_site are both set. Every other mode keeps today's re-exec with -I. run_from_env checks the same pair again (its isolated_start argument defaults to both flags). - Every refusal comes before the first write: the existing final name, a symlink included, is refused before open_store() can create the store or tighten its mode; the check is repeated through the directory handle and os.link still settles a later race. - The temporary file is a dot-file named after the final file (.tavily.env.<16 hex>.tmp). A kill before the link leaves it; the checker lists it as undeclared_store_file and never counts it as the stored key, and the writer never takes it for the final file. - The inventory note carries the -I -S chain. Failing first (StoreFromEnvTests, 12 tests, before the change): with -I alone the old writer stored the file, so test_cli_refuses_minus_I_without_minus_S and the venv control test failed with "0 != 2"; the plain-start refusal lacked "python3 -I -S"; the other tests errored on the new isolated_start argument. After the -I -S rule and before the pre-write check, test_never_replaces_an_existing_file_and_writes_nothing_first failed with "448 != 488": a refusal came after the store mode 0750 had been tightened to 0700. The leftover-temporary-file test passed at once (the name was already a dot-file); it is pinned by mutation below. Controls: a venv's site-packages .pth line records each interpreter start as <isolated><saw the variable>. A plain start records only "01" (the value was seen and nothing was re-executed), -I alone records "11" (site code ran beside the value under -I), and -I -S records nothing and stores the file. Mutation check, each in a scratch copy of the tree, StoreFromEnvTests only; each is caught by the intended test: allow abbreviations; os.replace for os.link; re-execute instead of refusing (the venv record shows an isolated second start); env.get for env.pop; no isolation check; a temporary name without the leading dot; no pre-write existence check; -I without -S accepted. After: TMPDIR=/var/tmp PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_credential_tools tests.test_credential_status tests.test_secret_path_guard tests.test_install_claude_profile tests.test_adoption_docs_consistency: 187 tests OK (4 skipped). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Docs and decision record: Tavily's stored key, the keyring as transport, the checker's new lists - docs/secret-storage.md: the store table lists tavily as <store>/tavily.env whose first write comes from the keyring copy. "Memory-only option: Linux kernel keyring (2026-09-26)" becomes "Kernel keyring: transport and per-boot spare (2026-09-29)", with an <a id> alias for the old anchor. It documents the create-only chain python3 scripts/kernel_keyring.py exec tavily_api_key TAVILY_API_KEY -- python3 -I -S tools/credentials/set_credential.py tavily --from-env and every refusal of --from-env, including why -I alone is refused and the leftover dot-file of a killed write. The Checker section describes memory_only rows, the required-row error and the two coverage lists, and rotation of the Tavily key moves to open_credential_terminal.sh tavily. - recipes/tavily.md: "Memory-only key on Linux and WSL2" becomes "Stored key (2026-09-29)", also with an alias for the old anchor. The keyring commands stay valid until the next kernel restart; after it Tavily has no key until a file loader lands, an accepted gap because Tavily is not the default web lane. The later injector's default use is not described here. - adoption/tools/README.md: tvly-keyring's link points at the renamed heading (tests/test_adoption_docs_consistency resolves anchors from headings only), and one sentence says the wrapper uses the keyring copy. - docs/decisions/2026-09-29-key-management.md (new): the selection, the dated reading of the 2026-09-26, 09-28 and 09-29 instructions, the alternatives so far (dotenvx run measured, the keyring alone, LoadCredential= mitigating only, checked against systemd.exec(5)), the evidence classes, known limits and the overturn comparisons, in sections that later changes append to. - tests/test_secret_path_guard.py: one ALLOWED row pins the exact chain string; the guard itself is unchanged. Checked: the guard's check() returns None for the chain (probe at this tree, guard file unchanged since the base); the 21 documented keyring lines of the three pages all pass it (test_documented_keyring_commands_pass needs at least 10). A scan of every added line and every commit message on the branch for the host user name, home or scratch paths and host names found none; the pattern's positive control matched. After: TMPDIR=/var/tmp PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_credential_tools tests.test_credential_status tests.test_secret_path_guard tests.test_install_claude_profile tests.test_adoption_docs_consistency: 187 tests OK (4 skipped). python3 scripts/validate.py reports only the SHA-256 and byte-count mismatches of the pinned files this branch changed, for the coordinator's manifest registration. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * credential_status: compare /proc/keys descriptions whole and report them in full Finding 1 of the read-only cross-family review (GPT-6, effort max) of the branch: the /proc/keys parser cut a description at the first character outside kernel_keyring.py's name grammar and matched a declared key_name as a prefix, so native-agent-stack:paper/backup was dropped and native-agent-stack:tavily_api_key:backup was hidden by a declared tavily_api_key row. user_describe() (linux v6.18, security/keys/user_defined.c) prints the whole description, which may hold "/", ":" or spaces, then ": <payload length>" for a positive key. Now each line is parsed to its type column (proc_keys_show() prints the type as "%-9.9s ") and its describe output. For a live, current-uid "user" key only the final ": <payload length>" is removed. A kernel_keyring row declares a key only when its key_name equals the whole rest after "native-agent-stack:"; every other such key is listed under its full name. Other key types are not listed (kernel_keyring.py stores only user keys). Undecodable bytes are kept as backslash escapes rather than replaced. The runbook's Checker section and the module docstring say the same. Failing first: tests.test_credential_status.CredentialStatusTests.test_proc_keys_descriptions_are_compared_whole_and_reported_in_full failed on the old parser with ['logon-key', 'note', 'tavily_api_key2'] != ['note: 12', 'paper/backup', 'tavily_api_key2', 'tavily_api_key:backup', 'with space']: "/" and space descriptions were dropped, "note: 12" was cut to "note", tavily_api_key:backup was hidden by the declared tavily_api_key, and a logon key was listed. The test also checks that a declared key that is present is not listed, and that tavily_api_key is listed under the real inventory, which has no kernel_keyring row. Mutation check in a scratch copy: splitting the name on ":" before the comparison, and dropping the "user" type filter, each fail the new test. A lazy suffix pattern is an equivalent mutant under fullmatch (it strips the same final ": <length>"). In those copies test_repository_has_no_tracked_sensitive_names also failed, only because a copy is not a git checkout. After: TMPDIR=/var/tmp PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_credential_tools tests.test_credential_status tests.test_secret_path_guard tests.test_install_claude_profile tests.test_adoption_docs_consistency: 188 tests OK (4 skipped). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Tavily rotation: a dated interim step to refresh the keyring copy Finding 2 of the read-only cross-family review (GPT-6, effort max): after this branch the file is the store of record, but adoption/tools/tvly-keyring and kernel_keyring.py exec still read the keyring copy until the id-based runner lands. Rotating only the file and then revoking the old key at Tavily would break Tavily commands before the next restart. One dated step, starting "Interim step (2026-09-29, until the id-based runner lands)", is added once in each place that tells how to rotate: the tavily notes in adoption/credential-inventory.json (as their last sentence), the keyring section of docs/secret-storage.md, and recipes/tavily.md. After open_credential_terminal.sh tavily, the operator also refreshes the copy in their own terminal with `python3 scripts/kernel_keyring.py store --replace tavily_api_key`, or accepts that the copy keeps the old key until the next kernel restart drops it. Each step is its own sentence or paragraph, so the runner's change can delete it together with its test. Only the wording changes: no code, and the rotation field and existing assertions are untouched. Failing first: tests.test_credential_status.CredentialStatusTests.test_interim_tavily_rotation_step_is_stated_once_in_each_place failed in all three places with "0 != 1" (the marker was absent). It checks that the marker appears exactly once in each place, and that the step's own paragraph names the rotation command, the refresh command and the next kernel restart. After: TMPDIR=/var/tmp PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_credential_tools tests.test_credential_status tests.test_secret_path_guard tests.test_install_claude_profile tests.test_adoption_docs_consistency: 189 tests OK (4 skipped); the inventory still parses as JSON. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Register the Tavily persistence changes in manifests/evidence.json (hot-file commit, last) Hashes of the eight changed pinned files on top of main's manifest at b1be50c (docs/lanes.md protocol: main's copy, register each file, component_matrix and new_host_grand_list --write; validate.py passes: 69 components, 7994 hashed files, 168 receipts). No receipt entry: this PR records no native run; the one host write follows the merge. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> --------- Co-authored-by: Scout <scout@local> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This was referenced Sep 29, 2026
5 tasks done
seathatflowsinourveins
added a commit
that referenced
this pull request
Oct 8, 2026
### Scope - What this PR changes: it adds the optional `anthropic-api-2` entry directly after `anthropic-api` in `adoption/credential-inventory.json`, so one command can select an additional Console key. It is an additional key: nothing is replaced. The setter, the runner and the guard are unchanged: the setter and the runner read the inventory, and the guard's rules already cover the variable and the whole store directory. Both entries declare only `ANTHROPIC_API_KEY`, which stays in `must_not_be_set`. - Base commit: `e48de2e6fa1dd3b740af4793ea5824ea35476dba` - Lane: `lane:foundation` - Owned paths touched: `adoption/credential-inventory.json`, `docs/secret-storage.md`, `docs/decisions/2026-10-08-anthropic-api-second-key.md`, `docs/harness-defaults.md`, `tests/test_credential_run.py`, `catalogs/foundation/upstream-surface-dispositions.json`, and `manifests/evidence.json` (the hashes of those six files, written by `scripts/host_receipts.py:register_file`). The new entry differs from the first in exactly six fields: `id`, `label`, `store.path_template` (file `anthropic-api-2.env` in the same directory), `loaders`, `rotation` and `notes`. Class, status, lane, variables, optional and pointer variables, consumers and store kind are identical. The repository precedent is `alpaca-paper-2` (#481, `c26800f3`). **Scope beyond the brief.** Four edits go beyond adding the entry. They are declared here so that the read of this PR covers them: 1. **One sentence of the first entry's `notes` is corrected.** Old: "exported in a shell it would override every Claude Code session's native sign-in". New: "a shell export can switch Claude Code from subscription sign-in to API billing (headless mode uses the key when present; interactive mode first asks for approval)". Source: Claude Code's [environment variables](https://code.claude.com/docs/en/env-vars#variables) page, where a present key is always used in non-interactive mode (`-p`) and is approved once in interactive mode before it overrides the subscription. The new entry gives the same reason, so the two entries agree. No other field of `anthropic-api` changes. 2. **One row is added to the anti-pattern log of `docs/harness-defaults.md`.** It records that correction, which that page's "Record a correction the same turn" rule asks for. 3. **`docs/secret-storage.md` gains the first key's table row as well.** The entry table had no `anthropic-api` row. It now lists both entries, followed by one paragraph on selecting a key for one command. 4. **Five line citations into `docs/secret-storage.md` are updated in `catalogs/foundation/upstream-surface-dispositions.json`,** because the added rows moved the cited lines down by 18. The `source` locators of `CLAUDE_CODE_ENABLE_TELEMETRY` (1697 → 1715), `OTEL_LOG_USER_PROMPTS` and `OTEL_LOG_ASSISTANT_RESPONSES` (1684 → 1702), and `OTEL_LOG_TOOL_CONTENT` and `OTEL_LOG_RAW_API_BODIES` (1686 → 1704) change; no disposition or reason does. Hosted `validate-shard-3` failed five subtests of `DispositionCitationTests.test_every_cited_line_names_the_key` at `88d981c7`; #858 made the same repair for its own insertion. ### SOTA sources Public primary documentation, fetched 2026-10-08; every cited anchor resolves. - Anthropic [API overview, Getting API keys](https://platform.claude.com/docs/en/api/overview#getting-api-keys) and [Authentication, Create and use a key](https://platform.claude.com/docs/en/manage-claude/authentication#create-and-use-a-key): keys are created on the Console key page, the client SDKs pick up `ANTHROPIC_API_KEY` automatically, and a key scoped to one workspace needs no workspace header. - Anthropic [Get your API key, Choose a key type](https://platform.claude.com/docs/en/get-api-key#choose-a-key-type), [Workspaces, API keys and resource scoping](https://platform.claude.com/docs/en/manage-claude/workspaces#api-keys-and-resource-scoping) and [Set workspace limits](https://platform.claude.com/docs/en/manage-claude/workspaces#set-workspace-limits): personal, service-account and legacy workspace keys; a multi-workspace key needs the `anthropic-workspace-id` header; spend limits are set per workspace. - Claude Code [environment variables](https://code.claude.com/docs/en/env-vars#variables) and [authentication precedence](https://code.claude.com/docs/en/authentication#authentication-precedence): in non-interactive mode (`-p`) a present key is always used; in interactive mode the key is approved once before it overrides the subscription. These support the corrected wording. - Anthropic [WIF reference, Profile configuration file](https://platform.claude.com/docs/en/manage-claude/wif-reference#profile-configuration-file): the configuration-profile alternative that the decision record names. - [This repository at `c9ab2212142398234b6ba39a4cf33c5c2a6a643e`](https://github.com/seathatflowsinourveins/native-agent-stack/tree/c9ab2212142398234b6ba39a4cf33c5c2a6a643e), the reference implementation this change follows; the cited files are unchanged at the base commit: inventory entries `anthropic-api` (#841, `a35369aa`) and `alpaca-paper-2`; `tools/credentials/set_credential.py:load_entry`; `tools/credentials/credential_run.py:find_entry` and `injectable`; the guard's `SECRET_NAMES`, `STORE_PATHS` and `ENV_FILE_WORD` in `scripts/hooks/secret_path_guard.py`; the `RunnerCase` fixtures in `tests/test_credential_run.py`; `scripts/host_receipts.py:register_file` and `scripts/validate.py`. No credential runtime or dependency is added. ### Evidence-class table | Claim | Evidence class | Command / receipt | | --- | --- | --- | | The Console key and environment contract, and the mode-dependent sign-in wording | source_review | The official pages linked above, fetched 2026-10-08 | | The new entry differs from the first in six fields only; the inventory goes from 20 to 21 entries and from 10 to 11 injectable ids; of `anthropic-api`, only `notes` changes | source_review | Field-by-field comparison of both entries against `origin/main`, using the runner's own `injectable()` | | The setter and the runner select separate files, leave the first file unchanged when the second is written, accept the same bare and quoted grammar, inject only the selected value and refuse expansion | local_integration; synthetic | `InjectionTests.test_second_anthropic_key_uses_the_same_setter_and_runner_grammar`: temporary store, generated fake values | | The guard needs no edit: it lists no ids, its store rule covers the whole store directory, and the variable is already a guarded name | source_review; synthetic | `STORE_PATHS` and `SECRET_NAMES` in the guard; `tests.test_secret_path_guard`; `guard.read_command` on nine synthetic command forms returns the same reason for both ids (four store-path forms `credential_store_path`, the documented runner form allowed) | | No leak in the six files of the first commit | local_integration | `betterleaks dir` 1.9.0 with `.gitleaks.toml` on a copy of those six files: no leaks, rc 0. The required gate is CI's pinned gitleaks 8.30.1, which is not installed on this host; hosted `secret-scan` passed at `88d981c7` | | Each of the five re-pointed catalog locators names its key again, and no other maintained citation moved | local_integration; source_review | `tests.test_upstream_surface_watch`: 5 of 128 repository citations failed with the catalog at `88d981c7`, 0 fail now. Of 263 line citations into the four edited files, 126 point past the inserted lines: these 5; 46 pinned to a commit; 75 in dated records, frozen evidence or code comments that already pointed elsewhere before this PR or carry their own source revision | Not measured here: the actual key type, the workspace spend limit, credit, fast mode and provider acceptance. A multi-workspace key needs the `anthropic-workspace-id` header and is outside this single-variable entry. ### Every changed existing test expectation One constant changes, and two existing tests assert on it. The exact injectable set was `alpaca-paper`, `alpaca-paper-2`, `sec-contact`, `databento`, `typesafe`, `omniroute`, `tavily`, `claude-oauth-token`, `canary-e2e` and `anthropic-api`. `INJECTABLE_IDS` now adds only `anthropic-api-2`. | Existing test | Old contract → new contract | | --- | --- | | `InjectionTests.test_only_the_selected_entrys_own_pointer_variables_stay_in_the_child` | Exactly the ten ids above are injectable, and each keeps only its own pointer variables → the same contract for eleven ids, adding `anthropic-api-2`, whose own pointer list is empty. | | `InventoryAndGrammarTests.test_every_injected_name_is_masked_or_public` | Exactly those ten ids classify every injected variable as masked or public → eleven ids, adding the masked required variable of `anthropic-api-2`. The exact optional-variable classification is unchanged, because the new entry has none. | No other existing assertion is edited. `NOT_INJECTABLE_IDS` is unchanged. The status module checks the canonical inventory dynamically and by subset. The boot-receipt allowlist test compares the receipt's rows with the canonical inventory, so its expected id sequence follows the inventory (20 → 21 rows; file-kind rows 18 → 19). The guard's tie tests read the inventory's variable and pointer names, which do not change. Fixtures are unchanged. `InjectionTests.test_second_anthropic_key_uses_the_same_setter_and_runner_grammar` is new coverage, not a relaxed assertion. ### Local commands run One module per command (no suite discovery). The twelve modules below ran on the tree committed as `5117734b`, the first commit on `e48de2e6`. The second commit, `88d981c7`, only removes two process claims from the decision record that nothing in the repository can verify; the docs module and the validator were run again on its tree. The third commit, `75fa64b3`, re-points five catalog citations; the last block covers it and the whole suite. ``` $ timeout 600 nice -n 10 ionice -c2 -n7 python3 -m unittest tests.<module> -v tests.test_credential_status rc 0 Ran 48 OK tests.test_credential_tools rc 0 Ran 34 OK tests.test_credential_run rc 0 Ran 94 OK tests.test_credential_boot_receipt rc 0 Ran 21 OK tests.test_secret_path_guard rc 0 Ran 90 OK (skipped=2: scratch adapter supplied by the permanent-test mutation driver) tests.test_canary_proof rc 0 Ran 143 OK (skipped=134: reviewed ripgrep unavailable on this host) tests.test_codex_worker_lane rc 0 Ran 123 OK (skipped=12: real app-server runs need NAS_CODEX_INTEGRATION=1) tests.test_host_requests rc 0 Ran 49 OK tests.test_new_wsl_definitive_defaults rc 0 Ran 130 OK (skipped=3: GNU chmod --reference and readlink -f commands) tests.test_adoption_docs_consistency rc 0 Ran 39 OK (skipped=1: no profile's coverage differs between the pinned release and HEAD) tests.test_sota_convergence rc 0 Ran 157 OK tests.test_ecosystem_manifest rc 0 Ran 82 OK $ python3 scripts/evidence_manifest.py --check rc 0 {"files": 10901, "status": "passed"} $ timeout 900 nice -n 10 ionice -c2 -n7 python3 scripts/validate.py rc 0 {"components": 70, "hashed_files": 10901, "profiles": 4, "receipts": 239, "status": "passed"} ``` On the tree committed as `88d981c7`: ``` $ timeout 600 nice -n 10 ionice -c2 -n7 python3 -m unittest tests.test_adoption_docs_consistency -v rc 0 Ran 39 OK (skipped=1) $ timeout 900 nice -n 10 ionice -c2 -n7 python3 scripts/validate.py rc 0 {"components": 70, "hashed_files": 10901, "profiles": 4, "receipts": 239, "status": "passed"} ``` Hosted validate at `88d981c7` (run 37846469750) failed one shard: `validate-shard-3`, `Ran 1514 tests`, `FAILED (failures=5, skipped=127)`. All five failures were the stale catalog citations. The third commit, `75fa64b3`, fixes them. On its tree: ``` $ timeout 600 python3 -m unittest tests.test_upstream_surface_watch -v rc 0 Ran 114 OK (skipped=6) $ timeout 600 python3 -m unittest tests.test_adoption_docs_consistency tests.test_credential_run rc 0 Ran 133 OK (skipped=1) $ timeout 900 python3 scripts/validate.py rc 0 {"components": 70, "hashed_files": 10901, "profiles": 4, "receipts": 239, "status": "passed"} $ for m in tests/test_*.py: timeout 600 python3 -m unittest tests.$m (each module on its own, no discovery) 276 modules: 255 rc 0, 21 rc 1; 10,725 tests ran, 911 skipped ``` The 21 nonzero modules all passed in hosted validate at `88d981c7`. Locally they fail for host reasons, not because of this diff: - **18 modules: `exchange_calendars` is not installed** in this host's Python 3.13. Hosted installs 4.13.2 from `.github/requirements-calendar.txt`. Sixteen modules do not import: the 13 `test_adaptive_paper_*` modules other than credential_race, plus `test_alpaca_admission_regressions`, `test_native_faults_min` and `test_order_throughput`. `test_ingest_snapshot` has 2 errors. `test_adaptive_paper_credential_race` has 5 mutation self-test failures, because its pristine run imports the runner module. All 18 fail the same way on a checkout of the base `e48de2e6`. - **`test_token_e2e_grader`:** `git clone --local` from the worktree (on disk) into `/tmp` (tmpfs) fails with `Invalid cross-device link`. It passes on the base checkout, which lives on tmpfs. - **`test_windows_terminal_defaults`:** the Claude Code client installed on this host reports a `plugin_notification` notification type that has no repository decision. The base gives the same failure. - **`test_new_wsl_mcp_conformance_lifecycle`:** host-state dependent. Three runs at this head gave a listener assertion, a missing `cleanup.json`, then a pass; it passes on the base. The fourth commit, `67d9e8d5`, corrected a stale comment citation in `blueprints/runtime-workers/openhands/resolver/patch_policy.py`. That citation was already wrong on main, so it is outside this PR's scope and will be handled separately. The fifth commit, `2fa607e1`, reverts it. The head's tree is identical to `75fa64b3`'s (tree `d399373a`). On it, `timeout 900 python3 scripts/validate.py` returns rc 0. ### Decision record `docs/decisions/2026-10-08-anthropic-api-second-key.md`: the decision, the handling rules, the sources, the `alpaca-paper-2` precedent, the correction, the evidence boundaries, the alternatives and what would reopen the decision, and the inverse. ### Host evidence Not applicable: this PR adds or changes no file under `evidence/hosts/`. ### Checklist - [x] New/changed GitHub Actions are pinned to a full commit SHA with a version comment (none changed). - [x] New/changed workflows declare least-privilege permissions (none changed). - [x] No secrets are printed, logged or committed; no new required secret was added. No credential value or credential store was read, inspected, created or changed for this PR: the tests use a temporary store and generated fake values, and the setter, launcher and runner were not run against a real entry. - [x] No new paid hosting, subscription or billing surface is introduced by this declaration. The key itself is supplied later through the existing hidden prompt; its credit and spend limit are not measured here. - [x] Peer-owned untracked files and worktrees were preserved. ### Landing notes - This PR declares a store; it neither creates one nor opens the paste window. - #850 (open) inserts `anthropic-admin` at the same position in the inventory and edits the same `INJECTABLE_IDS` line; #769 and #770 (drafts) also touch the inventory, `docs/secret-storage.md` and `tests/test_credential_run.py`. Whichever lands second needs a rebase and a fresh `register_file` pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Scope
secret_path_guard.pynow recognises a numbered credential pointer such as$PAPER_ENV_FILE_2(before, every reader, redirect,sourceand dump on it passed the guard while the account-1 pointer was blocked), andadoption/credential-inventory.jsongains thealpaca-paper-2row that names the second paper account's existing file (nothing here writes it).6d626654lane:foundation(the guard, the inventory and the docs are foundation paths indocs/lanes.md; the trading lane was told and asked for exactly this inventory-only form).scripts/hooks/secret_path_guard.py,adoption/hooks/claude/SHA256SUMS,adoption/credential-inventory.json,docs/secret-storage.md(a table row and one export line),tests/test_secret_path_guard.py,tests/test_credential_status.py, and the last commit'smanifests/evidence.jsonhashes.SOTA sources
PreToolUse(exit 2 blocks the call and returns the reason), https://code.claude.com/docs/en/hooks, fetched 2026-09-29 against Claude Code 2.1.284 (installed and the latest release,v2.1.284of 2026-09-28).adoption/credential-inventory.json(pointer_variables) and the existingtests/test_secret_path_guard.py::test_inventory_pointer_variables_are_guarded, which derives every pointer from the inventory and requires the guard to block a reader on it.op,bws,infisical,doppler, OpenBao/Vault Agent, SOPS+age, dotenvx, varlock, secretspec and others, with a live maintenance check of each) found none that stands in for a PreToolUse command guard; the guard stays the repository's own heuristic and is documented as such (docs/secret-storage.md, Threat model).ecosystem-roadmap-2026, 2026-09-29) had the same regex idea; it was read as untrusted input and every row here was checked against the previous guard.Evidence-class table
94894f54a reader, redirect,sourceand dump on"$PAPER_ENV_FILE_2"passed the guardlocal_integrationcheck()on inert strings, previous guard against the new one: all 9 new BLOCKED rows flip from allowed to blocked, and the 4 new ALLOWED rows (--env-file, the rate-limit probe,wc,stat) keep their verdicttest_inventory_pointer_variables_are_guardedfailed forPAPER_ENV_FILE_2(check()returnedNone, notcredential_file_read)local_integrationAssertionError: None != 'credential_file_read'; it passes with the regex changetest_host_profile_copy_is_verbatim, expected until the host copy is reinstalled; it skips in CI), 3 skippedlocal_integrationlocal_integrationpython3 scripts/validate.py: passed (69 components, 7990 hashed files, 165 receipts)_<digits>is recognised;PAPER_ENV_FILE_Bstill passeslocal_integrationEXPECTED_PASS_THROUGHso a change that closes it is noticedLocal commands run
Host step after merge (a host write, announced to the live sessions first): reinstall the guard so the user-level copy matches,
python3 tools/adoption/install_claude_profile.py --only guard, thentest_host_profile_copy_is_verbatimpasses.Decision record
None new for this change: it applies the existing inventory-to-guard contract. The key-management decision record (
docs/decisions/2026-09-29-key-management.md, selection, alternatives, sources with pins and the overturn comparison) follows with the larger change.Cross-family review: one read-only GPT-6 round (
codex exec -s read-only,gpt-6-astra, effort max, live search). One note (a new test row was already blocked by another rule before the change): repaired ina2cf068eby checking every candidate row against the previous guard; the reviewer then reported no further findings.Host evidence
Not applicable: no file under
evidence/hosts/changes.Checklist
permissions: contents: read: no workflow changes.🤖 Generated with Claude Code