Skip to content

fix(guard): cover the host's allocated OmniRoute gateway ports - #1244

Merged
seathatflowsinourveins merged 2 commits into
mainfrom
claude/guard-nhny-gateway-ports-20261017
Oct 17, 2026
Merged

seathatflowsinourveins merged 2 commits into
mainfrom
claude/guard-nhny-gateway-ports-20261017

Conversation

@seathatflowsinourveins

@seathatflowsinourveins seathatflowsinourveins commented Oct 17, 2026 •

Copy link
Copy Markdown
Collaborator

Scope

  • What this PR changes, in one or two sentences: the secret-path guard now refuses OmniRoute gateway management requests on every gateway port the host's port allocation (WSL install: add no-start mode and host port allocation #1180) names, within the read caps below, as well as on the fixed 20128, 20129, 21128 and 21129. Before this change, a host whose gitignored adoption/hosts/<host>.json moves the gateway let management requests to the new port pass unchecked, including the provider connection routes. This host's file sets NS_PORT_GATEWAY=23128, NS_PORT_GATEWAY_WS=23129 and NS_PORT_GATEWAY_EMBED_WS=23131.
  • Base commit: caf9ad7cd23a90d5dbe6fde1234011a7787fc0c6. The head is the original commit 3dd235c plus one fold commit for the CC verdict CHANGES_REQUESTED at 3dd235c (B1-B3, C-G).
  • Lane: lane:foundation
  • Owned paths touched:
    • scripts/hooks/secret_path_guard.py
    • tests/test_secret_path_guard.py
    • docs/secret-storage.md
    • adoption/hooks/claude/SHA256SUMS
    • manifests/evidence.json
    • catalogs/foundation/upstream-surface-dispositions.json: five docs/secret-storage.md line locators re-pointed after the docs insert; the line text is unchanged.

Design

  • Where the ports come from. The port tooling (evidence/artifacts/new-wsl-install-plan-20261002/config/observability_config.py at caf9ad7, load_service_ports() :182-194 and default_config_root() :197-198) reads ONE flat NS_PORT_* map per run. It takes the --host-file it is given (adoption/hosts/<host>.json in the allocation addendum's commands), else the file NS2604_HOST_FILE names, else the installed allocation. The guard cannot tell which file a run was given, so it reads all of these once when it starts:

    • the adoption/hosts/*.json under CLAUDE_PROJECT_DIR;
    • the adoption/hosts/*.json under the guard's own checkout;
    • the file NS2604_HOST_FILE names;
    • the installed allocation ${XDG_CONFIG_HOME:-$HOME/.config}/new-wsl-native-stack/.service-ports.json. This source also covers sessions in other projects that run the ~/.claude/hooks copy.

    The tooling reads no NS_PORT_GATEWAY* environment variables, so the guard reads none either.

  • The committed template. Every checkout, CI's included, holds the committed adoption/hosts/example.json, so its ports are covered wherever the guard runs from a checkout. 21128 and 21129 are fixed ports. Its NS_PORT_GATEWAY_EMBED_WS port, 21131, is added and has no allowed routes (G).

  • Which routes are allowed. Every NS_PORT_GATEWAY* key is a covered port.

    • An NS_PORT_GATEWAY port that is not a fixed port gets exactly 21128's allowed routes.
    • The other gateway keys get none, even where they repeat a port.
    • A fixed port keeps its own allowed routes; a host file never adds one to it.
    • A test ties the guard's key prefix to the gpt-gateway rows of SERVICE_PORTS (:45-47), so the guard adds no hardcoded allocation.
  • Fail-closed.

    • Sources only add ports.
    • A file that is not valid JSON still covers each "NS_PORT_GATEWAY*": "D" pair in its text.
    • An unreadable file, a non-regular file (a FIFO is never waited on), an absent file, or a source that raises an error adds nothing.
    • Each source has its own try (D), so the later sources and the fixed ports stay covered.
  • Read caps (B2).

    • Only the first 64 *.json files of an adoption/hosts directory are read, in sorted name order. A file after the 64th covers nothing.
    • Only the first 1 MiB of each file is read. A pair past that point covers nothing.
  • Bound (E, recorded, no hardening).

    • One guard start reads at most 130 files (two directories and two single files), and the ports are at most the 65,535 a port number allows.
    • The allocation is read at import, outside WORK_LIMITS and outside main()'s guard_error catch.
    • gateway_allocation() catches every exception. An import that still fails (exit 1) or outlasts the hook timeout blocks the call under the hook's onFailure: "block" (docs/decisions/2026-10-08-guard-hook-fails-closed.md, cases B and H).
  • Cost. Each guard start lists two directories and reads a few files of about 1 KiB each. Hook runs measured 0.08 to 0.12 s, the same as before.

  • In-process tests (F, recorded). The test module imports the guard in-process with the runner's environment, so the module's import state holds the runner's allocation. On a host with a moved gateway that means more covered ports, which never changes a fixed port's allowed routes. test_import_state_is_the_runners_allocation pins this. Every test that depends on the port set either uses configured() or runs the hook process in an isolated environment.

SOTA sources

Evidence-class table

Claim Evidence class Command / receipt
Non-allowlisted management requests to 23128, 23129 and 23131 refuse gateway_credential_route, as on 21128. On 23128, the allowlisted GET /api/settings/compression and POST /api/usage/provider-limits pass, and the 20129-only preview refuses local_integration (real hook process on a fixture host file; no request sent) test_allocated_gateway_ports_refuse_as_21128
Each source alone covers its own port, with every other source's port uncovered, and removing that source uncovers its port. The sources: CLAUDE_PROJECT_DIR, the guard's own checkout (a copied guard in a temporary checkout), NS2604_HOST_FILE, XDG_CONFIG_HOME, and the HOME/.config fallback. Each has its own port and its own config root local_integration test_each_allocation_source_covers_its_port; each source line deleted kills it (mutation runs below)
An allocated NS_PORT_GATEWAY port passes exactly the frozen policy rows that name 21128. It refuses the 20129-only preview and has the provider-limits and call-logs <id> rows synthetic (in-process, frozen rows) test_allocated_gateway_port_has_exactly_21128s_rows; widening every row, or not widening <id>, kills it
An absent host file leaves 20128, 20129, 21128 and 21129 covered local_integration + synthetic test_absent_host_file_keeps_fixed_ports
The real import, in an isolated process, covers the fixed ports plus the committed template's ports (21131 added, with no allowed routes). The in-process import state equals the runner's allocation local_integration test_import_state_is_the_runners_allocation
A file that is not JSON, a directory, a FIFO or bad values never removes coverage, and a fixed port gains no allowed route synthetic test_malformed_or_unreadable_host_file_fails_closed
A failing source drops no later source synthetic test_a_failing_source_drops_no_later_source (fails on 3dd235c)
The read caps are the documented 64 files and 1 MiB, and what lies past them covers nothing synthetic test_read_caps_are_the_documented_ones
The guard's gateway keys are the port tooling's gpt-gateway keys source_review (executed tie test) test_gateway_keys_are_the_port_toolings

Local commands run

# original head 3dd235c7: fail-first on caf9ad7c (git archive, overlay ONLY tests/test_secret_path_guard.py)
FAILED (failures=7, errors=3): AssertionError: 0 != 2 for :23128, :23129, :23131 and :24555 /api/providers/client  (exit 1)

# fold head: fail-first on 3dd235c7 (git archive, overlay ONLY tests/test_secret_path_guard.py)
$ python3 -m unittest -v tests.test_secret_path_guard.GatewayAllocationTests
FAILED (failures=1): test_a_failing_source_drops_no_later_source (D, the one behaviour change)     (exit 1)
  the other new tests pin 3dd235c7's behaviour; each one is shown failing by the mutation runs below

# mutation runs: 3dd235c7 archive + the fold's changed files, one guard line deleted or altered per run
B1 CLAUDE_PROJECT_DIR source deleted     -> test_each_allocation_source_covers_its_port (+1) fail     (exit 1)
B1 guard-checkout source deleted         -> test_each_allocation_source_covers_its_port, test_import_state_... fail (exit 1)
B1 NS2604_HOST_FILE source deleted       -> test_each_allocation_source_covers_its_port (+1) fail     (exit 1)
B1 XDG_CONFIG_HOME source deleted        -> test_each_allocation_source_covers_its_port fail          (exit 1)
B1 HOME/.config fallback deleted         -> test_each_allocation_source_covers_its_port fail          (exit 1)
B3 every row widened                     -> test_allocated_gateway_port_has_exactly_21128s_rows (+1) fail (exit 1)
B3 call-logs <id> ports not widened      -> test_allocated_gateway_port_has_exactly_21128s_rows fail  (exit 1)
B3 no row widened                        -> 4 tests fail                                              (exit 1)
D  one try over every source             -> test_a_failing_source_drops_no_later_source fail          (exit 1)
B2 file-count cap removed                -> test_read_caps_are_the_documented_ones fail               (exit 1)
B2 byte cap removed                      -> test_read_caps_are_the_documented_ones fail               (exit 1)
unmutated control                        -> OK                                                        (exit 0)
11 of 11 mutants killed

$ CI=true python3 -m unittest tests.test_secret_path_guard
Ran 99 tests ... OK (skipped=3)                                            (exit 0)
$ python3 -m unittest tests.test_secret_path_guard                          # on the host, not CI
FAILED (failures=1): test_host_profile_copy_is_verbatim (exit 1). The installed ~/.claude/hooks copy is still the old guard;
this clears after `tools/adoption/install_claude_profile.py --only guard` once the PR merges (skipped in CI).
$ PATH=/var/tmp/cc-cc-work/venv-ci/bin:$PATH python3 scripts/validate.py
{"components": 71, "hashed_files": 12290, "profiles": 4, "receipts": 243, "status": "passed"}   (exit 0)
$ python3 scripts/evidence_manifest.py --check
{"files": 12290, "status": "passed"}                                       (exit 0)
$ python3 -m unittest tests.test_runtime_worker_openhands_push_gate         # after commit
Ran 127 tests ... OK (skipped=1)                                           (exit 0)
$ git merge-tree --write-tree origin/main HEAD                               # after a fresh fetch
(exit 0, clean)

At 3dd235c, the related modules (docs consistency, Claude profile install, practice ledger, upstream surface watch) had four test_upstream_surface_watch.CachedArtifactTests failures. The same four fail on caf9ad7 unchanged, so they are main's, not this PR's.

Decision record

No new decision. The change applies the existing gateway policy (docs/secret-storage.md, "Gateway (GW): one matrix") to the ports that #1180's host allocation, docs/decisions/2026-10-16-new-wsl-host-allocation-addendum.md, already assigns to the gateway.

Host evidence

Not applicable: no evidence/hosts/ change.

Checklist

  • New/changed GitHub Actions are pinned to a full commit SHA with a
    version comment (no floating tags). (none changed)
  • New/changed workflows declare top-level permissions: {} and grant each job
    only what it needs (contents: read, or an explicitly justified addition). (none changed)
  • No secrets are printed, logged or committed; no new required secret was
    added without a documented owner.
  • No new paid hosting, subscription or billing surface was introduced.
  • Peer-owned untracked files and worktrees were preserved (not deleted,
    moved or overwritten).

🤖 Generated with Claude Code

The secret-path guard refused gateway management requests only on the
fixed ports 20128, 20129, 21128 and 21129. A host's port allocation
(#1180) can move the gateway: this host's gitignored adoption/hosts
file sets NS_PORT_GATEWAY=23128, NS_PORT_GATEWAY_WS=23129 and
NS_PORT_GATEWAY_EMBED_WS=23131, so management requests to 23128 passed
unchecked.

The guard now reads the allocation once at start from the maps the port
tooling reads (observability_config.py load_service_ports()): every
adoption/hosts/*.json under CLAUDE_PROJECT_DIR and the guard's own
checkout, NS2604_HOST_FILE, and the installed
new-wsl-native-stack/.service-ports.json. Every NS_PORT_GATEWAY* port
is covered; NS_PORT_GATEWAY takes the rows of 21128, the other gateway
keys none, and a fixed port keeps its own rows (a host file never adds
a row to it). It fails closed: sources only add ports, a non-JSON file
still covers its "NS_PORT_GATEWAY*": "D" pairs, and an unreadable,
non-regular or absent file leaves the fixed ports covered.

docs/secret-storage.md names the source; the shifted
upstream-surface-dispositions locators, SHA256SUMS and the evidence
manifest are updated. Regression tests fail on caf9ad7.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@seathatflowsinourveins seathatflowsinourveins added the lane:foundation Foundation lane: Claude/Codex setup, hosts, memory, RAG, research, workers label Oct 17, 2026
Folds the CC verdict CHANGES_REQUESTED at 3dd235c (B1-B3, C-G).

- B1: test_each_allocation_source_covers_its_port shows each source alone
  (CLAUDE_PROJECT_DIR, the guard's own checkout, NS2604_HOST_FILE,
  XDG_CONFIG_HOME, the HOME/.config fallback), each with its own port and
  config root, with every other source's port uncovered, then removed.
- B2: the docs and guard comment state both read caps (first 64 *.json
  per directory in sorted order, first 1 MiB per file) and that what lies
  past them covers nothing; test_read_caps_are_the_documented_ones.
- B3: test_allocated_gateway_port_has_exactly_21128s_rows checks an
  allocated port against every frozen policy row, including the 20129-only
  preview and the provider-limits and call-logs <id> rows.
- C: the docs and comment describe the tooling's one-file-per-run order
  and why the guard reads all candidates; the table note names the
  other-key caveat.
- D: each source has its own try, so a failing source drops no later one
  (test_a_failing_source_drops_no_later_source fails on 3dd235c).
- E: the bound (130 files, 65,535 ports) and the import-time fail-closed
  path under onFailure: block are recorded; no hardening.
- F: the in-process import with the runner's environment is recorded and
  pinned (test_import_state_is_the_runners_allocation).
- G: the docs say every checkout covers the committed example.json's
  ports (21131 added, serving no row); a real-import test in an isolated
  process pins it.

Each new test is shown failing with the line it pins deleted (11 of 11
mutants killed).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@seathatflowsinourveins
seathatflowsinourveins merged commit 83dff3b into main Oct 17, 2026
34 checks passed
@seathatflowsinourveins
seathatflowsinourveins deleted the claude/guard-nhny-gateway-ports-20261017 branch October 17, 2026 18:47
seathatflowsinourveins added a commit that referenced this pull request Oct 17, 2026
# Conflicts:
#	adoption/hooks/claude/SHA256SUMS
#	manifests/evidence.json
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

lane:foundation Foundation lane: Claude/Codex setup, hosts, memory, RAG, research, workers

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant