@openwop/openwop-conformance 1.53.1 → 1.57.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.
Files changed (32) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +2 -2
  3. package/api/openapi.yaml +9 -0
  4. package/coverage.md +3 -0
  5. package/package.json +1 -1
  6. package/schemas/README.md +1 -0
  7. package/schemas/agent-manifest.schema.json +23 -1
  8. package/schemas/capabilities.schema.json +80 -3
  9. package/schemas/connection-pack-manifest.schema.json +5 -0
  10. package/schemas/frontend-plugin-manifest.schema.json +9 -3
  11. package/schemas/residency.schema.json +16 -0
  12. package/schemas/run-snapshot.schema.json +6 -1
  13. package/schemas/workflow-chain-pack-manifest.schema.json +85 -6
  14. package/schemas/workflow-definition.schema.json +4 -3
  15. package/src/lib/anonymousActor.ts +99 -0
  16. package/src/lib/workflow-chain-expansion.ts +342 -0
  17. package/src/scenarios/agent-manifest-role-profile.test.ts +116 -0
  18. package/src/scenarios/anonymous-actor-audit-opaque.test.ts +71 -0
  19. package/src/scenarios/anonymous-actor-default-deny.test.ts +87 -0
  20. package/src/scenarios/anonymous-actor-egress-guarded.test.ts +54 -0
  21. package/src/scenarios/anonymous-actor-no-secret-reach.test.ts +78 -0
  22. package/src/scenarios/anonymous-actor-shape.test.ts +173 -0
  23. package/src/scenarios/anonymous-actor-write-gated.test.ts +83 -0
  24. package/src/scenarios/chain-produced-var-roundtrip.test.ts +152 -0
  25. package/src/scenarios/chain-subchain-cycle-rejected.test.ts +141 -0
  26. package/src/scenarios/chain-subchain-fanout.test.ts +139 -0
  27. package/src/scenarios/chain-subchain-sibling.test.ts +147 -0
  28. package/src/scenarios/chain-subchain-unsupported-refused.test.ts +66 -0
  29. package/src/scenarios/connection-pack-manifest-valid.test.ts +33 -0
  30. package/src/scenarios/data-residency-admission.test.ts +138 -0
  31. package/src/scenarios/edge-condition-truthy-falsy.test.ts +104 -0
  32. package/src/scenarios/frontend-plugin-packs.test.ts +24 -0
@@ -57,9 +57,14 @@
57
57
  "items": { "type": "string", "minLength": 1 },
58
58
  "description": "RFC 0092. Host-capability keys this agent needs to run fully — the agent-layer analogue of a node-pack's `peerDependencies`. Dotted identifiers from the discovery vocabulary (`capabilities.md` / `host-capabilities.md`), e.g. `host.workspace`, `aiProviders.toolCalling`, `multiAgent.executionModel.verifier`. A host that does NOT advertise a listed key MUST surface this agent as degraded on `GET /v1/agents` via the existing `degraded[]` inventory field (RFC 0072 §C); it MAY still dispatch at the RFC 0070 floor, but a silent satisfied-looking entry is non-conformant. Advisory about NEED only — never widens the agent's authority. Absent ⇒ no declared requirements."
59
59
  },
60
+ "role": {
61
+ "type": "string",
62
+ "enum": ["skill", "assistant"],
63
+ "description": "RFC 0131. The manifest's kind — EXPLICIT, never inferred from other fields (`handoff` presence does NOT imply `skill`; an assistant may carry a typed entry contract for cross-host interop). `skill`: a composable, task-scoped capability invoked via `handoff` (task→return) and composed by an assistant/roster agent — stateless and replay-clean; declaring this value OPTS IN to the Skill profile (the `allOf` conditional below: MUST declare `handoff`, MUST constrain `memoryShape` to scratchpad-only). `assistant`: a top-level conversational agent a client talks to and that composes skills — no profile binds it. OPTIONAL; when ABSENT the manifest is UNCONSTRAINED — no profile binds it and its meaning is exactly today's (byte- and behavior-identical). Advisory about kind only — never widens `toolAllowlist`, `requiresCapabilities`, or any scope. See `node-packs.md` §`agents[]` (the field) and `agent-memory.md` §B (the memoryShape constraint + the §C-vs-§B reject/degrade contrast)."
64
+ },
60
65
  "memoryShape": {
61
66
  "type": "object",
62
- "description": "Declares which memory backends the agent reads/writes. Hosts that advertise `capabilities.agents.memoryBackends` MAY filter manifests during install based on these flags. The full memory-shape contract lands in RFC 0004 (Phase 3); RFC 0003 only declares the descriptor field and reserves `longTerm: true` as the redaction-harness trigger.",
67
+ "description": "Declares which memory backends the agent reads/writes. Hosts that advertise `capabilities.agents.memoryBackends` MAY filter manifests during install based on these flags. The full memory-shape contract lands in RFC 0004 (Phase 3); RFC 0003 only declares the descriptor field and reserves `longTerm: true` as the redaction-harness trigger. RFC 0131: a `role: \"skill\"` manifest MUST constrain `conversation`/`longTerm` to `false`/absent (the Skill profile — persistent + multi-turn memory belong to the composing assistant/roster agent, RFC 0039; a stateful worker undermines replay determinism, RFC 0041).",
63
68
  "additionalProperties": false,
64
69
  "properties": {
65
70
  "scratchpad": {
@@ -130,5 +135,22 @@
130
135
  "oneOf": [
131
136
  { "required": ["systemPrompt"], "not": { "required": ["systemPromptRef"] } },
132
137
  { "required": ["systemPromptRef"], "not": { "required": ["systemPrompt"] } }
138
+ ],
139
+ "allOf": [
140
+ {
141
+ "$comment": "RFC 0131 §B — the Skill profile. Binds ONLY manifests that opt in with `role: \"skill\"` (absent/`assistant` ⇒ unconstrained). A `role:\"skill\"` manifest MUST declare `handoff` and MUST NOT declare `memoryShape.conversation` or `memoryShape.longTerm` as `true` (scratchpad-only). A violation is a malformed manifest (RFC 0003 §C — an author error caught at publish/install validation), NOT an RFC 0072 §C `degraded[]` runtime tier.",
142
+ "if": { "properties": { "role": { "const": "skill" } }, "required": ["role"] },
143
+ "then": {
144
+ "required": ["handoff"],
145
+ "properties": {
146
+ "memoryShape": {
147
+ "properties": {
148
+ "conversation": { "const": false },
149
+ "longTerm": { "const": false }
150
+ }
151
+ }
152
+ }
153
+ }
154
+ }
133
155
  ]
134
156
  }
@@ -486,6 +486,65 @@
486
486
  }
487
487
  }
488
488
  },
489
+ "dataResidency": {
490
+ "type": "object",
491
+ "additionalProperties": false,
492
+ "description": "RFC 0129 (`Active`). Data-residency — the host advertises the operator region codes it can pin a run to, and honors-or-rejects an OPTIONAL `residency.region` on run-creation (`POST /v1/runs`): accept iff the requested region is in `regions[]`, else reject `residency_unavailable` (never silently accept-and-ignore). This family advertises the ADMISSION decision only (falsifiable over the wire) — it does NOT assert that the run's data physically remains in-region (unobservable; declared operator intent under RFC 0129 §4, not conformance-gated). A host MUST advertise a region in `regions[]` only if it can actually process a run entirely within it. Absent block ⇒ the host makes no residency promise and MAY ignore or reject a `residency` constraint (but MUST NOT claim to honor it).",
493
+ "required": ["supported", "regions"],
494
+ "properties": {
495
+ "supported": {
496
+ "const": true,
497
+ "description": "RFC 0129. Present-and-true when the host performs region admission control per §3. The block is omitted entirely when the host offers no residency pinning."
498
+ },
499
+ "regions": {
500
+ "type": "array",
501
+ "items": { "type": "string", "minLength": 1 },
502
+ "uniqueItems": true,
503
+ "description": "RFC 0129. Opaque operator region codes the host can pin a run to (e.g. `\"eu\"`, `\"us\"`, `\"eu-west-1\"`). No closed registry — codes are the host's own vocabulary (a global region enum would be unfalsifiable). A host MUST advertise a code here only if it can process a run entirely within it; a run-create `residency.region` not in this list MUST be rejected `residency_unavailable`."
504
+ }
505
+ }
506
+ },
507
+ "anonymousActor": {
508
+ "type": "object",
509
+ "additionalProperties": false,
510
+ "description": "RFC 0132 (`Active`). Anonymous-actor authorization for public agent surfaces — an opaque, origin-bound, ephemeral, non-cross-linkable, non-PII principal (`run-snapshot.owner.principalKind: \"anonymous\"`) whose authority is a DEFAULT-DENY, explicit per-surface tool grant, never the default-on tool baseline (§C.1). Two tiers: `read` (tenant-scoped, no egress, no secret/BYOK reach — §C.2) and `bounded-write-egress` (writes/egress permitted ONLY behind a mandatory HITL/approval gate (RFC 0051) OR a hard rate-limit + per-session cap, over the RFC 0076 §B / RFC 0079 SSRF-guarded audience-bound egress path, never attaching a tenant credential out-of-audience — §C.3). Every anon tool call emits `authorization.decided` (RFC 0049) attributable to the opaque session id, carrying no PII/credential (§D). The block is OMITTED ENTIRELY when unsupported (const-true `supported`, `dataResidency`-style — never `supported: false`). Absent ⇒ the host runs no tool-enabled public surface under RFC 0132 (e.g. a no-tools single-turn public gateway needs no advertisement).",
511
+ "required": ["supported", "tiers"],
512
+ "properties": {
513
+ "supported": {
514
+ "const": true,
515
+ "description": "RFC 0132. Present-and-true when the host honors anonymous-actor authorization per §C. The block is omitted entirely when the host runs no tool-enabled public surface (never `supported: false`)."
516
+ },
517
+ "tiers": {
518
+ "type": "array",
519
+ "minItems": 1,
520
+ "uniqueItems": true,
521
+ "items": { "enum": ["read", "bounded-write-egress"] },
522
+ "description": "RFC 0132 §B. The anon capability tiers this host BEHAVIORALLY honors (truthful-advertisement — a tier the host does not enforce per §C is a dishonest claim; `OPENWOP_REQUIRE_BEHAVIOR=true` MUST fail it). `read`: tenant-scoped reads, no egress, no secrets (§C.2). `bounded-write-egress`: writes/egress ONLY behind a §C.3 control."
523
+ },
524
+ "writeEgressControls": {
525
+ "type": "array",
526
+ "minItems": 1,
527
+ "uniqueItems": true,
528
+ "items": { "enum": ["hitl", "rate-limit-session-cap"] },
529
+ "description": "RFC 0132 §B. REQUIRED and non-empty IFF `bounded-write-egress` ∈ `tiers`; MUST be absent otherwise. The mandatory control(s) the host enforces before an anon write/egress: `hitl` = a per-action approval gate (RFC 0051 — the action suspends pending a human decision); `rate-limit-session-cap` = a hard per-IP/per-window rate limit AND a per-session action cap. A `bounded-write-egress` tier with no advertised control is the fail-open shape §B.2 forbids."
530
+ },
531
+ "failClosed": {
532
+ "const": true,
533
+ "description": "RFC 0132. An anon tool call whose grant is absent, unresolvable, or errors MUST deny (never default-allow). There is no fail-open anonymous mode. Mirrors `capabilities.authorization.failClosed`."
534
+ }
535
+ },
536
+ "allOf": [
537
+ {
538
+ "$comment": "RFC 0132 §B.2 — writeEgressControls is REQUIRED and non-empty when bounded-write-egress is advertised, and MUST be absent otherwise (a control-less write/egress tier is the fail-open shape this RFC forbids).",
539
+ "if": {
540
+ "properties": { "tiers": { "contains": { "const": "bounded-write-egress" } } },
541
+ "required": ["tiers"]
542
+ },
543
+ "then": { "required": ["writeEgressControls"] },
544
+ "else": { "not": { "required": ["writeEgressControls"] } }
545
+ }
546
+ ]
547
+ },
489
548
  "credentials": {
490
549
  "type": "object",
491
550
  "description": "RFC 0046 (`Draft`). Portable credential resolution + lifecycle contract — sibling to `secrets`, first-class store-at-rest + workspace sharing + two-key-overlap rotation. A pack references a credential by `{ ref, scope }` (see `credential-reference.schema.json`); the host resolves it into the node sandbox ONLY — never into inputs, persisted variables, channels, any run.* event payload, the debug bundle, or replay state (SECURITY invariant `credential-payload-redaction`). Supersedes the informal BYOK annex; the `secrets` advertisement stays valid.",
@@ -1793,14 +1852,14 @@
1793
1852
  },
1794
1853
  "surfaces": {
1795
1854
  "type": "array",
1796
- "description": "Plugin surfaces this host renders. A pack's `uiPlugins[].surface` not in this set is installable-but-inert (§Degradation).",
1797
- "items": { "type": "string", "enum": ["artifact-viewer", "route", "settings-panel"] },
1855
+ "description": "Plugin surfaces this host renders. A pack's `uiPlugins[].surface` not in this set is installable-but-inert (§Degradation). `canvas-preview` (RFC 0130) mounts inside a host-owned canvas editor.",
1856
+ "items": { "type": "string", "enum": ["artifact-viewer", "route", "settings-panel", "canvas-preview"] },
1798
1857
  "uniqueItems": true
1799
1858
  },
1800
1859
  "hostApi": {
1801
1860
  "type": "array",
1802
1861
  "description": "The `ui-plugin/1` host-RPC methods this host honors. A plugin call to a method not in this set (regardless of the plugin's declared `hostApi`) MUST be rejected with `method_not_allowed` (`frontend-plugin-rpc-allowlist`). A host advertising `artifact.write` MUST enforce the `version`-token optimistic concurrency (RFC 0117 §Concurrency).",
1803
- "items": { "type": "string", "enum": ["artifact.read", "artifact.write", "host.toast", "host.navigate"] },
1862
+ "items": { "type": "string", "enum": ["artifact.read", "artifact.write", "host.toast", "host.navigate", "host.announce"] },
1804
1863
  "uniqueItems": true
1805
1864
  },
1806
1865
  "maxEntryBytes": { "type": "integer", "minimum": 1, "description": "Per-plugin entry-bundle byte ceiling the host will load." }
@@ -1901,6 +1960,24 @@
1901
1960
  "hostExpansionSeam": {
1902
1961
  "type": "boolean",
1903
1962
  "description": "RFC 0013 erratum (2026-07-05). OPTIONAL. A **conformance-only test seam** advertisement (category: test harness, cf. `observability.testSeams` — NOT a product capability): when `true`, the host serves `POST /v1/host/sample/workflow-chain:expand` returning the `vendor.openwop.workflow-chain-sample` v1.0.0 expansion that `conformance/src/scenarios/workflow-chain-host-expansion.test.ts` asserts against. Absent/`false` ⇒ that live-host expansion scenario soft-skips; the semantic `workflowChainPacks.supported` claim is witnessed by the server-free `workflow-chain-expansion.test.ts` legs. This flag is DISTINCT from `deferredParameters` (RFC 0124), which is witnessed through its own `POST /v1/host/sample/chain/deferred-expand` seam — so a host MAY advertise `supported` / `deferredParameters.supported` without standing up the RFC 0013 sample-pack seam it was never handed a published fixture for. Advertising `supported:true` no longer conscripts a host into the RFC 0013 host-expansion scenario."
1963
+ },
1964
+ "subChains": {
1965
+ "type": "object",
1966
+ "description": "RFC 0133 (workflow-chain composition, `Accepted`). OPTIONAL. When `supported: true`, the host's `POST …/workflows/from-chain` implements RUNTIME sub-chain composition per `workflow-chain-packs.md` §\"Sub-chain composition (RFC 0133)\": for each `config.subChainRef` reachable from a parent chain, it resolves the referenced chain (sibling or external), recursively expands + co-registers it as its own owned workflow (deterministic TENANT-SCOPED id from `(tenantId, childChainId, version)` — two tenants never collide on the global registry, a shared child registers once across parents in a tenant, a repeat instantiation converges), rewrites the referencing node's `subChainRef` → the minted child `config.workflowId`, and dispatches the child as a child run (`core.subWorkflow` / `core.dispatch` child-run); the `from-chain` response carries `{ workflowId, subChainWorkflowIds[], nodeCount }`. Bounded by a cycle check (`sub_chain_cycle`) + a depth cap (`sub_chain_max_depth_exceeded`). A host that does NOT advertise this block MUST refuse a `subChains`-bearing chain at author/instantiate time with `sub_chain_unsupported` (422) — it never silently flattens. `producedVariables` (RFC 0133 §2) needs NO flag: it is pure variable emission under the base `workflowChainPacks.supported`. Conformance scenarios `chain-subchain-fanout.test.ts` + `chain-subchain-unsupported-refused.test.ts` gate on this flag; the server-free `chain-subchain-sibling` / `chain-subchain-cycle-rejected` / `chain-produced-var-roundtrip` legs run unconditionally against the reference library.",
1967
+ "properties": {
1968
+ "supported": {
1969
+ "type": "boolean",
1970
+ "description": "Whether the host implements runtime sub-chain co-registration + child dispatch. `false` (or omission of this block) signals the host does NOT compose sub-chains and MUST refuse a `subChains`-bearing chain with `sub_chain_unsupported`."
1971
+ },
1972
+ "maxDepth": {
1973
+ "type": "integer",
1974
+ "minimum": 1,
1975
+ "default": 8,
1976
+ "description": "RFC 0133 §1.3 (resolves UQ3). RECOMMENDED default 8. The maximum sub-chain nesting depth the host co-expands; exceeding it fails closed with the DISTINCT code `sub_chain_max_depth_exceeded` (the DoS depth backstop, alongside the `sub_chain_cycle` self-composition check — SECURITY `sub-chain-expansion-bounded`)."
1977
+ }
1978
+ },
1979
+ "required": ["supported"],
1980
+ "additionalProperties": false
1904
1981
  }
1905
1982
  },
1906
1983
  "required": ["supported"],
@@ -65,6 +65,11 @@
65
65
  "type": "string",
66
66
  "enum": ["communication", "docs", "crm", "dev", "storage", "email-calendar", "ticketing", "data-warehouse", "marketing", "finance", "hr", "esignature", "support", "project-management", "payments", "other"]
67
67
  },
68
+ "vendor": {
69
+ "type": "string",
70
+ "minLength": 1,
71
+ "description": "RFC 0123. OPTIONAL. The commercial vendor / ecosystem this connector belongs to (e.g. \"Microsoft 365\", \"Google\", \"Workday\"). Presentational grouping ONLY — it is NOT the resolution key (RFC 0047 `provider` still resolves auth). Free-form (not an enum) so a new vendor needs no schema change; a host/registry groups connectors sharing a `vendor` under one heading and MUST fall back to the provider `displayName` when it is absent."
72
+ },
68
73
  "auth": {
69
74
  "type": "object",
70
75
  "required": ["kind"],
@@ -64,8 +64,8 @@
64
64
  },
65
65
  "surface": {
66
66
  "type": "string",
67
- "description": "Which host surface this plugin renders into. `artifact-viewer` renders a typed artifact (RFC 0071) in a sandbox; `route` mounts a standalone admin/tool page; `settings-panel` contributes a configuration panel. A host degrades a `surface` it does not advertise to nothing (§Degradation).",
68
- "enum": ["artifact-viewer", "route", "settings-panel"]
67
+ "description": "Which host surface this plugin renders into. `artifact-viewer` renders a typed artifact (RFC 0071) in a sandbox; `route` mounts a standalone admin/tool page; `settings-panel` contributes a configuration panel; `canvas-preview` renders the live document preview inside a host-owned canvas editor (RFC 0130). A host degrades a `surface` it does not advertise to nothing (§Degradation).",
68
+ "enum": ["artifact-viewer", "route", "settings-panel", "canvas-preview"]
69
69
  },
70
70
  "entry": {
71
71
  "type": "string",
@@ -77,10 +77,16 @@
77
77
  "description": "The closed allowlist of `ui-plugin/1` host-RPC methods this plugin is permitted to call. The host MUST reject (with an `error` response, never silent execution) any method not in BOTH this declared list AND the host's `capabilities.uiPlugins.hostApi` set (`frontend-plugin-rpc-allowlist`). The plugin holds no credentials; every method is host-mediated and authz-checked.",
78
78
  "items": {
79
79
  "type": "string",
80
- "enum": ["artifact.read", "artifact.write", "host.toast", "host.navigate"]
80
+ "enum": ["artifact.read", "artifact.write", "host.toast", "host.navigate", "host.announce"]
81
81
  },
82
82
  "uniqueItems": true
83
83
  },
84
+ "canvasTypes": {
85
+ "type": "array",
86
+ "description": "OPTIONAL; meaningful only for `surface: \"canvas-preview\"` (RFC 0130): the host canvas type id(s) this preview renders. A host mounts the plugin only for matching canvas documents and MUST ignore entries it does not recognize; absent/empty means never auto-mounted.",
87
+ "items": { "type": "string", "maxLength": 128 },
88
+ "uniqueItems": true
89
+ },
84
90
  "connectSrc": {
85
91
  "type": "array",
86
92
  "description": "OPTIONAL explicit `connect-src` CSP exceptions the plugin needs (e.g., a font CDN). Absent → the host serves the plugin under a DENY-EGRESS CSP (`frontend-plugin-egress`): no network egress beyond the host-RPC channel. Any entry here is part of the front-end-review checkpoint (`registry-operations.md`) a reviewer audits; a host MAY refuse a pack whose exceptions it will not grant.",
@@ -0,0 +1,16 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://openwop.dev/spec/v1/residency.schema.json",
4
+ "title": "Residency",
5
+ "description": "RFC 0129 — an OPTIONAL data-residency constraint on a run-creation request (`POST /v1/runs`). A host advertising `capabilities.dataResidency` MUST honor-or-reject: accept iff `region` is in the advertised `dataResidency.regions[]`, else reject `residency_unavailable` (HTTP one-of 400/404/422) and create no run — it MUST NOT silently accept-and-ignore. Absent ⇒ no constraint (host default region). Physical byte-confinement of the accepted run is a declared operator SHOULD (RFC 0129 §4), not a wire guarantee.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["region"],
9
+ "properties": {
10
+ "region": {
11
+ "type": "string",
12
+ "minLength": 1,
13
+ "description": "Requested processing region — an opaque operator code from the host's own vocabulary (e.g. `\"eu\"`, `\"us\"`, `\"eu-west-1\"`). Honored only if present in the host's advertised `capabilities.dataResidency.regions[]`; otherwise the run is rejected `residency_unavailable`."
14
+ }
15
+ }
16
+ }
@@ -40,7 +40,12 @@
40
40
  "properties": {
41
41
  "tenant": { "type": "string", "minLength": 1, "description": "Top-level isolation boundary." },
42
42
  "workspace": { "type": "string", "minLength": 1, "description": "Optional sub-tenant within the tenant (RFC 0048 workspace)." },
43
- "principal": { "type": "string", "minLength": 1, "description": "Acting identity (user or agent) — opaque id, never PII." }
43
+ "principal": { "type": "string", "minLength": 1, "description": "Acting identity (user or agent) — opaque id, never PII." },
44
+ "principalKind": {
45
+ "type": "string",
46
+ "enum": ["user", "agent", "anonymous"],
47
+ "description": "RFC 0132. The kind of the acting principal. OPTIONAL and EXPLICIT — absent ⇒ unconstrained (today's RFC 0048 behavior; a host that does not distinguish kinds omits it). `anonymous` ⇒ the RFC 0132 §A anonymous-actor rules and the §C default-deny per-surface grant bind: the `principal` id MUST be opaque, origin-bound, ephemeral, non-cross-linkable, non-PII, and never inherit a role or a default-on tool baseline. A host that advertises `capabilities.anonymousActor` and dispatches through a public surface MUST set `anonymous` on the resulting run. Resolves RFC 0048 §Unresolved-Q1 for the anonymous case."
48
+ }
44
49
  },
45
50
  "additionalProperties": false
46
51
  },
@@ -106,6 +106,76 @@
106
106
  },
107
107
  "uniqueItems": true,
108
108
  "description": "Capability traits to propagate to every expanded node. Hosts MUST copy this array into each expanded `WorkflowNode.capabilities` so existing capability gates apply uniformly."
109
+ },
110
+ "subChains": {
111
+ "type": "array",
112
+ "items": { "$ref": "#/$defs/SubChainRef" },
113
+ "description": "RFC 0133 §1.1. OPTIONAL. Child chains this chain composes at RUN TIME. Each entry names a chain (a sibling `chainId` in this pack, or an externally published chain) that a node references via `config.subChainRef`. Unlike RFC 0013's author-time inline splice, a referenced sub-chain is co-instantiated as its own registered workflow and dispatched by the parent as a child run (both parent and child stay owned + builder-editable). A host without runtime child dispatch MUST refuse instantiation with `sub_chain_unsupported` (422) rather than flatten. Requires `capabilities.workflowChainPacks.subChains.supported: true`. See `workflow-chain-packs.md` §\"Sub-chain composition (RFC 0133)\"."
114
+ },
115
+ "producedVariables": {
116
+ "type": "array",
117
+ "items": { "$ref": "#/$defs/ProducedVariable" },
118
+ "description": "RFC 0133 §2.2. OPTIONAL. Run-scoped values a node writes to the executor's variable bag during the run and downstream nodes read by name via a `{ type:\"variable\", variableName }` input binding — where the value is NOT an author-time `parameter` and rides no typed output port. Prefer edges (a typed `sourceOutput`→`targetInput` hand-off) wherever a node exposes a value on a port; `producedVariables` is only for values written to the run bag with no typed port. On expansion the host emits these into `WorkflowDefinition.variables[]` as run-scoped entries (name + type, no value). Any `{ type:\"variable\" }` read of an UNDECLARED name (not here and not a materialized parameter) is a `variable_undeclared` manifest error. See `workflow-chain-packs.md` §\"Produced (run-scoped) variables (RFC 0133)\"."
119
+ }
120
+ },
121
+ "additionalProperties": false
122
+ },
123
+ "SubChainRef": {
124
+ "type": "object",
125
+ "required": ["ref"],
126
+ "description": "RFC 0133 §1.1. A reference to a child chain the parent composes. `ref` is EITHER a string naming a SIBLING `chainId` in this pack's `chains[]`, OR an object naming an externally published chain (resolved + signature-verified like any pack dependency). A sibling `ref` that matches no `chainId`, or an external `ref` that fails resolution/verification, is `sub_chain_unresolved`. A chain that transitively composes itself is `sub_chain_cycle`.",
127
+ "properties": {
128
+ "ref": {
129
+ "oneOf": [
130
+ {
131
+ "type": "string",
132
+ "description": "A sibling `chainId` in the same pack's `chains[]`.",
133
+ "pattern": "^[a-z][a-zA-Z0-9._-]*$",
134
+ "minLength": 1,
135
+ "maxLength": 256
136
+ },
137
+ {
138
+ "type": "object",
139
+ "required": ["packName", "chainId", "version"],
140
+ "description": "An externally published chain, resolved + signature-verified like any pack dependency.",
141
+ "properties": {
142
+ "packName": { "type": "string", "minLength": 1, "maxLength": 256 },
143
+ "chainId": { "type": "string", "pattern": "^[a-z][a-zA-Z0-9._-]*$", "minLength": 1, "maxLength": 256 },
144
+ "version": {
145
+ "type": "string",
146
+ "description": "Semver range for the external chain. Co-registration SHOULD pin the resolved version into the parent's ownership record for reproducibility (RFC 0133 §Unresolved #1, resolved: pin)."
147
+ }
148
+ },
149
+ "additionalProperties": false
150
+ }
151
+ ]
152
+ }
153
+ },
154
+ "additionalProperties": false
155
+ },
156
+ "ProducedVariable": {
157
+ "type": "object",
158
+ "required": ["name", "producedBy", "type"],
159
+ "description": "RFC 0133 §2.2. A run-scoped variable a chain declares: written by one node and read by others via a `{ type:\"variable\", variableName }` input binding. Run-scoped — carries NO author-time value (that is what `parameters` are for); the manifest distinguishes the two.",
160
+ "properties": {
161
+ "name": {
162
+ "type": "string",
163
+ "minLength": 1,
164
+ "description": "The variable-bag key downstream nodes read via `{ type:\"variable\", variableName:\"<name>\" }`."
165
+ },
166
+ "producedBy": {
167
+ "type": "string",
168
+ "minLength": 1,
169
+ "description": "The `nodes[].id` (within this same fragment) that writes the value."
170
+ },
171
+ "type": {
172
+ "type": "string",
173
+ "enum": ["string", "number", "boolean", "object", "array"],
174
+ "description": "JSON-Schema type token for validation/inspection."
175
+ },
176
+ "description": {
177
+ "type": "string",
178
+ "description": "One-line description of the produced value's meaning."
109
179
  }
110
180
  },
111
181
  "additionalProperties": false
@@ -171,9 +241,17 @@
171
241
  },
172
242
  "config": {
173
243
  "type": "object",
174
- "description": "Node config — host-validated against the referenced typeId's config schema. String fields MAY contain `{{params.<name>}}` placeholders that the host MUST substitute at expansion time.",
244
+ "description": "Node config — host-validated against the referenced typeId's config schema. String fields MAY contain `{{params.<name>}}` placeholders that the host MUST substitute at expansion time. RFC 0133: a `core.subWorkflow` / `core.dispatch` (child-run) node MAY carry `subChainRef: string` — a declared `subChains[].ref` (sibling `chainId` or external ref) the host rewrites to the minted child `workflowId` at co-expansion time (§1.3 step 3). A concrete `config.workflowId` inside a fragment remains INVALID — a chain MUST NOT pin a host-specific workflow id (the `not` guard below enforces it).",
175
245
  "additionalProperties": true,
176
- "$comment": "Open by design — node config shapes are per-typeId and only known to the host at expansion time when the referenced typeId's config schema is resolved. Cross-typeId enforcement happens at the expansion step, not at manifest-validation time."
246
+ "not": { "required": ["workflowId"] },
247
+ "properties": {
248
+ "subChainRef": {
249
+ "type": "string",
250
+ "pattern": "^[a-z][a-zA-Z0-9._-]*$",
251
+ "description": "RFC 0133 §1.2. A declared `subChains[].ref` this node dispatches as a child run. The host rewrites it to the minted child `config.workflowId` at co-expansion time. MUST match a `subChains[].ref` (sibling `chainId` or external ref's `chainId`), else `sub_chain_unresolved`."
252
+ }
253
+ },
254
+ "$comment": "Open by design — node config shapes are per-typeId and only known to the host at expansion time when the referenced typeId's config schema is resolved. Cross-typeId enforcement happens at the expansion step, not at manifest-validation time. The `not: { required: ['workflowId'] }` guard is the ONE closed constraint: RFC 0133 forbids pinning a concrete host-specific workflow id inside a portable chain fragment (use `subChainRef` instead)."
177
255
  },
178
256
  "inputs": {
179
257
  "type": "object",
@@ -211,14 +289,15 @@
211
289
  },
212
290
  "EdgeCondition": {
213
291
  "type": "object",
214
- "description": "Edge condition — identical to workflow-definition.schema.json §EdgeCondition (RFC 0013 §edges: 'same shape as a top-level workflow definition'). Inlined here so the manifest schema is self-contained for pack-loader validators.",
292
+ "description": "Edge condition — identical to workflow-definition.schema.json §EdgeCondition (RFC 0013 §edges: 'same shape as a top-level workflow definition'). Inlined here so the manifest schema is self-contained for pack-loader validators. RFC 0134 added `truthy`/`falsy` (kept in sync with the top-level def).",
215
293
  "properties": {
216
294
  "type": {
217
295
  "type": "string",
218
- "enum": ["expression", "equals", "notEquals", "contains", "regex"]
296
+ "enum": ["expression", "equals", "notEquals", "contains", "regex", "truthy", "falsy"],
297
+ "description": "RFC 0134: `truthy`/`falsy` test the resolved value at `left` for truthiness and take NO `right` operand; `left` is required for all operators except `expression`."
219
298
  },
220
- "left": { "type": "string", "description": "Left operand path (e.g., 'status', 'output.approved')." },
221
- "right": { "description": "Right operand value (any JSON value)." },
299
+ "left": { "type": "string", "description": "Left operand path (e.g., 'status', 'output.approved'). Required for every operator except `expression`." },
300
+ "right": { "description": "Right operand value (any JSON value). Meaningless for `truthy`/`falsy` (RFC 0134)." },
222
301
  "expression": { "type": "string", "description": "Used when type='expression'." }
223
302
  },
224
303
  "additionalProperties": false
@@ -197,10 +197,11 @@
197
197
  "properties": {
198
198
  "type": {
199
199
  "type": "string",
200
- "enum": ["expression", "equals", "notEquals", "contains", "regex"]
200
+ "enum": ["expression", "equals", "notEquals", "contains", "regex", "truthy", "falsy"],
201
+ "description": "Operator. `equals`/`notEquals`/`contains` compare `left` to `right`; `regex` matches `left` against `right`; `expression` evaluates `expression`. RFC 0134: `truthy`/`falsy` test the resolved value at `left` for JS-style truthiness and take NO `right` operand (the edge contributes iff `left` is truthy — resp. falsy/absent). `left` is required for all operators except `expression`."
201
202
  },
202
- "left": { "type": "string", "description": "Left operand path (e.g., 'status', 'output.approved')." },
203
- "right": { "description": "Right operand value (any JSON value)." },
203
+ "left": { "type": "string", "description": "Left operand path (e.g., 'status', 'output.approved'). Required for every operator except `expression`." },
204
+ "right": { "description": "Right operand value (any JSON value). Meaningless for `truthy`/`falsy` (RFC 0134) — a host MUST ignore it there, not error." },
204
205
  "expression": { "type": "string", "description": "Used when type='expression'." }
205
206
  },
206
207
  "additionalProperties": false
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Shared helpers for the RFC 0132 anonymous-actor conformance scenarios.
3
+ *
4
+ * Reads the `capabilities.anonymousActor` discovery family at the document
5
+ * ROOT (RFC 0073) and drives the reference-host public-surface seam the
6
+ * behavioral scenarios probe. Lives in lib/ (not a *.test.ts) so the
7
+ * scenarios import it via `../lib/anonymousActor.js`.
8
+ *
9
+ * The seam is a HOST-EXTENSION sample surface (`/v1/host/sample/anon-surface/…`)
10
+ * — NOT a normative protocol endpoint. RFC 0132 pins the wire-observable
11
+ * contract (the capability advert, `owner.principalKind`, the default-deny
12
+ * grant, the audit reuse of `authorization.decided`); the surface→tool
13
+ * binding + the seam route are host-owned config. The behavioral scenarios
14
+ * soft-skip when the seam is unwired (404), so a host that has not yet landed
15
+ * tool-enabled public dispatch stays green.
16
+ *
17
+ * @see spec/v1/capabilities.md §"anonymousActor"
18
+ * @see RFCS/0132-anonymous-actor-authorization.md
19
+ */
20
+ import { driver } from './driver.js';
21
+ import { readCapabilityFamily } from './discovery-capabilities.js';
22
+
23
+ export interface AnonymousActorCap {
24
+ supported?: boolean;
25
+ tiers?: string[];
26
+ writeEgressControls?: string[];
27
+ failClosed?: boolean;
28
+ }
29
+
30
+ /** The reference-host sample public-surface seam (host-extension, not normative). */
31
+ export const ANON_SEAM = '/v1/host/sample/anon-surface';
32
+ /** A sample public surface id the seam resolves to a tenant + explicit grant. */
33
+ export const ANON_SURFACE = 'sample-public-widget';
34
+
35
+ /** Reads `capabilities.anonymousActor` from discovery root; null when unadvertised. */
36
+ export async function readAnonymousActorCap(): Promise<AnonymousActorCap | null> {
37
+ const fam = await readCapabilityFamily<AnonymousActorCap>('anonymousActor');
38
+ return fam && typeof fam === 'object' ? fam : null;
39
+ }
40
+
41
+ /** True when the host advertises the anonymous-actor family. */
42
+ export async function isAnonymousActorAdvertised(): Promise<boolean> {
43
+ const cap = await readAnonymousActorCap();
44
+ return cap?.supported === true;
45
+ }
46
+
47
+ /** The body the reference seam returns for a single anon tool dispatch. */
48
+ export interface AnonDispatchBody {
49
+ authorizationDecided?: {
50
+ event?: string;
51
+ payload?: {
52
+ principal?: string;
53
+ action?: string;
54
+ resource?: string;
55
+ allowed?: boolean;
56
+ reason?: string;
57
+ };
58
+ };
59
+ owner?: { tenant?: string; principal?: string; principalKind?: string };
60
+ egressDecided?: { decision?: string; reason?: string; credentialAttached?: boolean };
61
+ interrupt?: { kind?: string };
62
+ result?: unknown;
63
+ raw?: string;
64
+ }
65
+
66
+ export interface AnonDispatchResult {
67
+ status: number;
68
+ json: AnonDispatchBody | undefined;
69
+ }
70
+
71
+ /**
72
+ * Drive one anonymous-actor tool dispatch against the reference seam.
73
+ * `surface` binds the tenant + explicit grant; `tool` is the tool the anon
74
+ * session attempts. Returns the raw status so callers can soft-skip on 404
75
+ * (seam unwired).
76
+ */
77
+ export async function anonDispatch(body: {
78
+ surface?: string;
79
+ tool: string;
80
+ args?: Record<string, unknown>;
81
+ destination?: string;
82
+ }): Promise<AnonDispatchResult> {
83
+ const res = await driver.post(`${ANON_SEAM}/dispatch`, {
84
+ surface: body.surface ?? ANON_SURFACE,
85
+ tool: body.tool,
86
+ ...(body.args ? { args: body.args } : {}),
87
+ ...(body.destination ? { destination: body.destination } : {}),
88
+ });
89
+ return { status: res.status, json: res.json as AnonDispatchBody | undefined };
90
+ }
91
+
92
+ /** Read the RFC 0078 tool catalog scoped to an anon session on `surface`. */
93
+ export async function anonToolCatalog(
94
+ surface: string = ANON_SURFACE,
95
+ ): Promise<{ status: number; tools: Array<{ name?: string }> }> {
96
+ const res = await driver.get(`${ANON_SEAM}/tools?surface=${encodeURIComponent(surface)}`);
97
+ const body = res.json as { tools?: Array<{ name?: string }> } | undefined;
98
+ return { status: res.status, tools: Array.isArray(body?.tools) ? body!.tools! : [] };
99
+ }