@openwop/openwop-conformance 1.37.0 → 1.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/schemas/README.md CHANGED
@@ -32,6 +32,7 @@
32
32
  | `envelopes/media.audio.schema.json` | `ai-envelope.md` §"Media reference payloads" | RFC 0055 §C — optional `media.audio` payload; URL ref or inline base64 + optional `durationSeconds`. |
33
33
  | `envelopes/media.file.schema.json` | `ai-envelope.md` §"Media reference payloads" | RFC 0055 §C — optional `media.file` payload; downloadable asset by URL ref or inline base64 + optional `name`. |
34
34
  | `envelopes/ui.a2ui-surface.schema.json` | `ai-envelope.md` §"A2UI surfaces" | RFC 0102 — optional, advertised `ui.a2ui-surface` payload; closed A2UI component tree (`anyOf` + single-string-enum discriminator) + host-enumerated `catalogVersion`. Core `ui.*` content-primitive family beside `media.*`. |
35
+ | `a2ui-surface-delta-frame.schema.json` | `ai-envelope.md` §"Delta transport" + `stream-modes.md` | RFC 0114 (`Active`) — host-side TRANSPORT frame (`{ surfaceRef, catalogVersion, patch[] }`) carrying an RFC 6902 (JSON-Patch) delta over a recorded `ui.a2ui-surface` envelope; stream-only (`?a2uiDelta=1`), NOT a recorded shape (the envelope stays full). Op enum excludes `test`. Consumer re-validates the post-patch surface against the closed catalog fail-closed. |
35
36
  | `annotation.schema.json` | `RFCS/0056` + `observability.md` | RFC 0056 (`Draft`) — a non-blocking human/agent quality signal (rating / correction / label / flag) attached to a run, event, or node. A side-resource (not a replayable run-event-log entry); response of `POST/GET /v1/runs/{runId}/annotations` + payload of the `run.annotated` SSE notification. |
36
37
  | `annotation-create.schema.json` | `RFCS/0056` | RFC 0056 (`Draft`) — request body for `POST /v1/runs/{runId}/annotations` (host assigns `annotationId`/`createdAt`/`actor`; binds `target.runId` to the path). |
37
38
  | `heartbeat-evaluated.schema.json` | `RFCS/0060` + `host-capabilities.md` | RFC 0060 (`Active`) — payload of the heartbeat-scoped `heartbeat.evaluated` AsyncAPI event (`{ heartbeatId, status, changed }`); emitted every tick by a host advertising `capabilities.heartbeat`. Not a run-event-log entry. |
@@ -39,6 +40,7 @@
39
40
  | `audit-verify-result.schema.json` | `auth-profiles.md` §`openwop-audit-log-integrity` | Response payload from `GET /v1/audit/verify` — chain-validity verdict + checkpoints + anomalies |
40
41
  | `capabilities.schema.json` | `capabilities.md` | `/.well-known/openwop` response — protocolVersion + supportedEnvelopes + schemaVersions + limits + optional v1 discovery surface |
41
42
  | `channel-written-payload.schema.json` | `channels-and-reducers.md` §Channel write event | Payload of the `channel.written` RunEvent — write input + reducer name |
43
+ | `channel-presence-payload.schema.json` | RFC 0110 | Payload of the OPTIONAL `channel.presence` RunEvent — ephemeral channel presence (online + typing), membership-gated, non-persisted |
42
44
  | `chat-card-pack-manifest.schema.json` | `chat-card-packs.md` + RFC 0071 | DRAFT — manifest for `kind: "card"` registry packs (RFC 0071 Phase 2). Peer to the node/workflow-chain/prompt/artifact-type pack manifests; disjoint via the `kind` discriminator. Distributes AI chat cards: a prompt template + typed input subset bound to a typed `outputArtifactType`. |
43
45
  | `connection-pack-manifest.schema.json` | `connection-packs.md` + RFC 0095 | DRAFT — manifest for `kind: "connection"` registry packs (RFC 0095). Peer to the node/workflow-chain/prompt/artifact-type/chat-card pack manifests; disjoint via the `kind` discriminator. Distributes a portable provider definition — auth endpoints, read/write scope groups, exactly-one reach (`mcp`/`openapi`/`integration`) — that the RFC 0045/0047 `provider` string resolves against. Carries NO credential material (`connection-pack-no-credential-material`). |
44
46
  | `conformance-certification-bundle.schema.json` | `conformance-certification.md` + RFC 0089 | DRAFT — machine-readable attestation binding a host's claimed profiles to the reproducible run that substantiates them (suite version + per-scenario pass list + host identity/commit + captured discovery document). Out-of-band; a consumer re-derives each claim via the §B binding rule. |
@@ -74,6 +76,7 @@
74
76
  | `a2a-task-state.schema.json` | `a2a-integration.md` §"Async / durable Tasks" (RFC 0100) | The durable, persisted projection of an A2A `Task` an OpenWOP host keeps per backing run when `a2a.durableTasks: true` — `taskId == runId`, lowercase-hyphen `state`, `interruptKind`, optional SSRF-guarded `PushConfig`. Content-free of run inputs/outputs/artifacts (SR-1 / `a2a-push-egress-ssrf`). |
75
77
  | `budget-policy.schema.json` | `budget-policy.md` (RFC 0084) | The reserved `budget` run-options shape — `maxTokens`/`maxCostUsd`/`maxToolCalls`/`maxRetries`/`modelAllow[]`/`modelDeny[]`/`thresholdPercent`/`onExhaustion`. Enforceable per-run spend governance; wall-time/iterations delegated to RFC 0058 (`additionalProperties:false`). Content-free events; no pricing on the wire (`budget-no-pricing-leak`). |
76
78
  | `tool-descriptor.schema.json` | `tool-catalog.md` (RFC 0078) | Portable read-only description of one tool unifying the five tool surfaces (node-pack/workflow/mcp/connector/host-extension) — stable `toolId`, source, I/O schemas, auth/egress/approval requirements, replay policy, and `safetyTier` (`exec` ⇒ `host-extension`, RFC 0069). Returned by `GET /v1/tools`; secret-free (SR-1). |
79
+ | `compact-tool-descriptor.schema.json` | `tool-catalog.md` §compact (RFC 0112) | Lossy model-facing projection of `ToolDescriptor` returned by `GET /v1/tools?view=compact` (`{ tools: [] }` envelope) when the host advertises `toolCatalog.compactView`. Keeps `toolId`/`source`/`safetyTier` (+ optional `title`/`description`/`inputSchema`), drops the heavy fields, and bounds any `inputSchema` to a self-contained structural subset (top-level object; no `$ref`/`oneOf`/`allOf`/`anyOf`/`not`/`patternProperties`/`dependentSchemas`). Secret-free (SR-1); `exec` ⇒ `host-extension`. |
77
80
  | `suspend-request.schema.json` | `interrupt.md` | `InterruptPayload` with 8 `kind` discriminators (approval, clarification, external-event, custom, conversation.start, conversation.exchange, conversation.close, low-confidence) |
78
81
  | `workflow-chain-pack-manifest.schema.json` | `workflow-chain-packs.md` + RFC 0013 | Manifest for workflow-chain packs (`kind: "workflow-chain"`) — pre-configured DAG fragments expanded inline at workflow-author time. Peer to `node-pack-manifest.schema.json`; disjoint via the `kind` discriminator. |
79
82
  | `workflow-definition.schema.json` | `channels-and-reducers.md` + `node-packs.md` | DAG of nodes + edges + triggers + variables + channels |
@@ -0,0 +1,48 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://openwop.dev/spec/v1/a2ui-surface-delta-frame.schema.json",
4
+ "title": "A2uiSurfaceDeltaFrame",
5
+ "description": "RFC 0114. A HOST-SIDE TRANSPORT frame carrying an RFC 6902 (JSON-Patch) delta over a recorded `ui.a2ui-surface` envelope (RFC 0102). Delivered ONLY over the run event stream (`GET /v1/runs/{runId}/events`) to a subscriber that negotiated `?a2uiDelta=1`; every other consumer (the event-log read, replay, `:fork`, any non-negotiating subscriber) receives the materialized FULL surface. This is NOT a recorded-envelope shape: the canonical `ui.a2ui-surface.schema.json` envelope is UNCHANGED and always full. The consumer applies the `patch` to the surface last delivered under `surfaceRef`, then re-validates the result against the closed `catalogVersion` catalog before render (the same fail-closed validation a full surface receives, RFC 0102 §1); on ANY apply/validation failure it falls back to store-without-render and the host re-materializes the full surface.",
6
+ "type": "object",
7
+ "required": ["surfaceRef", "catalogVersion", "patch"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "surfaceRef": {
11
+ "type": "string",
12
+ "description": "REQUIRED — the recorded `ui.a2ui-surface` envelope id this delta patches. The consumer applies `patch` to the surface last delivered under this ref.",
13
+ "minLength": 1
14
+ },
15
+ "catalogVersion": {
16
+ "type": "string",
17
+ "description": "REQUIRED — the A2UI catalog version. MUST equal the referenced full surface's `catalogVersion`; a catalog-version change MUST start from a fresh full surface, never a delta.",
18
+ "minLength": 1
19
+ },
20
+ "patch": {
21
+ "type": "array",
22
+ "minItems": 1,
23
+ "description": "REQUIRED — a non-empty RFC 6902 (JSON-Patch) document applied over the surface last delivered under `surfaceRef`. The `test` op is EXCLUDED (a fire-and-forget transport frame cannot act on a failed conditional); `move`/`copy` are permitted but OPTIONAL to support.",
24
+ "items": {
25
+ "type": "object",
26
+ "required": ["op", "path"],
27
+ "additionalProperties": false,
28
+ "properties": {
29
+ "op": {
30
+ "enum": ["add", "remove", "replace", "move", "copy"],
31
+ "description": "RFC 6902 operation. `test` is deliberately excluded."
32
+ },
33
+ "path": {
34
+ "type": "string",
35
+ "description": "RFC 6901 JSON-Pointer into the target surface."
36
+ },
37
+ "from": {
38
+ "type": "string",
39
+ "description": "RFC 6901 JSON-Pointer source for `move`/`copy`."
40
+ },
41
+ "value": {
42
+ "description": "The value for `add`/`replace`. Walked by the SR-1 redaction harness exactly like a full-surface value (RFC 0114)."
43
+ }
44
+ }
45
+ }
46
+ }
47
+ }
48
+ }
@@ -583,6 +583,30 @@
583
583
  }
584
584
  }
585
585
  },
586
+ "conversationTurnModelProvenance": {
587
+ "type": "object",
588
+ "description": "RFC 0109 — Conversation-turn model provenance. When advertised with `supported: true`, the host stamps the OPTIONAL `agent.model` object (`{ provider, model }`) on `role: 'agent'` conversation turns (`conversation-turn.schema.json`), recording which model produced the turn. The stamp is NON-SECRET + NON-PII (`additionalProperties: false` on `agent.model` forbids any credential/endpoint/prompt — the SR-1 guard) and is read VERBATIM on `:fork` (never re-resolved, so a forked transcript preserves the original provenance). Absent block ⇒ no advertisement: the host omits `agent.model` and treats it as an opaque, unenforced field. Advertising `supported: true` without stamping is a dishonest wire claim (`OPENWOP_REQUIRE_BEHAVIOR=true` fails the gated scenario). Additive over RFC 0005 — extends the conversation primitive, does not replace it.",
589
+ "required": ["supported"],
590
+ "additionalProperties": false,
591
+ "properties": {
592
+ "supported": {
593
+ "type": "boolean",
594
+ "description": "RFC 0109. Host stamps `agent.model` ({ provider, model }) on `role: 'agent'` conversation turns. When `false` or absent, the RFC 0109 conformance scenario soft-skips and the host emits no model provenance."
595
+ }
596
+ }
597
+ },
598
+ "channelPresence": {
599
+ "type": "object",
600
+ "description": "RFC 0110 — Channel presence (online + typing). When advertised with `supported: true`, the host emits the OPTIONAL `channel.presence` RunEvent (`channel-presence-payload.schema.json`) for a `type:'channel'` conversation, carrying the currently-present member subject refs + optional per-member typing. Presence is EPHEMERAL live state: the host MUST NOT persist it to the replayable event log / transcript and it MUST NOT affect replay or `:fork` (the load-bearing distinction from the persisted `conversation.exchanged` turn). Membership-gated: every ref MUST be a current participant and the event MUST NOT be delivered to a non-member (DEFAULT-DENY, CTI-1). NON-PII (opaque RFC 0041 subject refs only). Absent block ⇒ no advertisement: the host emits no presence. Advertising `supported: true` without emitting is a dishonest wire claim (`OPENWOP_REQUIRE_BEHAVIOR=true` fails the gated scenario). Additive over RFC 0005.",
601
+ "required": ["supported"],
602
+ "additionalProperties": false,
603
+ "properties": {
604
+ "supported": {
605
+ "type": "boolean",
606
+ "description": "RFC 0110. Host emits the ephemeral `channel.presence` RunEvent for channel conversations. When `false` or absent, the RFC 0110 conformance scenario soft-skips and the host emits no presence."
607
+ }
608
+ }
609
+ },
586
610
  "multiAgent": {
587
611
  "type": "object",
588
612
  "description": "RFC 0037 — Multi-agent execution model + handoff state machine. Hosts that advertise implement the supervisor→dispatch→harvest loop + the 4-state handoff state machine + the `core.workflowChain.event` emission contract per spec/v1/multi-agent-execution.md. Absent block = host implements RFCs 0006/0007/0022 individually with implementation flexibility on integration semantics; conformance scenarios gating on this flag soft-skip on absence.",
@@ -705,6 +729,47 @@
705
729
  "type": "integer",
706
730
  "minimum": 1,
707
731
  "description": "RFC 0061 (`version >= 5`). Host-advertised count of recent event-log entries the host feeds each orchestrator turn as the iteration's transcript input (§C input 3). Advertise-and-honor; not a fixed wire constant. Absent ⇒ the host does not bound the transcript window on the wire."
732
+ },
733
+ "contextBudget": {
734
+ "type": "object",
735
+ "description": "RFC 0111 (`Active`, `version >= 5`). Opt-in, token-denominated bound on the orchestrator transcript the host feeds each iteration, plus a declared summarization contract for turns evicted beyond that budget. SCOPE: governs the RFC 0061 per-iteration ORCHESTRATOR-LOOP transcript (`multi-agent-execution.md` §\"Per-iteration state inputs\" input 3 — the same transcript `transcriptWindow` bounds), NOT a general chat-conversation history. A host whose orchestrator loop does not run real model turns (e.g. a mock supervisor) MUST NOT advertise this block, exactly as it MUST NOT dishonestly advertise `transcriptWindow`. Budget-only advertisement (`transcriptTokenBudget` + `tokenCounter` WITHOUT `summarization.supported`) is valid for a host that HAS a real orchestrator loop but does not summarize. Purely additive: a host that omits this block behaves exactly as today, including `transcriptWindow`'s `absent ⇒ unbounded` default (NOT flipped by this RFC). Complements (does not replace) the event-count `transcriptWindow`; when both bound a turn the host MUST honor whichever is tighter. Summarization here is a NONDETERMINISTIC host output governed exactly like an RFC 0041 envelope: each substitution MUST be recorded as a `context.summarized` event whose `summaryRef` artifact replay reuses (never re-summarizes) per `multi-agent-execution.md` §\"Context economy (RFC 0111)\".",
736
+ "additionalProperties": false,
737
+ "properties": {
738
+ "transcriptTokenBudget": {
739
+ "type": "integer",
740
+ "minimum": 1,
741
+ "description": "RFC 0111. Max tokens of transcript the host feeds any single orchestrator turn, measured in the unit named by `tokenCounter`. Advertise-and-honor; complements (does not replace) `transcriptWindow`. When both are present the host MUST honor whichever bound is tighter for a given turn. Absent ⇒ no token bound on the transcript (only the event-count `transcriptWindow`, if any, applies)."
742
+ },
743
+ "tokenCounter": {
744
+ "type": "string",
745
+ "enum": ["o200k_base", "cl100k_base", "chars", "host-defined"],
746
+ "description": "RFC 0111. The unit `transcriptTokenBudget` is denominated in, so the bound is interpretable across hosts. REQUIRED when `transcriptTokenBudget` is present (enforced via the `if/then` clause). `o200k_base`/`cl100k_base` are tokenizer encodings; `chars` counts UTF-8/Unicode characters (a tokenizer-free unit a client can reason about directly); `host-defined` is an opaque host unit. Same enum as RFC 0113 `memory.injectionBudget.tokenCounter` — transcript (0111) and memory (0113) budgets denominate in one consistent vocabulary; no shared `$ref` (decoupled)."
747
+ },
748
+ "summarization": {
749
+ "type": "object",
750
+ "additionalProperties": false,
751
+ "required": ["supported"],
752
+ "description": "RFC 0111. Declared contract for turns evicted beyond `transcriptTokenBudget`. When `supported: true`, the host MAY replace older in-window turns with a host-produced summary; it MUST keep the most recent `keepLastTurns` turns verbatim and MUST NOT summarize the active (most recent) turn. Each substitution MUST be recorded as a `context.summarized` event and is replay-governed under RFC 0041 (replay reuses the recorded `summaryRef`, never re-summarizes).",
753
+ "properties": {
754
+ "supported": {
755
+ "type": "boolean",
756
+ "description": "REQUIRED when the sub-block is present. When `true`, the host implements the RFC 0111 declared-summarization contract; conformance scenario `context-summarization-replay` gates on this flag and soft-skips when absent/false."
757
+ },
758
+ "strategy": {
759
+ "type": "string",
760
+ "enum": ["sliding-window", "recursive", "map-reduce"],
761
+ "description": "RFC 0111. Informational descriptor of the host's summarization strategy. `sliding-window` keeps a recent verbatim tail and summarizes the prefix; `recursive` folds prior summaries into new ones; `map-reduce` summarizes chunks then combines. Does not change the replay-determinism contract — all strategies record `context.summarized` and reuse `summaryRef` on replay."
762
+ },
763
+ "keepLastTurns": {
764
+ "type": "integer",
765
+ "minimum": 0,
766
+ "description": "RFC 0111. Number of most-recent turns kept verbatim at the head of the window; older in-window turns MAY be replaced by a summary. The active (most recent) turn MUST NOT be summarized regardless of this value. Absent ⇒ host-defined verbatim floor."
767
+ }
768
+ }
769
+ }
770
+ },
771
+ "if": { "required": ["transcriptTokenBudget"] },
772
+ "then": { "required": ["transcriptTokenBudget", "tokenCounter"] }
708
773
  }
709
774
  }
710
775
  }
@@ -856,6 +921,24 @@
856
921
  "turnDetection": ["transcription"],
857
922
  "bargeIn": ["transcription"]
858
923
  }
924
+ },
925
+ "promptPrefixCache": {
926
+ "type": "object",
927
+ "additionalProperties": false,
928
+ "description": "RFC 0116. Host honors the AI-envelope `generate` request's optional `cachePrefixId` as a provider-cache routing hint — tenant-namespaced (SECURITY invariant `prompt-prefix-cache-cross-tenant-isolation`), secret-free, and replay-invariant (a cache hit/miss MUST NOT change the recorded envelope or `provider.usage.inputTokens`/`outputTokens`). Absent ⇒ a host MUST ignore `cachePrefixId` (no error, no behavior change). PROVIDER-SCOPED: prefix caching is provider-specific (e.g. Anthropic ephemeral), so this is NOT a universal claim — see `providers`.",
929
+ "properties": {
930
+ "supported": {
931
+ "type": "boolean",
932
+ "description": "Whether the host honors `cachePrefixId` as a provider-cache routing hint."
933
+ },
934
+ "providers": {
935
+ "type": "array",
936
+ "items": { "type": "string", "minLength": 1 },
937
+ "uniqueItems": true,
938
+ "description": "RFC 0116. The subset of `aiProviders.supported[]` for which the host honors `cachePrefixId` (prefix caching is provider-specific). A request whose routed provider is NOT in this list MUST have `cachePrefixId` ignored. Absent ⇒ host-defined per-provider routing; NOT a universal claim across providers."
939
+ }
940
+ },
941
+ "required": ["supported"]
859
942
  }
860
943
  },
861
944
  "additionalProperties": false,
@@ -1341,6 +1424,22 @@
1341
1424
  "ttl": { "type": "boolean", "description": "When `true`, memory entries expire per `expiresAt` (the `ttlSupported` semantics surfaced as a named retention dimension)." },
1342
1425
  "forget": { "type": "boolean", "description": "When `true`, the host supports a tenant-scoped delete-by-subject forget operation (composes the CTI-1 cross-tenant invariant — a forget MUST NOT cross tenant boundaries)." }
1343
1426
  }
1427
+ },
1428
+ "injectionBudget": {
1429
+ "type": "object",
1430
+ "description": "RFC 0113 (`Active`). The host honors `MemoryListOptions.tokenBudget` — a token-denominated bound on a single injection read (the live read that feeds a turn), distinct from RFC 0062 distillation's background-compaction budget. Advertising it commits the host to return a token-bounded prefix of the ranked entry list (over-budget single entry omitted, never truncated mid-entry) over the SR-1-redacted, CTI-1-single-tenant result set. Relevance ranking is NOT advertised here — `rank:'relevance'` delegates to the existing `memory.search` semantic mode (RFC 0080), so there is exactly one relevance surface in the corpus. Hosts that omit this block do not honor `tokenBudget` (a supplied `tokenBudget` is ignored, today's `limit`/`tag` behavior).",
1431
+ "additionalProperties": false,
1432
+ "required": ["supported"],
1433
+ "properties": {
1434
+ "supported": { "type": "boolean", "description": "REQUIRED when the sub-block is present. When `true`, the host honors `MemoryListOptions.tokenBudget` per `agent-memory.md` §\"Injection budget\"." },
1435
+ "tokenCounter": {
1436
+ "type": "string",
1437
+ "enum": ["o200k_base", "cl100k_base", "chars", "host-defined"],
1438
+ "description": "The unit `tokenBudget` is denominated in. REQUIRED when `injectionBudget.supported` (enforced via the `if/then` clause). `o200k_base`/`cl100k_base` are tokenizer encodings; `chars` counts UTF-8/Unicode characters of the entry `content` (a tokenizer-free unit a client can reason about directly — preferred over opaque `host-defined`); `host-defined` is an opaque host unit. The over-budget-single-entry-omitted rule applies regardless of unit. RFC 0111 aligns to these same values when it lands (0113 lands first); no shared `$ref` (decoupled)."
1439
+ }
1440
+ },
1441
+ "if": { "properties": { "supported": { "const": true } }, "required": ["supported"] },
1442
+ "then": { "required": ["supported", "tokenCounter"] }
1344
1443
  }
1345
1444
  },
1346
1445
  "additionalProperties": true
@@ -1508,7 +1607,8 @@
1508
1607
  "items": { "type": "string", "enum": ["node-pack", "workflow", "mcp", "connector", "host-extension"] },
1509
1608
  "description": "Which tool sources the catalog projects. A host advertises only the sources it actually surfaces; a consumer MUST tolerate any subset. Absent ⇒ all sources the host implements."
1510
1609
  },
1511
- "sessionLifecycle": { "type": "boolean", "description": "`true` ⇒ the host emits the RFC 0078 §D tool-session lifecycle events (`tool.session.opened`/`tool.session.closed`, content-free) bracketing the existing RFC 0064 `agent.toolCalled`/`agent.toolReturned` call events for multi-step interactions. Absent ⇒ `false` (single-shot tool calls only)." }
1610
+ "sessionLifecycle": { "type": "boolean", "description": "`true` ⇒ the host emits the RFC 0078 §D tool-session lifecycle events (`tool.session.opened`/`tool.session.closed`, content-free) bracketing the existing RFC 0064 `agent.toolCalled`/`agent.toolReturned` call events for multi-step interactions. Absent ⇒ `false` (single-shot tool calls only)." },
1611
+ "compactView": { "type": "boolean", "description": "RFC 0112. `true` ⇒ the host honors `GET /v1/tools?view=compact` + `GET /v1/tools/{toolId}?view=compact`, returning the `{ tools: CompactToolDescriptor[] }` projection (`compact-tool-descriptor.schema.json`): the heavy descriptor fields (`outputSchema`/`auth`/`egress`/`approval`/`replayPolicy`/`costHint`/`latencyHint`) are dropped and any `inputSchema` is bounded to the compact structural subset. The compact `tools[]` carries the same `toolId` set as the standard view for the same principal. Absent ⇒ the host treats `view=compact` as an unknown query param (standard view)." }
1512
1612
  }
1513
1613
  },
1514
1614
  "httpClient": {
@@ -2197,6 +2297,33 @@
2197
2297
  }
2198
2298
  }
2199
2299
  }
2300
+ },
2301
+ "restTransport": {
2302
+ "type": "object",
2303
+ "additionalProperties": false,
2304
+ "description": "RFC 0115 (`Active`). Conditional-GET + Content-Encoding negotiation on run reads (`GET /v1/runs/{runId}`). Optional. Distinct from the file-egress `fileHandling.transport` (ftp/sftp/ssh) sub-capability — this advertises HTTP-layer poll economy on the run-read REST surface.",
2305
+ "properties": {
2306
+ "conditionalRunGet": {
2307
+ "type": "boolean",
2308
+ "description": "RFC 0115. Host emits a strong, event-log-sequence-derived `ETag` on `GET /v1/runs/{runId}` and honors `If-None-Match` with a `304 Not Modified` (empty body) when the validator matches the current state version."
2309
+ },
2310
+ "contentEncodings": {
2311
+ "type": "array",
2312
+ "items": { "type": "string", "enum": ["gzip", "br", "zstd"] },
2313
+ "description": "RFC 0115. Content-Encoding values the host will negotiate on run reads (advertisement of standard-HTTP behavior). `gzip` is the baseline; `br`/`zstd` are OPTIONAL — the host advertises only the subset it can actually serve. For each advertised value the decoded body MUST be byte-identical to the identity body."
2314
+ }
2315
+ }
2316
+ },
2317
+ "a2uiSurface": {
2318
+ "type": "object",
2319
+ "additionalProperties": false,
2320
+ "description": "RFC 0114 (`Active`). Host-side TRANSPORT features over the recorded `ui.a2ui-surface` envelope (RFC 0102). A transport optimization, NOT an envelope-payload kind — the recorded envelope stays the full surface. Optional.",
2321
+ "properties": {
2322
+ "deltaTransport": {
2323
+ "type": "boolean",
2324
+ "description": "RFC 0114. Host delivers RFC 6902 (`a2ui-surface-delta-frame.schema.json`) delta frames over the run event stream to subscribers that negotiate `?a2uiDelta=1`; the full surface is materialized for everyone else, the event-log read, and replay. The recorded `ui.a2ui-surface` envelope stays full. The consumer re-validates the post-patch surface against the closed `catalogVersion` catalog before render and falls back fail-closed on any apply/validation failure (the host then re-materializes full)."
2325
+ }
2326
+ }
2200
2327
  }
2201
2328
  },
2202
2329
  "additionalProperties": true
@@ -0,0 +1,41 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://openwop.dev/spec/v1/channel-presence-payload.schema.json",
4
+ "title": "ChannelPresencePayload",
5
+ "description": "Payload of the OPTIONAL `channel.presence` RunEvent (RFC 0110). EPHEMERAL live presence for a `type:'channel'` conversation — who is currently present + (optionally) who is typing. A host MUST NOT persist this event to the replayable event log / transcript, and it MUST NOT affect replay or `:fork` determinism (presence is live state, the load-bearing distinction from the persisted `conversation.exchanged` turn — see replay.md). Membership-gated: every ref MUST be a current channel participant and the event MUST NOT be delivered to a non-member (the same DEFAULT-DENY visibility as the channel's messages; cross-tenant delivery is forbidden, CTI-1). NON-PII: subject refs are the opaque RFC 0041 vocabulary only — no IP/location/device. A host MAY emit; if it advertises `channelPresence.supported` it MUST. Gated on `channelPresence.supported` (capabilities.schema.json). See RFC 0110.",
6
+ "type": "object",
7
+ "required": ["conversationId", "present"],
8
+ "additionalProperties": false,
9
+ "properties": {
10
+ "conversationId": {
11
+ "type": "string",
12
+ "description": "The `type:'channel'` conversation this presence is for.",
13
+ "minLength": 1,
14
+ "maxLength": 256
15
+ },
16
+ "present": {
17
+ "type": "array",
18
+ "description": "Subject refs (RFC 0041 vocabulary `user:<id>` / `agent:<id>` — opaque, non-PII) of members CURRENTLY present in the channel. MUST be a subset of the channel's current participants; a non-member MUST NOT appear.",
19
+ "items": { "type": "string", "minLength": 1, "maxLength": 256 }
20
+ },
21
+ "typing": {
22
+ "type": "array",
23
+ "description": "OPTIONAL subset of `present` that is currently typing. Boolean-by-presence: a ref appears iff that member is typing. No payload beyond the subject refs (no free text, no PII).",
24
+ "items": { "type": "string", "minLength": 1, "maxLength": 256 }
25
+ }
26
+ },
27
+ "examples": [
28
+ {
29
+ "$comment": "RFC 0110 POSITIVE — two members present in a channel, one typing.",
30
+ "conversationId": "chan-eng",
31
+ "present": ["user:alice", "agent:iris"],
32
+ "typing": ["user:alice"]
33
+ },
34
+ {
35
+ "$comment": "RFC 0110 POSITIVE — typing is OPTIONAL; a presence snapshot with no one typing.",
36
+ "conversationId": "chan-eng",
37
+ "present": ["user:alice"]
38
+ }
39
+ ],
40
+ "$comment": "RFC 0110 NEGATIVE (not validatable as an examples[] entry, which must be VALID): a payload carrying any field beyond conversationId/present/typing (e.g. `ip`, `location`) MUST FAIL validation via `additionalProperties: false` — the no-PII guard; see conformance/src/scenarios/channel-presence-shape.test.ts."
41
+ }
@@ -0,0 +1,51 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://openwop.dev/spec/v1/compact-tool-descriptor.schema.json",
4
+ "title": "CompactToolDescriptor",
5
+ "description": "RFC 0112. A lossy, model-facing projection of `ToolDescriptor` (tool-descriptor.schema.json) returned by `GET /v1/tools?view=compact` + `GET /v1/tools/{toolId}?view=compact` when the host advertises `capabilities.toolCatalog.compactView: true`. Heavy descriptive fields (`outputSchema`, `auth`, `egress`, `approval`, `replayPolicy`, `costHint`, `latencyHint`) are dropped, and any `inputSchema` is bounded to the self-contained compact structural subset (top-level `type: \"object\"` with `properties`; no `$ref`/`oneOf`/`allOf`/`anyOf`/`not`/`patternProperties`/`dependentSchemas`). The subset is the stable structural core of RFC 0030 Tier-1, cited as rationale but pinned HERE so conformance is machine-checkable and immune to that informative table's drift. Like the full descriptor, a CompactToolDescriptor never carries credential material (SR-1) and is not an invocation path.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["toolId", "source", "safetyTier"],
9
+ "properties": {
10
+ "toolId": {
11
+ "type": "string",
12
+ "minLength": 1,
13
+ "description": "Unchanged from ToolDescriptor. Stable, host-unique tool identifier in the `<scope>:<tool-id>` form; MUST be stable across catalog reads for a given host version (tool-catalog.md §C MUST 3). The compact `tools[]` MUST carry the same `toolId` set as the standard view for the same principal (projection completeness)."
14
+ },
15
+ "source": {
16
+ "type": "string",
17
+ "enum": ["node-pack", "workflow", "mcp", "connector", "host-extension"],
18
+ "description": "Retained from ToolDescriptor so the `safetyTier: \"exec\" ⇒ source: \"host-extension\"` cross-field MUST (RFC 0069) stays expressible. Which surface backs the tool: `node-pack`/`workflow`/`mcp`/`connector`/`host-extension`."
19
+ },
20
+ "safetyTier": {
21
+ "type": "string",
22
+ "enum": ["pure", "read", "write", "exec"],
23
+ "description": "REQUIRED. The tool's DATA-EFFECT classification (`pure`/`read`/`write`/`exec`); per RFC 0069 an `exec` tool MUST be `source: \"host-extension\"`. Host-assigned, not derivable from a permission/approval/risk tier."
24
+ },
25
+ "title": { "type": "string", "description": "MAY — short human-readable label for a model-facing tool picker." },
26
+ "description": {
27
+ "type": "string",
28
+ "description": "MAY — one-line summary; the host SHOULD truncate to a model-facing summary in the compact view."
29
+ },
30
+ "inputSchema": {
31
+ "type": "object",
32
+ "description": "MAY — the tool's argument schema bounded to the compact structural subset (RFC 0112). When present it MUST have top-level `type: \"object\"` with an explicit `properties` map, and MUST NOT use `$ref`, `oneOf`, `allOf`, `anyOf`, `not`, `patternProperties`, or `dependentSchemas` AT ANY NESTING DEPTH (including inside nested property schemas). Absent ⇒ opaque/host-interpreted args. NOTE: the `propertyNames` clause below is a TOP-LEVEL structural floor only; the total any-depth constraint is enforced by the RFC 0112 conformance scenario (a schema-aware recursive walk), since pure JSON Schema cannot express it without recursion gymnastics.",
33
+ "required": ["type", "properties"],
34
+ "properties": {
35
+ "type": { "const": "object", "description": "Top-level MUST be `\"object\"`." },
36
+ "properties": { "type": "object", "description": "The argument property map (MUST be present)." }
37
+ },
38
+ "propertyNames": {
39
+ "$comment": "RFC 0112 compact structural subset (TOP-LEVEL FLOOR): forbid the heavy/non-portable keywords as top-level inputSchema keys. The total any-depth constraint is enforced by conformance.",
40
+ "not": { "enum": ["$ref", "oneOf", "allOf", "anyOf", "not", "patternProperties", "dependentSchemas"] }
41
+ }
42
+ }
43
+ },
44
+ "allOf": [
45
+ {
46
+ "$comment": "RFC 0069 / tool-catalog.md §C-1: an exec-tier tool MUST be host-extension-sourced (exec is never protocol-tier). Mirrors tool-descriptor.schema.json.",
47
+ "if": { "properties": { "safetyTier": { "const": "exec" } }, "required": ["safetyTier"] },
48
+ "then": { "properties": { "source": { "const": "host-extension" } }, "required": ["source"] }
49
+ }
50
+ ]
51
+ }
@@ -58,6 +58,16 @@
58
58
  "memoryRef": {
59
59
  "type": "string",
60
60
  "description": "Opaque memory reference per RFC 0002 §A + RFC 0004 §A. Resolves to `MemoryEntry[]` via host `MemoryAdapter` for cross-run conversation continuity."
61
+ },
62
+ "model": {
63
+ "type": "object",
64
+ "description": "RFC 0109 — OPTIONAL provenance: which model produced this `role: 'agent'` turn. NON-SECRET, NON-PII: provider + model identifiers ONLY (`additionalProperties: false` forbids any credential/endpoint/prompt leaking in — the SR-1 secret-redaction guard). Stamped at emit; read VERBATIM on `:fork` (never re-resolved). A host that stamps it MUST advertise `conversationTurnModelProvenance.supported: true`. Absent ⇒ no advertisement; clients MUST tolerate its absence.",
65
+ "additionalProperties": false,
66
+ "required": ["provider", "model"],
67
+ "properties": {
68
+ "provider": { "type": "string", "minLength": 1, "maxLength": 128, "description": "Provider identifier (e.g. `anthropic`, `openai`, `google`). Non-secret." },
69
+ "model": { "type": "string", "minLength": 1, "maxLength": 256, "description": "Model identifier as the provider names it (e.g. `claude-opus-4-8`). Non-secret." }
70
+ }
61
71
  }
62
72
  },
63
73
  "additionalProperties": true
@@ -15,6 +15,22 @@
15
15
  "minLength": 1,
16
16
  "maxLength": 256,
17
17
  "description": "Filter to entries whose `tags` array contains this string (set-membership check). Hosts MAY support more advanced filtering as a host extension under a `vendor.<host>.*` field."
18
+ },
19
+ "tokenBudget": {
20
+ "type": "integer",
21
+ "minimum": 1,
22
+ "description": "RFC 0113. Max cumulative tokens across returned entries, denominated in the unit named by `capabilities.memory.injectionBudget.tokenCounter`. The adapter MUST return a prefix of the ranked entry list whose cumulative token count does not exceed this; a single entry exceeding the budget on its own MUST be omitted (not truncated mid-entry). MAY combine with `limit` — the adapter honors whichever yields fewer entries. Requires the host to advertise `memory.injectionBudget.supported`."
23
+ },
24
+ "rank": {
25
+ "type": "string",
26
+ "enum": ["recency", "relevance"],
27
+ "description": "RFC 0113. Selection order for the (optionally `tokenBudget`-bounded) read. `recency` (default when absent) orders most-recent-first — today's behavior. `relevance` DELEGATES to the existing `memory.search` semantic mode (RFC 0080): it requires `query` AND that the host advertise `capabilities.memory.search` with `semantic` mode, and is the budgeted projection OVER that existing search surface — NOT a new ranking capability. A host that does not advertise `memory.search` semantic mode MUST reject `rank:'relevance'` (or fall back to `'recency'` per its documented behavior)."
28
+ },
29
+ "query": {
30
+ "type": "string",
31
+ "minLength": 1,
32
+ "maxLength": 4096,
33
+ "description": "RFC 0113. Free-text relevance anchor (the `memory.search` semantic query) — REQUIRED when `rank:'relevance'`, ignored otherwise."
18
34
  }
19
35
  },
20
36
  "additionalProperties": false
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://openwop.dev/spec/v1/run-event-payloads.schema.json",
4
4
  "title": "RunEventPayloads",
5
- "description": "Per-RunEventType payload schemas. The base RunEventDoc shape (run-event.schema.json) leaves `payload` permissive for forward-compat. This schema defines the canonical payload contract for each known RunEventType. Consumers MAY pin strict payload validation via `$defs.<typeId>` and `ajv.validate(schema.$defs[event.type], event.payload)`. Unknown event types MUST be tolerated (no $defs match → fold best-effort).\n\n107 variants from `run-event.schema.json#$defs.RunEventType` are covered, grouped into ~20 shape families with shared $defs. Naming convention: camelCase keys mirror dotted RunEventType names (e.g., `run.started` → `runStarted`).",
5
+ "description": "Per-RunEventType payload schemas. The base RunEventDoc shape (run-event.schema.json) leaves `payload` permissive for forward-compat. This schema defines the canonical payload contract for each known RunEventType. Consumers MAY pin strict payload validation via `$defs.<typeId>` and `ajv.validate(schema.$defs[event.type], event.payload)`. Unknown event types MUST be tolerated (no $defs match → fold best-effort).\n\n109 variants from `run-event.schema.json#$defs.RunEventType` are covered, grouped into ~20 shape families with shared $defs. Naming convention: camelCase keys mirror dotted RunEventType names (e.g., `run.started` → `runStarted`).",
6
6
  "type": "object",
7
7
  "$defs": {
8
8
  "_typeIndex": {
@@ -38,6 +38,8 @@
38
38
  "interrupt.requested": { "$ref": "#/$defs/interruptRequested" },
39
39
  "interrupt.resolved": { "$ref": "#/$defs/interruptResolved" },
40
40
  "channel.written": { "$ref": "#/$defs/channelWritten" },
41
+ "channel.presence": { "$ref": "#/$defs/channelPresence" },
42
+ "context.summarized": { "$ref": "#/$defs/contextSummarized" },
41
43
  "artifact.created": { "$ref": "#/$defs/artifactCreated" },
42
44
  "output.chunk": { "$ref": "#/$defs/outputChunk" },
43
45
  "variable.changed": { "$ref": "#/$defs/variableChanged" },
@@ -781,6 +783,25 @@
781
783
  "$ref": "https://openwop.dev/spec/v1/channel-written-payload.schema.json"
782
784
  },
783
785
 
786
+ "channelPresence": {
787
+ "$ref": "https://openwop.dev/spec/v1/channel-presence-payload.schema.json"
788
+ },
789
+
790
+ "contextSummarized": {
791
+ "type": "object",
792
+ "description": "RFC 0111. Emitted when the host replaces older in-window orchestrator transcript turns with a host-produced summary to honor `multiAgent.executionModel.contextBudget.transcriptTokenBudget`. CONTENT-FREE: the summary text is NEVER inlined — `summaryRef` is an artifactId resolved via `GET /v1/runs/{runId}/artifacts/{artifactId}`. The summary is a NONDETERMINISTIC host output governed like an RFC 0041 envelope: on `:fork mode:replay` the host MUST reuse this recorded `summaryRef` and MUST NOT re-summarize (see multi-agent-execution.md §\"Context economy (RFC 0111)\"). `replacedTurns` lists the event ids the summary stands in for, so a replay engine reconstructs the exact transcript.",
793
+ "required": ["iteration", "replacedTurns", "summaryRef", "tokenCounter", "tokensBefore", "tokensAfter"],
794
+ "properties": {
795
+ "iteration": { "type": "integer", "minimum": 0, "description": "The orchestrator-loop iteration (the `runOrchestrator.decided.iteration` counter) whose transcript assembly triggered this summarization." },
796
+ "replacedTurns": { "type": "array", "items": { "type": "string", "minLength": 1 }, "description": "Event ids (run event-log entries) the summary stands in for. A replay engine reconstructs the exact model-facing transcript by substituting the `summaryRef` artifact for this contiguous range." },
797
+ "summaryRef": { "type": "string", "minLength": 1, "description": "ArtifactId of the persisted summary (read via `GET /v1/runs/{runId}/artifacts/{artifactId}`). The summary text is NOT on the wire. MUST be persisted/readable as-of the iteration's event-log index; a host that cannot serve it at the requested `fromSeq` MUST refuse the fork (mirrors `replay_memory_snapshot_unavailable`)." },
798
+ "tokenCounter": { "type": "string", "enum": ["o200k_base", "cl100k_base", "chars", "host-defined"], "description": "The unit `tokensBefore`/`tokensAfter` are denominated in. MUST equal the advertised `contextBudget.tokenCounter`." },
799
+ "tokensBefore": { "type": "integer", "minimum": 0, "description": "Token count of the `replacedTurns` range before summarization, in `tokenCounter` units." },
800
+ "tokensAfter": { "type": "integer", "minimum": 0, "description": "Token count of the summary that replaced the range, in `tokenCounter` units. Expected ≤ `tokensBefore`." }
801
+ },
802
+ "additionalProperties": false
803
+ },
804
+
784
805
  "artifactCreated": {
785
806
  "type": "object",
786
807
  "description": "Emitted when a node produces a typed artifact (PRD, theme, plan, etc.).",
@@ -1049,7 +1070,7 @@
1049
1070
 
1050
1071
  "providerUsage": {
1051
1072
  "type": "object",
1052
- "description": "RFC 0026. Per-call usage record emitted after every LLM provider invocation. Durably persisted in the run event log; consumed by replay, webhook subscribers, billing reconciliation. The OTel `openwop.cost.*` attribute group (per `observability.md §\"Cost attribution attributes\"`) is the observability sibling — this event type is the durable record. Replay determinism: `inputTokens` + `outputTokens` MUST replay identically; `costEstimateUsd` MAY be omitted on replay. The payload MUST NOT carry credentialRefs, hashed credential identifiers, or prompt/response substrings per `SECURITY/threat-model-secret-leakage.md §SR-1` (enforced by SECURITY invariant `provider-usage-no-credential-leak`).",
1073
+ "description": "RFC 0026. Per-call usage record emitted after every LLM provider invocation. Durably persisted in the run event log; consumed by replay, webhook subscribers, billing reconciliation. The OTel `openwop.cost.*` attribute group (per `observability.md §\"Cost attribution attributes\"`) is the observability sibling — this event type is the durable record. Replay determinism: `inputTokens` + `outputTokens` MUST replay identically; `costEstimateUsd` MAY be omitted on replay, and so MAY the RFC 0116 cost-only `cacheReadTokens`/`cacheWriteTokens` (they are NOT replay-asserted — a prompt-prefix cache hit vs miss MUST NOT change `inputTokens`/`outputTokens` or the recorded envelope). The payload MUST NOT carry credentialRefs, hashed credential identifiers, or prompt/response substrings per `SECURITY/threat-model-secret-leakage.md §SR-1` (enforced by SECURITY invariant `provider-usage-no-credential-leak`).",
1053
1074
  "required": ["provider", "model", "inputTokens", "outputTokens"],
1054
1075
  "properties": {
1055
1076
  "provider": { "type": "string", "minLength": 1, "description": "Canonical provider id (lowercase ASCII, e.g. \"anthropic\", \"openai\", \"google\"). Same value as the `openwop.cost.provider` OTel attribute." },
@@ -1060,6 +1081,8 @@
1060
1081
  "costEstimateUsd": { "type": "number", "minimum": 0, "description": "ADVISORY estimate in USD computed by the host's static rate table. MUST NOT be used for billing — real billing is external. Hosts SHOULD omit when no rate is known rather than emit 0." },
1061
1082
  "currency": { "type": "string", "pattern": "^[A-Z]{3}$", "description": "ISO 4217 code when `costEstimateUsd` is non-USD; the field name stays `costEstimateUsd` for back-compat but `currency` overrides the implied denomination." },
1062
1083
  "cacheHit": { "type": "boolean", "description": "True iff this call was served from the LLM response cache per `replay.md §\"LLM cache-key recipe\"`. When true, inputTokens/outputTokens reflect the ORIGINAL call's billed values; the cached invocation incurred zero new provider cost." },
1084
+ "cacheReadTokens": { "type": "integer", "minimum": 0, "description": "RFC 0116. Optional, cost-only. Provider-reported tokens served from the prompt-prefix context cache on this call — a cache HIT shows > 0. This is a cost hint, NOT a semantic input: it MAY be omitted on replay (NOT replay-asserted, like `costEstimateUsd`), and a hit-vs-miss difference MUST NOT change `inputTokens`/`outputTokens` or the recorded envelope (RFC 0116 replay-invariance MUST). MUST NOT carry prompt/response substrings per `SECURITY/threat-model-secret-leakage.md §SR-1`." },
1085
+ "cacheWriteTokens": { "type": "integer", "minimum": 0, "description": "RFC 0116. Optional, cost-only. Provider-reported tokens written to the prompt-prefix context cache on this call (a cache PRIME). MAY be omitted on replay (NOT replay-asserted). MUST NOT change `inputTokens`/`outputTokens` or the recorded envelope, and MUST NOT carry prompt/response substrings (SR-1)." },
1063
1086
  "nodeId": { "type": "string", "description": "The node id that initiated the provider call. Required for per-node cost attribution dashboards." },
1064
1087
  "traceId": { "type": "string", "description": "OTel trace id linking this event to the matching `openwop.cost.*` span. Lets observability backends correlate event-log entries with traces." }
1065
1088
  },
@@ -100,6 +100,8 @@
100
100
  "interrupt.requested",
101
101
  "interrupt.resolved",
102
102
  "channel.written",
103
+ "channel.presence",
104
+ "context.summarized",
103
105
  "artifact.created",
104
106
  "output.chunk",
105
107
  "variable.changed",
@@ -72,6 +72,95 @@ export async function driveToolSession(
72
72
  return (res.json as ToolSessionResult | undefined) ?? {};
73
73
  }
74
74
 
75
+ /** A compact descriptor (RFC 0112) — the lossy `?view=compact` projection.
76
+ * Closed field set: `toolId`/`source`/`safetyTier` (+ optional
77
+ * `title`/`description`/`inputSchema`). The index signature lets a scenario
78
+ * assert the heavy fields are ABSENT without a cast. */
79
+ export interface CompactToolDescriptor {
80
+ toolId?: string;
81
+ source?: string;
82
+ safetyTier?: string;
83
+ title?: string;
84
+ description?: string;
85
+ inputSchema?: Record<string, unknown>;
86
+ [k: string]: unknown;
87
+ }
88
+
89
+ /** GET the compact tool catalog (RFC 0112 `GET /v1/tools?view=compact`).
90
+ * Returns the `{ tools: CompactToolDescriptor[] }` envelope's `tools` array;
91
+ * null when the host doesn't serve the read (404/405/501) or the body isn't
92
+ * the expected envelope shape. */
93
+ export async function listToolsCompact(): Promise<CompactToolDescriptor[] | null> {
94
+ const res = await driver.get('/v1/tools?view=compact');
95
+ if (res.status === 404 || res.status === 405 || res.status === 501) return null;
96
+ const body = res.json;
97
+ if (!body || typeof body !== 'object') return null;
98
+ const tools = (body as { tools?: unknown }).tools;
99
+ return Array.isArray(tools) ? (tools as CompactToolDescriptor[]) : null;
100
+ }
101
+
102
+ /** Heavy `ToolDescriptor` fields that a `CompactToolDescriptor` MUST drop
103
+ * (RFC 0112). */
104
+ export const COMPACT_DROPPED_FIELDS = [
105
+ 'outputSchema',
106
+ 'auth',
107
+ 'egress',
108
+ 'approval',
109
+ 'replayPolicy',
110
+ 'costHint',
111
+ 'latencyHint',
112
+ ];
113
+
114
+ /** JSON-Schema keywords a compact `inputSchema` MUST NOT use (RFC 0112 compact
115
+ * structural subset). */
116
+ export const COMPACT_INPUT_SCHEMA_BANNED = [
117
+ '$ref',
118
+ 'oneOf',
119
+ 'allOf',
120
+ 'anyOf',
121
+ 'not',
122
+ 'patternProperties',
123
+ 'dependentSchemas',
124
+ ];
125
+
126
+ /** Schema-bearing keywords whose VALUES are subschemas the compact subset still
127
+ * permits (object/array nesting). We recurse into these — but NOT into property
128
+ * *names* — so a tool field literally named `oneOf` is not a false positive. */
129
+ const COMPACT_SUBSCHEMA_KEYWORDS = ['items', 'additionalProperties', 'contains', 'propertyNames'];
130
+
131
+ /** Recursively searches a compact `inputSchema` for any banned keyword in SCHEMA
132
+ * position at ANY nesting depth (RFC 0112's structural subset is total, not just
133
+ * top-level — a nested `oneOf`/`$ref` is exactly the verbosity the compact view
134
+ * exists to drop). Schema-aware: it checks keywords in schema position and
135
+ * recurses only into subschema-bearing positions (`properties` values, `items`,
136
+ * `additionalProperties`, `prefixItems`, …), never treating a property NAME as a
137
+ * keyword. Returns the first offending keyword, or null when clean. */
138
+ export function findBannedInputSchemaKeyword(schema: unknown): string | null {
139
+ if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return null;
140
+ const obj = schema as Record<string, unknown>;
141
+ for (const kw of COMPACT_INPUT_SCHEMA_BANNED) {
142
+ if (kw in obj) return kw;
143
+ }
144
+ const props = obj.properties;
145
+ if (props && typeof props === 'object' && !Array.isArray(props)) {
146
+ for (const sub of Object.values(props as Record<string, unknown>)) {
147
+ const hit = findBannedInputSchemaKeyword(sub);
148
+ if (hit) return hit;
149
+ }
150
+ }
151
+ for (const key of COMPACT_SUBSCHEMA_KEYWORDS) {
152
+ const hit = findBannedInputSchemaKeyword(obj[key]);
153
+ if (hit) return hit;
154
+ }
155
+ if (Array.isArray(obj.prefixItems)) {
156
+ for (const sub of obj.prefixItems) {
157
+ const hit = findBannedInputSchemaKeyword(sub);
158
+ if (hit) return hit;
159
+ }
160
+ }
161
+ return null;
162
+ }
163
+
75
164
  /** The closed tool-source vocabulary (RFC 0078 §C). */
76
165
  export const TOOL_SOURCES = ['node-pack', 'workflow', 'mcp', 'connector', 'host-extension'];
77
166
  /** The closed safety-tier vocabulary (RFC 0078 §C). */