Fail closed on contracts and secure MCP domains - #108
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Reviewed against Three things I'd want resolved before merge. 1. Delegated sub-spec contracts fail closed after a model call
That contradicts the MUST this PR adds to §13.2 ("before any model call is made"). Either extend preflight to local 2. MCP domain check is per-task, not chain-preflightContracts are preflighted before
3. No conformance cases for the new MUST rulesAGENTS.md: normative behaviour needs a case under
Non-blocking, but worth picking up in the same pass:
Full write-up with the reproductions and the rest of the detail to follow. |
|
PR review details: |
sgriffiths
left a comment
There was a problem hiding this comment.
Reviewed cfcf5d4 against my earlier comment. Checked out the branch, re-ran both original reproductions and the full suites. All three blocking points are resolved — approving.
Blocking points cleared
1. Delegated sub-spec contracts. _preflight_contract_support → _preflight_runtime_guards, now recursing into local spec: refs with a _visited_specs guard for delegation cycles. My original reproduction is clean:
raised: CONTRACTS_UNAVAILABLE task=work stage=contract
model calls before error: [] # was ['FIRST']
test_local_delegated_contract_is_preflighted_before_chain asserts calls == [], which is the right assertion — it pins the actual regression rather than just the error code. I also probed whether transitive dependencies (run → mid → deep) leave a hole; they don't, because §7.3 makes execution direct-only, so preflight scope exactly matches execution scope. Leaving remote refs to the post-fetch boundary is the correct call.
2. MCP chain preflight. _check_mcp_endpoints moved into the same preflight walk, covering chosen task + direct depends_on before the chain starts. sandbox/mcp-domain-preflight.yaml uses the multi-task shape the old single-task test couldn't catch.
3. Conformance coverage. Four new cases plus the requires_absent: capability mechanism in the harness and PROTOCOL.md — that's a clean answer to the CONTRACTS_UNAVAILABLE awkwardness. Verified locally: Python 36 passed / 1 unsupported, node 33 passed / 4 unsupported, 537 pytest passed, npm 10 passed, ruff check + format clean.
One caveat on the coverage, not blocking: CI installs .[dev], which pins behavioural-contracts, so the Python adapter always declares contracts and errors/contracts-unavailable is permanently UNSUPPORTED on the Python side. Only npm actually exercises that case in CI. The Python fail-closed path is covered by unit tests, so this is fine — just worth knowing the conformance matrix isn't what's guarding it.
Also picked up every non-blocking item from last round: the §13.2 error row, both oas-schema copies, the landing page, 1.6.1 in pyproject.toml + npm/package.json, and malformed allow_domains now caught at oa validate. The three-way doc disagreement resolved in favour of the accurate wording, including splitting CONTRACTS_UNAVAILABLE out of the "before any model call" MUST list — good.
Follow-ups (not blocking this merge)
A. Sandbox doesn't propagate across delegation. Verified: a parent with allow_domains: [safe.example] delegating to a child whose MCP endpoint is blocked.example runs with no SANDBOX_DOMAIN_VIOLATION — only the child's own sandbox applies. Add a sandbox block to the child and it blocks correctly, so the mechanism works; it's inheritance that's absent. Pre-existing (main behaves identically) and genuinely a spec-level gap, since §11.1 only defines root+task resolution within a single document. Worth naming because it softens this PR's headline claim: moving a tool into a delegated sub-spec still escapes the declared network boundary. Filing separately.
B. Normative document edited in place while stamped 1.6.0. spec/open-agent-spec-1.6.md still reads "Version: 1.6.0" but this PR rewrites two MUSTs and redefines SANDBOX_DOMAIN_VIOLATION — a runtime built against published 1.6.0 now fails conformance. Both schema description strings still say "OA 1.6.0" too. Related: §799's "every valid 1.5.x document is a valid 1.6.0 document" is weaker now that allow_domains entries which used to validate no longer do. Filing separately.
C. Schema regex is looser than the Python validator. Three inputs disagree — localhost:0, localhost:99999, and user@host.com all pass the JSON-Schema pattern but are rejected by _validate_allow_domain. npm's separate port check covers the first two but not user@host.com, which is the exact userinfo-confusion shape the parsed.hostname fix guards against. Low practical impact since npm refuses sandbox: specs anyway, but the schema is the shared cross-runtime contract, so I'd tighten the pattern when convenient.
D. CHANGELOG mis-files the npm fix. [Unreleased] sits above [1.6.1] and still holds the #100 bare-spec fix — but that fix is already on main and will ship inside the 1.6.1 npm artifact. It should move down into the 1.6.1 section.
C and D are small enough to fold into this PR if you'd rather not carry them; either way, not holding the merge. Minor: the PR body's "What changed" still uses the old "before any model call" wording, and the version-bump checkbox is unticked though the work is done.
Generated by Claude Code
What changed
What changed
Makes behavioural contracts fail closed when enforcement is unavailable, raising CONTRACTS_UNAVAILABLE before any model call. It also extends sandbox domain enforcement
to support host:port restrictions and validates MCP endpoints before discovery or network access.
Why
Closes #103 and #104.
Declared behavioural contracts were previously skipped when the optional enforcement dependency was unavailable, allowing execution without a promised constraint. Sandbox
domain rules also could not restrict ports and did not apply to MCP endpoints.
How tested
Added unit tests for unavailable contract enforcement on direct and dependency tasks.
Added domain-matching tests covering bare hosts, pinned ports, effective default ports and case-insensitive matching.
Added MCP endpoint preflight and integration tests confirming violations occur before discovery or model execution.
Ran python3 -m pytest tests/ -q: 529 passed, 3 skipped.
Ran ruff check . --exclude test_output/.
Ran ruff format --check . --exclude test_output/.
Ran the npm build and test suite: 7 passed.
Ran the cross-runtime conformance matrix:
All existing tests pass (pytest tests/)
New tests added (if applicable)
Type of change
Breaking changes
Specs declaring behavioural_contract no longer continue with a warning when the behavioural-contracts dependency is unavailable. They now fail before model execution with
CONTRACTS_UNAVAILABLE.
Install contract enforcement support with:
pip install 'open-agent-spec[contracts]'
Bare allow_domains hostnames retain their existing any-port behaviour. Port restrictions are opt-in using host:port.
MCP endpoints are now subject to the effective sandbox.http.allow_domains policy. Existing specs with MCP tools and restrictive domain allowlists must add the MCP
endpoint host—or host:port—to that allowlist.
Checklist