fix(deploy-local): require usable CLAUDE.md + advisory MCP credential-gap warnings (#950) - #982
Conversation
…-gap warnings (#950) Deferred hardening for #950. is_trinity_compatible() now hard-fails an archive with no usable CLAUDE.md (missing / empty / whitespace-only / non-UTF-8) with a clean 400 NOT_TRINITY_COMPATIBLE instead of warning and deploying an instruction-less agent. Binary CLAUDE.md is caught explicitly so it yields a 400, not an unhandled 500. New collect_mcp_credential_warnings() scans .mcp.json[.template] for ${VAR} references absent from the post-merge .env and not platform-injected (static allowlist mirroring crud.py), surfaced as advisory DeployLocalResponse.warnings[] (also added to the MCP deploy_local_agent response type). Server names are sanitized (control-char strip + length bound) before echoing — operator-supplied JSON keys (#950 L1). Tests: 14 unit (validation + writable-dir fail-fast) + 3 integration (missing-CLAUDE.md 400, MCP warnings, valid deploy). Verified 16/16 against a sibling backend running this branch. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nings (#950) Update local-agent-deploy.md and template-processing.md for the #950 hardening (blocking CLAUDE.md validation, collect_mcp_credential_warnings, warnings[] response field) and reconcile already-merged #971 behavior (credentials request field, MAX_DEPLOY_CREDENTIALS, require_role("creator"), DEPLOYED_TEMPLATES_DIR_UNWRITABLE/WORKSPACE_PREPOP_FAILED codes). Add the feature-flows index Recent Updates row. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CLEAR verdict — no secrets/dependency/auth/injection regressions. One LOW finding (L1, operator-supplied MCP server-name terminal-escape echo) resolved in the same working tree via _sanitize_for_warning() with TDD. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-injection (#950) CodeQL flagged 2 high-severity py/path-injection alerts on the new collect_mcp_credential_warnings() .exists() calls (template_service.py:766/768). The operator-supplied agent name flows through sanitize_agent_name (which already strips path separators) into dest_path, but CodeQL can't model the regex sanitizer as a barrier, so the PR check hard-fails on a false positive. Add the barrier CodeQL documents it recognizes, inline at the dest_path build site: normalize via os.path.normpath, inline startswith prefix-check against the deployed-templates base, and flow the normalized Path downstream so the value reaching copytree and the .exists() sink is provably contained. Doubles as genuine defense-in-depth at the filesystem-write boundary; legitimate single-slug names are unaffected. Add a sanitizer-invariant unit battery asserting sanitize_agent_name never yields a traversal-capable value (no separators, no '..' component, single basename) across a table of hostile inputs — proving the upstream guarantee that makes the alert a false positive and guarding future regressions. Server-free; runs in the existing unit suite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI fix: CodeQL
|
|
Resolve by running |
vybe
left a comment
There was a problem hiding this comment.
Approved via /validate-pr with in-code security verification.
Confirmed in the diff (not just from the description):
- Path-containment guard uses normpath +
base + os.sepprefix check — no/foo-evil//foobypass; normalized value flows downstream. is_trinity_compatibledecodes CLAUDE.md strictly and catches UnicodeDecodeError/OSError → clean 400, never a 500 fall-through. Single caller (deploy.py:382) so the new blocking behavior does NOT touch the GitHub-template clone path._sanitize_for_warningstrips non-printables via isprintable() (ANSI/C0/C1/RTL-override) and bounds length — L1 resolved.- Credential warnings echo ${VAR} names + sanitized server name only; values are never read in.
- Three-layer boundaries respected; MCP
agents.tsresponse type updated (Invariant #13); feature-flows + CSO audit included; CI 13/13 + 30 tests green.
Non-blocking: the static _PLATFORM_INJECTED_* mirror of crud.py can drift (advisory-only impact) — a parity test would catch it. models.py overlaps #986; sequence the merges so the second rebases.
Resolve changelog append-conflict in docs/memory/feature-flows.md: keep both Recent Updates rows in newest-first order (GUARD-001/002/003 2026-06-02 above #950 2026-05-29). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary
Deferred hardening for #950 (the deploy-local empty-agent bug; core fix landed in #971). Two behavior changes plus docs and a CSO audit:
is_trinity_compatible()— CLAUDE.md is now blocking. A deploy archive with no usableCLAUDE.md(missing / empty / whitespace-only / non-UTF-8) returns a clean400 NOT_TRINITY_COMPATIBLEinstead of warning and deploying an instruction-less agent. A binary/non-UTF-8CLAUDE.mdis caught explicitly (UnicodeDecodeError/OSError) so it yields a 400, not an unhandled 500.collect_mcp_credential_warnings()— advisory credential-gap warnings. Scans.mcp.json.template/.mcp.jsonfor${VAR}references that are absent from the post-merge.envand not platform-injected (static allowlist mirroringcrud.py), surfaced as non-fatalDeployLocalResponse.warnings[](also added to the MCPdeploy_local_agentresponse type). MCP server names (operator-supplied JSON keys) are sanitized — control-char strip + length bound — before echoing (#950 L1).docs/reconciles already-merged #971 behavior too:credentialsrequest field,MAX_DEPLOY_CREDENTIALS,require_role("creator"), and theDEPLOYED_TEMPLATES_DIR_UNWRITABLE/WORKSPACE_PREPOP_FAILEDcodes.Closes #950.
Test Coverage
All new code paths covered.
Tests: +3 files (2 unit, 1 integration class extension).test_deploy_local_validation.py(CLAUDE.md missing/empty/whitespace/binary/valid/unicode; warnings: no-config/satisfied/platform-injected/unsatisfied/multiple/malicious-name/overlong) +test_deploy_writable_templates.py(DEPLOYED_TEMPLATES_DIR_UNWRITABLEfail-fast).Verification:
tests/test_deploy_local.pyneeds a backend running this branch. The shared local stack runs a different checkout, so I stood up a sibling backend on this branch (isolated DB, sibling Redis, real Docker socket) and ran the full suite:Pre-Landing Review
No issues. Three-layer boundaries respected (router → service → db); new logic is pure functions in
template_service.py. No new dependencies. No auth-surface change (therequire_role("creator")gate is pre-existing/documented). Pydanticwarnings: List[str] = []default is safe (Pydantic deep-copies field defaults).Security (CSO diff audit)
CLEAR —
docs/security-reports/cso-diff-2026-05-29-950.md. No secrets, no dependency CVEs, no injection/auth regressions; net security-positive (500→400 crash-path hardening, no-silent-fallback). One LOW finding (L1, operator-supplied MCP server-name terminal-escape echo) resolved in this branch via_sanitize_for_warning()with TDD. Credential values are never read into warnings (variable names only).Test plan
test_deploy_local.pypass against a backend running this branch🤖 Generated with Claude Code