Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion adoption/credential-inventory.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion adoption/hooks/claude/SHA256SUMS
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/secret-storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 | `<store>/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 | `<store>/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 | `<store>/sec-contact.env` | `SEC_USER_AGENT` (optional: `EDGAR_IDENTITY`) |
| `databento` | Databento API key | only when you buy it | `<store>/databento.env` | `DATABENTO_API_KEY` |
| `typesafe` | Typesafe key, for the live-judge mode of `gap_crosswalk.py` only | only when you pay for it | `<store>/typesafe.env` | `TYPESAFE_API_KEY` |
Expand Down Expand Up @@ -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
Expand Down
22 changes: 11 additions & 11 deletions manifests/evidence.json
Original file line number Diff line number Diff line change
Expand Up @@ -3714,8 +3714,8 @@
},
{
"path": "adoption/credential-inventory.json",
"sha256": "5e20ce0d08dd203b5b9d86ee2375cd03131a4c1f3c18b096bb48f66371bb6e1a",
"bytes": 17862
"sha256": "2404df6c8d17b8dea544f162d439ddf8a03a5f12f0c2799a63707c9f88e98e35",
"bytes": 20076
},
{
"path": "adoption/hardware-profiles.json",
Expand All @@ -3724,7 +3724,7 @@
},
{
"path": "adoption/hooks/claude/SHA256SUMS",
"sha256": "b611e824165ab4465bed6729054a1c1eaed75a7c0044506f508e4c6adacd62d4",
"sha256": "06385c800b70072e9163096f06e73874cfed63040cd9db87ae221d1cbd2aed62",
"bytes": 861
},
{
Expand Down Expand Up @@ -14700,8 +14700,8 @@
},
{
"path": "docs/secret-storage.md",
"sha256": "6d7a9d5e33efc186c2876daa5990713be7cc8e3f7491190de2022b4d952624f2",
"bytes": 82026
"sha256": "b02073ef9f01bd52a51886e2cac24ef555b23d7e7b4279a467199f91bd138ec7",
"bytes": 82401
},
{
"path": "docs/stack.md",
Expand Down Expand Up @@ -42035,8 +42035,8 @@
},
{
"path": "scripts/hooks/secret_path_guard.py",
"sha256": "abf269ba41a126400e084b4f03828eb4f0e6fc2375633c0e788136158a3054b1",
"bytes": 43178
"sha256": "f9be81b240762dc58da874f135040d182ca9fb5e9056ab9b8a977dd909453244",
"bytes": 43564
},
{
"path": "scripts/host_receipts.py",
Expand Down Expand Up @@ -42545,8 +42545,8 @@
},
{
"path": "tests/test_credential_status.py",
"sha256": "f1f3a7e431ebf092db8ed1ca121b80462aac950ec4280fb71c1b9a7ea0ccfca5",
"bytes": 27639
"sha256": "a2b96d0c75ffc67950544632562ff83f23d3d248c2800c34efd64f928e51b426",
"bytes": 28598
},
{
"path": "tests/test_credential_tools.py",
Expand Down Expand Up @@ -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",
Expand Down
5 changes: 4 additions & 1 deletion scripts/hooks/secret_path_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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\}?)(?:/\**)?$")
Expand Down
14 changes: 13 additions & 1 deletion tests/test_credential_status.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
31 changes: 31 additions & 0 deletions tests/test_secret_path_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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_<digits> 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.
Expand Down
Loading