Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ User-visible changes to the spec and SDKs. Versioning rules:

## Unreleased

- **CTK: declared `tool_seam_host_error` posture (§13.1).** A harness declares `continue` (default) or `terminate`, and `expect.run_outcome_by_posture` resolves the 13 tool-seam `host_error:*` vectors to the single outcome that declared surface must produce — the §6.2 terminate clause is now claimable (#68).
- **CTK: AH-CTK-100 asserts §6.1 substance, not transcript cosmetics.** New `context_must_contain`/`context_must_not_contain` interception assertions pin non-incorporation of the denied tool result and the deny surfacing to the model in some form, leaving message layout and payload shape to the host (#69).
- **The §12.1 incremental exception is CTK-testable.** The vectors
the alpha.5 entry below left as future work exist: a
`streaming/incremental` part (`AH-CTK-110`–`AH-CTK-113`) exercises
Expand Down
10 changes: 8 additions & 2 deletions conformance/CLAIMS.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ or baseline profiles — the claim attaches the CTK's **per-part report**
(runner results grouped by each vector's `part` tag), which
communicates *what was exercised*, not a tier name.

A claim with `identity_provider: null` MUST state that its records and
A claim with a non-default posture (§13.1) MUST state it (e.g.
`tool_seam_host_error: terminate` — the host terminates the turn on a
`host_error:*` deny at the tool seam, which §6.2 permits); the report's
passing vectors attest that posture's outcomes, not the default's. A
claim with `identity_provider: null` MUST state that its records and
approvals are identity-unbound (§10.1). A claim with a host-defined
provider MUST disclose whether the provider is **content-derived**
(a pure function of the projected context, like `jcs-sha256`) or not —
Expand Down Expand Up @@ -49,7 +53,9 @@ artefacts, in the PR:
wires the framework — specifically confirming it drives the
framework's **production dispatch path** with only model/tool I/O
mocked (a harness that re-implements dispatch attests nothing).
3. **Disclosure flags** where applicable: `identity_provider: null` →
3. **Disclosure flags** where applicable: a non-default posture
(`tool_seam_host_error: terminate`) → the claim states it;
`identity_provider: null` →
the claim states records/approvals are identity-unbound;
custom provider → content-derived or not; `buffered_output: false`
→ the claim states a deny at `output` cannot retract streamed
Expand Down
26 changes: 26 additions & 0 deletions conformance/HARNESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,32 @@ Non-finite floats (NaN/Infinity) and lone surrogates cannot be
expressed in a JSON vector at all — those §4.4 marshalling guards are
pinned by per-SDK unit tests, not vectors.

## Postures

Where the spec permits two host behaviors, the harness **declares**
which one its host implements and the runner selects the single
expected outcome for that declared surface — a vector never accepts
"either outcome", so a pass always attests one specific behavior.

`tool_seam_host_error: continue | terminate` (default `continue`)
declares what the host does with the run after a `host_error:*` deny
at `pre_tool_call`/`post_tool_call` (§6.2): `continue` surfaces a tool
error to the model and keeps the loop going; `terminate` means the
host's own semantics terminate the turn — the posture §6.2's "unless
the host's own semantics terminate the turn" clause permits. Vectors
whose run ends in such a deny carry `expect.run_outcome_by_posture`,
and the runner resolves it against this declaration (forwarded in the
run-record wire as `postures.tool_seam_host_error`).

Declare it per SDK convention: a `tool_seam_host_error` attribute
(Python), `toolSeamHostError` (TypeScript), the optional
`ToolSeamHostErrorDeclarer` interface (Go), the `ToolSeamHostError`
property (default interface member, .NET), or the
`tool_seam_host_error()` trait method (Rust). Omitting it declares
`continue` — the posture every in-tree reference harness implements.
The declaration belongs in the host's §13.3 claim alongside its
capabilities.

## Incremental mediation

Vectors in the `streaming/incremental` part carry a chunked mock
Expand Down
6 changes: 5 additions & 1 deletion conformance/RUNNER.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,11 @@ for each vector file in conformance/vectors/*.json:
{outcome:rr.outcome, final_output:rr.final_output,
tool_invocations:rr.tool_invocations, error:rr.error,
identities:rr.identities, # (input, enforced) per emission
records:rr.records}) # wire-shaped §10.3 records
records:rr.records, # wire-shaped §10.3 records
postures:{ # harness *declarations* (§13.1),
tool_seam_host_error: # not observed behavior — they
harness.tool_seam_host_error # select run_outcome_by_posture
?? "continue"}}) # (HARNESS.md "Postures")
```

## The conformance report (§13.1)
Expand Down
59 changes: 58 additions & 1 deletion conformance/vectors.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,20 @@
"context_must_validate": {
"type": "boolean",
"default": true
},
"context_must_contain": {
"type": "array",
"items": {
"type": "string"
},
"description": "Substrings that MUST appear somewhere in the serialization of the recorded AgentContext — shape-agnostic: pins that content surfaced to this interception point in SOME form (e.g. a deny reason surfaced to the model per §6.2) without prescribing message layout or payload format. Needles should avoid characters JSON escapes (quotes, backslashes, control characters)."
},
"context_must_not_contain": {
"type": "array",
"items": {
"type": "string"
},
"description": "Substrings that MUST NOT appear anywhere in the serialization of the recorded AgentContext — pins non-incorporation (§6.1): a discarded result must not surface here in any form. Same needle caveat as context_must_contain."
}
}
}
Expand Down Expand Up @@ -449,7 +463,50 @@
"completed",
"blocked",
"error"
]
],
"description": "The run outcome for a host with the spec-default posture. When run_outcome_by_posture is present, this MUST equal its outcomes entry for the default posture value (the engine enforces the equality), so pre-posture runners resolve the same expectation for default-posture hosts."
},
"run_outcome_by_posture": {
"type": "object",
"additionalProperties": false,
"required": [
"posture",
"outcomes"
],
"properties": {
"posture": {
"type": "string",
"enum": [
"tool_seam_host_error"
],
"description": "The declared host posture (§13.1) that selects the expected outcome. tool_seam_host_error: what the host does with the run after a host_error:* deny at pre_tool_call/post_tool_call — \"continue\" (the §6.2 default: surface a tool error to the model and keep the loop going) or \"terminate\" (the host's own semantics terminate the turn, which §6.2 explicitly permits)."
},
"outcomes": {
"type": "object",
"additionalProperties": false,
"required": [
"continue",
"terminate"
],
"properties": {
"continue": {
"enum": [
"completed",
"blocked",
"error"
]
},
"terminate": {
"enum": [
"completed",
"blocked",
"error"
]
}
}
}
},
"description": "Posture-conditional run outcome, for behavior the spec permits either way (§6.2). The runner forwards the harness's declared posture and the engine selects the single outcome that declared surface must produce — each (vector, declared surface) pair stays single-valued, so a pass never means \"one of several outcomes happened\"."
},
"identities_equal": {
"type": "boolean",
Expand Down
6 changes: 5 additions & 1 deletion conformance/vectors/AH-CTK-070-interceptor-raises.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
],
"interceptions_absent": ["post_tool_call"],
"tool_not_invoked": ["http_get"],
"run_outcome": "completed"
"run_outcome": "completed",
"run_outcome_by_posture": {
"posture": "tool_seam_host_error",
"outcomes": { "continue": "completed", "terminate": "blocked" }
}
}
}
6 changes: 5 additions & 1 deletion conformance/vectors/AH-CTK-071-malformed-verdict.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
],
"interceptions_absent": ["post_tool_call"],
"tool_not_invoked": ["http_get"],
"run_outcome": "completed"
"run_outcome": "completed",
"run_outcome_by_posture": {
"posture": "tool_seam_host_error",
"outcomes": { "continue": "completed", "terminate": "blocked" }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@
}
}
],
"run_outcome": "completed"
"run_outcome": "completed",
"run_outcome_by_posture": {
"posture": "tool_seam_host_error",
"outcomes": { "continue": "completed", "terminate": "blocked" }
}
}
}
6 changes: 5 additions & 1 deletion conformance/vectors/AH-CTK-073-resolver-raises.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@
}
}
],
"run_outcome": "completed"
"run_outcome": "completed",
"run_outcome_by_posture": {
"posture": "tool_seam_host_error",
"outcomes": { "continue": "completed", "terminate": "blocked" }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@
}
}
],
"run_outcome": "completed"
"run_outcome": "completed",
"run_outcome_by_posture": {
"posture": "tool_seam_host_error",
"outcomes": { "continue": "completed", "terminate": "blocked" }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,10 @@
}
}
],
"run_outcome": "completed"
"run_outcome": "completed",
"run_outcome_by_posture": {
"posture": "tool_seam_host_error",
"outcomes": { "continue": "completed", "terminate": "blocked" }
}
}
}
6 changes: 5 additions & 1 deletion conformance/vectors/AH-CTK-092-evidence-cap.json
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@
}
}
],
"run_outcome": "completed"
"run_outcome": "completed",
"run_outcome_by_posture": {
"posture": "tool_seam_host_error",
"outcomes": { "continue": "completed", "terminate": "blocked" }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@
}
}
],
"run_outcome": "completed"
"run_outcome": "completed",
"run_outcome_by_posture": {
"posture": "tool_seam_host_error",
"outcomes": { "continue": "completed", "terminate": "blocked" }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@
}
}
],
"run_outcome": "completed"
"run_outcome": "completed",
"run_outcome_by_posture": {
"posture": "tool_seam_host_error",
"outcomes": { "continue": "completed", "terminate": "blocked" }
}
}
}
6 changes: 5 additions & 1 deletion conformance/vectors/AH-CTK-097-runall-failure-slot.json
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,11 @@
}
}
],
"run_outcome": "completed"
"run_outcome": "completed",
"run_outcome_by_posture": {
"posture": "tool_seam_host_error",
"outcomes": { "continue": "completed", "terminate": "blocked" }
}
},
"composition": {
"profile": "sequential/run_all"
Expand Down
6 changes: 5 additions & 1 deletion conformance/vectors/AH-CTK-098-approve-carrying-deny.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@
}
}
],
"run_outcome": "completed"
"run_outcome": "completed",
"run_outcome_by_posture": {
"posture": "tool_seam_host_error",
"outcomes": { "continue": "completed", "terminate": "blocked" }
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"id": "AH-CTK-100",
"title": "deny at post_tool_call: the tool result is discarded, not incorporated into agent state (§6.1)",
"title": "deny at post_tool_call: the tool result is discarded, not incorporated into agent state; the deny surfaces to the model in some form, transcript shape unprescribed (§6.1)",
"spec_clause": "6.1, 6.2",
"part": "enforcement/post_action_deny",
"capabilities": [
Expand Down Expand Up @@ -67,9 +67,15 @@
},
{
"interception_point": "pre_model_call",
"context": {
"messages[1].role": "tool",
"messages[1].content": "blocked: ctk:tainted-result"
"context_must_contain": ["ctk:tainted-result"],
"context_must_not_contain": ["SECRET-RESULT"]
}
],
"tool_invocations": [
{
"name": "http_get",
"args": {
"url": "https://x"
}
}
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,10 @@
}
}
],
"run_outcome": "completed"
"run_outcome": "completed",
"run_outcome_by_posture": {
"posture": "tool_seam_host_error",
"outcomes": { "continue": "completed", "terminate": "blocked" }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,10 @@
}
}
],
"run_outcome": "completed"
"run_outcome": "completed",
"run_outcome_by_posture": {
"posture": "tool_seam_host_error",
"outcomes": { "continue": "completed", "terminate": "blocked" }
}
}
}
10 changes: 10 additions & 0 deletions sdk/dotnet/src/AgentHooks.Conformance/IHarness.cs
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@ public interface IHarness
string Name { get; }
IReadOnlySet<Capability> Capabilities { get; }

/// <summary>Declared §6.2 posture at the tool seam (§13.1): what the
/// host does with the run after a <c>host_error:*</c> deny at
/// <c>pre_tool_call</c>/<c>post_tool_call</c>. <c>"continue"</c> (the
/// default — surface a tool error to the model and keep the loop
/// going) or <c>"terminate"</c> (the host's own semantics terminate
/// the turn, which §6.2 explicitly permits). The runner forwards this
/// declaration so <c>expect.run_outcome_by_posture</c> vectors resolve
/// to the single outcome this surface must produce.</summary>
string ToolSeamHostError => "continue";

/// <summary>Wire the scenario's mock model + tools into the framework,
/// register the interceptors and resolver, set the enforcement mode,
/// the vector's composition profile (§7.1), and its identity provider
Expand Down
10 changes: 8 additions & 2 deletions sdk/dotnet/src/AgentHooks.Conformance/Runner.cs
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ public static async Task<VectorResult> RunVectorAsync(

var recordedJson = new JsonArray(
recorded.Select(c => (JsonNode)c).ToArray()).ToJsonString(Compact);
var rrJson = RunRecordToWire(rr);
var rrJson = RunRecordToWire(rr, harness.ToolSeamHostError);
var result = (JsonObject)JsonNode.Parse(
Native.CtkAssert(vectorJson, recordedJson, rrJson))!;
return new VectorResult(
Expand All @@ -126,7 +126,7 @@ public static async Task<VectorResult> RunVectorAsync(
(result["failures"] as JsonArray)?.Select(n => (string)n!).ToList() ?? []);
}

private static string RunRecordToWire(RunRecord rr)
private static string RunRecordToWire(RunRecord rr, string toolSeamHostError)
{
var identities = new JsonArray();
foreach (var (i, e) in rr.Identities ?? [])
Expand All @@ -146,6 +146,12 @@ private static string RunRecordToWire(RunRecord rr)
["error"] = rr.Error,
["identities"] = identities,
["records"] = records,
// Harness *declarations* (§13.1), not observed behavior: the
// engine selects expect.run_outcome_by_posture entries by them.
["postures"] = new JsonObject
{
["tool_seam_host_error"] = toolSeamHostError,
},
};
return o.ToJsonString(Compact);
}
Expand Down
Loading
Loading