@openwop/openwop-conformance 1.4.0 → 1.6.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/CHANGELOG.md +60 -0
- package/README.md +2 -2
- package/api/asyncapi.yaml +8 -3
- package/api/openapi.yaml +305 -0
- package/coverage.md +35 -10
- package/fixtures/conformance-phase4-nondet-tool.json +53 -0
- package/fixtures/conformance-phase4-replay-divergence.json +40 -0
- package/fixtures.md +5 -3
- package/package.json +1 -1
- package/schemas/README.md +2 -0
- package/schemas/capabilities.schema.json +176 -3
- package/schemas/credential-reference.schema.json +21 -0
- package/schemas/node-pack-manifest.schema.json +112 -1
- package/schemas/run-diff-response.schema.json +64 -0
- package/schemas/run-event-payloads.schema.json +104 -2
- package/schemas/run-event.schema.json +8 -1
- package/schemas/run-snapshot.schema.json +11 -0
- package/src/lib/behavior-gate.ts +51 -0
- package/src/lib/driver.ts +13 -1
- package/src/lib/saml-idp.ts +179 -0
- package/src/scenarios/approval-gate-events.test.ts +61 -0
- package/src/scenarios/approval-gate-flow.test.ts +68 -0
- package/src/scenarios/auth-saml-profile.test.ts +119 -0
- package/src/scenarios/auth-scim-profile.test.ts +65 -0
- package/src/scenarios/authorization-fail-closed.test.ts +80 -0
- package/src/scenarios/authorization-roles-shape.test.ts +83 -0
- package/src/scenarios/connector-manifest-validity.test.ts +142 -0
- package/src/scenarios/credential-payload-redaction.test.ts +93 -0
- package/src/scenarios/credentials-capability-shape.test.ts +90 -0
- package/src/scenarios/cross-engine-append-behavior.test.ts +204 -0
- package/src/scenarios/cross-host-traceparent-propagation.test.ts +13 -6
- package/src/scenarios/cross-workspace-isolation.test.ts +72 -0
- package/src/scenarios/deadletter-capability-shape.test.ts +59 -0
- package/src/scenarios/deadletter-retry-exhaustion.test.ts +62 -0
- package/src/scenarios/experimental-tier-shape.test.ts +192 -0
- package/src/scenarios/identity-owner-shape.test.ts +64 -0
- package/src/scenarios/multi-agent-confidence-escalation.test.ts +59 -21
- package/src/scenarios/multi-agent-memory-lifecycle.test.ts +87 -12
- package/src/scenarios/multi-region-idempotency-behavior.test.ts +203 -0
- package/src/scenarios/oauth-capability-shape.test.ts +97 -0
- package/src/scenarios/oauth-connector-redaction.test.ts +91 -0
- package/src/scenarios/pack-registry-isolation.test.ts +108 -0
- package/src/scenarios/pack-registry-publish.test.ts +1 -1
- package/src/scenarios/prompt-mutation-workspace-membership-enforced.test.ts +126 -0
- package/src/scenarios/prompt-read-workspace-membership-enforced.test.ts +183 -0
- package/src/scenarios/replay-divergence-at-refusal.test.ts +187 -7
- package/src/scenarios/replay-observable-sequence-determinism.test.ts +20 -6
- package/src/scenarios/run-diff.test.ts +143 -0
- package/src/scenarios/sandbox-capability-gate-respected.test.ts +15 -13
- package/src/scenarios/sandbox-memory-cap.test.ts +7 -8
- package/src/scenarios/sandbox-mvp-behavior.test.ts +280 -0
- package/src/scenarios/sandbox-no-cross-pack-mutation.test.ts +14 -13
- package/src/scenarios/sandbox-no-host-env-leak.test.ts +14 -21
- package/src/scenarios/sandbox-no-host-fs-escape.test.ts +20 -15
- package/src/scenarios/sandbox-no-host-process-escape.test.ts +18 -13
- package/src/scenarios/sandbox-no-network-escape.test.ts +14 -31
- package/src/scenarios/sandbox-timeout-cap.test.ts +7 -8
- package/src/scenarios/scheduling-capability-shape.test.ts +81 -0
- package/src/scenarios/scheduling-cron-fires-once.test.ts +66 -0
- package/src/scenarios/secret-leakage-otel-attribute.test.ts +241 -0
- package/src/scenarios/spec-corpus-validity.test.ts +2 -2
|
@@ -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\
|
|
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.",
|
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
"run.paused",
|
|
72
72
|
"run.resumed",
|
|
73
73
|
"run.restored-from-snapshot",
|
|
74
|
+
"run.dead_lettered",
|
|
74
75
|
"node.started",
|
|
75
76
|
"node.completed",
|
|
76
77
|
"node.failed",
|
|
@@ -82,6 +83,9 @@
|
|
|
82
83
|
"node.cancelled",
|
|
83
84
|
"approval.requested",
|
|
84
85
|
"approval.received",
|
|
86
|
+
"approval.granted",
|
|
87
|
+
"approval.rejected",
|
|
88
|
+
"approval.overridden",
|
|
85
89
|
"clarification.requested",
|
|
86
90
|
"clarification.resolved",
|
|
87
91
|
"interrupt.requested",
|
|
@@ -126,7 +130,10 @@
|
|
|
126
130
|
"conversation.closed",
|
|
127
131
|
"memory.compacted",
|
|
128
132
|
"core.workflowChain.event",
|
|
129
|
-
"core.workflowChain.confidence-escalated"
|
|
133
|
+
"core.workflowChain.confidence-escalated",
|
|
134
|
+
"connector.authorized",
|
|
135
|
+
"connector.auth_expired",
|
|
136
|
+
"authorization.decided"
|
|
130
137
|
]
|
|
131
138
|
}
|
|
132
139
|
}
|
|
@@ -32,6 +32,17 @@
|
|
|
32
32
|
],
|
|
33
33
|
"description": "Current run state. `waiting-external` MUST be used when the suspended interrupt's `kind` is `external-event` per `interrupt-profiles.md §openwop-interrupt-external-event` — distinguishes external-event waits from HITL waits at the wire level. Forward-compat: future statuses MAY be added; readers SHOULD treat unknown values as terminal-unknown rather than throw."
|
|
34
34
|
},
|
|
35
|
+
"owner": {
|
|
36
|
+
"type": "object",
|
|
37
|
+
"description": "RFC 0048. The identity triple that owns this run. Redaction-safe — `principal` is an opaque identifier, never PII or credential material. Optional: single-tenant hosts omit it. A principal scoped to one `workspace` MUST NOT read a run owned by another (`run_forbidden`).",
|
|
38
|
+
"required": ["tenant"],
|
|
39
|
+
"properties": {
|
|
40
|
+
"tenant": { "type": "string", "minLength": 1, "description": "Top-level isolation boundary." },
|
|
41
|
+
"workspace": { "type": "string", "minLength": 1, "description": "Optional sub-tenant within the tenant (RFC 0048 workspace)." },
|
|
42
|
+
"principal": { "type": "string", "minLength": 1, "description": "Acting identity (user or agent) — opaque id, never PII." }
|
|
43
|
+
},
|
|
44
|
+
"additionalProperties": false
|
|
45
|
+
},
|
|
35
46
|
"currentNodeId": {
|
|
36
47
|
"type": "string",
|
|
37
48
|
"description": "Set when the run is suspended at a specific node (`waiting-approval` / `waiting-input` / `waiting-external`) — identifies which node holds the interrupt."
|
package/src/lib/behavior-gate.ts
CHANGED
|
@@ -105,3 +105,54 @@ export function behaviorGate(profileName: string, advertised: boolean): boolean
|
|
|
105
105
|
);
|
|
106
106
|
return false;
|
|
107
107
|
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* RFC 0042 §D — experimental-tier soft-skip router for capability-gated
|
|
111
|
+
* scenarios. Wraps `behaviorGate` with an additional tier check.
|
|
112
|
+
*
|
|
113
|
+
* Returns true if the scenario should proceed with assertions, false if
|
|
114
|
+
* it should skip:
|
|
115
|
+
* - tier === 'experimental' AND OPENWOP_REQUIRE_EXPERIMENTAL not set → soft-skip
|
|
116
|
+
* (the scenario does NOT count toward "Failed" or "Skipped (capability-gated)"
|
|
117
|
+
* in the four-bucket taxonomy — a new fifth bucket "Skipped (experimental)"
|
|
118
|
+
* SHOULD be tallied by reporters; logged via the dedicated console.warn below.)
|
|
119
|
+
* - tier === 'experimental' AND OPENWOP_REQUIRE_EXPERIMENTAL=true → behave as
|
|
120
|
+
* stable (hand off to behaviorGate; honor OPENWOP_REQUIRE_BEHAVIOR + opt-out).
|
|
121
|
+
* - tier === 'stable' (default) → identical to behaviorGate.
|
|
122
|
+
*
|
|
123
|
+
* The `experimentalUntil` ISO-8601 date is logged for telemetry but does NOT
|
|
124
|
+
* gate behavior — the spec contract is that the wire shape MAY shift before
|
|
125
|
+
* the date, not that the scenario MUST stop running.
|
|
126
|
+
*/
|
|
127
|
+
export function experimentalGate(
|
|
128
|
+
profileName: string,
|
|
129
|
+
advertised: boolean,
|
|
130
|
+
tier: 'stable' | 'experimental' | undefined,
|
|
131
|
+
experimentalUntil?: string,
|
|
132
|
+
): boolean {
|
|
133
|
+
if (tier === 'experimental') {
|
|
134
|
+
const env = loadEnv();
|
|
135
|
+
const requireExperimental = process.env.OPENWOP_REQUIRE_EXPERIMENTAL === 'true';
|
|
136
|
+
if (!requireExperimental) {
|
|
137
|
+
// eslint-disable-next-line no-console
|
|
138
|
+
console.warn(
|
|
139
|
+
`[experimental capability: ${profileName}] host advertises tier='experimental'` +
|
|
140
|
+
(experimentalUntil ? ` until ${experimentalUntil}` : '') +
|
|
141
|
+
`; scenario skipped under default mode (set OPENWOP_REQUIRE_EXPERIMENTAL=true to run)`,
|
|
142
|
+
);
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
// Strict-mode experimental: hand off to behaviorGate with the standard
|
|
146
|
+
// advertised+opt-out rules. A host that advertises tier='experimental'
|
|
147
|
+
// AND opts the profile out via OPENWOP_OPTED_OUT_PROFILES would surface
|
|
148
|
+
// the standard advertise+opt-out conflict warning from behaviorGate.
|
|
149
|
+
// Don't strip the env.requireBehavior flag — that's a separate axis.
|
|
150
|
+
// eslint-disable-next-line no-console
|
|
151
|
+
console.warn(
|
|
152
|
+
`[experimental capability: ${profileName}] OPENWOP_REQUIRE_EXPERIMENTAL=true; ` +
|
|
153
|
+
`running as strict assertion against advertised='${advertised}'`,
|
|
154
|
+
);
|
|
155
|
+
void env;
|
|
156
|
+
}
|
|
157
|
+
return behaviorGate(profileName, advertised);
|
|
158
|
+
}
|
package/src/lib/driver.ts
CHANGED
|
@@ -50,7 +50,19 @@ class OpenWOPDriver {
|
|
|
50
50
|
|
|
51
51
|
const fetchInit: RequestInit = { method, headers };
|
|
52
52
|
if (init.body !== undefined) {
|
|
53
|
-
|
|
53
|
+
// Buffer / Uint8Array bodies are sent as raw bytes — needed by the
|
|
54
|
+
// RFC 0025 test-mode publish scenarios so the host's body-shape
|
|
55
|
+
// check sees the bytes the caller actually wrote (rather than a
|
|
56
|
+
// JSON-stringified `{"type":"Buffer","data":[...]}` envelope).
|
|
57
|
+
if (typeof Buffer !== 'undefined' && Buffer.isBuffer(init.body)) {
|
|
58
|
+
fetchInit.body = new Uint8Array(init.body);
|
|
59
|
+
} else if (init.body instanceof Uint8Array) {
|
|
60
|
+
fetchInit.body = init.body;
|
|
61
|
+
} else if (typeof init.body === 'string') {
|
|
62
|
+
fetchInit.body = init.body;
|
|
63
|
+
} else {
|
|
64
|
+
fetchInit.body = JSON.stringify(init.body);
|
|
65
|
+
}
|
|
54
66
|
}
|
|
55
67
|
const res = await fetch(url, fetchInit);
|
|
56
68
|
|