security(skills): withhold agent config from skill test-run snapshots (PEN-2839) - #1581
Merged
Merged
Conversation
… (PEN-2839) `GET /companies/:companyId/skills/:skillId/test-runs[/:runId]` returned the subject agent's `adapterConfig` and `runtimeConfig` verbatim, including `adapterConfig.env`, and the same pair was persisted unredacted to the `agent_config_snapshot` column. `agents.getById` returns the raw row -- redaction of this material lives at the route layer -- so `snapshotAgentConfig` received plaintext. Both endpoints are gated by `assertCompanyAccess` alone, which admits any same-company agent actor. That is a weaker entitlement than `GET /agents/:id`, which hands the same material only to a caller holding `agent_config:read`: the sibling-path asymmetry PEN-2777 closed on the `hire_agent` approval card, recurring here without even the value masking that path already had. Fixed at both ends so neither is load-bearing for the other: - Read projection withholds the pair. This is what repairs rows persisted before the write path was fixed; a write-only fix leaves every existing run disclosing. - Write path redacts before persisting, so the column is not credential material at rest. The read filter is `withholdAgentConfigKeys`, extracted from `withholdAgentConfigFromApprovalPayload` rather than copied. This class propagates by copying -- doors #7-#10 are the same disclosure re-derived on paths whose authors could not see each other -- and a second copy would not have carried the array/JSON-string bypass Ally caught in #1574. The approval wrapper keeps its hire-only precondition, asserted by a test. Refs PEN-2370 (ask 3, criterion b2), PEN-2777, PEN-2778. Signed-off-by: Cto <cto@paperclip.blockcast.net>
Author
1 similar comment
Author
Author
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
Author
There was a problem hiding this comment.
Ally — Consolidated PR Review
Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. The configured toolkit and gstack executors were unavailable in this isolated runtime; their checks were applied directly to the exact PR diff.
Reviewed head: 5daee94
Critical Issues (0)
Important Issues (0)
Suggestions (0)
Strengths
- The read projection and write-path protections are independently implemented, covering both historical plaintext snapshots and newly persisted snapshots.
withholdAgentConfigKeysis shared without widening the hire-approval wrapper's type precondition, and the tests cover nested, non-object, and pre-existing snapshot shapes.- The synthetic tests assert both credential values and MCP topology are absent from served responses, while preserving the documented non-sensitive snapshot fields.
Recommended Action
- No Critical or Important issues found; this review is a formal self-review comment because the PR is authored by the Ally GitHub App.
- Consider the Suggestions opportunistically.
kkroo
approved these changes
Sep 1, 2026
kkroo
left a comment
There was a problem hiding this comment.
Approved after Ally exact-head review. Ally reports zero Critical and Important findings on the current head; all required checks are green.
This was referenced Sep 1, 2026
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.
Thinking Path
Linked Issues or Issue Description
adapterConfig.envbut is a different concern (making theclaude_k8sadapter's env default-deny at the adapter layer, BLO-22546); it does not overlap this read/write projection. No duplicate PR exists.What Changed
toCompanySkillTestRunwithholdsadapterConfig/runtimeConfigfrom the served snapshot on both…/test-runsand…/test-runs/:runId. This is what repairs rows already persisted in the clear, which a write-path fix cannot reach.snapshotAgentConfigroutes the pair throughredactAgentConfigPayloadbefore persisting, so the column stops being credential material at rest.withholdAgentConfigKeys, extracted fromwithholdAgentConfigFromApprovalPayloadso both callers delegate to one implementation. This class propagates by copying: doors fix(heartbeat): trust SDK success signal when wrapper exits non-zero #7–ops(opencode-k8s): bump to kubectl-logs streaming (Staff Engineer 'failed' fix) #10 are the same disclosure re-derived on paths whose authors could not see each other, and a fresh copy would not have carried the array/JSON-string bypass Ally caught in fix(approvals): gate hire_agent embedded agent config on agent_config:read #1574.hire_agent-only precondition, pinned by a test so the extraction cannot silently widen PEN-2777's scoping.name,model,assignedSkills,instructionsRef) are derived before withholding and survive. The only UI consumer isagentConfigSnapshot?.name.Why the gate did not hold
assertCompanyAccessrefuses only cross-company access, so any same-company agent passed it. Note the verb asymmetry: creating a test run is gated (skills.test+assertCanOrchestrateSkillTestHarness), but reading one was not — an agent with no skill-test permission still read every recorded snapshot.adapterConfigGET /agents/:idagent_config:readhire_agentapproval cardcompany_scope:readGET …/test-runs/:runId(this PR)company_scope:readVerification
Both suites run locally against embedded Postgres; 67 passed, 0 skipped. The DB suite
describe.skips silently when Postgres is unsupported, so I confirmed by name that each test actually executed rather than trusting the aggregate.Fail-first, each end reverted independently — this is the evidence that neither end is load-bearing for the other:
Reverting the read projection surfaced the credential verbatim in the served response body:
Each end failing under its own mutation while the other stays green is the point: a write-only fix would have left every pre-existing row disclosing, and the read projection alone would have left the column plaintext at rest.
Pre-existing PEN-2777 tests stay green across the extraction (they are among the 67).
Typecheck:
tsc --noEmitoutput is byte-identical between this branch and pristinemasterin the same environment. Two residual errors aboutagentMeRecoveryActionsQuerySchemareproduce on unmodifiedmasterhere and are an artifact of my local dependency wiring, not this branch.Risks
agentConfigSnapshot.adapterConfigand.runtimeConfigare now absent. The only consumer read in the UI isagentConfigSnapshot?.name, which is unaffected; any out-of-tree consumer depending on the withheld pair would see it disappear. That is the intended security outcome, but it is a behavioural change rather than a pure internal refactor.withholdAgentConfigFromApprovalPayloadis a shipped security control (PEN-2777/fix(approvals): gate hire_agent embedded agent config on agent_config:read #1574); refactoring it risks widening or narrowing its scope. Mitigated by keeping thehire_agent-only precondition in the wrapper and pinning it with a test, and by the pre-existing fix(approvals): gate hire_agent embedded agent config on agent_config:read #1574 tests staying green.Model Used
Claude Opus 5 (
claude-opus-5[1m]), 1M context window, extended thinking enabled, driven through the Claude Code agent harness with tool use (file edits, local test execution, GitHub API reads).Handling
No credential value was read, quoted, or committed; all fixtures are synthetic. The vulnerable endpoint was never called — calling it is the exposure. Every claim in the finding was read from source at
masterc75a306.Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue templateRefs PEN-2370 (ask 3, criterion b2), PEN-2777, PEN-2778, PRs #1567 #1573 #1574 #1578.