Skip to content

Credential inventory: github-review-app key file and a --from-file custody mode - #1057

Merged
seathatflowsinourveins merged 4 commits into
mainfrom
claude/api-actions-github-review-app-20261012
Oct 13, 2026
Merged

seathatflowsinourveins merged 4 commits into
mainfrom
claude/api-actions-github-review-app-20261012

Conversation

@seathatflowsinourveins

@seathatflowsinourveins seathatflowsinourveins commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Scope

Source. The command center's request of 2026-10-12 (22:06Z): the owner creates the review GitHub App, and its key needs custody that no agent reads.

The slot.

  • Class github_app_private_key, new in scripts/credential_status.py.
  • Status optional; lane foundation.
  • Store private_file at ${XDG_CONFIG_HOME:-$HOME/.config}/native-agent-stack/github-review-app.pem.
  • No variable in the file. GITHUB_REVIEW_APP_ID, the App's ID, is not secret and is not in the file: it is an optional public variable, set beside the loader that will sign the App's tokens.
  • Pointer GITHUB_REVIEW_APP_KEY_PATH; no loader yet.
  • credential_status.py checks the file's mode, owner and directory and never reads its contents (it parses only private_env_file stores).

--from-file (set_credential.py, through open_credential_terminal.sh github-review-app --from-file):

  • A separate strict load_file_entry takes only an operator-supplied private_file of a key-file class. The hidden-prompt mode and --from-env keep refusing the entry, so no export line is ever written to a key file.
  • The path is typed at a visible prompt in the owner's window. It is never written into the session script or onto a command line.
  • The source is opened with O_NOFOLLOW; anything but a regular file of 1 byte to 64 KiB, owned by the operator and with exactly one link, is refused. Custody removes that link; a second link would keep the key where custody cannot see it. Mode bits are not checked, since a Windows drive in WSL reports invented ones.
  • The bytes are copied, unread and unprinted, into a new O_EXCL 0600 temporary file through the checked store handle and fsynced.
  • The stored copy goes to openssl pkey -noout -passin pass: on standard input, with output discarded. Only the exit status counts, so a passphrase-protected key fails instead of prompting.
  • On failure the copy is removed and the original stays. On success the copy takes its final name (an existing key needs replace at the hidden prompt), the directory is fsynced, and only then is the original removed, provided the typed path still names the copied inode and it still has one link.
  • A failed file operation never shows its text or a path. Before the key is stored, it is a refusal with fixed text and nothing moves. After that, the tool reports the key stored and the source cleanup incomplete (exit 3).
  • The output names the entry and the store template, never contents.

Guard. Literal store paths (~/.config/native-agent-stack/…, the $XDG_CONFIG_HOME spellings, readers, copies and openssl on them) are already refused as credential_store_path, and the settings deny Read and Edit under the store. The new pointer was the one gap: cat "$GITHUB_REVIEW_APP_KEY_PATH" passed main's guard. It now refuses as credential_file_read; stat and ls on it stay allowed. The interpreter-reader protection family is not part of this change (the scope reduction below).

Follow-ups (not in this change):

  • The review worker's App token loader, which will name its loader in the inventory row and read GITHUB_REVIEW_APP_ID.
  • Optionally, print the stored key's public SHA-256 fingerprint, which GitHub shows beside each key, so the owner can match them. That is a public value, but it is a separate decision.
  • From Astra's first read at d9a7199d (P3):
    • blueprints/runtime-workers/openhands/resolver/patch_policy.py:137 cites docs/secret-storage.md:199-200, a locator already stale on main; a section anchor would hold. It is inherited and outside this change's paths.
    • Inherited hostname labels in the dispositions catalog and the evidence manifest; none is on an added line.
    • The reader's packet lacked the two cited captures (the GitHub private-keys page and openssl-pkey(1)). They are in the lane's readers/review-app-20261012/upstream/.
  • From Astra's delta read at 74cd74c3 (P3):
    • An interrupt in the short interval between the replace and the stored flag can still report "nothing written" while the key is stored. Cancellation reporting should be conservative across that interval, with a path-free instruction to check the stored key and the source.
    • The inherited patch_policy.py:137 locator and hostname labels, and the two captures, as above.
    • The completed same-source item is removed from this list.
  • From Astra's ACK at f3619dfc (P3), each with its trigger:
    • The replace/flag cancellation interval: an interrupt right after the replace, before the stored flag is set, can still report "nothing written" while the key is stored. Trigger: the next change to set_credential.py's custody path. Make cancellation reporting conservative across that interval, with a path-free instruction to check the stored key and the source, and add a real-replace-then-interrupt regression (CPython v3.13.16 Doc/library/signal.rst:783-801).
    • The inherited patch_policy.py:137 locator and hostname labels. Trigger: the owning cleanup of those files; use a stable section anchor and generic host roles there.
    • The two public captures (the GitHub private-keys page and openssl-pkey(1)). Trigger: the next reader packet for this code; supply them with matching hashes, plus the OpenSSL source or manual at the stated version.
    • The stale ordering paragraph in the Scope section. Done in this body edit: it is marked superseded by the split ruling.
  • Moved out of this change (the command center's split ruling of 2026-10-13, 06:24Z): the interpreter-reader protection family for the key-file pointer. Main's guard is unchanged for it. It goes to the guard's owner under route B, the native sandbox's filesystem read denial, as a cc-native-practice item.

Fix round 1 (60fbe1c6). Astra's first read at d9a7199d (the read's verdict, a private record cited by name) asked for changes with 2 P1s and 1 P2. The command center's tests-only check had passed. Every test below uses keys generated per test under a temporary root; no key byte or digest is printed or stored.

  • Finding 1: the source may not have another hard link or another owner. Both are refused before anything moves, and the source is removed only if it still has one link.
  • Finding 2: no failed file operation shows its text or a path.
    • Before the key is stored: a fixed refusal; nothing moves, and no temporary file is left.
    • After it: the tool reports the key stored and the source cleanup incomplete (exit 3), with the state of both files.
    • The CLI's file mode gains the value-free handler --from-env already has.
    • run_from_file looks up input and stdout per call. Defaults bound at import time had made the CLI path untestable.
    • Six FileCustodyTests regressions cover a second link, another owner, a link made during the copy, failures of the source check or removal after storing, failures of the store write before it (fsync, replace), and the CLI. Each fails on d9a7199d's set_credential.py (8 failing subtests) and passes here.
  • Finding 3: a dated advisory gate-read delta (unclassified-gate-reads-20261013-pr1057-delta.json) is appended to the index; earlier deltas and baselines are unchanged.
    • It records the four custody reads, from the native derivation (GitTree, derive_ci_protected) at d9a7199d: 348 reads against the Finalize 373 starred repositories with guarded retention and source coverage #1016 baseline's 344.
    • Every other added location is a line move of a baseline read.
    • This round moves lines only: the derivation at 60fbe1c6 gives the same total, script counters and shapes.
    • The two advisory tests fail at d9a7199d and pass here.
  • The P3s are in Follow-ups.

Fix round 2 (74cd74c3). Astra's delta read of 60fbe1c6 (the read's verdict, a private record cited by name) found 2 P1s and 5 P3s. The command center ruled one commit for the P1s and the P3s (05:31Z). Keys are still generated per test under a temporary root.

  • Finding 1 (removed again by the scope reduction below): interpreter code that looks the review App's key-file pointer up from its environment is refused as credential_file_read. That covers os.environ, os.getenv, process.env and ENV.
    • It is a whole-text rule after B(T)'s, bounded like SECRET_LOOKUP, and passes the guard's no-backtracking test.
    • stat, ls and test -f on the pointer stay allowed.
    • Eight rows join BLOCKED, so the keyring-exec and rtk-proxy tests read them too. A hook-protocol test runs the read's three forms through the real hook.
    • All fail on 60fbe1c6's guard (8 rows; 3 hook exits of 0) and pass here.
  • Finding 2: after removing the source, the still-open descriptor must show no link left. Otherwise custody reports the source cleanup incomplete (exit 3) and removes no unknown name.
    • The regression makes a real link right after the final check.
  • P3s taken:
    • An interrupt after the key is stored reports that state (exit 3) instead of "nothing written".
    • The stored key typed as its own source changes nothing.
    • The custody steps cite CPython v3.13.16 (cbc944f4): Doc/library/os.rst:2754 (replace), :1108 (fsync), :2412 (lstat) and :3125 (st_nlink).
    • The command transcript below, which an earlier body edit had split mid-line, is repaired.
  • Fail-before: on 60fbe1c6's set_credential.py, exactly the three new custody tests fail, one by an uncaught interrupt. The other 13 pass.
  • The advisory gate-read count is unchanged: the derivation at 74cd74c3 gives 348 with the same counters and shapes, so the dated delta of round 1 stands.
  • P3s in Follow-ups: the stale patch_policy.py:137 locator and hostnames (inherited), and the two captures, which the lane holds for the packet.

Scope reduction (f3619dfc). Astra's delta read of 74cd74c3 (the read's verdict, a private record cited by name) found one more P1 in the interpreter-reader protection family, and 4 P3s. Round 2 was the last fix round. The command center ruled a split (2026-10-13, 06:24Z).

  • Dropped: the family leaves this change. scripts/hooks/secret_path_guard.py and tests/test_secret_path_guard.py return to their 60fbe1c6 bytes, and the docs drop their sentences on it.
    • Main's guard is unchanged for that family, so nothing is weaker than main.
    • The guard pin returns to round 1's value.
  • Kept:
    • the --from-file custody mode, with finding 2's post-removal link check and the round-2 custody P3s;
    • the github-review-app inventory entry and the pointer row;
    • their tests.
  • The 4 P3s are in Follow-ups, the completed same-source item is removed there, and the moved family is recorded there with its owner and route.

SOTA sources

  • GitHub Docs, "Managing private keys for GitHub Apps": https://docs.github.com/en/apps/creating-github-apps/authenticating-with-a-github-app/managing-private-keys-for-github-apps, read 2026-10-12T22:16Z through the docs article API (sha256 f07ab5ee42454bed23f650d9d24eff2ecb239dccaf7fe988c61e37dad5893b1a). Line 39: the key downloads as a PEM file and "GitHub only stores the public portion of the key". Line 42: the PEM is PKCS#1 RSAPrivateKey. Lines 81-85: storing private keys. Captured in the api-actions lane's readers/review-app-20261012/upstream/.
  • OpenSSL 3.5.5 (installed), openssl-pkey(1): -noout "Do not output the key in encoded form" (line 88 of the capture, sha256 407550a20cc1c7d0f034e7315ed8456c347620308ffce9438a5ae00623c5c23e); -passin takes the password source.
  • The repository's own store practice: set_credential.py's checked directory handle, O_EXCL temporary files and atomic replace, reused here.

Evidence-class table

Claim Evidence class Command / receipt
A downloaded key moves into a 0600 store file byte for byte, and the original is removed only after the check local_integration FileCustodyTests with RSA keys generated per test
A non-key file, a passphrase-protected key, a symlink, a missing, empty or oversized source, and a missing openssl move nothing local_integration FileCustodyTests
The new tests fail on main local_integration the new tests with main's inventory, set_credential.py, launcher, guard and credential_status.py swapped in: FAILED (failures=5, errors=13)
The pointer was the guard's one gap for this file local_integration main's guard: ten literal-path and pointer forms; the eight literal forms refuse (credential_store_path), the two pointer forms pass

Local commands run

$ python3 -m unittest <the 31 test modules that name the inventory, set_credential, the launcher, the guard, credential_status, secret-storage, the dispositions, the hook pins or the profile installer> tests.test_upstream_surface_watch tests.test_evidence_manifest tests.test_validate
Ran 2294 tests in 849.476s
FAILED (failures=1, skipped=160)   # test_host_profile_copy_is_verbatim, local only and skipped in CI (the installed guard is main's)
$ (the new tests with main's inventory, set_credential.py, launcher, guard and credential_status.py swapped in)
FAILED (failures=5, errors=13)
$ bash tools/credentials/open_credential_terminal.sh github-review-app --from-file --dry-run
mode: file custody github-review-app ... python3 -I tools/credentials/set_credential.py github-review-app --from-file   (rc 0)
$ python3 scripts/validate.py
(rc 0)
$ python3 scripts/evidence_manifest.py --check
{"files": 11856, "status": "passed"}
$ gitleaks git --log-opts="origin/main..HEAD" --redact .
no leaks found   (rc 0)
$ git merge-tree --write-tree origin/main HEAD   # main 703d588c, after the commit
rc 0
# After the rebase onto main c63c83a8 (d9a7199d):
$ python3 -m unittest tests.test_secret_path_guard tests.test_credential_tools tests.test_credential_status tests.test_upstream_surface_watch tests.test_validate tests.test_evidence_manifest tests.test_install_claude_profile
Ran 517 tests in 95.742s
FAILED (failures=1, skipped=9)   # test_host_profile_copy_is_verbatim, local only: the installed user copy is main's guard until the post-landing reinstall
$ python3 scripts/validate.py; python3 scripts/evidence_manifest.py --check
(rc 0); {"files": 11862, "status": "passed"}
$ gitleaks git --log-opts="origin/main..HEAD" --redact .
no leaks found
# Fix round 1 (60fbe1c6):
$ python3 -m unittest tests.test_credential_tools.FileCustodyTests   # with d9a7199d's set_credential.py
FAILED (failures=2, errors=6)   # the six new tests (8 subtests); the seven earlier ones pass
$ python3 -m unittest tests.test_runtime_worker_openhands_push_gate.RepositoryWorkflowTests.<the two advisory tests>   # at d9a7199d
FAILED (failures=2)
$ python3 -m unittest tests.test_runtime_worker_openhands_push_gate tests.test_credential_tools tests.test_credential_status tests.test_secret_path_guard tests.test_upstream_surface_watch tests.test_validate tests.test_evidence_manifest tests.test_install_claude_profile
Ran 650 tests in 192.801s
FAILED (failures=1, skipped=9)   # test_host_profile_copy_is_verbatim, local only (the installed copy is main's guard)
$ python3 scripts/validate.py; python3 scripts/evidence_manifest.py --check
(rc 0); {"files": 11863, "status": "passed"}
$ gitleaks git --log-opts="origin/main..HEAD" --redact .
no leaks found
$ git merge-tree --write-tree origin/main HEAD   # main c63c83a8
rc 0
# Fix round 2 (74cd74c3):
$ python3 -m unittest tests.test_secret_path_guard.SecretPathGuardTests.test_blocked_commands <...>.test_interpreter_reads_of_the_review_app_key_pointer_are_refused_by_the_hook   # with 60fbe1c6's guard
FAILED (failures=11)   # the 8 new BLOCKED rows and the 3 hook forms
$ custody_against.py <worktree> <60fbe1c6's set_credential.py>
ran 16 failed 2 interrupted 1   # the three new custody tests
$ python3 -m unittest tests.test_runtime_worker_openhands_push_gate tests.test_credential_tools tests.test_credential_status tests.test_secret_path_guard tests.test_upstream_surface_watch tests.test_validate tests.test_evidence_manifest tests.test_install_claude_profile
Ran 654 tests in 223.896s
FAILED (failures=1, skipped=9)   # the same local-only test_host_profile_copy_is_verbatim
$ python3 scripts/validate.py; python3 scripts/evidence_manifest.py --check
(rc 0); {"files": 11863, "status": "passed"}
$ gitleaks git --log-opts="origin/main..HEAD" --redact .
no leaks found
$ git merge-tree --write-tree origin/main HEAD   # main c63c83a8, after the round-2 commit
rc 0
# Scope reduction (f3619dfc):
$ python3 -m unittest tests.test_credential_status tests.test_credential_tools tests.test_secret_path_guard tests.test_runtime_worker_openhands_push_gate tests.test_upstream_surface_watch tests.test_validate tests.test_evidence_manifest
Ran 528 tests in 153.013s
FAILED (failures=1, skipped=8)   # the same local-only test_host_profile_copy_is_verbatim
$ python3 scripts/validate.py; python3 scripts/evidence_manifest.py --check
(rc 0); {"files": 11863, "status": "passed"}
$ gitleaks git --log-opts="origin/main..HEAD" --redact .
no leaks found
$ git merge-tree --write-tree origin/main HEAD   # main dbe7a8da
rc 0

Decision record

None: the inventory row and docs/secret-storage.md record the command center's request cited above.

Host evidence

Not applicable: no receipt changes.

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

@seathatflowsinourveins seathatflowsinourveins added the lane:foundation Foundation lane: Claude/Codex setup, hosts, memory, RAG, research, workers label Oct 12, 2026
…stody mode

The command center's request of 2026-10-12 (22:06Z): the owner is
creating the review GitHub App, and its .pem private key needs custody
that no agent reads.

- adoption/credential-inventory.json: github-review-app, class
  github_app_private_key (new in scripts/credential_status.py), store a
  private_file at <store>/github-review-app.pem, no variable in the
  file, GITHUB_REVIEW_APP_ID (the App's ID, not secret) as an optional
  public variable kept beside its future loader, pointer
  GITHUB_REVIEW_APP_KEY_PATH, no loader yet.
- tools/credentials/set_credential.py --from-file (separate strict
  load_file_entry; the hidden prompt and --from-env still refuse the
  entry):
  - in the owner's terminal it asks for the downloaded file's path at a
    visible prompt;
  - it opens the file without following a symlink and refuses anything
    but a regular file of 1 byte to 64 KiB;
  - it copies the file unread and unprinted into a new 0600 temporary
    file through the checked store handle;
  - it checks that copy with `openssl pkey -noout -passin pass:` on
    standard input (exit status only);
  - only then does it give the copy its final name (replace needs the
    hidden word) and remove the downloaded file, if the typed path still
    names the copied inode.
  A failed check removes the copy and keeps the original.
- tools/credentials/open_credential_terminal.sh --from-file: the
  window's session runs the --from-file mode; the path is typed there,
  never written into the script.
- scripts/hooks/secret_path_guard.py: the new pointer joins
  POINTER_VARIABLE (a reader, copy or key tool on it is
  credential_file_read). Literal store paths were already refused.
- docs/secret-storage.md: the table row, the terminal usage and the
  custody paragraph, and the pointer line.
- Tests use RSA keys generated per test with openssl genpkey, never
  embedded:
  - custody: success, a non-key file, a passphrase-protected key, a
    symlink, a missing, empty or oversized source, replace, and no
    openssl;
  - the entry refused by the other modes, the launcher's dry run, the
    inventory row, and guard rows for the pointer.
  With main's files they give 5 failures and 13 errors.

The guard pin, five dispositions locators and the manifest are
re-derived.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@seathatflowsinourveins
seathatflowsinourveins force-pushed the claude/api-actions-github-review-app-20261012 branch from ea6e0e0 to d9a7199 Compare October 13, 2026 03:41
…ecord the custody reads

Fix round 1 of #1057, after Astra's first read at d9a7199 (findings 1-3) and the command center's tests-only check.

- Finding 1: --from-file refuses a source the operator does not own or that has another hard link, before anything
  moves, and removes the source only if it still has one link. Mode bits stay unchecked (WSL reports invented ones);
  the one link is removed once the stored copy passes its check.
- Finding 2: a failed file operation never shows its text or a path. Before the key is stored it is a refusal with
  fixed text and nothing moves; after it, the tool reports the key stored and the source cleanup incomplete (exit 3).
  The CLI's file mode gains the value-free handler --from-env already has. run_from_file looks up input and stdout
  per call. Six FileCustodyTests regressions fail on d9a7199's set_credential.py and pass here.
- Finding 3: a dated advisory gate-read delta, appended to the index, records the four custody reads from the
  native derivation at d9a7199 (348 reads against the #1016 baseline's 344); earlier deltas are unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…dy link race

Fix round 2 of #1057, after Astra's delta read at 60fbe1c (two P1s and P3s; the command center's ruling of 05:31Z).

- Finding 1: interpreter code that looks the review App's key-file pointer up from its environment (os.environ,
  os.getenv, process.env, ENV) is refused as credential_file_read, as a whole-text rule after B(T)'s. stat, ls and
  test -f on the pointer stay allowed. Rows join BLOCKED and ALLOWED; a hook-protocol test runs the read's three
  forms. All fail on 60fbe1c's guard and pass here.
- Finding 2: after removing the source, the still-open descriptor must show no link left; otherwise custody reports
  the source cleanup incomplete (exit 3) and removes no unknown name.
- P3s: an interrupt after the key is stored reports that state; the stored key given as its own source changes
  nothing; the custody steps cite CPython v3.13.16 (os.rst replace, fsync, lstat, st_nlink).
- The guard pin, the evidence manifest and five catalog locators follow the new bytes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…-file custody

Scope reduction of #1057 (the command center's ruling of 2026-10-13, 06:24Z, after Astra's delta read at 74cd74c).

- The interpreter-reader protection family leaves this change: scripts/hooks/secret_path_guard.py and
  tests/test_secret_path_guard.py return to their 60fbe1c bytes, and the docs drop their sentences on it. Main's guard
  is unchanged for that family, so nothing is weaker than main; the family moves to the guard's owner under route B
  (native sandbox read denial, cc-native-practice).
- Kept: the --from-file custody mode with finding 2's post-removal link check and the round-2 custody P3s, the
  github-review-app inventory entry, the pointer row and their tests.
- The guard pin returns to round 1's value; the evidence manifest and five catalog locators follow the new bytes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@seathatflowsinourveins
seathatflowsinourveins marked this pull request as ready for review October 13, 2026 07:33
@seathatflowsinourveins
seathatflowsinourveins merged commit 107bcca into main Oct 13, 2026
35 checks passed
@seathatflowsinourveins
seathatflowsinourveins deleted the claude/api-actions-github-review-app-20261012 branch October 13, 2026 07:45
seathatflowsinourveins added a commit that referenced this pull request Oct 13, 2026
… round 1 removes from scripts/skills_status.py

Round 1 moved the registry read of the native plugin checker into plugin_status and removed the twin check's own registry reader, so the OpenHands push gate's derivation counts two reads fewer in
scripts/skills_status.py (346 against the 348 that main's newest delta expects). tests.test_runtime_worker_openhands_push_gate derives its reads from the committed tree; round 1 was first checked on the
working tree, so the continuous integration found the count first.

- blueprints/runtime-workers/openhands/evidence: unclassified-gate-reads-20261013-pr1074-delta.json and its index entry (removed_by_script: scripts/skills_status.py 2; the four set_credential.py reads are
  #1057's, restated because the newest delta is cumulative from its baseline), derived with the native GitTree and derive_ci_protected at the committed head.
- the record and the receipts README name the delta. Nothing else changes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
seathatflowsinourveins added a commit that referenced this pull request Oct 13, 2026
… round 1 removes from scripts/skills_status.py

Round 1 moved the registry read of the native plugin checker into plugin_status and removed the twin check's own registry reader, so the OpenHands push gate's derivation counts two reads fewer in
scripts/skills_status.py (346 against the 348 that main's newest delta expects). tests.test_runtime_worker_openhands_push_gate derives its reads from the committed tree; round 1 was first checked on the
working tree, so the continuous integration found the count first.

- blueprints/runtime-workers/openhands/evidence: unclassified-gate-reads-20261013-pr1074-delta.json and its index entry (removed_by_script: scripts/skills_status.py 2; the four set_credential.py reads are
  #1057's, restated because the newest delta is cumulative from its baseline), derived with the native GitTree and derive_ci_protected at the committed head.
- the record and the receipts README name the delta. Nothing else changes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
seathatflowsinourveins added a commit that referenced this pull request Oct 13, 2026
… round 1 removes from scripts/skills_status.py

Round 1 moved the registry read of the native plugin checker into plugin_status and removed the twin check's own registry reader, so the OpenHands push gate's derivation counts two reads fewer in
scripts/skills_status.py (346 against the 348 that main's newest delta expects). tests.test_runtime_worker_openhands_push_gate derives its reads from the committed tree; round 1 was first checked on the
working tree, so the continuous integration found the count first.

- blueprints/runtime-workers/openhands/evidence: unclassified-gate-reads-20261013-pr1074-delta.json and its index entry (removed_by_script: scripts/skills_status.py 2; the four set_credential.py reads are
  #1057's, restated because the newest delta is cumulative from its baseline), derived with the native GitTree and derive_ci_protected at the committed head.
- the record and the receipts README name the delta. Nothing else changes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>
seathatflowsinourveins added a commit that referenced this pull request Oct 13, 2026
…whatever skillOverrides says, so each of the two doubly delivered skills keeps one copy per client (CPA-V-07) (#1074)

* Plugin skill identity: a plugin's skill is listed under its own name whatever skillOverrides says, so each of the two doubly delivered skills keeps one copy per client (CPA-V-07)

typesafe-ai and claude-api are each delivered to Claude Code by two routes (this repository's skills.sh copy and the vendor's plugin; the client's bundled copy and the pinned plugin), so each is listed twice with one description
(660 + 1,068 characters in every session), and the practice page said that skillOverrides governs skills. Measured on Claude Code 2.1.296 in headless init records: an entry hides only the copy it names (the personal or the
bundled one), the qualified names typesafe:typesafe-ai and claude-api:claude-api stay listed and a qualified key changes nothing; the documentation says so (skills.md L909, L928). Ten preload probes show that an agent that
preloads the skill by its short or its qualified name still gets the body with the personal copy hidden.

- adoption/skills/manifest.json: typesafe-ai names its plugin (claude_plugin, claude_skill_name typesafe:typesafe-ai); claude-api names the bare name the client lists its own copy under (bundled_twin). load_manifest checks the
  fields.
- scripts/skills_status.py: where the plugin is present (a user-scope install in installed_plugins.json and enabledPlugins true) the other copy must be hidden; the row gets a claude_twin entry, duplicate_listing when it is not,
  and the personal copy's expected listing there is off. A host without the plugin is unchanged.
- tools/skill-usage/skill_usage.py: the usage tracker adds the uses and the context of the two listings of a skill.
- The practice page (instructions-skills.md, slots.json) states the plugin exception.
- Derived files that follow: the line citations of two catalogs that the added manifest lines shifted (upstream-surface-dispositions, new-wsl-architecture), the generated new-WSL handbook and its receipt's digests (regenerated
  with scripts/build_new_wsl_handbook.py --write), and the practice snapshot label's hash of slots.json; their tests failed in the first sweeps and pass.
- The claude-api currency comparison is recorded: 91 of the 94 files of the bundled folder are byte for byte the plugin's, the other three differ in early-access wording, so the plugin stays and the bundled copy is the one
  hidden. The host step (skillOverrides typesafe-ai off, claude-api off) is the command center's act and is simulated against the first host's real settings: fail before, ok after. The profile template is unchanged.

Tests: 22 of the 142 tests of the three modules fail on the sources of the base, among them the audit's two fail-first tests; 41 of 41 mutants killed.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

* Plugin skill identity, round 1: a globally disabled bundled twin is one copy, the replacement plugin must be the pinned one, the registry is read from the client's folder, and the last use is the most recent

Astra's read of 2dae975 (CHANGES_REQUESTED: two P1 and a P2) and the local codex review (three P2) were reproduced on that head; each of the six has a test that fails there (receipts/fail-before-round1.txt).

- evidence/.../receipts/claude-api-currency.txt (P1, Astra): the digest of the prompt that the client expands for its bundled skill is removed, with every prefix of it: its first line names the folder the client
  unpacked the skill into on this host, and no published file hashes to it. The plugin file's digest (an upstream blob at the pin) stays. tests/test_plugin_skill_identity_receipts.py holds that every digest in the
  evidence, its README and the record resolves to a registered file, a manifest pin or a public page quoted with its URL and fetch time.
- scripts/skills_status.py (P1, Astra): a bundled twin is hidden by disabling bundled skills as a whole, disableBundledSkills true or CLAUDE_CODE_DISABLE_BUNDLED_SKILLS. Measured on 2.1.296, 30 init-record runs
  (receipts/global-disable-init-records.json): 77 skills listed, 61 with either, the bundled claude-api gone and the plugin's kept; the spellings and the precedence of the setting, the variable and the settings env
  entry are the tests'. A personal twin is not a bundled skill and stays a duplicate.
- scripts/skills_status.py (P2, local review): the plugin that stays must be the pinned one (pin, folder, bytes: plugin_status, the native plugin checker's own read); an installed and enabled plugin that is not is
  plugin_invalid whether or not the other copy is hidden.
- scripts/skills_status.py (P2, local review): the registry is read from the folder of the settings that enable the plugin (the client's configuration folder), not from the skills CLI's trimmed link folder; the native
  plugin status reads it too.
- scripts/skills_status.py (P2, local review): a Claude-only copied row with claude_plugin gets the twin check and the adjusted listing, as a linked one does.
- tools/skill-usage/skill_usage.py (P2, Astra): the combined last use is the most recent of the used listings, the cells put in order (never, today, N days, N hours ago); an unorderable cell makes it unknown.
- The record has a review round 1, the receipts README five rows, the older fail-before receipt names the base it was taken against and its replay at the exact base 993cc89 gives the same 22 of 142; 30 of 30
  mutants killed. The new helper sits beside claude_row so that no cited line number of the file moves.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

* Plugin skill identity, round 1: the advisory delta for the reads that round 1 removes from scripts/skills_status.py

Round 1 moved the registry read of the native plugin checker into plugin_status and removed the twin check's own registry reader, so the OpenHands push gate's derivation counts two reads fewer in
scripts/skills_status.py (346 against the 348 that main's newest delta expects). tests.test_runtime_worker_openhands_push_gate derives its reads from the committed tree; round 1 was first checked on the
working tree, so the continuous integration found the count first.

- blueprints/runtime-workers/openhands/evidence: unclassified-gate-reads-20261013-pr1074-delta.json and its index entry (removed_by_script: scripts/skills_status.py 2; the four set_credential.py reads are
  #1057's, restated because the newest delta is cumulative from its baseline), derived with the native GitTree and derive_ci_protected at the committed head.
- the record and the receipts README name the delta. Nothing else changes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

* Plugin skill identity, round 2: the client's switch for its bundled claude-api hides the bundled twin, and a bundled twin that no route lists is not_listed

The local codex review of f7e0cd7 said "patch is incorrect" (one P2) and Astra's read of 48e083e is CHANGES_REQUESTED (one P2 blocker, answered by f7e0cd7, and an adjacent P2); the co-op asked for one commit
for the two visibility cases. Each was reproduced on f7e0cd7 and has a test that fails there (receipts/fail-before-round2.txt).

- scripts/skills_status.py (P2, local review): the bundled twin is also hidden by the client's switch for that skill, CLAUDE_CODE_DISABLE_CLAUDE_API_SKILL: registerClaudeApiSkill is called with
  disabled: env.CLAUDE_CODE_DISABLE_CLAUDE_API_SKILL === true in the installed 2.1.296 binary (the function that initializes the bundled skills; neither the documentation nor the changelog at the pin names it).
  Measured in 14 init-record runs (receipts/skill-switch-init-records.json): 77 skills listed, 76 with the switch, the bundled copy gone and the plugin's kept; the spellings are the all-bundled switch's, and the
  settings env entry overrides the process environment in both directions. A bundled twin of another name has no such switch; a personal twin is not hidden by it.
- scripts/skills_status.py (P2, Astra, follow-up 2): with the plugin not listed (disabled, or not registered) and the bundled twin hidden (skillOverrides, the all-bundled switch or the skill's own) no route lists
  the skill: the twin entry is not_listed and the row fails. A disabled plugin with the bundled copy visible is one copy and passes.
- The push gate's read inventory (Astra's blocker 1) was answered by f7e0cd7 (the delta, 346 reads); this commit changes no count (the module passes on the committed tree, 127 tests).
- Truth fixes: the record says what is reported (never an install path or a credential) and what is read (the plugin's SKILL.md inside the plugin cache); the receipts README names the base that the older
  fail-before receipt was taken against.
- 14 of 14 mutants killed (receipts/mutants-round2.txt). Nothing else changes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

* Plugin skill identity, round 3: a number or an array in a settings env entry is read as the client reads it, and a missing settings file is empty settings

The local review of the rebased head found two states in which the checker said ok or duplicate_listing where the client's listing is otherwise, and the command center ruled a checker that reports a wrong
state a gate defect: one final bounded commit, pushed with the registry-only rebase onto main 24a2090. Each case was reproduced on 7c7d667 and has a test that fails there
(receipts/fail-before-round3.txt).

- scripts/skills_status.py env_text (P2-1, :482-484): the client reads a settings env value as JavaScript's String() of the parsed JSON, so a switch spelled 1.0, 1e0 or [1] is set as 1 is. The reader
  took only Python int and bool and str, so 1.0 and 1e0 (floats) were no text and the switch was reported unset. Measured on 2.1.296 in 28 init-record runs
  (receipts/numeric-env-init-records.json): 61 skills listed, the bundled claude-api gone, with 1, 1.0, 1e0, 10e-1, 0.1e1, 1E0, 1.00, true, "1", [1], ["1"], [true] and [[1]]; 77 with 0, 0.0, 2, 1.5, -0,
  null, [], an object, [1, 2], ["1", "x"], [null], [null, 1] and [1, true]; the skill's own switch reads 1.0 and 1e0 the same way. env_text now writes a number as its integral value without a point or an
  exponent below 1e21, an array as its elements joined by commas, an object as "[object Object]"; an array nested beyond the interpreter's depth is no switch.
- scripts/skills_status.py read_switches (P2-2, :514-516): a settings file that does not exist is empty settings, so the process environment's CLAUDE_CODE_DISABLE_BUNDLED_SKILLS (or the skill's own
  switch) still counts, as the client starts with none and reads its environment; with no file no plugin is enabled either, so a hidden bundled copy is not_listed and the row fails instead of passing. A
  file that exists and cannot be read still disables nothing.
- tests/test_skills_status.py: test_a_number_or_an_array_in_an_env_entry_is_the_text_javascript_makes_of_it (27 subtests of raw settings text through the reader and the row's twin state, three for the
  skill's own switch, the deep array) and test_a_missing_settings_file_is_empty_settings_and_the_environment_switch_still_counts; 12 of 12 mutants killed (receipts/mutants-round3-12.txt, the first run
  left an array read as its first element only, and the test now holds the measured multi-element spellings).
- The push gate's read inventory is not changed by this commit (the module passes on the committed tree); the rebase re-derived the delta of the round-1 commit over main's tree (347 reads).
- Nothing else changes.

Co-Authored-By: Claude Sonnet 5.5 <[email protected]>

---------

Co-authored-by: Claude Sonnet 5.5 <[email protected]>
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