fix: close CEG open-PR audit findings F280-1/2, F283-1, F284-1/2 (supersedes #280, #281, #283, #284) - #285
Conversation
C-009 named make cypher-lint as automated enforcement but the target did not exist. Add the scanner, wire it to Make and pre-commit, and parameterize the live quoted interpolations it now finds. Issue-Remediation-Cycle: #272/cycle-1 Co-authored-by: Cursor <cursoragent@cursor.com>
Ruff SIM103 failed the PR writer wave on the two-return form. Issue-Remediation-Cycle: #272/cycle-1 Co-authored-by: Cursor <cursoragent@cursor.com>
Carry the isolate follow-up tests onto the open PR so identifier quoting has the same unit coverage as sanitize_label. Issue-Remediation-Cycle: #272/cycle-1 Co-authored-by: Cursor <cursoragent@cursor.com>
CEG is Python-only but had no .l9/ci.json, so REPO_CLASS=auto left Analyze (central Core) fail-closed on ambiguous SDK language detect. Consumer metadata is the documented escape hatch and does not touch workflows. Issue-Remediation-Cycle: #273/cycle-1 Co-authored-by: Cursor <cursoragent@cursor.com>
CEG-001 supply chain. The release set kept the moving @v1 tag and closed the finding on the lock instead: poetry.lock records resolved_reference e9f829f, which is what every deployed image installs, and validate_sdk_pin.py now prints that commit on PASS so a build log says which SDK object was taken. EIE's validator enforces the same contract over its requirements.lock. The trade-off the moving tag accepts — a live-resolving build takes whatever v1 points at that minute — is now stated in the script rather than left to be discovered. CEG-002 configuration. Both compose files set L9_REQUIRE_SIGNATURE=true and neither set L9_VERIFYING_KEYS_JSON. That flag also verifies the responses Gate signs, and Gate signs with its own key id, so as shipped CEG rejected every signed Gate response — the E2E passed only because its harness supplied the map. Dev carries the shared HMAC secret; prod requires the variable rather than defaulting it, because a wrong key here is a silent trust failure. CEG-003 configuration. Compose set L9_NODE_SPEC_PATH, which nothing reads. The SDK reads GATE_NODE_SPEC_PATH. Registration worked only because the SDK default happens to be engine/spec.yaml and WORKDIR is /app, so relocating the spec would have silently had no effect until the default path stopped existing — at which point register_with_gate swallows FileNotFoundError and returns False. CEG-005 architecture. engine/spec.yaml now declares owner: ceg. Registration previously succeeded only because Gate's _OWNER_ALIASES maps the node name "graph" to "ceg": correct today, and an alias rather than a declaration. Renaming the node would have broken registration for every canonical action at once. Pairs with the resolve ownership entry added in Constellation.Gate. CEG-006 + EIE-008 architecture. Two halves of one loop, neither connected. engine/health/api.py implemented three handlers and was imported by nothing, so request_enrichment — the entire CEG -> Gate -> EIE direction — had no trigger an inbound packet could reach; it now hangs off `admin` alongside trigger_gds, where operator surfaces belong. In the other direction EIE advertises and fully implements `graph-inference-result` and no code here ever produced one, so the feedback loop had a consumer and no producer. emit_graph_inference_result sends it through Gate, filtering at the same 0.55 confidence floor EIE drops below — the two sides were built to the same number and never joined. Flag-gated (GRAPH_INFERENCE_FEEDBACK_ENABLED), because each emission spends EIE budget. CEG-007 dependency hygiene. No import of redis exists anywhere under engine/ or chassis/, yet redis was a requirement, a service in both compose files with depends_on: service_healthy (so the api service could not start without it), a CI service container in two workflows, a required env var in the env contract, and a dependency contract claiming "Scoring result caching" and "Domain pack cache" that were never implemented. Four contract tests asserted that description continuously, which is what made an unused dependency look load-bearing. All of it removed together. CEG-008 operability. match and sync route to a Neo4j database named after the domain id and nothing created it, so on a fresh instance every one failed until an operator ran CREATE DATABASE by hand. GraphDriver now provisions it on first use under AUTO_CREATE_DOMAIN_DATABASE (once per database per process, Enterprise-only), and either way an absent database raises DatabaseNotProvisionedError naming the database and the exact command. The name is validated before it is quoted into the statement: CREATE DATABASE takes no query parameter, and sanitize_label cannot be used because domain ids legitimately contain dashes. CEG-009 configuration. Nine specs used a flat <name>_domain_spec.yaml shape the loader never reads — it resolves domains/<domain_id>/spec.yaml — so they looked like available verticals and no tenant id could select them. Migrated. Two were additionally invalid, with edges to undeclared Skill and ZipCode nodes; nothing caught that, because a file the loader never opens is never validated either. Loadable domains go from one to ten, idea-portfolio still behind its flag. Verification: 1818 passed, 12 skipped, 56 xfailed across unit, contracts, compliance, architecture and invariants. Contract 21 caught the new feature flag as undocumented and FEATURE_GATES.md now carries both new gates. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CoomAnwQt9gxGhHAJJSDz2
Recursive Alignment found the redis removal stopped at the declarations. The Makefile still drove the service: `health` exec'd l9-graph-redis, `redis-shell` opened a CLI on a container that no longer exists, `local-dbs` ran `docker compose up -d neo4j redis` against a service absent from both compose files, and two local-api targets exported PLASTICOS_REDIS_URL for a setting that was deleted. docs/CI_PIPELINE.md still listed the CI redis service. Also registered the three capabilities this branch adds in .claude/rules/capability-registry.md. CLAUDE.md's first "Always" is to check that registry before building, so a capability missing from it is how a second implementation gets written — which is exactly what EIE-006 turned out to be. Apply report: .l9/autonomy/kernel-apply.md. Suites unchanged at 1818 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CoomAnwQt9gxGhHAJJSDz2
Nine findings from the PR #283 review plus the single mypy error that turned all four CEG lint jobs red. CI fix - handlers.py: the inference-rule walrus reused `result`, already bound to dict[str, Any] earlier in handle_admin. Renamed to `inferred`. mypy: "Success: no issues found in 133 source files". Correctness - graph/driver.py: ensure_database() claimed the database name AFTER the await, so concurrent first-use requests each issued their own CREATE DATABASE. The claim now precedes the suspension point and is released on failure so a later attempt can retry. - handlers.py: `rules: []` in emit_inference_feedback silently became "run every rule"; a bare string was iterated character by character. Absent key still means all rules; a non-list of non-strings is now rejected. - gate_egress.py: get_gate_client() raises ValueError on missing or invalid SDK environment material. Both egress paths caught only GateClientError, so a configuration fault crashed the caller instead of returning the typed failure result every other path returns. - health/health_report.py: _conversion_events was an unbounded list fed by every Seed-tier assess/report call — a leak CEG-006 made reachable, and one CLAUDE.md's "never create unbounded caches" covers. Now a deque(maxlen=10_000). - scripts/validate_sdk_pin.py: printed "PASS ... -> None" when the tree has no poetry.lock. Says what is true instead. Feature-flag discipline (C-21) - health_* admin subactions now gate on health_api_enabled. auto_enrich_via_gate gates only the outbound Gate request, not assessment, reporting or conversion tracking, so the surface needed its own flag. - admin feature_status reports all five Constellation-seam flags. Domain packs — correction to the PR body - Five of the nine packs CEG-009 made readable compile to Cypher that cannot execute: `type: traversal` gates written with pattern/condition fall back to a RELATES_TO edge no ontology declares, and scalar queryparams (85.0, 5, 1) are emitted as parameter NAMES, producing $85.0. Readable is not correct. They are gated behind unvalidated_domain_packs_enabled until the compiler or query schema grows the support they assume. - The PR's "loadable domains 1 -> 10" is therefore "1 -> 5 discoverable-and-executable, plus 5 migrated-but-dormant". - test_domain_pack_shape.py compiles every discoverable pack's gates and fails on either defect shape; a shrink-only test fails if a gated pack starts compiling cleanly, so the flag cannot become a place defects go to be forgotten. Verification - pytest tests/unit tests/contracts tests/compliance tests/architecture tests/invariants: exit 0 (jsonschema installed so the two previously skipped contract modules actually ran: 34 passed). - mypy engine/: clean. ruff check/format clean on changed files under the CI-pinned rule set. - tools/contract_scanner.py: no violations. tools/verify_contracts.py: 27/27. - scripts/validate_sdk_pin.py: PASS Quantum-L9/Gate_SDK@v1 -> e9f829f. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CoomAnwQt9gxGhHAJJSDz2
Restore the owner-managed l9-analysis.yml deleted in #262. Keep Scorecard on GHCR-backed v2.4.4 (v2.4.0 still pulls gcr.io/openssf). Protocol methods are docstring-only so STUB-001, ruff, and code-quality agree. Issue-Remediation-Cycle: #265/cycle-1 Issue-Remediation-Cycle: #267/cycle-1 Issue-Remediation-Cycle: #269/cycle-1 Co-authored-by: Cursor <cursoragent@cursor.com>
…1, F280-2) Audit findings F280-1/F280-2 on PR #280: - tools/cypher_lint.py is now an AST scanner. Every f-string that builds Cypher (by text, by sink, or by compiler context) has each interpolation classified by syntactic role: quoted value and LIMIT/SKIP always fail; label, property, parameter-name and back-quoted positions must be validated; a bare fragment fails when it reads a raw spec value. No keyword precondition, multi-line f-strings analysed as one unit, and waivers are explicit, reasoned, and printed on every run. - cypher_quoted_ident() is removed: it sanitized data as if it were a label. EnumMapGate mapping keys/values and GDS equipment type names now travel as $parameters via BaseGate._bind_param() / query_params. - ExclusionGate sanitizes edge/node identifiers; _prop_ref/_param_ref validate identifiers; operators and composite logic pass through literal allow-lists aligned with the schema's validated set. - GateCompiler validates query parameter names with sanitize_label() and casts numeric spec values; scoring builders validate dimension aliases and property/parameter names. - sanitize_database_name() added for CREATE DATABASE quoting. Tests cover both directions of the scanner (including the audit's false-negative NOT EXISTS shape and false-positive module-constant MERGE), non-identifier and injection-shaped enum values, and the live engine tree. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018dcSADmR2nDESm69Szd1fT
…ioning (F283-1) ensure_database() claimed the name before awaiting CREATE DATABASE, so a second request arriving mid-flight saw the name, returned True, and ran its domain query against a database that did not exist yet. In-flight provisioning is now a per-database asyncio.Task kept apart from the ensured set; every concurrent caller awaits (shielded) the same task and receives its result, exactly one CREATE is issued, a failed CREATE is retried on the next call, and cancelling one waiter does not cancel the CREATE. Tests prove one CREATE and no domain query before provisioning completes under concurrency, shared outcome, retry after failure, and cancellation isolation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018dcSADmR2nDESm69Szd1fT
…odies (F284-1, F284-2) - Drop the restored consumer-local .github/workflows/l9-analysis.yml. Current l9-ci-core owner law (consumer_copy_required=false) prohibits copied L9 workflows in consumers; Analyze (central Core) already passes on this repository, and the local duplicate only produced a startup_failure plus Scorecard/Sonar alerts. - test_protocol_bodies.py now strips the optional leading docstring and fails on any remaining statement with its location, instead of only recognising pass / raise NotImplementedError / ellipsis. Negative fixtures cover return, assignment, arbitrary raise, calls, expressions and compound statements. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018dcSADmR2nDESm69Szd1fT
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
|
❌ Too Many Reviewable Files Changed ❌ PR Too Large 📋 Best Practices for Large Changes
🚫 This PR is blocked until reviewable size limits are met. |
L9 Audit Harness Report
Step Results
Architecture Audit Findings
See Spec Coverage
See Next StepsAll checks passed. Safe to merge. |
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
The PR is highly cross-cutting (security scanner + query construction + domain pack activation + driver provisioning), and there are confirmed correctness/security issues that should be resolved before merge.
Review effort: Lite
Findings: 2
Open (2)
What changed in this PR
This PR restores and strengthens Contract C-009 enforcement by adding an AST-based cypher-lint scanner, wiring it into make/pre-commit/CI, and updating engine code to eliminate unsafe Cypher value interpolation. It also includes adjacent operational hardening (domain DB auto-provisioning with concurrency control, new Gate egress path for inference feedback), domain-pack reshaping/validation, and removal of the unused Redis dependency.
Changes:
- Add
tools/cypher_lint.py(AST f-string classifier) +make cypher-lintand a pre-commit hook to enforce C-009. - Parameterize or validate Cypher construction across gates/scoring/GDS/driver, including new helpers for database-name validation and query-param collection.
- Fix/modernize operational and config surfaces (domain pack shape tests + feature gating, DB provisioning, Gate inference feedback, remove Redis from deps/docs/compose/CI).
| File | Description |
|---|---|
| .claude/rules/capability-registry.md | Document new/confirmed capabilities (cypher-lint, egress, provisioning, etc.) |
| .claude/rules/feature-flags.md | Document new seam/provisioning/domain-pack flags |
| .claude/rules/subsystems.md | Document new admin subactions for health/inference feedback |
| .env.template | Remove Redis vars; correct SDK env var names; add verifying-keys guidance |
| .github/workflows/ci-quality.yml | Remove Redis service/env; align with removed dependency |
| .github/workflows/ci.yml | Remove Redis service/env; align with removed dependency |
| .github/workflows/supply-chain.yml | Pin Scorecard action to GHCR-backed version |
| .l9/ci.json | Declare repo_class for org CI language detection |
| .pre-commit-config.yaml | Add cypher-lint hook |
| AGENTS.md | Update local dev stack description (Postgres, no Redis) |
| CHANGELOG.md | Summarize new C-009 enforcement and related fixes |
| Makefile | Add cypher-lint target; remove Redis targets; adjust local-dbs |
| README.md | Update stack diagram/docs to remove Redis references |
| Readme-Requirements.md | Remove Redis-related dependency references |
| TODO.md | Update issue tracking notes |
| chassis/auth/settings.py | Remove unused redis_url setting |
| docker-compose.prod.yml | Remove Redis service; correct spec env var; require verifying-keys map |
| docker-compose.yml | Remove Redis service; correct spec env var; add verifying-keys map for local |
| docs/CI_PIPELINE.md | Remove Redis service mention |
| docs/DEPLOYMENT.md | Remove Redis deployment references/diagram |
| docs/FEATURE_GATES.md | Add documentation for new flags (inference feedback, provisioning, etc.) |
| docs/TROUBLESHOOTING.md | Update cypher-lint failure guidance to match new AST scanner |
| docs/contracts/BANNED_PATTERNS.md | Add Protocol-body docstring-only contract |
| docs/contracts/config/env-contract.yaml | Remove REDIS_URL contract; explain removal rationale |
| docs/contracts/dependencies/redis.yaml | Delete Redis dependency contract |
| domains/aios-god-agent/spec.yaml | Move/standardize domain pack into loader-readable shape |
| domains/executive-assistant/spec.yaml | Move/standardize domain pack; add missing ontology node |
| domains/freight-matching/spec.yaml | Move/standardize domain pack into loader-readable shape |
| domains/healthcare-referral/spec.yaml | Move/standardize domain pack into loader-readable shape |
| domains/legal-discovery/spec.yaml | Move/standardize domain pack into loader-readable shape |
| domains/mortgage-brokerage/spec.yaml | Move/standardize domain pack into loader-readable shape |
| domains/repo-as-agent/spec.yaml | Move/standardize domain pack into loader-readable shape |
| domains/research-agent/spec.yaml | Move/standardize domain pack into loader-readable shape |
| domains/roofing-company/spec.yaml | Move/standardize domain pack; add missing ontology node |
| engine/causal/attribution.py | Cast depth limits before Cypher interpolation |
| engine/causal/causal_compiler.py | Cast depth limits before Cypher interpolation |
| engine/config/loader.py | Add feature gating for known-broken migrated packs |
| engine/config/settings.py | Add new feature flags; remove Redis settings |
| engine/gate_egress.py | Add inference feedback egress path + idempotency + floor filtering |
| engine/gates/compiler.py | Validate operators/logic/param names; sanitize identifiers |
| engine/gates/types/all_gates.py | Parameterize gate runtime values via query_params; sanitize identifiers |
| engine/gds/scheduler.py | Parameterize GDS graph name/write prop; parameterize equipment type names |
| engine/graph/driver.py | Add per-domain DB provisioning w/ concurrency control + actionable error |
| engine/handlers.py | Merge scoring assembler query params into query execution; add admin subactions |
| engine/health/health_report.py | Bound in-memory conversion events (deque maxlen) |
| engine/hoprag/indexer.py | Remove Protocol ellipsis bodies per docstring-only contract |
| engine/scoring/assembler.py | Sanitize dimension aliases; cast numeric interpolations; collect query params |
| engine/scoring/helpfulness.py | Sanitize props; cast numeric interpolations |
| engine/scoring/importance.py | Sanitize props/params; cast numeric interpolations |
| engine/spec.yaml | Declare node.owner explicitly |
| engine/sync/idea_portfolio.py | Remove Protocol ellipsis bodies per docstring-only contract |
| engine/traversal/multihop.py | Remove Protocol ellipsis bodies per docstring-only contract |
| engine/traversal/pseudo_query.py | Remove Protocol ellipsis bodies per docstring-only contract |
| engine/utils/security.py | Add sanitize_database_name; retain/extend label safety helpers |
| poetry.lock | Remove redis package; update lock metadata |
| pyproject.toml | Remove redis dependency |
| requirements.txt | Remove redis dependency |
| scripts/validate_sdk_pin.py | Print resolved commit on PASS; handle missing poetry.lock clearly |
| templates/.env.recommended.template | Remove REDIS_URL entry |
| tests/contracts/_constants.py | Remove REDIS_URL from required env vars |
| tests/contracts/conftest.py | Remove redis_dep fixture |
| tests/contracts/test_dependency_contracts.py | Remove Redis dependency-contract tests |
| tests/unit/test_cypher_lint.py | Unit tests for new AST cypher-lint scanner |
| tests/unit/test_cypher_utils.py | Add tests for sanitize_database_name |
| tests/unit/test_domain_database_provisioning.py | Unit tests for DB provisioning + concurrency behavior |
| tests/unit/test_domain_pack_shape.py | Enforce domains//spec.yaml shape + gate compilation sanity |
| tests/unit/test_gates_all_types.py | Add tests for gate parameterization + identifier validation |
| tests/unit/test_graph_inference_egress.py | Unit tests for inference feedback egress shaping/dispatch |
| tests/unit/test_protocol_bodies.py | Enforce docstring-only Protocol method bodies in engine/ |
| tools/cypher_lint.py | New AST-based Cypher interpolation scanner (C-009 enforcement) |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a130b35901
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…awaits in tests
- Semgrep Policy Check (float-requires-try-except): numeric spec values now
pass through engine.utils.security.cypher_number(), which does the float()
conversion inside try/except and raises ValueError with the offending
value; cypher-lint treats it as a validator alongside sanitize_label.
- Check Terminology Consistency (print( on changed files): docstring usage
examples in hoprag/indexer, scoring/helpfulness, scoring/importance,
traversal/multihop and traversal/pseudo_query use logger.info; the
Protocol-body fixture uses len("x") as its call statement.
- github-code-quality "statement has no effect": the provisioning tests
assert on the awaited task results instead of bare await statements.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018dcSADmR2nDESm69Szd1fT
|
Status after
Local evidence on the new head: ruff/format clean, mypy clean (134 files), contract scanner clean, cypher-lint 0 blocking (2 visible waivers), local Semgrep rules 0 findings on changed files, 1911 tests passed. Generated by Claude Code |
CEG already declares the moving major channel correctly — pyproject rev = "v1", requirements.txt @v1, poetry.lock reference = "v1" — and none of that changes here. What was missing is the one failure a moving tag makes possible and structural checks cannot see: Gate_SDK advances `v1`, and this repository's lock silently stops matching it without a single local file changing. - scripts/validate_sdk_pin.py: adds --verify-tag. It resolves Quantum-L9/Gate_SDK@v1 at the canonical remote and requires poetry.lock's resolved_reference to equal it, failing closed when the remote cannot be resolved — an unverifiable lock has not been verified. check_text() and check_tree() are untouched, so the existing offline contract and its tests are unchanged; the new logic is three additive pure functions plus an argparse front door. - tests/unit/test_validate_sdk_pin.py: covers the comparison directly — current lock, stale lock, unresolvable channel, lock with no resolved_reference. Pure logic, no network in the test suite. - ci.yml: the validator was never actually wired into CI; it was only reachable through a unit test. Both modes now run in the merge-blocking validate job. poetry.lock is NOT regenerated: resolved_reference is already e9f829f982110be13752da8f18c7a9692e8ed908, which is what v1 resolves to today, so there is nothing to refresh and no reason to churn the file. Verified: ruff check, ruff format --check, 9 passed in tests/unit/test_validate_sdk_pin.py, and both validator modes against the live remote. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0153NHEJmFreprQ5tHLztSGq
- cypher-lint (Codex P1, Copilot): name facts are now lexically scoped — a sanitizer binding in one function no longer certifies a same-named variable in a sibling function; nested functions see enclosing scopes; a helper counts as validated only by its own return statements, never by returns inside a nested def. Scoping exposed five private helpers (resolution/similarity, feedback/drift_detector, feedback/signal_weights) that interpolated a label parameter relying on their callers to have sanitized it; each now validates the parameter itself. - GraphDriver (Codex P2): execute_write provisions the domain database on first use and translates a missing-database error exactly like execute_query, via shared _provision_on_first_use / _translate_absent_database helpers. - domain_extractor (Codex P2): writes domains/<domain-id>/spec.yaml, the only layout DomainPackLoader discovers. - strictwhen (Copilot): declared by GateSpec, consumed by no compiler, so legal-discovery and research-agent are withheld behind unvalidated_domain_packs_enabled; the pack-shape test records strictwhen as a defect so the shrink-only guard still holds. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018dcSADmR2nDESm69Szd1fT
SHARED_MODELS.md is status: active and CLAUDE.md tells agents to load it
before touching engine/packet/. Under a heading reading "Installation
(pyproject.toml)" it prescribed:
constellation-node-sdk = {git = "https://github.com/cryptoxdog/Gate_SDK.git"}
which is wrong twice over — the forbidden fork, and no ref at all, so it
floats on that fork's default branch. scripts/validate_sdk_pin.py fails
closed on both, meaning the doc instructed the next agent to write
something the repository's own gate rejects.
This is not the historical seam-audit evidence the release-identity
campaign deliberately leaves alone; it is a live instruction. Corrected
to the canonical repo on the v1 compatibility channel, with a note on
why the resolved object belongs in poetry.lock instead.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153NHEJmFreprQ5tHLztSGq
#285 closed CEG-001 by keeping the moving tag and making the lock the verified identity, and it stated the residual risk plainly rather than hiding it: a live manifest resolve takes whatever v1 points at that minute, a lock-driven build takes resolved_reference, "moving the v1 tag without refreshing the lock is what would separate them". That is exactly the failure --verify-tag detects. The risk stays a deliberate act, but it is no longer one this repository has to notice by hand. Both sides of the file are kept: - #285's resolved_commit() and its PASS line, so a build log records which SDK object was actually taken, including its no-poetry.lock wording — extended to also name the mode. - This branch's lock_resolution / resolve_remote_tag / compare_lock_to_tag and the --verify-tag front door. - The docstring carries #285's CEG-001 rationale and then says what changed about the trade-off, instead of one narrative replacing the other. - .github/workflows/ci.yml auto-merged; both validator modes run in the merge-blocking validate job. ruff check, ruff format --check, 9 passed in tests/unit/test_validate_sdk_pin.py, and both modes against the live remote reporting e9f829f982110be13752da8f18c7a9692e8ed908. poetry.lock is untouched: resolved_reference already equals what v1 resolves to, so there is nothing to refresh. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0153NHEJmFreprQ5tHLztSGq
SonarCloud pythonsecurity:S8705 on the new --verify-tag code, and it is a real finding, not a false positive. Passing argv as a list and never invoking a shell stops *command* injection but not *argument* injection: `git ls-remote --upload-pack=<cmd> <repo>` executes <cmd>, so a --remote value beginning with `-` is an execution vector on its own. Two independent guards, because either alone is a single point of failure: - safe_remote() rejects an empty remote or one starting with `-`, before it reaches git. - --end-of-options is passed so git treats the remote as a positional even if the guard is ever loosened. The rejection fails closed through the same path as any other unresolvable channel, but names which of the two reasons it was rather than reporting a generic resolution failure. Tests cover both directions: a canonical https remote and a local fixture path are accepted, and --upload-pack=..., -u, --exec=sh and "" are refused. The Gate_SDK test drives it end to end through the CLI and asserts the payload file was never created. Introduced by my own --verify-tag commit on this branch; SonarCloud was green on the previous head and red on mine. The same code was written into all four validators, so all four carry this fix. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0153NHEJmFreprQ5tHLztSGq
My previous commit guarded the remote with a negative check and
--end-of-options. That is correct on the merits but SonarCloud still
reported pythonsecurity:S8705, and it was right to: a `startswith("-")`
rejection is not a sanitizer, so the argparse value still reached git
having only been filtered, not replaced. On Gate_SDK the report simply
moved from the resolver to the _git() helper.
The real problem was the flag existing at all. `--remote` let an
operator point a release-identity check at a repository that is not
Gate_SDK — which is precisely what this contract exists to prevent. So
the flag is gone:
- main() passes the CANONICAL_REMOTE module constant. No command-line
argument reaches git.
- safe_remote() returns the constant on an equality match, so the value
handed to the sink is provably not derived from any input. A
non-canonical value is only accepted if it is an existing directory —
the local fixture repositories the tests use — and a `-`-leading or
empty value is still refused outright. --end-of-options stays as the
third layer.
- The Gate_SDK tests that drove --verify-tag through the CLI now call
validate(..., remote=...) in process, so the fixture path is a
function parameter rather than a command-line argument.
This narrows what the tool can be pointed at, which is the behaviour the
release-identity contract wanted in the first place. Nothing about the
v1 channel, the lock comparison or the fail-closed semantics changes.
Verified in every repo: ruff, ruff format, the validator suites
(Gate_SDK 22, Gate 126, CEG 15, EIE 25 passed) and --verify-tag against
the live remote still reporting
e9f829f982110be13752da8f18c7a9692e8ed908.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0153NHEJmFreprQ5tHLztSGq
|




Problem
The 2026-09-20 open-PR audit (
CEG_Open_PR_Audit_2026-09-20) found every open PR NOT READY, with five confirmed, merge-blocking root findings:#281 (
.l9/ci.json) had no local defect but was stacked on blocked #280.Closes #272, closes #273, closes #265, closes #267, closes #269. Supersedes #280, #281, #283, #284.
Fix
This branch is
main+ the content of #280, #281, #283, #284 (clean merges, no conflicts) + the audit fixes on top. Grouped by intent:C-009 scanner (F280-2) —
tools/cypher_lint.pyrewritten as an AST scanner. Every f-string that builds Cypher (by text, by execution sink, or by compiler/assembler context) has each interpolation classified by syntactic role: quoted value andLIMIT/SKIPalways fail; label, property,$nameand back-quoted positions must be validated (sanitize_label,sanitize_database_name,int/float, allow-list dict lookup, or a name/self-attribute/helper derived from those); a bare fragment fails when it reads a rawspec/gate/dim/metadatavalue. Multi-line f-strings are one unit. Diagnostics are skipped by AST context. Waivers require# cypher-lint: allow <reason>and are printed on every run (2 on the tree: the traversal gate's spec-authoredpattern/conditionescape hatch, tied to CEG-009).Value parameterization (F280-1) —
cypher_quoted_ident()removed.EnumMapGatemapping keys/values and GDS equipment type names travel as$parametersviaBaseGate._bind_param()/gate.query_params;CompositeGatecollects sub-gate params. What the new scanner then surfaced and is also fixed:GateCompilerinterpolatedgate.queryparamraw as a parameter name at 10 sites (nowsanitize_label), operator and composite logic now go through allow-lists aligned with the schema's validated set,ExclusionGate/_prop_ref/_param_refvalidate identifiers, scoring aliases (AS {dim.name}) are sanitized, numeric spec values are cast before interpolation,helpfulness/importancebuilders validate property and parameter names.sanitize_database_name()added forCREATE DATABASE.Provisioning race (F283-1) —
GraphDriver.ensure_database()tracks the in-flightCREATE DATABASEas a per-databaseasyncio.Taskseparate from the ensured set; concurrent callers await it (shielded), one CREATE is issued, a failed CREATE is retried on the next call, and cancelling one waiter does not cancel the CREATE.Central CI ownership (F284-1) — the consumer-local
l9-analysis.ymlis not restored;Analyze (central Core)stays the owner. The Scorecard pin comment from #284 is kept.Protocol bodies (F284-2) — the test strips the optional docstring and fails on any remaining statement with its location; 15 negative fixtures.
Rejected alternative: pushing fixes to each PR branch — this session is confined to its designated branch, and #280/#281 were 15 commits behind main with a stale merge-ref validation, so a single current-main base was the shorter path to a green head.
Risk
Blast radius: gate compilation (
engine/gates/*), scoring assembly, GDS equipment sync, domain database provisioning, plus everything #283 already carried (compose/redis removal, feature gates, gate egress). Behavioral change to be aware of: a gate whosequeryparamis not an identifier (e.g. the scalar85.0in the CEG-009 packs) is now refused at compile time instead of emitting$85.0; the pack-shape test records that as the same defect.Rollback: revert the merge commit; no schema or data migration.
Known policy blocker:
Enforce PR Policieswill fail — the consolidated branch exceeds the 1000 reviewable-additions threshold by construction (#283 alone was 1251). Splitting keeps the audit fixes coupled to code that exists only on the superseded PRs; an operator decision (threshold override or accepting the consolidated PR) is needed.Evidence
Local, Python 3.12 venv (no Neo4j container, so integration tests did not run):
What the gate caught that I had not: pip-audit's ephemeral venv resolved to Python 3.11 while the Gate SDK requires 3.12 (fixed by pinning
UV_PYTHONfor the run, not a code change); the governance ruff profile reports three advisoryPLR0917hits in code inherited from #283/#284 (engine/gate_egress.py:147,engine/traversal/multihop.py:162,298), not blocking.🤖 Generated with Claude Code
https://claude.ai/code/session_018dcSADmR2nDESm69Szd1fT
Generated by Claude Code