@openwop/openwop-conformance 1.5.0 → 1.6.1

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 (72) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +2 -2
  3. package/api/asyncapi.yaml +25 -4
  4. package/api/openapi.yaml +371 -0
  5. package/coverage.md +31 -4
  6. package/fixtures/conformance-phase4-nondet-tool.json +53 -0
  7. package/fixtures/conformance-phase4-replay-divergence.json +40 -0
  8. package/fixtures.md +5 -3
  9. package/package.json +1 -1
  10. package/schemas/README.md +4 -0
  11. package/schemas/annotation-create.schema.json +37 -0
  12. package/schemas/annotation.schema.json +56 -0
  13. package/schemas/capabilities.schema.json +191 -3
  14. package/schemas/credential-reference.schema.json +21 -0
  15. package/schemas/node-pack-manifest.schema.json +112 -1
  16. package/schemas/run-diff-response.schema.json +64 -0
  17. package/schemas/run-event-payloads.schema.json +104 -2
  18. package/schemas/run-event.schema.json +8 -1
  19. package/schemas/run-snapshot.schema.json +11 -0
  20. package/src/lib/behavior-gate.ts +51 -0
  21. package/src/lib/driver.ts +13 -1
  22. package/src/lib/feedback.ts +31 -0
  23. package/src/lib/saml-idp.ts +179 -0
  24. package/src/scenarios/approval-gate-events.test.ts +61 -0
  25. package/src/scenarios/approval-gate-flow.test.ts +68 -0
  26. package/src/scenarios/auth-saml-profile.test.ts +119 -0
  27. package/src/scenarios/auth-scim-profile.test.ts +65 -0
  28. package/src/scenarios/authorization-fail-closed.test.ts +80 -0
  29. package/src/scenarios/authorization-roles-shape.test.ts +83 -0
  30. package/src/scenarios/connector-manifest-validity.test.ts +142 -0
  31. package/src/scenarios/credential-payload-redaction.test.ts +93 -0
  32. package/src/scenarios/credentials-capability-shape.test.ts +90 -0
  33. package/src/scenarios/cross-engine-append-behavior.test.ts +204 -0
  34. package/src/scenarios/cross-host-traceparent-propagation.test.ts +13 -6
  35. package/src/scenarios/cross-workspace-isolation.test.ts +72 -0
  36. package/src/scenarios/deadletter-capability-shape.test.ts +59 -0
  37. package/src/scenarios/deadletter-retry-exhaustion.test.ts +62 -0
  38. package/src/scenarios/experimental-tier-shape.test.ts +192 -0
  39. package/src/scenarios/feedback-capability-shape.test.ts +35 -0
  40. package/src/scenarios/feedback-correction-redaction.test.ts +35 -0
  41. package/src/scenarios/feedback-cross-tenant-isolation.test.ts +37 -0
  42. package/src/scenarios/feedback-fork-not-copied.test.ts +40 -0
  43. package/src/scenarios/feedback-on-terminal-run.test.ts +32 -0
  44. package/src/scenarios/feedback-record-and-list.test.ts +32 -0
  45. package/src/scenarios/feedback-unsupported-501.test.ts +32 -0
  46. package/src/scenarios/identity-owner-shape.test.ts +64 -0
  47. package/src/scenarios/multi-agent-confidence-escalation.test.ts +13 -12
  48. package/src/scenarios/multi-agent-memory-lifecycle.test.ts +87 -12
  49. package/src/scenarios/multi-region-idempotency-behavior.test.ts +203 -0
  50. package/src/scenarios/oauth-capability-shape.test.ts +97 -0
  51. package/src/scenarios/oauth-connector-redaction.test.ts +91 -0
  52. package/src/scenarios/pack-registry-isolation.test.ts +108 -0
  53. package/src/scenarios/pack-registry-publish.test.ts +1 -1
  54. package/src/scenarios/prompt-mutation-workspace-membership-enforced.test.ts +126 -0
  55. package/src/scenarios/prompt-read-workspace-membership-enforced.test.ts +183 -0
  56. package/src/scenarios/redaction.test.ts +4 -1
  57. package/src/scenarios/replay-divergence-at-refusal.test.ts +187 -7
  58. package/src/scenarios/replay-observable-sequence-determinism.test.ts +20 -6
  59. package/src/scenarios/run-diff.test.ts +143 -0
  60. package/src/scenarios/sandbox-capability-gate-respected.test.ts +7 -1
  61. package/src/scenarios/sandbox-memory-cap.test.ts +7 -5
  62. package/src/scenarios/sandbox-mvp-behavior.test.ts +280 -0
  63. package/src/scenarios/sandbox-no-cross-pack-mutation.test.ts +7 -1
  64. package/src/scenarios/sandbox-no-host-env-leak.test.ts +5 -1
  65. package/src/scenarios/sandbox-no-host-fs-escape.test.ts +9 -1
  66. package/src/scenarios/sandbox-no-host-process-escape.test.ts +5 -1
  67. package/src/scenarios/sandbox-no-network-escape.test.ts +5 -1
  68. package/src/scenarios/sandbox-timeout-cap.test.ts +7 -5
  69. package/src/scenarios/scheduling-capability-shape.test.ts +81 -0
  70. package/src/scenarios/scheduling-cron-fires-once.test.ts +66 -0
  71. package/src/scenarios/secret-leakage-otel-attribute.test.ts +241 -0
  72. package/src/scenarios/spec-corpus-validity.test.ts +6 -3
@@ -370,10 +370,10 @@
370
370
  "type": "array",
371
371
  "items": {
372
372
  "type": "string",
373
- "enum": ["tenant", "user", "run"]
373
+ "enum": ["tenant", "user", "run", "workspace"]
374
374
  },
375
375
  "uniqueItems": true,
376
- "description": "Subset of scopes the host implements. Tenant-scoped secrets are workspace-shared; user-scoped are per-end-user; run-scoped are ephemeral per-run."
376
+ "description": "Subset of scopes the host implements. Tenant-scoped secrets are workspace-shared; user-scoped are per-end-user; run-scoped are ephemeral per-run; `workspace` (RFC 0046/0048) is the explicit sub-tenant scope. Appended `workspace` is additive — hosts that omit it are unaffected."
377
377
  },
378
378
  "resolution": {
379
379
  "type": "string",
@@ -383,6 +383,114 @@
383
383
  },
384
384
  "additionalProperties": false
385
385
  },
386
+ "credentials": {
387
+ "type": "object",
388
+ "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.",
389
+ "required": ["supported"],
390
+ "properties": {
391
+ "supported": {
392
+ "type": "boolean",
393
+ "description": "Host implements the host.credentials resolution + lifecycle contract."
394
+ },
395
+ "scopes": {
396
+ "type": "array",
397
+ "items": { "type": "string", "enum": ["user", "workspace", "tenant"] },
398
+ "uniqueItems": true,
399
+ "description": "Subset of resolution scopes the host implements. `workspace` is the RFC 0048 sub-tenant; `tenant` and `user` align with the `secrets.scopes` vocabulary."
400
+ },
401
+ "encryptionAtRest": {
402
+ "type": "boolean",
403
+ "description": "Host encrypts stored credential material at rest."
404
+ },
405
+ "rotation": {
406
+ "type": "string",
407
+ "enum": ["none", "two-key-overlap"],
408
+ "description": "`two-key-overlap`: old + new credential both resolve as valid during a grace window, then the old fails with `credential_not_found` (mirrors `openwop-auth-api-key-rotation`). `none`: no rotation surface."
409
+ },
410
+ "sharing": {
411
+ "type": "boolean",
412
+ "description": "A single stored credential can be referenced by many workflows within a scope (e.g. a workspace-shared key) without copying material between references."
413
+ }
414
+ },
415
+ "additionalProperties": false
416
+ },
417
+ "feedback": {
418
+ "type": "object",
419
+ "description": "RFC 0056 (`Draft`). Non-blocking human/agent quality signals (rating / correction / label / flag) attached to a run, event, or node. Annotations are a per-run side-resource recorded via `POST /v1/runs/{runId}/annotations`, listed via `GET`, and surfaced live via the `run.annotated` SSE notification — they are NOT entries in the replayable run event log (see RFC 0056 §B/§D). Hosts that do not advertise `supported: true` return `501 capability_not_provided` on the annotation endpoints.",
420
+ "required": ["supported"],
421
+ "properties": {
422
+ "supported": {
423
+ "type": "boolean",
424
+ "description": "Host implements the RFC 0056 annotation side-store + endpoints + `run.annotated` notification."
425
+ },
426
+ "targets": {
427
+ "type": "array",
428
+ "items": { "type": "string", "enum": ["run", "event", "node"] },
429
+ "uniqueItems": true,
430
+ "description": "Which annotation-target granularities the host accepts. Absent = `run` only."
431
+ },
432
+ "signals": {
433
+ "type": "array",
434
+ "items": { "type": "string", "enum": ["rating", "correction", "label", "flag"] },
435
+ "uniqueItems": true,
436
+ "description": "Which signal kinds the host accepts. Absent = all four."
437
+ }
438
+ },
439
+ "additionalProperties": false
440
+ },
441
+ "oauth": {
442
+ "type": "object",
443
+ "description": "RFC 0047 (`Draft`). Host performs OAuth 2.0 grants (authorization-code + refresh) on a user's behalf for connector nodes, stores the acquired token as a `host.credentials` (RFC 0046) entry, refreshes it transparently, and resolves it into the node sandbox as a bearer token. Token material NEVER crosses the wire (SECURITY invariant `credential-payload-redaction`). Distinct from `auth` host-authentication profiles (RFC 0010 = who is the caller; this = what third-party token a node holds).",
444
+ "required": ["supported"],
445
+ "properties": {
446
+ "supported": { "type": "boolean", "description": "Host implements the host.oauth third-party token acquisition + refresh contract." },
447
+ "grants": {
448
+ "type": "array",
449
+ "items": { "type": "string", "enum": ["authorization_code", "client_credentials", "refresh_token"] },
450
+ "uniqueItems": true,
451
+ "description": "OAuth 2.0 grant types the host performs on a node's behalf."
452
+ },
453
+ "providers": {
454
+ "type": "array",
455
+ "description": "Provider catalog the host can acquire tokens for. A connector node's `auth.provider` MUST match an `id` here.",
456
+ "items": {
457
+ "type": "object",
458
+ "required": ["id"],
459
+ "properties": {
460
+ "id": { "type": "string", "minLength": 1, "description": "Stable provider id, e.g. `slack`, `google`." },
461
+ "authUrl": { "type": "string", "format": "uri", "description": "Authorization endpoint." },
462
+ "tokenUrl": { "type": "string", "format": "uri", "description": "Token endpoint." },
463
+ "scopesSupported": { "type": "array", "items": { "type": "string" }, "description": "Scopes this provider exposes." }
464
+ },
465
+ "additionalProperties": false
466
+ }
467
+ }
468
+ },
469
+ "additionalProperties": false
470
+ },
471
+ "authorization": {
472
+ "type": "object",
473
+ "description": "RFC 0049 (`Draft`). Maps an RFC 0048 principal's role to scopes (reusing the API-key scope grammar in `auth.md`) and surfaces authorization decisions as `authorization.decided` events. Fail-closed: an absent/unseeded role denies (SECURITY invariant `authorization-fail-closed`).",
474
+ "required": ["supported"],
475
+ "properties": {
476
+ "supported": { "type": "boolean", "description": "Host implements the role→scope authorization-decision contract." },
477
+ "failClosed": { "const": true, "description": "Absent/unseeded role denies; resolver errors deny. MUST be true when present — see SECURITY invariant `authorization-fail-closed`." },
478
+ "roles": {
479
+ "type": "array",
480
+ "description": "Role catalog. A principal's role resolves to this scope set; a request is authorized when any role-derived scope matches the required scope per the API-key scope-match semantics.",
481
+ "items": {
482
+ "type": "object",
483
+ "required": ["role", "scopes"],
484
+ "properties": {
485
+ "role": { "type": "string", "minLength": 1 },
486
+ "scopes": { "type": "array", "items": { "type": "string", "minLength": 1 }, "uniqueItems": true }
487
+ },
488
+ "additionalProperties": false
489
+ }
490
+ }
491
+ },
492
+ "additionalProperties": false
493
+ },
386
494
  "runtimeCapabilities": {
387
495
  "type": "array",
388
496
  "items": { "type": "string", "minLength": 1 },
@@ -398,11 +506,30 @@
398
506
  "type": "object",
399
507
  "additionalProperties": false,
400
508
  "required": ["supported", "version"],
509
+ "if": {
510
+ "properties": { "tier": { "const": "experimental" } },
511
+ "required": ["tier"]
512
+ },
513
+ "then": {
514
+ "required": ["experimentalUntil"]
515
+ },
401
516
  "properties": {
402
517
  "supported": {
403
518
  "type": "boolean",
404
519
  "description": "Host implements the execution loop + handoff state machine per spec/v1/multi-agent-execution.md §\"Execution loop\" + §\"Handoff state machine\". When true, the host MUST emit `core.workflowChain.event` records on every handoff transition per the §\"Transition events\" table."
405
520
  },
521
+ "tier": {
522
+ "type": "string",
523
+ "enum": ["stable", "experimental"],
524
+ "default": "stable",
525
+ "description": "RFC 0042 — stability claim for this capability advertisement. `stable` (the default when absent) means the host commits to the wire shape across v1.x minors. `experimental` means the host advertises the surface as a preview; the wire shape MAY shift compatibly without notice until the underlying RFC graduates to `Accepted` and the host re-advertises as `stable`. Hosts MUST omit the field for capabilities whose underlying RFC is already `Accepted`."
526
+ },
527
+ "experimentalUntil": {
528
+ "type": "string",
529
+ "format": "date",
530
+ "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
531
+ "description": "RFC 0042 §B — required when `tier` is `experimental`. ISO-8601 `YYYY-MM-DD` no more than 12 months past the discovery response date. Reaching this date without graduating the underlying RFC to `Accepted` MUST result in the host either flipping tier to `stable` OR retracting the capability advertisement (or — with an open deprecation RFC — extending with a new `experimentalUntil` per §B sub-block 2)."
532
+ },
406
533
  "version": {
407
534
  "type": "integer",
408
535
  "minimum": 1,
@@ -836,6 +963,29 @@
836
963
  },
837
964
  "additionalProperties": false
838
965
  },
966
+ "scheduling": {
967
+ "type": "object",
968
+ "description": "RFC 0052 (`Draft`). Time-based run initiation behind the `schedule` trigger — gives the trigger a portable, durable, once-per-tick execution contract. Composes with `queueBus` (RFC 0017) where the host backs scheduling with a queue; orthogonal to the in-DAG `core.control.delay` primitive (which delays a node mid-run, not run initiation).",
969
+ "required": ["supported"],
970
+ "properties": {
971
+ "supported": { "type": "boolean" },
972
+ "cron": { "type": "boolean", "description": "Host honors cron-expression schedules." },
973
+ "delayed": { "type": "boolean", "description": "Host honors one-shot delayed execution." },
974
+ "calendar": { "type": "boolean", "description": "Host honors calendar-reference schedules." },
975
+ "maxFutureHorizon": { "type": "string", "description": "ISO-8601 duration (e.g. `P90D`); the farthest-future a run may be scheduled. Schedules beyond it MUST be rejected with `schedule_horizon_exceeded`." }
976
+ },
977
+ "additionalProperties": false
978
+ },
979
+ "deadLetter": {
980
+ "type": "object",
981
+ "description": "RFC 0053 (`Draft`). Run-level dead-letter sink for terminally-failed runs/nodes. On retry exhaustion (RFC 0009), the run is routed to a durable, inspectable sink and a `run.dead_lettered` event is emitted; dead-lettered runs remain fork-eligible (RFC 0011) for the retention window. Distinct from `queueBus.deadLetterSupported`, which dead-letters transport *messages*, not *runs*.",
982
+ "required": ["supported"],
983
+ "properties": {
984
+ "supported": { "type": "boolean" },
985
+ "retentionDays": { "type": "integer", "minimum": 1, "description": "Days a dead-lettered run is retained for inspection/fork before purge." }
986
+ },
987
+ "additionalProperties": false
988
+ },
839
989
  "sql": {
840
990
  "type": "object",
841
991
  "description": "RFC 0018 (`Active`). SQL database adapter with parametric-only enforcement. Hosts MUST reject non-parametric queries that inline user input (`sql-parametric-only` invariant — guards against SQL injection across every workflow).",
@@ -919,6 +1069,44 @@
919
1069
  "required": ["supported"],
920
1070
  "additionalProperties": false
921
1071
  },
1072
+ "packs": {
1073
+ "type": "object",
1074
+ "description": "RFC 0025 (`Active`). Pack-registry surface advertisement. The baseline `/v1/packs/*` read surface (per `spec/v1/node-packs.md` §\"Registry HTTP API\") is unconditional for hosts that ship a pack catalog and does NOT require a capability flag; this object carries optional sub-blocks (currently the test-mode mirror namespace). Hosts that don't expose any optional pack-registry sub-block MAY omit this block entirely.",
1075
+ "properties": {
1076
+ "testMode": {
1077
+ "type": "object",
1078
+ "description": "RFC 0025 §A. Optional `/v1/packs-test/*` mirror surface that exposes the production publish/get/delete/sig contract against an isolated catalog. Lets the conformance suite (`pack-registry-publish.test.ts`) exercise the documented 19-code publish error catalog without `packs:publish` scope on the real registry. Hosts that advertise `supported: true` MUST honor the §C isolation guarantees and MUST surface the same error envelopes and HTTP statuses as the production `/v1/packs/*` surface.",
1079
+ "properties": {
1080
+ "supported": {
1081
+ "type": "boolean",
1082
+ "description": "Host exposes `/v1/packs-test/*` per RFC 0025 §B. When `true`, the conformance suite drives publish-error-catalog assertions through the test namespace; when `false` or absent, the 26 scenarios in `pack-registry-publish.test.ts` soft-skip cleanly."
1083
+ },
1084
+ "isolated": {
1085
+ "type": "boolean",
1086
+ "description": "RFC 0025 §C point 1. MUST be `true` when `supported` is `true` — guarantees the test catalog is persisted distinctly from the production catalog and that a pack PUT'd via `/v1/packs-test/*` MUST NOT appear in `/v1/packs/*` listings."
1087
+ },
1088
+ "catalogResetEndpoint": {
1089
+ "type": "string",
1090
+ "description": "RFC 0025 §C point 4. Optional URL path (e.g. `/v1/packs-test/reset`) that clears the entire test catalog. When advertised, conformance-suite teardown SHOULD call it; the endpoint MUST be idempotent. Hosts MAY omit; in that case the suite leaves disposable timestamped pack names in place and relies on the next host restart to clear in-memory state.",
1091
+ "pattern": "^/"
1092
+ },
1093
+ "scopes": {
1094
+ "type": "array",
1095
+ "items": {
1096
+ "type": "string",
1097
+ "enum": ["core", "vendor", "community", "private", "local"]
1098
+ },
1099
+ "uniqueItems": true,
1100
+ "minItems": 1,
1101
+ "description": "RFC 0025 §A. Which namespace scopes the test catalog accepts in pack names. Public test catalogs SHOULD refuse `private` and `local` (matching the production-registry rule for `packs.openwop.dev`); private dev catalogs MAY accept all five. When omitted, the test catalog defaults to the same scope set as the production namespace it mirrors."
1102
+ }
1103
+ },
1104
+ "required": ["supported"],
1105
+ "additionalProperties": false
1106
+ }
1107
+ },
1108
+ "additionalProperties": false
1109
+ },
922
1110
  "mcp": {
923
1111
  "type": "object",
924
1112
  "description": "RFC 0020 (`Active`). MCP (Model Context Protocol) composition surface. The client half is consumed implicitly via `host.mcp` host-surface; this block adds the optional server half — workflow exposed AS an MCP server with bidirectional sampling/elicitation bridges.",
@@ -1126,7 +1314,7 @@
1126
1314
  "type": "array",
1127
1315
  "items": { "type": "string", "minLength": 1 },
1128
1316
  "uniqueItems": true,
1129
- "description": "Auth profiles the host claims. Canonical ids: `openwop-audit-log-integrity` (auth-profiles.md §Audit-log integrity), `openwop-auth-api-key-rotation`, `openwop-auth-oauth2-client-credentials`, `openwop-auth-oidc-user-bearer`, `openwop-auth-mtls`. Clients SHOULD tolerate unknown profile ids."
1317
+ "description": "Auth profiles the host claims. Canonical ids: `openwop-audit-log-integrity` (auth-profiles.md §Audit-log integrity), `openwop-auth-api-key-rotation`, `openwop-auth-oauth2-client-credentials`, `openwop-auth-oidc-user-bearer`, `openwop-auth-mtls`, `openwop-auth-saml` + `openwop-auth-scim` + `openwop-auth-ldap` (RFC 0050 enterprise identity). Clients SHOULD tolerate unknown profile ids."
1130
1318
  },
1131
1319
  "rotation": {
1132
1320
  "type": "object",
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://openwop.dev/spec/v1/credential-reference.schema.json",
4
+ "title": "CredentialReference",
5
+ "description": "RFC 0046. An opaque, host-issued handle to a stored credential. This is the ONLY credential artifact permitted on the wire — it NEVER carries key material. The host's host.credentials resolver dereferences it into the node sandbox at execution time (SECURITY invariant `credential-payload-redaction`).",
6
+ "type": "object",
7
+ "required": ["ref"],
8
+ "properties": {
9
+ "ref": {
10
+ "type": "string",
11
+ "minLength": 1,
12
+ "description": "Opaque host-issued identifier, e.g. `cred_a3b9c2`. Hosts MUST NOT encode secret material in the ref."
13
+ },
14
+ "scope": {
15
+ "type": "string",
16
+ "enum": ["user", "workspace", "tenant"],
17
+ "description": "Resolution scope. MUST match a scope in `capabilities.credentials.scopes`. Absent ⇒ the host's default scope."
18
+ }
19
+ },
20
+ "additionalProperties": false
21
+ }
@@ -69,7 +69,8 @@
69
69
  "description": "Optional agent manifests shipped alongside this pack. Each entry is an AgentManifest (see agent-manifest.schema.json + RFC 0003 §`agents[]` extension). Pure-agent packs MUST set `runtime.language: 'remote'` — agents are interpreted by the host, not bundled as executable artifacts. Mixed packs (nodes + agents) declare the runtime that loads the node implementations; agents remain host-interpreted."
70
70
  },
71
71
  "runtime": { "$ref": "#/$defs/Runtime" },
72
- "signing": { "$ref": "#/$defs/Signing" }
72
+ "signing": { "$ref": "#/$defs/Signing" },
73
+ "connector": { "$ref": "#/$defs/Connector" }
73
74
  },
74
75
  "additionalProperties": false,
75
76
  "$defs": {
@@ -161,6 +162,15 @@
161
162
  "items": { "$ref": "#/$defs/SecretRequirement" },
162
163
  "description": "Secrets the node needs to execute. Resolved by the host's secret-resolution adapter at dispatch time. Hosts that don't advertise `Capabilities.secrets.supported` MUST refuse to dispatch a node with non-empty `requiresSecrets` and return `credential_unavailable`."
163
164
  },
165
+ "requiredCredentials": {
166
+ "type": "array",
167
+ "items": { "$ref": "#/$defs/CredentialRequirement" },
168
+ "description": "RFC 0046. Credentials the node needs, resolved by the host's `host.credentials` resolver at dispatch time (distinct from `requiresSecrets`, which targets the informal BYOK annex). Hosts that don't advertise `Capabilities.credentials.supported` MUST refuse to dispatch a node with non-empty `requiredCredentials` and return `credential_unavailable` (peerDependency `credentials: 'supported'`)."
169
+ },
170
+ "auth": {
171
+ "$ref": "#/$defs/NodeAuth",
172
+ "description": "RFC 0047. The node's third-party authentication need. The host acquires + refreshes the token via the `host.oauth` flow and resolves it into the node sandbox as a bearer token. Hosts that don't advertise `Capabilities.oauth.supported` (or lack the named provider/scope) MUST refuse to register the pack (`oauth_provider_unsupported` / `oauth_scope_unsupported`)."
173
+ },
164
174
  "requiredModelCapabilities": {
165
175
  "type": "array",
166
176
  "items": {
@@ -219,6 +229,107 @@
219
229
  },
220
230
  "additionalProperties": false
221
231
  },
232
+ "CredentialRequirement": {
233
+ "type": "object",
234
+ "required": ["key"],
235
+ "description": "RFC 0046. A credential the node needs, resolved by the host's `host.credentials` resolver into the node sandbox at dispatch time. The node declares only the requirement; raw key material NEVER reaches the protocol surface (events, logs, traces, replay) per the `credential-payload-redaction` invariant.",
236
+ "properties": {
237
+ "key": {
238
+ "type": "string",
239
+ "minLength": 1,
240
+ "description": "Stable identifier the node executor uses to look up the resolved credential (e.g., `'slack-bot-token'`)."
241
+ },
242
+ "scope": {
243
+ "type": "string",
244
+ "enum": ["user", "workspace", "tenant"],
245
+ "description": "Resolution scope. MUST match a scope in `Capabilities.credentials.scopes`. Defaults to the host's default scope when omitted."
246
+ },
247
+ "displayName": {
248
+ "type": "string",
249
+ "description": "Optional human-readable label for credential-management UIs."
250
+ }
251
+ },
252
+ "additionalProperties": false
253
+ },
254
+ "NodeAuth": {
255
+ "type": "object",
256
+ "required": ["type", "provider"],
257
+ "description": "RFC 0047. A node's third-party authentication declaration. The node declares only which provider + scopes it needs; the host performs the OAuth dance and resolves the token in-sandbox. Raw token material NEVER reaches the protocol surface (`credential-payload-redaction` invariant).",
258
+ "properties": {
259
+ "type": {
260
+ "type": "string",
261
+ "enum": ["oauth2"],
262
+ "description": "Authentication mechanism. `oauth2` routes through the `host.oauth` flow."
263
+ },
264
+ "provider": {
265
+ "type": "string",
266
+ "minLength": 1,
267
+ "description": "Provider id; MUST match an advertised `capabilities.oauth.providers[].id` (e.g. `slack`, `google`)."
268
+ },
269
+ "scopes": {
270
+ "type": "array",
271
+ "items": { "type": "string" },
272
+ "description": "OAuth scopes the node requires; each MUST be in the provider's advertised `scopesSupported`."
273
+ }
274
+ },
275
+ "additionalProperties": false
276
+ },
277
+ "ConnectorAuth": {
278
+ "description": "RFC 0045. The auth declaration shared by a connector's actions. Either an RFC 0047 OAuth2 declaration or an RFC 0046 stored-credential reference.",
279
+ "oneOf": [
280
+ { "$ref": "#/$defs/NodeAuth" },
281
+ {
282
+ "type": "object",
283
+ "required": ["type", "key"],
284
+ "properties": {
285
+ "type": { "type": "string", "enum": ["credential"], "description": "Static stored-credential auth via the RFC 0046 host.credentials resolver." },
286
+ "key": { "type": "string", "minLength": 1, "description": "CredentialRequirement key the host resolves into the node sandbox." },
287
+ "scope": { "type": "string", "enum": ["user", "workspace", "tenant"], "description": "Resolution scope; MUST match a scope in `capabilities.credentials.scopes`." }
288
+ },
289
+ "additionalProperties": false
290
+ }
291
+ ]
292
+ },
293
+ "Connector": {
294
+ "type": "object",
295
+ "required": ["id", "displayName"],
296
+ "description": "RFC 0045. Declares this pack a named connector — a typed integration exposing actions (and reusing the existing trigger model). Optional; packs without it remain plain node packs. Actions are normal side-effectful nodes from this pack's `nodes[]` annotated with scheduler hints; the connector block adds metadata, not a new execution kind.",
297
+ "properties": {
298
+ "id": { "type": "string", "pattern": "^[a-z][a-z0-9.-]*$", "description": "Stable connector id, e.g. `salesforce`." },
299
+ "displayName": { "type": "string", "minLength": 1 },
300
+ "auth": { "$ref": "#/$defs/ConnectorAuth", "description": "RFC 0047/0046 auth declaration shared by the connector's actions." },
301
+ "actions": {
302
+ "type": "array",
303
+ "description": "Typed actions the connector exposes. Each `typeId` MUST resolve to a node typeId defined in this pack's `nodes[]` (validation error `connector_action_unresolved` otherwise).",
304
+ "items": {
305
+ "type": "object",
306
+ "required": ["typeId", "displayName"],
307
+ "properties": {
308
+ "typeId": { "type": "string", "minLength": 1, "description": "MUST match a `nodes[].typeId` in this manifest." },
309
+ "displayName": { "type": "string", "minLength": 1 },
310
+ "idempotent": { "type": "boolean", "description": "Action is safe to auto-retry without an idempotency key. Absent/false ⇒ the host MUST NOT auto-retry without one (composes with idempotency.md)." },
311
+ "rateLimit": {
312
+ "type": "object",
313
+ "properties": {
314
+ "requests": { "type": "integer", "minimum": 1 },
315
+ "perSeconds": { "type": "integer", "minimum": 1 }
316
+ },
317
+ "additionalProperties": false,
318
+ "description": "Advertised rate-limit hint the host scheduler SHOULD honor. Does not change the node's wire shape."
319
+ },
320
+ "paginated": { "type": "boolean", "description": "Action returns paginated results." }
321
+ },
322
+ "additionalProperties": false
323
+ }
324
+ },
325
+ "triggers": {
326
+ "type": "array",
327
+ "items": { "type": "string", "minLength": 1 },
328
+ "description": "typeIds of triggers (from the existing trigger model) this connector exposes. Each MUST resolve to a node/trigger typeId in this pack."
329
+ }
330
+ },
331
+ "additionalProperties": false
332
+ },
222
333
  "Runtime": {
223
334
  "type": "object",
224
335
  "required": ["language", "entry"],
@@ -0,0 +1,64 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://openwop.dev/spec/v1/run-diff-response.schema.json",
4
+ "title": "RunDiffResponse",
5
+ "description": "RFC 0054 — deterministic, replay-aware structured diff of two runs' event sequences and terminal states, returned by GET /v1/runs/{runId}:diff?against={otherRunId}. The diff is a pure function of the two event logs (see spec/v1/replay.md determinism contract).",
6
+ "type": "object",
7
+ "required": ["a", "b", "eventDiffs", "stateDiff"],
8
+ "properties": {
9
+ "a": {
10
+ "type": "string",
11
+ "description": "The {runId} run (the path resource)."
12
+ },
13
+ "b": {
14
+ "type": "string",
15
+ "description": "The {against} run (the query parameter)."
16
+ },
17
+ "divergedAtSeq": {
18
+ "type": ["integer", "null"],
19
+ "minimum": 0,
20
+ "description": "Event sequence number at which the two logs first diverge; null if identical. Aligns with replay.diverged.divergencePoint in spec/v1/replay.md."
21
+ },
22
+ "eventDiffs": {
23
+ "type": "array",
24
+ "description": "Ordered per-sequence differences. Empty when the logs are identical.",
25
+ "items": {
26
+ "type": "object",
27
+ "required": ["seq", "op"],
28
+ "properties": {
29
+ "seq": {
30
+ "type": "integer",
31
+ "minimum": 0,
32
+ "description": "Event sequence number this diff entry applies to."
33
+ },
34
+ "op": {
35
+ "type": "string",
36
+ "enum": ["added", "removed", "changed"],
37
+ "description": "added = present in b only; removed = present in a only; changed = present in both at this seq but differ by canonical comparison."
38
+ },
39
+ "aEvent": {
40
+ "type": "object",
41
+ "additionalProperties": true,
42
+ "description": "The run-a event at this seq (omitted when op = added). Shape per run-event.schema.json."
43
+ },
44
+ "bEvent": {
45
+ "type": "object",
46
+ "additionalProperties": true,
47
+ "description": "The run-b event at this seq (omitted when op = removed). Shape per run-event.schema.json."
48
+ }
49
+ },
50
+ "additionalProperties": false
51
+ }
52
+ },
53
+ "stateDiff": {
54
+ "type": "object",
55
+ "additionalProperties": true,
56
+ "description": "Diff of terminal RunSnapshot states (status, variables, channels) — redaction-safe (never carries credential material)."
57
+ },
58
+ "truncated": {
59
+ "type": "boolean",
60
+ "description": "True if either run was in-flight and only a prefix was compared."
61
+ }
62
+ },
63
+ "additionalProperties": false
64
+ }
@@ -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\n64 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\n71 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": {
@@ -18,6 +18,7 @@
18
18
  "run.paused": { "$ref": "#/$defs/runPaused" },
19
19
  "run.resumed": { "$ref": "#/$defs/runResumed" },
20
20
  "run.restored-from-snapshot":{ "$ref": "#/$defs/runRestoredFromSnapshot" },
21
+ "run.dead_lettered": { "$ref": "#/$defs/runDeadLettered" },
21
22
  "node.started": { "$ref": "#/$defs/nodeStarted" },
22
23
  "node.completed": { "$ref": "#/$defs/nodeCompleted" },
23
24
  "node.failed": { "$ref": "#/$defs/nodeFailed" },
@@ -29,6 +30,9 @@
29
30
  "node.cancelled": { "$ref": "#/$defs/nodeCancelled" },
30
31
  "approval.requested": { "$ref": "#/$defs/approvalRequested" },
31
32
  "approval.received": { "$ref": "#/$defs/approvalReceived" },
33
+ "approval.granted": { "$ref": "#/$defs/approvalGranted" },
34
+ "approval.rejected": { "$ref": "#/$defs/approvalRejected" },
35
+ "approval.overridden": { "$ref": "#/$defs/approvalOverridden" },
32
36
  "clarification.requested": { "$ref": "#/$defs/clarificationRequested" },
33
37
  "clarification.resolved": { "$ref": "#/$defs/clarificationResolved" },
34
38
  "interrupt.requested": { "$ref": "#/$defs/interruptRequested" },
@@ -73,7 +77,48 @@
73
77
  "conversation.closed": { "$ref": "#/$defs/conversationClosed" },
74
78
  "memory.compacted": { "$ref": "#/$defs/memoryCompacted" },
75
79
  "core.workflowChain.event": { "$ref": "#/$defs/coreWorkflowChainEvent" },
76
- "core.workflowChain.confidence-escalated": { "$ref": "#/$defs/coreWorkflowChainConfidenceEscalated" }
80
+ "core.workflowChain.confidence-escalated": { "$ref": "#/$defs/coreWorkflowChainConfidenceEscalated" },
81
+ "connector.authorized": { "$ref": "#/$defs/connectorAuthorized" },
82
+ "connector.auth_expired": { "$ref": "#/$defs/connectorAuthExpired" },
83
+ "authorization.decided": { "$ref": "#/$defs/authorizationDecided" }
84
+ }
85
+ },
86
+
87
+ "authorizationDecided": {
88
+ "description": "RFC 0049 — emitted on a role-based authorization decision (allow or deny). Redaction-safe: `principal` is an opaque RFC 0048 id; `reason` carries no credential material. Every deny SHOULD be emitted and SHOULD feed the audit log (RFC 0009/0010). MUST NOT be emitted unless `capabilities.authorization.supported: true`.",
89
+ "type": "object",
90
+ "additionalProperties": false,
91
+ "required": ["principal", "action", "resource", "allowed"],
92
+ "properties": {
93
+ "principal": { "type": "string", "minLength": 1, "description": "Opaque RFC 0048 principal id — never PII." },
94
+ "action": { "type": "string", "minLength": 1, "description": "The attempted action, e.g. `runs:cancel`." },
95
+ "resource": { "type": "string", "minLength": 1, "description": "The target, e.g. a runId or workflowId." },
96
+ "allowed": { "type": "boolean" },
97
+ "reason": { "type": "string", "description": "Human-readable, redaction-safe — no credential material." }
98
+ }
99
+ },
100
+
101
+ "connectorAuthorized": {
102
+ "description": "RFC 0047 — emitted when the host acquires (or re-authorizes) a third-party OAuth token on a user's behalf via the `host.oauth` flow. Redaction-safe: carries the credential REFERENCE (RFC 0046), never token material. MUST NOT be emitted unless `capabilities.oauth.supported: true`.",
103
+ "type": "object",
104
+ "additionalProperties": false,
105
+ "required": ["provider", "credentialRef"],
106
+ "properties": {
107
+ "provider": { "type": "string", "minLength": 1, "description": "Provider id, matching an advertised `capabilities.oauth.providers[].id` (e.g. `slack`, `google`)." },
108
+ "credentialRef": { "type": "string", "minLength": 1, "description": "Opaque RFC 0046 credential reference where the acquired token is stored. NEVER the token itself." },
109
+ "scopes": { "type": "array", "items": { "type": "string" }, "description": "Scopes granted for the acquired token." }
110
+ }
111
+ },
112
+
113
+ "connectorAuthExpired": {
114
+ "description": "RFC 0047 — emitted when a stored OAuth token's refresh fails terminally (revoked/expired refresh token). Redaction-safe: no token material. MUST NOT be emitted unless `capabilities.oauth.supported: true`.",
115
+ "type": "object",
116
+ "additionalProperties": false,
117
+ "required": ["provider", "credentialRef"],
118
+ "properties": {
119
+ "provider": { "type": "string", "minLength": 1, "description": "Provider id, matching an advertised `capabilities.oauth.providers[].id`." },
120
+ "credentialRef": { "type": "string", "minLength": 1, "description": "Opaque RFC 0046 credential reference for the expired token." },
121
+ "reason": { "type": "string", "description": "Redaction-safe human-readable reason (e.g. `refresh_token_revoked`)." }
77
122
  }
78
123
  },
79
124
 
@@ -205,6 +250,17 @@
205
250
  "inputs": { "$ref": "#/$defs/_inputsObject" },
206
251
  "transport": { "type": "string", "enum": ["rest", "mcp", "a2a", "ui"] },
207
252
  "engineVersion": { "type": "string" },
253
+ "owner": {
254
+ "type": "object",
255
+ "description": "RFC 0048. Redaction-safe echo of the run's owning identity triple (matches `RunSnapshot.owner`). Optional; single-tenant hosts omit it.",
256
+ "required": ["tenant"],
257
+ "properties": {
258
+ "tenant": { "type": "string", "minLength": 1 },
259
+ "workspace": { "type": "string", "minLength": 1 },
260
+ "principal": { "type": "string", "minLength": 1 }
261
+ },
262
+ "additionalProperties": false
263
+ },
208
264
  "tags": { "type": "array", "items": { "type": "string" } },
209
265
  "metadata": { "type": "object" }
210
266
  },
@@ -272,6 +328,19 @@
272
328
  "additionalProperties": true
273
329
  },
274
330
 
331
+ "runDeadLettered": {
332
+ "description": "RFC 0053 — emitted when a run/node exhausts its retry policy (RFC 0009) and is routed to the durable dead-letter sink. The run remains fork-eligible (RFC 0011) for `capabilities.deadLetter.retentionDays`. Redaction-safe: `reason` carries no credential material. MUST NOT be emitted unless `capabilities.deadLetter.supported: true`.",
333
+ "type": "object",
334
+ "additionalProperties": false,
335
+ "required": ["runId", "reason", "attempts"],
336
+ "properties": {
337
+ "runId": { "type": "string", "minLength": 1 },
338
+ "nodeId": { "type": "string", "description": "The node whose retry exhaustion dead-lettered the run; absent for run-level failures." },
339
+ "reason": { "type": "string", "minLength": 1, "description": "Redaction-safe terminal-failure reason." },
340
+ "attempts": { "type": "integer", "minimum": 1, "description": "Total attempts made before dead-lettering." }
341
+ }
342
+ },
343
+
275
344
  "nodeStarted": {
276
345
  "type": "object",
277
346
  "description": "Emitted when a node begins execution.",
@@ -419,6 +488,39 @@
419
488
  },
420
489
  "additionalProperties": true
421
490
  },
491
+ "approvalGranted": {
492
+ "description": "RFC 0051 — emitted when an authorized principal grants a `core.openwop.governance.approvalGate`. Redaction-safe: `principal` is an opaque RFC 0048 id. MUST NOT be emitted unless the gate node is registered (peerDependency `authorization: 'supported'`).",
493
+ "type": "object",
494
+ "additionalProperties": false,
495
+ "required": ["gateId", "principal"],
496
+ "properties": {
497
+ "gateId": { "type": "string", "minLength": 1, "description": "Identifier of the approval gate node." },
498
+ "principal": { "type": "string", "minLength": 1, "description": "Opaque RFC 0048 principal id of the granting actor." },
499
+ "quorumProgress": { "type": "object", "additionalProperties": false, "properties": { "granted": { "type": "integer", "minimum": 0 }, "required": { "type": "integer", "minimum": 1 } }, "description": "When the gate requires a quorum: grants accumulated vs required." }
500
+ }
501
+ },
502
+ "approvalRejected": {
503
+ "description": "RFC 0051 — emitted when an authorized principal rejects an approval gate. The run loops back per the workflow edges (does not terminate by default). Redaction-safe.",
504
+ "type": "object",
505
+ "additionalProperties": false,
506
+ "required": ["gateId", "principal"],
507
+ "properties": {
508
+ "gateId": { "type": "string", "minLength": 1 },
509
+ "principal": { "type": "string", "minLength": 1, "description": "Opaque RFC 0048 principal id of the rejecting actor." },
510
+ "reason": { "type": "string", "description": "Redaction-safe human-readable reason." }
511
+ }
512
+ },
513
+ "approvalOverridden": {
514
+ "description": "RFC 0051 — emitted when a role-gated `override` path bypasses the gate (e.g. owner force-publish). MUST feed the audit log (RFC 0009/0010). Redaction-safe.",
515
+ "type": "object",
516
+ "additionalProperties": false,
517
+ "required": ["gateId", "principal", "reason"],
518
+ "properties": {
519
+ "gateId": { "type": "string", "minLength": 1 },
520
+ "principal": { "type": "string", "minLength": 1, "description": "Opaque RFC 0048 principal id of the overriding actor (MUST satisfy the override role)." },
521
+ "reason": { "type": "string", "minLength": 1, "description": "Required, redaction-safe rationale — the audit breadcrumb." }
522
+ }
523
+ },
422
524
  "clarificationRequested": {
423
525
  "type": "object",
424
526
  "description": "Legacy kind-specific event for HITL clarification requests.",