From 7db88ca4e72b0ede4dc4837d658e45fa61233953 Mon Sep 17 00:00:00 2001 From: Andrew whitehouse Date: Tue, 1 Sep 2026 10:05:34 +1000 Subject: [PATCH 1/2] Secure remote delegation and stamp OA 1.6.1 --- CHANGELOG.md | 6 +++ README.md | 2 +- docs/REFERENCE.md | 4 +- oas_cli/runner.py | 23 +++++++++- oas_cli/schemas/oas-schema.json | 6 +-- spec/conformance/README.md | 2 +- .../sandbox/remote-delegation-preflight.yaml | 42 +++++++++++++++++++ spec/open-agent-spec-1.6.md | 18 +++++--- spec/schema/oas-schema-1.6.json | 6 +-- tests/test_iis.py | 33 +++++++++++++++ tests/test_registry.py | 35 ++++++++++++++++ 11 files changed, 159 insertions(+), 18 deletions(-) create mode 100644 spec/conformance/cases/sandbox/remote-delegation-preflight.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index ccd3297..80e50ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Security +- **Remote delegated-spec fetches respect the sandbox** — `http://`, `https://`, and resolved `oa://` destinations are checked against the delegating task's effective `sandbox.http.allow_domains` before any network request. (#112) + +### Documentation +- Stamp the OA 1.6 normative document and schema metadata as 1.6.1, add revision history, and narrow the compatibility statement to acknowledge stricter `allow_domains` validation. (#111) + ### Fixed - **npm CLI accepts a bare spec path** — `oa validate ` and `oa run ` now work without `--spec` in the npm runtime, matching the Python CLI (1.6.0). Same guardrails: `--spec` unchanged, bare path + `--spec` together is an explicit error, and a non-YAML bare argument gets a clear error naming the valid forms. First Jest tests land with this (`npm/tests/`), and both CI and the npm publish workflow now run them. (#100) diff --git a/README.md b/README.md index 5d1a594..58a7617 100644 --- a/README.md +++ b/README.md @@ -619,7 +619,7 @@ The formal specification defines what a conforming OA runtime must do, independe | Resource | Contents | |----------|----------| -| [spec/open-agent-spec-1.6.md](spec/open-agent-spec-1.6.md) | Formal specification — normative MUST/SHOULD/MAY requirements for OA 1.6.0 | +| [spec/open-agent-spec-1.6.md](spec/open-agent-spec-1.6.md) | Formal specification — normative MUST/SHOULD/MAY requirements for OA 1.6.1 | | [spec/schema/oas-schema-1.6.json](spec/schema/oas-schema-1.6.json) | Canonical JSON Schema for validating spec documents | | [spec/conformance/README.md](spec/conformance/README.md) | Conformance test structure and contribution guide | | [spec/conformance/PROTOCOL.md](spec/conformance/PROTOCOL.md) | Runtime-agnostic adapter protocol — certify any runtime, in any language | diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index c6bb8e4..3ebe8b9 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -598,12 +598,12 @@ All sandbox violations raise `OARunError` immediately with one of three structur | Code | Trigger | |------|---------| | `SANDBOX_TOOL_VIOLATION` | Tool name not in `allow` list, or in `deny` list | -| `SANDBOX_DOMAIN_VIOLATION` | HTTP or MCP destination not in `allow_domains`; `host:port` rules require that port | +| `SANDBOX_DOMAIN_VIOLATION` | HTTP, MCP, or remote delegated-spec destination not in `allow_domains`; `host:port` rules require that port | | `SANDBOX_PATH_VIOLATION` | File path outside `allow_paths` (for `file.read` / `file.write`) | Path traversal (`../../`) is caught automatically — paths are resolved to absolute before comparison. -MCP endpoints are static spec configuration and are checked against the effective `http.allow_domains` policy before tool discovery or model execution. A bare hostname preserves the original any-port behaviour; use `host:port` when the agent must reach only one service on that host. +MCP endpoints and remote delegated-spec URLs are static spec configuration and are checked against the effective `http.allow_domains` policy before discovery, fetch, or model execution. For `oa://` references, allow the resolved registry host (`openagentspec.dev`). A bare hostname preserves the original any-port behaviour; use `host:port` when the agent must reach only one service on that host. ### Input immutability diff --git a/oas_cli/runner.py b/oas_cli/runner.py index 9cee770..0cf3a08 100644 --- a/oas_cli/runner.py +++ b/oas_cli/runner.py @@ -493,8 +493,9 @@ def _preflight_runtime_guards( The selected task and its direct dependencies are checked together. Local delegated specs are recursively inspectable and are therefore included in - the same preflight. Remote specs are guarded after fetch at their execution - boundary because their contents are not locally available. + the same preflight. Remote delegation destinations are checked before + fetch; guards requiring remote contents run after fetch at the execution + boundary because those contents are not locally available. """ tasks = spec_data.get("tasks") or {} task_def = tasks.get(task_name) or {} @@ -509,6 +510,7 @@ def _preflight_runtime_guards( continue raw_ref = delegation_ref.strip() if _is_remote_ref(raw_ref): + _check_remote_spec_endpoint(raw_ref, sandbox, resolved_task) continue delegated_path = Path(raw_ref) @@ -670,6 +672,21 @@ def _check_mcp_endpoints( ) +def _check_remote_spec_endpoint( + ref: str, sandbox: dict[str, Any], task_name: str +) -> None: + """Preflight a remote delegated-spec destination before network access.""" + allow_domains = (sandbox.get("http") or {}).get("allow_domains") + if allow_domains is None: + return + _check_url_domain( + _resolve_spec_url(ref), + allow_domains, + task_name, + source="Delegated spec", + ) + + _MAX_TOOL_ITERATIONS = 10 @@ -853,6 +870,8 @@ def _run_single_task( # ── Remote spec (oa:// or https://) ────────────────────────────── if _is_remote_ref(raw_ref): + sandbox = _resolve_sandbox(spec_data, task_name) + _check_remote_spec_endpoint(raw_ref, sandbox, task_name) url = _resolve_spec_url(raw_ref) # Use URL string as the cycle-detection key. canonical_key: Any = url diff --git a/oas_cli/schemas/oas-schema.json b/oas_cli/schemas/oas-schema.json index 5564391..02d37af 100644 --- a/oas_cli/schemas/oas-schema.json +++ b/oas_cli/schemas/oas-schema.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://openagents.org/schemas/oas-schema.json", "title": "Open Agent Spec", - "description": "Schema for Open Agent Spec YAML files (OA 1.6.0)", + "description": "Schema for Open Agent Spec YAML files (OA 1.6.1)", "type": "object", "required": [ "open_agent_spec", @@ -589,10 +589,10 @@ "type": "string", "pattern": "^(?![A-Za-z][A-Za-z0-9+.-]*://)(?:\\[[^\\]]+\\]|[^:/\\s]+)(?::[0-9]{1,5})?$" }, - "description": "Hosts or host:port destinations permitted for http.get/http.post and MCP endpoints. A destination host must equal a listed domain or be a subdomain; an optional port must match the effective destination port. Otherwise SANDBOX_DOMAIN_VIOLATION." + "description": "Hosts or host:port destinations permitted for http.get/http.post, MCP endpoints, and remote delegated-spec fetches. A destination host must equal a listed domain or be a subdomain; an optional port must match the effective destination port. Otherwise SANDBOX_DOMAIN_VIOLATION." } }, - "description": "Network destination constraints for native HTTP tools and MCP endpoints." + "description": "Network destination constraints for native HTTP tools, MCP endpoints, and remote delegated-spec fetches." }, "file": { "type": "object", diff --git a/spec/conformance/README.md b/spec/conformance/README.md index 7cae57d..eb9f3fb 100644 --- a/spec/conformance/README.md +++ b/spec/conformance/README.md @@ -6,7 +6,7 @@ The suite is **runtime-agnostic**: a single harness drives the YAML cases agains ## Purpose -The spec at `../open-agent-spec-1.6.md` defines what a conforming runtime MUST do. These tests operationalise that definition — any runtime that passes the full suite can claim OA 1.6.0 conformance. +The spec at `../open-agent-spec-1.6.md` defines what a conforming runtime MUST do. These tests operationalise that definition — any runtime that passes the full suite can claim OA 1.6.1 conformance. Note: individual cases embed the **minimum** `open_agent_spec` version their behaviour requires (many say `"1.5.0"` or lower), not the suite version. This is deliberate — the additive-compatibility guarantee means a 1.6-conforming runtime must accept those documents unchanged, and the pinned versions exercise exactly that. diff --git a/spec/conformance/cases/sandbox/remote-delegation-preflight.yaml b/spec/conformance/cases/sandbox/remote-delegation-preflight.yaml new file mode 100644 index 0000000..f43cb7d --- /dev/null +++ b/spec/conformance/cases/sandbox/remote-delegation-preflight.yaml @@ -0,0 +1,42 @@ +# Spec §11.3 — remote delegated specs MUST be checked before fetch/model execution. +description: "A blocked remote delegated-spec dependency fails during chain preflight" +spec_section: "11.3" +requires: sandbox + +spec: | + open_agent_spec: "1.6.1" + agent: + name: test + description: test + intelligence: + type: llm + engine: openai + model: gpt-4o + sandbox: + http: + allow_domains: [safe.example] + tasks: + first: + description: first + output: {type: object} + prompts: {system: "First.", user: "first"} + delegated: + description: blocked remote delegation + spec: https://blocked.example/spec.yaml + task: work + run: + description: run + depends_on: [first, delegated] + output: {type: object} + prompts: {system: "Run.", user: "run"} + +mock_responses: + first: '{}' + +invoke: + task: run + input: {} + +expect_error: + code: SANDBOX_DOMAIN_VIOLATION + stage: sandbox diff --git a/spec/open-agent-spec-1.6.md b/spec/open-agent-spec-1.6.md index e5c1daa..f859745 100644 --- a/spec/open-agent-spec-1.6.md +++ b/spec/open-agent-spec-1.6.md @@ -1,8 +1,8 @@ # Open Agent Spec — Formal Specification -**Version:** 1.6.0 +**Version:** 1.6.1 **Status:** Release -**Date:** 2026-07-12 +**Date:** 2026-08-31 --- @@ -598,7 +598,7 @@ tasks: |-----|-----------|-----------| | `tools.allow` | every tool dispatch | If present, a tool not in the list MUST be refused (`SANDBOX_TOOL_VIOLATION`) | | `tools.deny` | every tool dispatch | If present, a tool in the list MUST be refused (`SANDBOX_TOOL_VIOLATION`). Deny is checked in addition to allow. | -| `http.allow_domains` | `http.get`, `http.post`, MCP endpoints | The destination hostname MUST equal a listed domain or be a subdomain. A bare hostname permits any port for backwards compatibility; a `host:port` entry MUST match the destination's effective port. Otherwise raise `SANDBOX_DOMAIN_VIOLATION`. | +| `http.allow_domains` | `http.get`, `http.post`, MCP endpoints, remote delegated-spec fetches | The destination hostname MUST equal a listed domain or be a subdomain. A bare hostname permits any port for backwards compatibility; a `host:port` entry MUST match the destination's effective port. Otherwise raise `SANDBOX_DOMAIN_VIOLATION`. For an `oa://` reference, the resolved registry URL is the destination checked. | | `file.allow_paths` | `file.read`, `file.write` | The resolved absolute path MUST fall under one of the listed path prefixes (after resolving symlinks and `..`); otherwise `SANDBOX_PATH_VIOLATION` | An absent constraint key imposes no restriction of that type. An empty `allow` list denies everything of that type. @@ -612,6 +612,7 @@ A runtime that supports sandboxing MUST: 3. Surface violations as structured errors with stage `sandbox` and the specific code for the constraint type (Section 13.2) — never as a generic run failure. 4. Continue to enforce the sandbox regardless of what the model requests; sandbox constraints are not visible to or negotiable by the model. 5. Validate statically configured MCP endpoints against `http.allow_domains` before MCP tool discovery or model execution. +6. Validate remote delegated-spec destinations against the delegating task's effective `http.allow_domains` before fetching the delegated document. **Honesty rule.** A runtime that does not implement sandboxing MUST refuse to run a spec that declares a `sandbox:` block, rather than silently ignoring it. Silent degradation of a declared security constraint is itself a conformance violation (see `spec/conformance/PROTOCOL.md`). @@ -750,7 +751,7 @@ A runtime MUST surface errors as structured objects with the following fields: | `DELEGATION_CYCLE_ERROR` | `delegation` | Circular spec delegation detected (A→B→A) | | `PRICING_CONFIG_ERROR` | `cost` | A cost-rate override (`config.pricing` or an implementation-defined global override) is present but invalid | | `SANDBOX_TOOL_VIOLATION` | `sandbox` | Tool blocked by the effective `tools.allow`/`tools.deny` sandbox constraint | -| `SANDBOX_DOMAIN_VIOLATION` | `sandbox` | HTTP or MCP destination host/port not permitted by `http.allow_domains` | +| `SANDBOX_DOMAIN_VIOLATION` | `sandbox` | HTTP, MCP, or remote delegated-spec destination host/port not permitted by `http.allow_domains` | | `SANDBOX_PATH_VIOLATION` | `sandbox` | File path outside `file.allow_paths` after resolution | A runtime MUST detect and raise `CHAIN_CYCLE_ERROR`, `DELEGATION_CYCLE_ERROR`, and `PRICING_CONFIG_ERROR` before any model call is made. It MUST raise `CONTRACTS_UNAVAILABLE` before the affected task invokes a model; statically resolvable tasks SHOULD be checked before their containing chain starts. @@ -761,7 +762,7 @@ A runtime MUST detect and raise `CHAIN_CYCLE_ERROR`, `DELEGATION_CYCLE_ERROR`, a ### 14.1 Conformance Requirements -A runtime conforms to OA 1.6.0 if it: +A runtime conforms to OA 1.6.1 if it: 1. **MUST** accept spec documents that validate against `spec/schema/oas-schema-1.6.json` and reject documents that do not. 2. **MUST** implement the execution pipeline defined in Section 7.1, raising all statically detectable errors before any model call. @@ -796,7 +797,12 @@ The `open_agent_spec` field in a document declares the minimum spec version requ The version string MUST conform to Semantic Versioning. Minor and patch increments MUST be backward compatible. Major increments MAY introduce breaking changes. -OA 1.6.0 is additive over 1.5.x: every valid 1.5.x document is a valid 1.6.0 document. +OA 1.6 remains behaviourally compatible with 1.5.x. OA 1.6.1 tightens validation of `sandbox.http.allow_domains`: entries MUST use `host` or `host:port` form rather than a full URL or malformed port. Documents that relied on previously unconstrained strings in that field require correction before validation. + +### 14.4 Revision History + +- **1.6.1 (2026-08-31):** Contracts fail closed when enforcement is unavailable; sandbox domain rules cover host/port-pinned MCP endpoints and remote delegated-spec fetches; malformed `allow_domains` entries are rejected. +- **1.6.0 (2026-07-28):** Initial OA 1.6 specification. --- diff --git a/spec/schema/oas-schema-1.6.json b/spec/schema/oas-schema-1.6.json index 5564391..02d37af 100644 --- a/spec/schema/oas-schema-1.6.json +++ b/spec/schema/oas-schema-1.6.json @@ -2,7 +2,7 @@ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://openagents.org/schemas/oas-schema.json", "title": "Open Agent Spec", - "description": "Schema for Open Agent Spec YAML files (OA 1.6.0)", + "description": "Schema for Open Agent Spec YAML files (OA 1.6.1)", "type": "object", "required": [ "open_agent_spec", @@ -589,10 +589,10 @@ "type": "string", "pattern": "^(?![A-Za-z][A-Za-z0-9+.-]*://)(?:\\[[^\\]]+\\]|[^:/\\s]+)(?::[0-9]{1,5})?$" }, - "description": "Hosts or host:port destinations permitted for http.get/http.post and MCP endpoints. A destination host must equal a listed domain or be a subdomain; an optional port must match the effective destination port. Otherwise SANDBOX_DOMAIN_VIOLATION." + "description": "Hosts or host:port destinations permitted for http.get/http.post, MCP endpoints, and remote delegated-spec fetches. A destination host must equal a listed domain or be a subdomain; an optional port must match the effective destination port. Otherwise SANDBOX_DOMAIN_VIOLATION." } }, - "description": "Network destination constraints for native HTTP tools and MCP endpoints." + "description": "Network destination constraints for native HTTP tools, MCP endpoints, and remote delegated-spec fetches." }, "file": { "type": "object", diff --git a/tests/test_iis.py b/tests/test_iis.py index 06d0e16..821f866 100644 --- a/tests/test_iis.py +++ b/tests/test_iis.py @@ -18,6 +18,7 @@ from oas_cli.runner import ( OARunError, _check_mcp_endpoints, + _check_remote_spec_endpoint, _check_sandbox, _resolve_sandbox, run_task_from_spec, @@ -234,6 +235,38 @@ def test_no_allowlist_does_not_restrict_mcp(self): ) +class TestRemoteSpecEndpointSandbox: + def test_https_destination_is_allowed(self): + sandbox = {"http": {"allow_domains": ["specs.example"]}} + _check_remote_spec_endpoint( + "https://specs.example/agent.yaml", sandbox, "delegate" + ) + + def test_https_destination_is_blocked(self): + sandbox = {"http": {"allow_domains": ["safe.example"]}} + with pytest.raises(OARunError) as exc_info: + _check_remote_spec_endpoint( + "https://blocked.example/spec.yaml", sandbox, "delegate" + ) + + assert exc_info.value.code == "SANDBOX_DOMAIN_VIOLATION" + assert exc_info.value.stage == "sandbox" + assert exc_info.value.task == "delegate" + + def test_oa_reference_checks_resolved_registry_destination(self): + sandbox = {"http": {"allow_domains": ["openagentspec.dev"]}} + _check_remote_spec_endpoint("oa://prime-vector/summariser", sandbox, "delegate") + + def test_oa_reference_requires_registry_in_allowlist(self): + sandbox = {"http": {"allow_domains": ["safe.example"]}} + with pytest.raises(OARunError) as exc_info: + _check_remote_spec_endpoint( + "oa://prime-vector/summariser", sandbox, "delegate" + ) + + assert exc_info.value.code == "SANDBOX_DOMAIN_VIOLATION" + + # ── _check_sandbox — path enforcement ──────────────────────────────────────── diff --git a/tests/test_registry.py b/tests/test_registry.py index 66c580c..d6a3a8b 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -224,6 +224,41 @@ def test_https_url_delegation_executes_remote_spec(self, tmp_path): assert result["task"] == "delegate" assert "https://example.com" in result["delegated_to"] + def test_blocked_remote_dependency_is_preflighted_before_fetch_or_model( + self, tmp_path + ): + coordinator_spec = _make_coordinator_spec("https://blocked.example/spec.yaml") + coordinator_spec["sandbox"] = {"http": {"allow_domains": ["safe.example"]}} + coordinator_spec["tasks"] = { + "first": { + "description": "would spend tokens", + "output": {"type": "object"}, + "prompts": {"system": "first", "user": "first"}, + }, + "delegate": coordinator_spec["tasks"]["delegate"], + "run": { + "description": "chain", + "depends_on": ["first", "delegate"], + "output": {"type": "object"}, + "prompts": {"system": "run", "user": "run"}, + }, + } + coordinator = tmp_path / "coordinator.yaml" + coordinator.write_text(yaml.dump(coordinator_spec)) + + with ( + patch("oas_cli.runner._fetch_remote_spec") as mock_fetch, + patch("oas_cli.runner.invoke_intelligence") as mock_invoke, + pytest.raises(OARunError) as exc_info, + ): + run_task_from_file(coordinator, task_name="run") + + assert exc_info.value.code == "SANDBOX_DOMAIN_VIOLATION" + assert exc_info.value.stage == "sandbox" + assert exc_info.value.task == "delegate" + mock_fetch.assert_not_called() + mock_invoke.assert_not_called() + def test_remote_cycle_detection(self, tmp_path): """A remote spec that delegates back to the same URL raises DELEGATION_CYCLE_ERROR.""" remote_url = "https://example.com/cycle.yaml" From 3c44e14bd1be2d15c1b8b34bb35f8b2811b266b1 Mon Sep 17 00:00:00 2001 From: Andrew whitehouse Date: Wed, 2 Sep 2026 10:55:21 +1000 Subject: [PATCH 2/2] Address OA 1.6.1 review feedback --- CHANGELOG.md | 19 ++++----- docs/REFERENCE.md | 4 +- oas_cli/runner.py | 3 +- oas_cli/schemas/oas-schema.json | 4 +- spec/conformance/CONFORMANCE.md | 12 ++++-- spec/conformance/README.md | 2 +- .../cases/sandbox/domain-port-mismatch.yaml | 2 +- .../cases/sandbox/mcp-domain-preflight.yaml | 2 +- .../sandbox/oa-registry-domain-preflight.yaml | 42 +++++++++++++++++++ spec/open-agent-spec-1.6.md | 15 +++---- spec/schema/oas-schema-1.6.json | 4 +- tests/test_registry.py | 31 ++++++++++++++ 12 files changed, 110 insertions(+), 30 deletions(-) create mode 100644 spec/conformance/cases/sandbox/oa-registry-domain-preflight.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index 80e50ca..fb34dd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,15 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Security -- **Remote delegated-spec fetches respect the sandbox** — `http://`, `https://`, and resolved `oa://` destinations are checked against the delegating task's effective `sandbox.http.allow_domains` before any network request. (#112) - -### Documentation -- Stamp the OA 1.6 normative document and schema metadata as 1.6.1, add revision history, and narrow the compatibility statement to acknowledge stricter `allow_domains` validation. (#111) - -### Fixed -- **npm CLI accepts a bare spec path** — `oa validate ` and `oa run ` now work without `--spec` in the npm runtime, matching the Python CLI (1.6.0). Same guardrails: `--spec` unchanged, bare path + `--spec` together is an explicit error, and a non-YAML bare argument gets a clear error naming the valid forms. First Jest tests land with this (`npm/tests/`), and both CI and the npm publish workflow now run them. (#100) - ### Added (older, pre-1.4 notes) - This changelog. - **Agents-as-code documentation** — new section in REFERENCE.md explaining the `.agents/` pattern, bundled examples table, and scaffold/run/generate workflows. @@ -31,14 +22,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed (older, pre-1.4 notes) - Removed broken references to non-existent `security-threat-analyzer.yaml` template and `SECURITY_TEMPLATES.md` from REFERENCE.md. -## [1.6.1] - 2026-08-31 +## [1.6.1] - Unreleased ### Security - **Declared behavioural contracts now fail closed** — if a resolved task declares a contract but the `behavioural-contracts` enforcement dependency is unavailable, execution stops before the affected model call with `CONTRACTS_UNAVAILABLE` instead of logging a warning and continuing without the promised constraint. Direct dependencies and locally delegated tasks are preflighted before their chain starts. (#103) - **Sandbox domain rules cover ports and MCP endpoints** — `http.allow_domains` entries may pin `host:port` while bare hosts retain backwards-compatible any-port semantics. Statically configured MCP endpoints are preflighted across the selected task and direct dependencies before discovery or model execution. Malformed allowlist entries now fail validation. (#104) +- **Declared remote delegated-spec URLs respect the delegating task's sandbox** — `http://`, `https://`, and resolved `oa://` destinations are checked against that task's effective `sandbox.http.allow_domains` before the initial request. Redirect destinations remain tracked in #114; cross-document sandbox inheritance remains tracked in #110. (#112) + +### Fixed +- **npm CLI accepts a bare spec path** — `oa validate ` and `oa run ` now work without `--spec` in the npm runtime, matching the Python CLI (1.6.0). Same guardrails: `--spec` unchanged, bare path + `--spec` together is an explicit error, and a non-YAML bare argument gets a clear error naming the valid forms. First Jest tests land with this (`npm/tests/`), and both CI and the npm publish workflow now run them. (#100) + +### Documentation +- Stamp the OA 1.6 normative document and schema metadata as 1.6.1, add revision history, and narrow the compatibility statement to acknowledge stricter `allow_domains` validation. (#111) ### Breaking - Existing specs that combine MCP tools with `sandbox.http.allow_domains` must add each MCP endpoint host (or `host:port`) to the effective allowlist. Runtimes now enforce the declared network boundary for MCP instead of limiting it to native HTTP tools. +- Existing specs that combine `oa://` delegation with `sandbox.http.allow_domains` must add `openagentspec.dev` to the delegating task's effective allowlist so the registry URL can be fetched. - Specs that declare `behavioural_contract` must install `open-agent-spec[contracts]`; execution no longer continues without enforcement. ## [1.6.0] - 2026-07-28 diff --git a/docs/REFERENCE.md b/docs/REFERENCE.md index 3ebe8b9..e25ffb8 100644 --- a/docs/REFERENCE.md +++ b/docs/REFERENCE.md @@ -598,12 +598,12 @@ All sandbox violations raise `OARunError` immediately with one of three structur | Code | Trigger | |------|---------| | `SANDBOX_TOOL_VIOLATION` | Tool name not in `allow` list, or in `deny` list | -| `SANDBOX_DOMAIN_VIOLATION` | HTTP, MCP, or remote delegated-spec destination not in `allow_domains`; `host:port` rules require that port | +| `SANDBOX_DOMAIN_VIOLATION` | HTTP, MCP, or declared/resolved remote delegated-spec destination not in `allow_domains`; `host:port` rules require that port | | `SANDBOX_PATH_VIOLATION` | File path outside `allow_paths` (for `file.read` / `file.write`) | Path traversal (`../../`) is caught automatically — paths are resolved to absolute before comparison. -MCP endpoints and remote delegated-spec URLs are static spec configuration and are checked against the effective `http.allow_domains` policy before discovery, fetch, or model execution. For `oa://` references, allow the resolved registry host (`openagentspec.dev`). A bare hostname preserves the original any-port behaviour; use `host:port` when the agent must reach only one service on that host. +MCP endpoints and declared remote delegated-spec URLs are static spec configuration and are checked against the current task's effective `http.allow_domains` policy before discovery, the initial fetch, or model execution. For `oa://` references, allow the resolved registry host (`openagentspec.dev`). Redirect destinations and sandbox inheritance across delegated documents are separate concerns. A bare hostname preserves the original any-port behaviour; use `host:port` when the agent must reach only one service on that host. ### Input immutability diff --git a/oas_cli/runner.py b/oas_cli/runner.py index 0cf3a08..af894e9 100644 --- a/oas_cli/runner.py +++ b/oas_cli/runner.py @@ -676,11 +676,12 @@ def _check_remote_spec_endpoint( ref: str, sandbox: dict[str, Any], task_name: str ) -> None: """Preflight a remote delegated-spec destination before network access.""" + url = _resolve_spec_url(ref) allow_domains = (sandbox.get("http") or {}).get("allow_domains") if allow_domains is None: return _check_url_domain( - _resolve_spec_url(ref), + url, allow_domains, task_name, source="Delegated spec", diff --git a/oas_cli/schemas/oas-schema.json b/oas_cli/schemas/oas-schema.json index 02d37af..928e6f5 100644 --- a/oas_cli/schemas/oas-schema.json +++ b/oas_cli/schemas/oas-schema.json @@ -589,10 +589,10 @@ "type": "string", "pattern": "^(?![A-Za-z][A-Za-z0-9+.-]*://)(?:\\[[^\\]]+\\]|[^:/\\s]+)(?::[0-9]{1,5})?$" }, - "description": "Hosts or host:port destinations permitted for http.get/http.post, MCP endpoints, and remote delegated-spec fetches. A destination host must equal a listed domain or be a subdomain; an optional port must match the effective destination port. Otherwise SANDBOX_DOMAIN_VIOLATION." + "description": "Hosts or host:port destinations permitted for http.get/http.post, MCP endpoints, and declared or resolved remote delegated-spec URLs. A destination host must equal a listed domain or be a subdomain; an optional port must match the effective destination port. Otherwise SANDBOX_DOMAIN_VIOLATION." } }, - "description": "Network destination constraints for native HTTP tools, MCP endpoints, and remote delegated-spec fetches." + "description": "Network destination constraints for native HTTP tools, MCP endpoints, and declared or resolved remote delegated-spec URLs." }, "file": { "type": "object", diff --git a/spec/conformance/CONFORMANCE.md b/spec/conformance/CONFORMANCE.md index c0b9eb4..1513287 100644 --- a/spec/conformance/CONFORMANCE.md +++ b/spec/conformance/CONFORMANCE.md @@ -1,7 +1,8 @@ # OA Conformance Matrix -| Case | python-reference 1.5.2 | npm 1.5.2 | +| Case | python-reference 1.6.1 | npm 1.6.1 | |---|---|---| +| schema/invalid-allow-domain | ✅ PASS | ✅ PASS | | schema/invalid-engine | ✅ PASS | ✅ PASS | | schema/invalid-version | ✅ PASS | ✅ PASS | | schema/missing-agent | ✅ PASS | ✅ PASS | @@ -33,14 +34,19 @@ | errors/chain-cycle | ✅ PASS | ✅ PASS | | errors/chain-input-missing | ✅ PASS | ✅ PASS | | errors/contract-violation | ✅ PASS | ⬜ UNSUPPORTED | +| errors/contracts-unavailable | ⬜ UNSUPPORTED | ✅ PASS | | errors/error-structure | ✅ PASS | ✅ PASS | | errors/task-not-found | ✅ PASS | ✅ PASS | +| sandbox/domain-port-mismatch | ✅ PASS | ⬜ UNSUPPORTED | +| sandbox/mcp-domain-preflight | ✅ PASS | ⬜ UNSUPPORTED | +| sandbox/oa-registry-domain-preflight | ✅ PASS | ⬜ UNSUPPORTED | +| sandbox/remote-delegation-preflight | ✅ PASS | ⬜ UNSUPPORTED | ## Summary | Runtime | Pass | Fail | Unsupported | Adapter errors | |---|---|---|---|---| -| python-reference 1.5.2 | 33 | 0 | 0 | 0 | -| npm 1.5.2 | 31 | 0 | 2 | 0 | +| python-reference 1.6.1 | 38 | 0 | 1 | 0 | +| npm 1.6.1 | 33 | 0 | 6 | 0 | Legend: ✅ PASS · ❌ FAIL · ⬜ UNSUPPORTED (capability not declared) · 💥 adapter error diff --git a/spec/conformance/README.md b/spec/conformance/README.md index eb9f3fb..20913ea 100644 --- a/spec/conformance/README.md +++ b/spec/conformance/README.md @@ -1,6 +1,6 @@ # OA Conformance Tests -This directory contains the conformance test suite for Open Agent Spec 1.6.0. Conformance tests validate **runtime behaviour**, not LLM output. +This directory contains the conformance test suite for Open Agent Spec 1.6.1. Conformance tests validate **runtime behaviour**, not LLM output. The suite is **runtime-agnostic**: a single harness drives the YAML cases against any OA runtime through a thin subprocess adapter (JSON over stdin/stdout). The protocol is defined in [PROTOCOL.md](PROTOCOL.md). Reference adapters for the Python and npm runtimes live in `adapters/`. diff --git a/spec/conformance/cases/sandbox/domain-port-mismatch.yaml b/spec/conformance/cases/sandbox/domain-port-mismatch.yaml index 97447ff..25fd772 100644 --- a/spec/conformance/cases/sandbox/domain-port-mismatch.yaml +++ b/spec/conformance/cases/sandbox/domain-port-mismatch.yaml @@ -4,7 +4,7 @@ spec_section: "11.2" requires: sandbox spec: | - open_agent_spec: "1.6.0" + open_agent_spec: "1.6.1" agent: name: test description: test diff --git a/spec/conformance/cases/sandbox/mcp-domain-preflight.yaml b/spec/conformance/cases/sandbox/mcp-domain-preflight.yaml index f9357ec..2651c7d 100644 --- a/spec/conformance/cases/sandbox/mcp-domain-preflight.yaml +++ b/spec/conformance/cases/sandbox/mcp-domain-preflight.yaml @@ -4,7 +4,7 @@ spec_section: "11.3" requires: sandbox spec: | - open_agent_spec: "1.6.0" + open_agent_spec: "1.6.1" agent: name: test description: test diff --git a/spec/conformance/cases/sandbox/oa-registry-domain-preflight.yaml b/spec/conformance/cases/sandbox/oa-registry-domain-preflight.yaml new file mode 100644 index 0000000..550d6c9 --- /dev/null +++ b/spec/conformance/cases/sandbox/oa-registry-domain-preflight.yaml @@ -0,0 +1,42 @@ +# Spec §11.2/§11.3 — oa:// references check their resolved registry destination. +description: "A blocked oa registry destination fails during chain preflight" +spec_section: "11.2, 11.3" +requires: sandbox + +spec: | + open_agent_spec: "1.6.1" + agent: + name: test + description: test + intelligence: + type: llm + engine: openai + model: gpt-4o + sandbox: + http: + allow_domains: [safe.example] + tasks: + first: + description: first + output: {type: object} + prompts: {system: "First.", user: "first"} + delegated: + description: blocked registry delegation + spec: oa://prime-vector/summariser + task: work + run: + description: run + depends_on: [first, delegated] + output: {type: object} + prompts: {system: "Run.", user: "run"} + +mock_responses: + first: '{}' + +invoke: + task: run + input: {} + +expect_error: + code: SANDBOX_DOMAIN_VIOLATION + stage: sandbox diff --git a/spec/open-agent-spec-1.6.md b/spec/open-agent-spec-1.6.md index f859745..3daec2e 100644 --- a/spec/open-agent-spec-1.6.md +++ b/spec/open-agent-spec-1.6.md @@ -1,16 +1,16 @@ # Open Agent Spec — Formal Specification **Version:** 1.6.1 -**Status:** Release -**Date:** 2026-08-31 +**Status:** Release Candidate +**Date:** Unreleased --- ## Abstract -This document defines the Open Agent Spec (OA) 1.6.0. It specifies the structure of an OA document, the semantics that a conforming runtime MUST implement, and the boundaries of what OA deliberately does not do. An independent implementor MUST be able to build a conforming runtime from this document alone. +This document defines the Open Agent Spec (OA) 1.6.1. It specifies the structure of an OA document, the semantics that a conforming runtime MUST implement, and the boundaries of what OA deliberately does not do. An independent implementor MUST be able to build a conforming runtime from this document alone. -OA 1.6.0 consolidates the runtime definition around four pillars: a **typed contract** (schemas validated on both sides of every model call), a **deterministic execution pipeline** (no hidden control flow), **first-class cost observability** (normalised token usage and best-effort spend reporting on every result), and **declarative safety constraints** (sandboxing enforced before I/O). This revision formalises features that previous drafts left implementation-defined — sandboxing, history threading, input immutability — and promotes usage/cost reporting from an envelope footnote to a runtime obligation. +OA 1.6.1 consolidates the runtime definition around four pillars: a **typed contract** (schemas validated on both sides of every model call), a **deterministic execution pipeline** (no hidden control flow), **first-class cost observability** (normalised token usage and best-effort spend reporting on every result), and **declarative safety constraints** (sandboxing enforced before I/O). This revision formalises features that previous drafts left implementation-defined — sandboxing, history threading, input immutability — and promotes usage/cost reporting from an envelope footnote to a runtime obligation. --- @@ -379,7 +379,8 @@ Delegation semantics: 1. Resolve the `spec:` reference: - Local path: resolve relative to the calling spec's directory. - `oa://namespace/name` or `oa://namespace/name@version`: expand to the registry URL. - - `http://` or `https://`: fetch directly. + - `http://` or `https://`: use the declared URL directly. + - Before fetching a remote reference, enforce the delegating task's effective sandbox against the declared or resolved URL as required by Section 11.3(6). 2. Load the referenced spec. 3. Identify the target task: use `task:` if provided, else use the calling task's name. 4. Validate the target task exists in the referenced spec. Raise `TASK_NOT_FOUND` if not. @@ -612,7 +613,7 @@ A runtime that supports sandboxing MUST: 3. Surface violations as structured errors with stage `sandbox` and the specific code for the constraint type (Section 13.2) — never as a generic run failure. 4. Continue to enforce the sandbox regardless of what the model requests; sandbox constraints are not visible to or negotiable by the model. 5. Validate statically configured MCP endpoints against `http.allow_domains` before MCP tool discovery or model execution. -6. Validate remote delegated-spec destinations against the delegating task's effective `http.allow_domains` before fetching the delegated document. +6. Validate the declared URL for an `http://` or `https://` delegated spec, or the resolved registry URL for an `oa://` reference, against the delegating task's effective `http.allow_domains` before the initial fetch. Redirect destinations are outside this requirement. This per-task check does not define whether sandbox constraints propagate across a delegation boundary. **Honesty rule.** A runtime that does not implement sandboxing MUST refuse to run a spec that declares a `sandbox:` block, rather than silently ignoring it. Silent degradation of a declared security constraint is itself a conformance violation (see `spec/conformance/PROTOCOL.md`). @@ -801,7 +802,7 @@ OA 1.6 remains behaviourally compatible with 1.5.x. OA 1.6.1 tightens validation ### 14.4 Revision History -- **1.6.1 (2026-08-31):** Contracts fail closed when enforcement is unavailable; sandbox domain rules cover host/port-pinned MCP endpoints and remote delegated-spec fetches; malformed `allow_domains` entries are rejected. +- **1.6.1 (Unreleased):** Contracts fail closed when enforcement is unavailable; sandbox domain rules cover host/port-pinned MCP endpoints and declared or resolved remote delegated-spec URLs; malformed `allow_domains` entries are rejected. - **1.6.0 (2026-07-28):** Initial OA 1.6 specification. --- diff --git a/spec/schema/oas-schema-1.6.json b/spec/schema/oas-schema-1.6.json index 02d37af..928e6f5 100644 --- a/spec/schema/oas-schema-1.6.json +++ b/spec/schema/oas-schema-1.6.json @@ -589,10 +589,10 @@ "type": "string", "pattern": "^(?![A-Za-z][A-Za-z0-9+.-]*://)(?:\\[[^\\]]+\\]|[^:/\\s]+)(?::[0-9]{1,5})?$" }, - "description": "Hosts or host:port destinations permitted for http.get/http.post, MCP endpoints, and remote delegated-spec fetches. A destination host must equal a listed domain or be a subdomain; an optional port must match the effective destination port. Otherwise SANDBOX_DOMAIN_VIOLATION." + "description": "Hosts or host:port destinations permitted for http.get/http.post, MCP endpoints, and declared or resolved remote delegated-spec URLs. A destination host must equal a listed domain or be a subdomain; an optional port must match the effective destination port. Otherwise SANDBOX_DOMAIN_VIOLATION." } }, - "description": "Network destination constraints for native HTTP tools, MCP endpoints, and remote delegated-spec fetches." + "description": "Network destination constraints for native HTTP tools, MCP endpoints, and declared or resolved remote delegated-spec URLs." }, "file": { "type": "object", diff --git a/tests/test_registry.py b/tests/test_registry.py index d6a3a8b..29db160 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -259,6 +259,37 @@ def test_blocked_remote_dependency_is_preflighted_before_fetch_or_model( mock_fetch.assert_not_called() mock_invoke.assert_not_called() + def test_malformed_oa_dependency_is_preflighted_without_sandbox(self, tmp_path): + coordinator_spec = _make_coordinator_spec("oa://missing-namespace") + coordinator_spec["tasks"] = { + "first": { + "description": "would spend tokens", + "output": {"type": "object"}, + "prompts": {"system": "first", "user": "first"}, + }, + "delegate": coordinator_spec["tasks"]["delegate"], + "run": { + "description": "chain", + "depends_on": ["first", "delegate"], + "output": {"type": "object"}, + "prompts": {"system": "run", "user": "run"}, + }, + } + coordinator = tmp_path / "coordinator.yaml" + coordinator.write_text(yaml.dump(coordinator_spec)) + + with ( + patch("oas_cli.runner._fetch_remote_spec") as mock_fetch, + patch("oas_cli.runner.invoke_intelligence") as mock_invoke, + pytest.raises(OARunError) as exc_info, + ): + run_task_from_file(coordinator, task_name="run") + + assert exc_info.value.code == "SPEC_LOAD_ERROR" + assert exc_info.value.stage == "delegation" + mock_fetch.assert_not_called() + mock_invoke.assert_not_called() + def test_remote_cycle_detection(self, tmp_path): """A remote spec that delegates back to the same URL raises DELEGATION_CYCLE_ERROR.""" remote_url = "https://example.com/cycle.yaml"