diff --git a/adoption/credential-inventory.json b/adoption/credential-inventory.json index 88ad7c968..9e28caf23 100644 --- a/adoption/credential-inventory.json +++ b/adoption/credential-inventory.json @@ -1,6 +1,6 @@ { "schema_version": 1, - "as_of": "2026-09-27", + "as_of": "2026-09-29", "decision": "docs/decisions/2026-09-24-secret-storage.md", "runbook": "docs/secret-storage.md", "checker": "scripts/credential_status.py", @@ -42,6 +42,38 @@ "rotation": "On any exposure trigger; before any live-scope work; unconditionally once after adopting this layout because broker-prefixed names were observed in a launcher environment on 2026-09-23.", "notes": "Paper-only keys. Live keys are out of scope for this repository; live trading is handled outside it. ALPACA_API_KEY/ALPACA_SECRET_KEY are read-only fallback names in pit-availability/measure.py and are never written." }, + { + "id": "alpaca-paper-2", + "label": "Alpaca paper broker key pair, second paper account", + "class": "broker_api_key_pair", + "status": "optional", + "lane": "us-equities-paper", + "store": { + "kind": "private_env_file", + "path_template": "${XDG_CONFIG_HOME:-$HOME/.config}/native-agent-stack/alpaca-paper-2.env" + }, + "variables": ["APCA_API_KEY_ID", "APCA_API_SECRET_KEY"], + "optional_variables": ["APCA_API_BASE_URL"], + "pointer_variables": ["PAPER_ENV_FILE_2"], + "loaders": [ + "blueprints/us-equities/adaptive-paper/runner.py#credentials", + "blueprints/us-equities/adaptive-paper/market_research.py#credentials", + "blueprints/us-equities/broad-universe/collect_daily.py#read_credentials", + "blueprints/us-equities/mover-early-entry/collect.py#credentials", + "blueprints/us-equities/extreme-gainer-audit/audit.py#credentials", + "blueprints/us-equities/incentive-monitor/monitor.py#credentials" + ], + "environment_only_consumers": [ + "blueprints/us-equities/alpaca-paper/paper_runner.py", + "blueprints/us-equities/alpaca-historical/collect.py", + "blueprints/us-equities/security-identity/probe.py", + "blueprints/us-equities/security-identity/quality.py", + "blueprints/us-equities/delisting-coverage/collect.py", + "blueprints/us-equities/alpaca-history-fixture/fixture.py" + ], + "rotation": "On any exposure trigger; before any live-scope work. Regenerate at Alpaca, then run tools/credentials/open_credential_terminal.sh alpaca-paper-2 and type replace at the hidden prompt.", + "notes": "Paper-only keys for a second Alpaca paper account, assigned to the isolated incentive-monitor forward study. The file already exists on a host that runs it; this entry names it and no tool here writes it unless the operator asks. PAPER_ENV_FILE_2 is its pointer, and scripts/hooks/secret_path_guard.py covers it (tests/test_secret_path_guard.py ties every inventory pointer to the guard). Kernel keyring copies, memory only and lost at a restart: alpaca-paper-2-id and alpaca-paper-2-secret. Live keys are out of scope for this repository." + }, { "id": "sec-contact", "label": "SEC/EDGAR declared contact identity", diff --git a/adoption/hooks/claude/SHA256SUMS b/adoption/hooks/claude/SHA256SUMS index 429656956..f8dba92ea 100644 --- a/adoption/hooks/claude/SHA256SUMS +++ b/adoption/hooks/claude/SHA256SUMS @@ -1,5 +1,5 @@ 2a5f087f2c10437bab302e19469ae6b2e715828083a8bae07a7b177540b63e0e effort-default-guard.py -abf269ba41a126400e084b4f03828eb4f0e6fc2375633c0e788136158a3054b1 ../../../scripts/hooks/secret_path_guard.py +f9be81b240762dc58da874f135040d182ca9fb5e9056ab9b8a977dd909453244 ../../../scripts/hooks/secret_path_guard.py d6c3c19d6e94d3f48d2788963c8c9be45ec57e6bb81cab92b2c30d9fd5c8e8d6 token-lanes-block.md c81a91c4ffb2fa906feb403d5a11bd294b6cf2db45795d80f9acd662c5004973 token-lanes-block.builder.md 47cc80b262468557b8cfe5c243598030757270674bc7bf286fba8c0c573abc28 token-lanes-block.researcher.md diff --git a/docs/secret-storage.md b/docs/secret-storage.md index f01ac7cd8..81d5e3a2d 100644 --- a/docs/secret-storage.md +++ b/docs/secret-storage.md @@ -14,6 +14,7 @@ against it. | Inventory id | What it is | Status | Where it lives | Variable names | | --- | --- | --- | --- | --- | | `alpaca-paper` | Alpaca paper broker key pair | required now | `/alpaca-paper.env` | `APCA_API_KEY_ID`, `APCA_API_SECRET_KEY` (optional, not secret: `APCA_API_BASE_URL`) | +| `alpaca-paper-2` | Alpaca paper broker key pair, second paper account (isolated incentive-monitor study) | optional | `/alpaca-paper-2.env`, pointer `PAPER_ENV_FILE_2` | `APCA_API_KEY_ID`, `APCA_API_SECRET_KEY` (optional, not secret: `APCA_API_BASE_URL`) | | `sec-contact` | SEC/EDGAR contact string. This is private personal data, not an auth secret | required now | `/sec-contact.env` | `SEC_USER_AGENT` (optional: `EDGAR_IDENTITY`) | | `databento` | Databento API key | only when you buy it | `/databento.env` | `DATABENTO_API_KEY` | | `typesafe` | Typesafe key, for the live-judge mode of `gap_crosswalk.py` only | only when you pay for it | `/typesafe.env` | `TYPESAFE_API_KEY` | @@ -84,6 +85,7 @@ Put the file paths, never the values, in your shell startup file once: # ~/.bashrc or ~/.zshrc: pointers only; no credential value is exported _nas_store="${XDG_CONFIG_HOME:-$HOME/.config}/native-agent-stack" export PAPER_ENV_FILE="$_nas_store/alpaca-paper.env" +export PAPER_ENV_FILE_2="$_nas_store/alpaca-paper-2.env" # second paper account, where this host holds one export SEC_CONTACT_ENV="$_nas_store/sec-contact.env" export PIT_ALPACA_ENV_PATH="$PAPER_ENV_FILE" PIT_SEC_ENV_PATH="$SEC_CONTACT_ENV" unset _nas_store diff --git a/manifests/evidence.json b/manifests/evidence.json index dd6d32c52..f4cbf47f9 100644 --- a/manifests/evidence.json +++ b/manifests/evidence.json @@ -3714,8 +3714,8 @@ }, { "path": "adoption/credential-inventory.json", - "sha256": "5e20ce0d08dd203b5b9d86ee2375cd03131a4c1f3c18b096bb48f66371bb6e1a", - "bytes": 17862 + "sha256": "2404df6c8d17b8dea544f162d439ddf8a03a5f12f0c2799a63707c9f88e98e35", + "bytes": 20076 }, { "path": "adoption/hardware-profiles.json", @@ -3724,7 +3724,7 @@ }, { "path": "adoption/hooks/claude/SHA256SUMS", - "sha256": "b611e824165ab4465bed6729054a1c1eaed75a7c0044506f508e4c6adacd62d4", + "sha256": "06385c800b70072e9163096f06e73874cfed63040cd9db87ae221d1cbd2aed62", "bytes": 861 }, { @@ -14700,8 +14700,8 @@ }, { "path": "docs/secret-storage.md", - "sha256": "6d7a9d5e33efc186c2876daa5990713be7cc8e3f7491190de2022b4d952624f2", - "bytes": 82026 + "sha256": "b02073ef9f01bd52a51886e2cac24ef555b23d7e7b4279a467199f91bd138ec7", + "bytes": 82401 }, { "path": "docs/stack.md", @@ -42035,8 +42035,8 @@ }, { "path": "scripts/hooks/secret_path_guard.py", - "sha256": "abf269ba41a126400e084b4f03828eb4f0e6fc2375633c0e788136158a3054b1", - "bytes": 43178 + "sha256": "f9be81b240762dc58da874f135040d182ca9fb5e9056ab9b8a977dd909453244", + "bytes": 43564 }, { "path": "scripts/host_receipts.py", @@ -42545,8 +42545,8 @@ }, { "path": "tests/test_credential_status.py", - "sha256": "f1f3a7e431ebf092db8ed1ca121b80462aac950ec4280fb71c1b9a7ea0ccfca5", - "bytes": 27639 + "sha256": "a2b96d0c75ffc67950544632562ff83f23d3d248c2800c34efd64f928e51b426", + "bytes": 28598 }, { "path": "tests/test_credential_tools.py", @@ -42975,8 +42975,8 @@ }, { "path": "tests/test_secret_path_guard.py", - "sha256": "a46943e13beaf956f6c96c4623451f6d398e4d9f416d41be6d50507260b63ff8", - "bytes": 62922 + "sha256": "767d26f16a70a9eb20a907fbe762126b9e2c132c8256e2ba19a6373cdb585e6c", + "bytes": 65660 }, { "path": "tests/test_security_identity.py", diff --git a/scripts/hooks/secret_path_guard.py b/scripts/hooks/secret_path_guard.py index 8f69279be..5a2a51fdb 100644 --- a/scripts/hooks/secret_path_guard.py +++ b/scripts/hooks/secret_path_guard.py @@ -90,8 +90,11 @@ SECRET_NAME = re.compile(r"\b(?:" + _NAMES + r")\b") SECRET_EXPANSION = re.compile(r"\$\{?!?(?:" + _NAMES + r")\b") SECRET_LOOKUP = re.compile(r"(?:environ|getenv|process\.env|ENV\[)[^;\n]{0,40}\b(?:" + _NAMES + r")\b") +# Every inventory `pointer_variables` name (tests/test_secret_path_guard.py checks each), with an optional numeric suffix: a +# further paper account's pointer is PAPER_ENV_FILE_2. Without the suffix group the word boundary after PAPER_ENV_FILE failed +# before `_2`, so a reader, a redirect or a `source` on "$PAPER_ENV_FILE_2" passed while the account-1 forms were blocked. POINTER_VARIABLE = re.compile( - r"\$\{?(?:PAPER_ENV_FILE|ENV_FILE|SEC_CONTACT_ENV|PIT_ALPACA_ENV_PATH|PIT_SEC_ENV_PATH|HF_TOKEN_PATH)\b") + r"\$\{?(?:PAPER_ENV_FILE|ENV_FILE|SEC_CONTACT_ENV|PIT_ALPACA_ENV_PATH|PIT_SEC_ENV_PATH|HF_TOKEN_PATH)(?:_[0-9]+)?\b") # The Hugging Face home itself (or everything in it) as a reader's operand: a recursive search or a # copy of it includes both token files. Its subdirectories such as hub/ stay readable. HF_HOME_ROOT = re.compile(r"(?:(?:\.cache|XDG_CACHE_HOME)\}?/huggingface\}?|^\$\{?HF_HOME\}?)(?:/\**)?$") diff --git a/tests/test_credential_status.py b/tests/test_credential_status.py index 075df0a4a..abd50a099 100644 --- a/tests/test_credential_status.py +++ b/tests/test_credential_status.py @@ -71,11 +71,23 @@ def run_cli(self, *args): def test_real_inventory_is_valid(self): self.assertEqual(cs.inventory_errors(self.inventory, ROOT), []) ids = {e["id"] for e in self.inventory["entries"]} - self.assertTrue({"alpaca-paper", "sec-contact", "claude-native", "codex-native", "gh-native", + self.assertTrue({"alpaca-paper", "alpaca-paper-2", "sec-contact", "claude-native", "codex-native", "gh-native", "huggingface-native", "huggingface-native-stored"} <= ids) self.assertIn("HUGGING_FACE_HUB_TOKEN", self.inventory["must_not_be_set"]) self.assertIn("HF_TOKEN", self.inventory["must_not_be_set"]) + def test_second_paper_row_mirrors_the_first(self): + # alpaca-paper-2 names the second paper account's existing file: same class, lane, store kind and variables as + # account 1, its own file and its own pointer (2026-09-29), and neither row shares a pointer with the other. + rows = {e["id"]: e for e in self.inventory["entries"]} + first, second = rows["alpaca-paper"], rows["alpaca-paper-2"] + for key in ("class", "lane", "variables", "optional_variables"): + self.assertEqual(second[key], first[key], key) + self.assertEqual(second["store"]["kind"], first["store"]["kind"]) + self.assertEqual(second["store"]["path_template"], first["store"]["path_template"].replace("alpaca-paper.env", "alpaca-paper-2.env")) + self.assertEqual(second["pointer_variables"], ["PAPER_ENV_FILE_2"]) + self.assertFalse(set(second["pointer_variables"]) & set(first["pointer_variables"])) + def test_inventory_rejects_non_home_template_and_bad_names(self): broken = copy.deepcopy(self.inventory) broken["entries"][0]["store"]["path_template"] = "/srv/shared/alpaca.env" diff --git a/tests/test_secret_path_guard.py b/tests/test_secret_path_guard.py index 5d34df2d4..bc947315e 100644 --- a/tests/test_secret_path_guard.py +++ b/tests/test_secret_path_guard.py @@ -94,6 +94,18 @@ "ls ${XDG_CONFIG_HOME:-$HOME/.config}/native-agent-stack": "credential_store_path", "rg KEY \"$XDG_CONFIG_HOME/native-agent-stack/\"": "credential_store_path", "while read -r line; do :; done < \"$PAPER_ENV_FILE\"": "credential_file_read", + # A numbered pointer (PAPER_ENV_FILE_2 is the second paper account's, 2026-09-29): the word boundary after + # PAPER_ENV_FILE used to fail before `_2`, so each of these passed the guard (checked against the previous guard) + # while the account-1 form was blocked. + "cat \"$PAPER_ENV_FILE_2\"": "credential_file_read", + "cat \"${PAPER_ENV_FILE_2}\"": "credential_file_read", + "head -n 3 \"$PAPER_ENV_FILE_2\"": "credential_file_read", + "while read -r l; do :; done < \"$PAPER_ENV_FILE_2\"": "credential_file_read", + "cat \"$SEC_CONTACT_ENV_2\"": "credential_file_read", + "set -x; . \"$PAPER_ENV_FILE_2\"": "trace_while_sourcing", + "set -o xtrace; source \"${PAPER_ENV_FILE_2}\"": "trace_while_sourcing", + ". \"$PAPER_ENV_FILE_2\"; declare -p APCA_API_KEY_ID": "environment_dump_after_source", + "( . \"$PAPER_ENV_FILE_2\"; python3 -c 'import os; print(dict(os.environ))' )": "environment_dump_after_source", # Shell tracing or verbose mode while sourcing a credential file prints its assignments. "set -x; . \"$PAPER_ENV_FILE\"": "trace_while_sourcing", "set -euxo pipefail; set -a; . \"$SEC_CONTACT_ENV\"; set +a": "trace_while_sourcing", @@ -462,6 +474,18 @@ "wc -c \"$PAPER_ENV_FILE\"", "stat -c '%a %U' \"$PAPER_ENV_FILE\"", "( set -a; . \"$PAPER_ENV_FILE\"; set +a; exec python3 blueprints/us-equities/alpaca-paper/paper_runner.py --once )", + # The second paper account (2026-09-29): its pointer handed to a loader or a size check, as account 1's is. + "python3 runner.py preflight --env-file \"$PAPER_ENV_FILE_2\" --output out.json", + "python3 -I tools/credentials/alpaca_rate_limit_probe.py --env-file \"$PAPER_ENV_FILE_2\" --out rate-limit.json", + "wc -c \"$PAPER_ENV_FILE_2\"", + "stat -c '%a %U' \"$PAPER_ENV_FILE_2\"", + # The trading lane's loader path (2026-09-29): a unit started with systemd-run --user whose bash -ic hands the pointer to a + # loader as --env-file. It must keep passing for both accounts, so closing the numbered-pointer hole never blocks a unit. + "systemd-run --user --unit=overnight-volume-watch --collect /bin/bash -ic " + "'exec python3 blueprints/us-equities/adaptive-paper/runner.py run --env-file \"$PAPER_ENV_FILE\"'", + "systemd-run --user --unit=paper-series-2 --collect /bin/bash -ic " + "'exec python3 blueprints/us-equities/adaptive-paper/runner.py run --env-file \"$PAPER_ENV_FILE_2\"'", + "systemd-run --user --unit=x --collect /bin/bash -ic \"exec python3 X --env-file \\\"$PAPER_ENV_FILE_2\\\" --output out.json\"", # Hugging Face: hf reads its own store, so checking the sign-in, the operator's interactive # login, revision-pinned downloads and checksum verification never expose the token. "hf auth whoami", @@ -697,6 +721,13 @@ # Known heuristic gaps, asserted so a change that closes one is noticed. EXPECTED_PASS_THROUGH = [ + # A pointer with a NON-numeric suffix is not recognised (only PAPER_ENV_FILE_ is, 2026-09-29): a new pointer name + # goes into the inventory and POINTER_VARIABLE together, and test_inventory_pointer_variables_are_guarded enforces it. + "cat \"$PAPER_ENV_FILE_B\"", + # systemd-run is not a modelled launcher: a reader it starts is not inspected, for either account (found 2026-09-29 while + # adding the loader-path rows above); with --pipe --wait its output returns to the caller. + "systemd-run --user --pipe --wait cat \"$PAPER_ENV_FILE\"", + "systemd-run --user --pipe --wait /bin/bash -ic 'cat \"$PAPER_ENV_FILE_2\"'", "python3 -c \"import runner; print(runner.credentials(__import__('os').path.expandvars('$PAPER_ENV_FILE')))\"", "python3 -c 'import os;print(dict(os.environ))'", # huggingface_hub's own loader, and an archiver on the whole Hugging Face home.