@openwop/openwop-conformance 2.44.3 → 2.44.5
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 +14 -0
- package/dist/spec-artifacts.lock.json +2 -2
- package/fixtures/conformance-approval-reject-loopback.json +1 -1
- package/fixtures/conformance-replay-ordinal-loop.json +2 -2
- package/fixtures/conformance-replay-side-effect.json +2 -2
- package/fixtures.md +5 -4
- package/package.json +2 -2
- package/requirements.json +44 -17
- package/schemas/CORPUS-STAMP.json +26 -25
- package/src/scenarios/replay-side-effect-suppression.test.ts +5 -3
- package/src/scenarios/v2-approval-reject-disposition.test.ts +106 -3
- package/src/scenarios/v2-replay-suppression-ordinal.test.ts +8 -7
- package/src/scenarios/webhook-signed-delivery.test.ts +11 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# `@openwop/openwop-conformance` Changelog
|
|
2
2
|
|
|
3
|
+
## [2.44.5] — 2026-09-29 — a replay short-circuit raises no new interrupt request, and the hold node's outputs are pinned
|
|
4
|
+
|
|
5
|
+
- **The 2.44.5 cycle opens.** Class 3 and editorial follow-ups to 2.44.4 change the packed `@openwop/spec-artifacts` tree (`replay.md`, `fixtures.md`). No scenario is added or removed.
|
|
6
|
+
- **`core.conformance.hold`'s outputs are pinned, and `0223.replay-derives-rejection` cites its MUST.** The hold node's outputs are exactly its resolved inputs, `delayMs` included, under the same keys (`fixtures.md`), so hosts converge. The leg's requirement text now cites `replay.md` §Determinism caveats rule 2, which says a replay short-circuit raises no new `interrupt.requested` (clarified, Class 3).
|
|
7
|
+
|
|
8
|
+
## [2.44.4] — 2026-09-29 — six more core families get v2 normative homes, replay derives a rejection and counts terminals, and the replay fixtures wait on a pure hold node
|
|
9
|
+
|
|
10
|
+
- **`0223.replay-derives-rejection`: replay derives a rejection, never re-decides it** (#1756). Two new legs in `v2-approval-reject-disposition` (target major 2), gated on `replay` advertising mode `replay` and on each fixture. The leg forks `mode: "replay"` at the gate's recorded `interrupt.resolved`, so the prefix holds the request and suspension and the fork must supply the resolution. It asserts four things. (i) No new `interrupt.requested` / `approval.requested` for the gate at or after `fromSeq`; this is the discriminator. (ii) The fork's `interrupt.resolved` carries the source's `decision`, `action` and `reason`. (iii) The fork fails the same way: `approval_rejected`, `failedNodeId: gate`, a non-retryable `node.failed`. (iv) Leg A only: a resolve answers `409 interrupt_already_resolved`. Leg B uses `conformance-approval-timeout-approve` and compares the fork's `resolvedBy` with the source's (a host subject or absent, RFC 0183), rather than requiring it absent. `interruptId` equality is not asserted. Forking at the gate's `node.started` was rejected: a host may re-emit the recorded request as history, which cannot be told from a new one. Measured on the v2 reference host (openwop-examples `b6302cd`): both legs `executed-pass`. With its replay derivation disabled, both fail.
|
|
11
|
+
- **The loop fixtures are bounded by their scenarios, not a host cap** (#1748). `conformance-approval-reject-loopback` and `conformance-replay-ordinal-loop` no longer claim to be bounded by `settings.maxLoopbackIterations`, which no spec binds. `v2-approval-reject-disposition`'s loopback leg and `v2-replay-suppression-ordinal` create the run with `configurable: { version: 1, run: { recursionLimit: 20 } }` as a runaway guard. A cycle-running host no longer needs a definition-level loopback cap to advertise either fixture.
|
|
12
|
+
- **`core.conformance.hold` replaces `core.delay` in the replay fixtures** (#1769). A new conformance-reserved PURE node (`fixtures.md`): it completes after `inputs.delayMs` with its inputs as outputs, is never classified side-effecting, and re-executes live in a replay. `conformance-replay-ordinal-loop` and the v1 `conformance-replay-side-effect` now wait on it. With `core.delay`, which the registry's flow pack declares `role: "side-effect"`, a host honouring `replay.md` rule 4 failed the wait with `replay_source_missing` before the node under test. `v2-replay-suppression-ordinal`'s docstring follows rule 2's re-correction: the ordinal counts recorded terminals, not `node.started`.
|
|
13
|
+
- **No leg change for #1770** (`interrupt.md` §Events: the kind-specific interrupt types are now a MAY). The suite never asserted their emission. `spec-artifacts` carries the corrected `interrupt.md`.
|
|
14
|
+
- **`webhook-signed-delivery`'s tunnel control registered the tunnel, not loopback, and failed conformant hosts.** The control leg ("with a tunnel wired, the host still refuses the loopback receiver") posted `receiver.url`, commented as "deliberately the raw loopback URL, NOT the tunnel". Since 2.37.0, `startScopedReceiver` returns the PUBLIC FRONT as `url` whenever `OPENWOP_WEBHOOK_RECEIVER_URL` is set: the tunnel, plus the exercise's nonce. So on every tunnelled cut the control registered a legitimate public https destination, a host with a working SSRF guard accepted it, and the row recorded `executed-fail` with "the guard is absent (a finding)". Measured on openwop-app production (2026-09-28, suite 2.43.1): the control failed while the same host answers `https://127.0.0.1:…` and `https://localhost:…` with `400 webhook_url_rejected` ("denied (loopback / link-local / private-IP)"). The control now registers `receiver.localUrl` rewritten to `https:`, so a refusal can only come from the loopback deny list and not from a scheme check. Untunnelled runs are unaffected, because the control soft-skips without a tunnel.
|
|
15
|
+
- **The 2.44.4 cycle opens.** Six more core families get a v2 normative home (RFC 0189), which changes the packed `@openwop/spec-artifacts` tree: `memory`, `prompts` and `aiProviders` in `host-services.md`; `limits` in `runs.md`; `artifactTypes` in a new `artifact-type-packs.md`, with field-level rules in `schemas/v2/artifact-type-pack-manifest.schema.json` descriptions. v1-dependent core families: 8 → 3 (`agents`, `budget`, `production`, each blocked on error codes v2 does not register). No scenario changes.
|
|
16
|
+
|
|
3
17
|
## [2.44.3] — 2026-09-28 — fifteen more core families get v2 normative homes, and webhook retry rows record the wait path they measured
|
|
4
18
|
|
|
5
19
|
- **Webhook retry rows say which wait path ran and what they measured** (RFC 0225 witness). A passing dead-letter row carried no detail, so a bundle could not show whether its wait came from the host's advertised `retryPolicy.maxElapsedMs`, an operator-raised cap or the 20 s floor, or when the dead-letter arrived. MyndHyve's 2.44.1 cut was the case: it advertises 600000, and it could not be counted as RFC 0225's witness. Rows driven by `retryWaitFor` now carry an informational detail, on passes too. `0173.webhook-durable-delivery.dead-letter` and `0188.dead-letter-content-free` record, for example, `observed: advertised-bound (maxElapsedMs 600000): waited ≤630000ms; 5 attempt(s), last attempt after 225031ms, sink after 231412ms`. The last attempt is a lower bound on the dead-letter time, and the sink read an upper bound. The retry leg, `v2-webhook-unregister-stops-delivery` and `v2-webhook-message-id-stable` record the path and window. The detail is prefixed `observed: `, never `partial-witness: `, so `check-accepted-predicate` counts the row as a full witness. The bundle already carries `detail` on every row inside the witness digest, so the note is signed. New `lib/row-observation.ts`, attached in `setup.ts`; `retryWaitSelection` / `waitObservation` in `lib/webhook-retry-window.ts`; server-free tests in `row-observation.test.ts` and `webhook-retry-window.test.ts`, each sabotage-checked.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "conformance-approval-reject-loopback",
|
|
3
3
|
"name": "Conformance: Approval (reject loops back)",
|
|
4
4
|
"version": "1.0",
|
|
5
|
-
"description": "openwop#1697. A rejected approval gate routed over an any_failed edge to revise, which loops back to the gate (bounded by settings.maxLoopbackIterations
|
|
5
|
+
"description": "openwop#1697. A rejected approval gate routed over an any_failed edge to revise, which loops back to the gate (the loop is bounded by the scenario, a single reject, not by the host; the scenario passes configurable.run.recursionLimit 20 as a runaway guard; settings.maxLoopbackIterations is authoring metadata, openwop#1748). Rejected once, the gate is visited again: the host MUST raise a new interrupt.requested with a different key and wait for a new decision, never replaying the first rejection. Conformance scenario: v2-approval-reject-disposition.test.ts.",
|
|
6
6
|
"nodes": [
|
|
7
7
|
{ "id": "start", "typeId": "core.noop", "name": "Start", "position": { "x": 0, "y": 0 }, "config": {}, "inputs": {} },
|
|
8
8
|
{
|
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
"id": "conformance-replay-ordinal-loop",
|
|
3
3
|
"name": "Conformance: Replay outcome keyed on the execution ordinal",
|
|
4
4
|
"version": "1.0",
|
|
5
|
-
"description": "openwop#1718. start → effect (a side-effecting node) → wait (core.
|
|
5
|
+
"description": "openwop#1718. start → effect (a side-effecting node) → wait (core.conformance.hold, a reserved PURE node) → back to effect, bounded by the scenario (cancel plus fail-closed), not by the host; the scenario passes configurable.run.recursionLimit 20 as a runaway guard, and settings.maxLoopbackIterations is authoring metadata (openwop#1748), so effect executes twice. The suite cancels the source run inside wait, after effect's first execution completed and before its second, then forks mode:\"replay\" at effect's first node.started. In the fork, effect's first execution resolves from the recorded outcome for n = 1, and its second execution has no recorded outcome for n = 2, so it MUST fail closed with replay_source_missing (replay.md §Suppression rule 2). A host keying on (nodeId, attempt) or selecting the latest outcome resolves the second execution from the first and completes it. core.conformance.side-effect is the reserved side-effecting typeId (see conformance-replay-side-effect). Advertised only by a host that runs cycles and suppresses side effects in replay.",
|
|
6
6
|
"nodes": [
|
|
7
7
|
{ "id": "start", "typeId": "core.noop", "name": "Start", "position": { "x": 0, "y": 0 }, "config": {}, "inputs": {} },
|
|
8
8
|
{ "id": "effect", "typeId": "core.conformance.side-effect", "name": "Side Effect", "position": { "x": 200, "y": 0 }, "config": {}, "inputs": {} },
|
|
9
9
|
{
|
|
10
10
|
"id": "wait",
|
|
11
|
-
"typeId": "core.
|
|
11
|
+
"typeId": "core.conformance.hold",
|
|
12
12
|
"name": "Wait",
|
|
13
13
|
"position": { "x": 400, "y": 0 },
|
|
14
14
|
"config": {},
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
"id": "conformance-replay-side-effect",
|
|
3
3
|
"name": "Conformance: Replay Side-Effect Suppression",
|
|
4
4
|
"version": "1.0",
|
|
5
|
-
"description": "RFC 0140 fixture for `replay-side-effect-suppression.test.ts`. A `core.delay`
|
|
5
|
+
"description": "RFC 0140 fixture for `replay-side-effect-suppression.test.ts`. A `core.conformance.hold` wait (a conformance-reserved PURE node that re-executes live in a replay; it replaced `core.delay` on 2026-09-29, openwop#1769, because the registry declares `core.delay` side-effecting) followed by a node the host classifies as SIDE-EFFECTING. The test starts a run, cancels it mid-delay so the effect node never reaches a terminal outcome, then forks `mode:\"replay\"` — a host advertising `replay.sideEffectSuppression: \"recorded-outcome\"` MUST fail that node closed with `replay_source_missing` (replay.md §\"Side-effect suppression in replay\" requirement 3) rather than executing it. `core.conformance.side-effect` is a conformance-RESERVED typeId (same pattern as `core.conformance.mock-agent`): a host advertising the capability MUST map it to a node its replay classifier treats as side-effecting. The node's actual effect is irrelevant to the assertion — what matters is that the host would refuse to perform it during a replay. Hosts that do not advertise the capability need not supply this fixture; the scenario skips.",
|
|
6
6
|
"nodes": [
|
|
7
7
|
{
|
|
8
8
|
"id": "wait",
|
|
9
|
-
"typeId": "core.
|
|
9
|
+
"typeId": "core.conformance.hold",
|
|
10
10
|
"name": "Wait",
|
|
11
11
|
"position": {
|
|
12
12
|
"x": 0,
|
package/fixtures.md
CHANGED
|
@@ -255,7 +255,7 @@ The `messages`-mode stream fixture (AI token streaming) is covered by the determ
|
|
|
255
255
|
|
|
256
256
|
- **Purpose**: witness that `key` is per visit (`spec/v2/core/interrupt.md` §Re-entry and resume values, openwop#1697).
|
|
257
257
|
- **Inputs**: none.
|
|
258
|
-
- **Graph**: `start` (`core.noop`) → `gate` (`any_success`); `gate` → `revise` (`core.noop`, `any_failed`); `revise` → `gate` (`any_success`). `settings.maxLoopbackIterations: 2
|
|
258
|
+
- **Graph**: `start` (`core.noop`) → `gate` (`any_success`); `gate` → `revise` (`core.noop`, `any_failed`); `revise` → `gate` (`any_success`). The loop is bounded by the scenario (a single reject; cancel plus fail-closed), not by the host. The scenario creates the run with `configurable: { version: 1, run: { recursionLimit: 20 } }` as a runaway guard (`runs.md` §run section). `settings.maxLoopbackIterations: 2` stays in the JSON as authoring metadata; the scenario does not depend on it (openwop#1748).
|
|
259
259
|
- **Behavior**:
|
|
260
260
|
1. Run reaches `gate` and MUST be `waiting-approval`.
|
|
261
261
|
2. Client POSTs `{action: 'reject'}`. `gate` fails with `approval_rejected`, and `revise` runs.
|
|
@@ -306,8 +306,8 @@ The `messages`-mode stream fixture (AI token streaming) is covered by the determ
|
|
|
306
306
|
|
|
307
307
|
### `conformance-replay-ordinal-loop`
|
|
308
308
|
|
|
309
|
-
- **Purpose**: witness that a replay fork keys a side-effecting node's recorded outcome on `(sourceRunId, nodeId, n)`, where `n`
|
|
310
|
-
- **Shape**: `start` (`core.noop`) → `effect` (`core.conformance.side-effect`, `any_success`) → `wait` (`core.
|
|
309
|
+
- **Purpose**: witness that a replay fork keys a side-effecting node's recorded outcome on `(sourceRunId, nodeId, n)`, where `n` is one more than the node's recorded terminals (`node.completed`, `node.failed`) before this execution (`spec/v2/core/replay.md` §Suppression rule 2, openwop#1718, re-corrected by openwop#1769).
|
|
310
|
+
- **Shape**: `start` (`core.noop`) → `effect` (`core.conformance.side-effect`, `any_success`) → `wait` (`core.conformance.hold`) → `effect` (`any_success`), so `effect` executes twice. The loop is bounded by the scenario (a single reject; cancel plus fail-closed), not by the host. The scenario creates the run with `configurable: { version: 1, run: { recursionLimit: 20 } }` as a runaway guard (`runs.md` §run section). `settings.maxLoopbackIterations: 2` stays in the JSON as authoring metadata; the scenario does not depend on it (openwop#1748).
|
|
311
311
|
- **Inputs**: `delayMs` (integer, default 3000), long enough to cancel inside `wait`.
|
|
312
312
|
- **Expected behavior**:
|
|
313
313
|
1. The source run completes `effect`'s first execution. The client cancels it inside `wait`, before the second execution.
|
|
@@ -318,7 +318,7 @@ The `messages`-mode stream fixture (AI token streaming) is covered by the determ
|
|
|
318
318
|
### `conformance-replay-side-effect`
|
|
319
319
|
|
|
320
320
|
- **Purpose**: verify RFC 0140 — a `mode:"replay"` fork MUST NOT re-perform a side-effecting node's effect.
|
|
321
|
-
- **Shape**: `core.
|
|
321
|
+
- **Shape**: `core.conformance.hold` (`wait`) → `core.conformance.side-effect` (`effect`). *(Corrected 2026-09-29, openwop#1769: `wait` was `core.delay`, which the registry's flow pack declares `role: "side-effect"`; a host honouring that floor resolved `wait` from the cancelled source and failed it with `replay_source_missing` before `effect` was reached.)*
|
|
322
322
|
- **`core.conformance.side-effect` is a conformance-RESERVED typeId** (same pattern as `core.conformance.mock-agent`): a host advertising `replay.sideEffectSuppression: "recorded-outcome"` MUST map it to a node its replay classifier treats as side-effecting. The node's actual effect is irrelevant to the assertion — what matters is that the host would refuse to perform it during a replay. Hosts that do not advertise the capability need not supply this fixture; the scenario skips.
|
|
323
323
|
- **Inputs**:
|
|
324
324
|
- `delayMs` (integer, required, 1 ≤ value ≤ 60000) — long enough for the test to cancel before `effect` runs.
|
|
@@ -590,6 +590,7 @@ The fixtures reference these typeIds:
|
|
|
590
590
|
| `core.delay` | delay, cancellable | Sleep `config.delayMs` ms |
|
|
591
591
|
| `core.fail` | failure | Throw with `code: "example.conformance_failure"` (a vendor code under the registered `example` org; errors.md §The registry, openwop#1698), message: "Intentional conformance failure" |
|
|
592
592
|
| `core.approvalGate` | approval | Call `ctx.interrupt({kind: 'approval', ...})` |
|
|
593
|
+
| `core.conformance.hold` | replay-side-effect, replay-ordinal-loop | Conformance-RESERVED (openwop#1769). Completes after `inputs.delayMs`; its outputs are exactly its resolved inputs, `delayMs` included, under the same keys. It performs nothing observable outside the run's own event log. A host advertising a fixture that uses it MUST NOT classify it side-effecting (`replay.md` §Suppression rule 1) and MUST re-execute it live in a replay. |
|
|
593
594
|
| `core.clarificationGate` | clarification | Call `ctx.interrupt({kind: 'clarification', ...})` |
|
|
594
595
|
| `conformance.requiresMissing` | capability-missing | Declares `requires: ['conformance.never-provided']`; engine MUST refuse dispatch. Opt-in fixture registration is recommended so production deployments don't expose the fixture surface. |
|
|
595
596
|
| `conformance.artifact.emit` | artifact-emit | Opt-in (RFC 0205). Produce one artifact of `config.artifactType` with payload `config.data`; emit `artifact.created` naming it; readable through `getArtifact`. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openwop/openwop-conformance",
|
|
3
|
-
"version": "2.44.
|
|
3
|
+
"version": "2.44.5",
|
|
4
4
|
"description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -56,6 +56,6 @@
|
|
|
56
56
|
"@openwop/spec-artifacts": "file:../spec-artifacts"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@openwop/spec-artifacts": "2.44.
|
|
59
|
+
"@openwop/spec-artifacts": "2.44.5"
|
|
60
60
|
}
|
|
61
61
|
}
|
package/requirements.json
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
"generatedFrom": "src/scenarios/*.test.ts",
|
|
4
4
|
"counts": {
|
|
5
5
|
"files": 648,
|
|
6
|
-
"tests":
|
|
7
|
-
"withStableId":
|
|
6
|
+
"tests": 2492,
|
|
7
|
+
"withStableId": 2492,
|
|
8
8
|
"interpolatedTitles": 0,
|
|
9
9
|
"explicitIds": 2372
|
|
10
10
|
},
|
|
@@ -22926,7 +22926,7 @@
|
|
|
22926
22926
|
{
|
|
22927
22927
|
"id": "openwop.it.replay-side-effect-suppression.sideeffectsuppression-when-present-is-one-of-the-two-defined-values",
|
|
22928
22928
|
"file": "replay-side-effect-suppression.test.ts",
|
|
22929
|
-
"line":
|
|
22929
|
+
"line": 103,
|
|
22930
22930
|
"title": "sideEffectSuppression, when present, is one of the two defined values",
|
|
22931
22931
|
"explicitId": "openwop.it.replay-side-effect-suppression.sideeffectsuppression-when-present-is-one-of-the-two-defined-values",
|
|
22932
22932
|
"citations": [
|
|
@@ -22939,7 +22939,7 @@
|
|
|
22939
22939
|
{
|
|
22940
22940
|
"id": "openwop.it.replay-side-effect-suppression.fails-a-side-effecting-node-closed-with-replay-source-missing-when-the-source-ne",
|
|
22941
22941
|
"file": "replay-side-effect-suppression.test.ts",
|
|
22942
|
-
"line":
|
|
22942
|
+
"line": 117,
|
|
22943
22943
|
"title": "fails a side-effecting node CLOSED with replay_source_missing when the source never recorded it",
|
|
22944
22944
|
"explicitId": "openwop.it.replay-side-effect-suppression.fails-a-side-effecting-node-closed-with-replay-source-missing-when-the-source-ne",
|
|
22945
22945
|
"citations": [
|
|
@@ -22974,7 +22974,7 @@
|
|
|
22974
22974
|
{
|
|
22975
22975
|
"id": "openwop.it.replay-side-effect-suppression.the-manifest-still-offers-the-role-value-the-requirement-binds",
|
|
22976
22976
|
"file": "replay-side-effect-suppression.test.ts",
|
|
22977
|
-
"line":
|
|
22977
|
+
"line": 242,
|
|
22978
22978
|
"title": "the manifest still offers the role value the requirement binds",
|
|
22979
22979
|
"explicitId": "openwop.it.replay-side-effect-suppression.the-manifest-still-offers-the-role-value-the-requirement-binds",
|
|
22980
22980
|
"citations": [
|
|
@@ -22988,7 +22988,7 @@
|
|
|
22988
22988
|
{
|
|
22989
22989
|
"id": "openwop.it.replay-side-effect-suppression.replay-md-binds-that-declaration-naming-the-schema-it-comes-from",
|
|
22990
22990
|
"file": "replay-side-effect-suppression.test.ts",
|
|
22991
|
-
"line":
|
|
22991
|
+
"line": 253,
|
|
22992
22992
|
"title": "replay.md binds that declaration, naming the schema it comes from",
|
|
22993
22993
|
"explicitId": "openwop.it.replay-side-effect-suppression.replay-md-binds-that-declaration-naming-the-schema-it-comes-from",
|
|
22994
22994
|
"citations": [
|
|
@@ -23002,7 +23002,7 @@
|
|
|
23002
23002
|
{
|
|
23003
23003
|
"id": "openwop.it.replay-side-effect-suppression.a-throw-is-named-a-backstop-not-a-discharge",
|
|
23004
23004
|
"file": "replay-side-effect-suppression.test.ts",
|
|
23005
|
-
"line":
|
|
23005
|
+
"line": 263,
|
|
23006
23006
|
"title": "a throw is named a backstop, not a discharge",
|
|
23007
23007
|
"explicitId": "openwop.it.replay-side-effect-suppression.a-throw-is-named-a-backstop-not-a-discharge",
|
|
23008
23008
|
"citations": [
|
|
@@ -23016,7 +23016,7 @@
|
|
|
23016
23016
|
{
|
|
23017
23017
|
"id": "openwop.it.replay-side-effect-suppression.the-floor-direction-is-stated-not-left-to-inference",
|
|
23018
23018
|
"file": "replay-side-effect-suppression.test.ts",
|
|
23019
|
-
"line":
|
|
23019
|
+
"line": 287,
|
|
23020
23020
|
"title": "the floor direction is stated, not left to inference",
|
|
23021
23021
|
"explicitId": "openwop.it.replay-side-effect-suppression.the-floor-direction-is-stated-not-left-to-inference",
|
|
23022
23022
|
"citations": [
|
|
@@ -27856,7 +27856,7 @@
|
|
|
27856
27856
|
{
|
|
27857
27857
|
"id": "openwop.it.v2-approval-reject-disposition.a-rejected-gate-with-no-routing-edge-fails-the-run-with-approval-rejected",
|
|
27858
27858
|
"file": "v2-approval-reject-disposition.test.ts",
|
|
27859
|
-
"line":
|
|
27859
|
+
"line": 141,
|
|
27860
27860
|
"title": "a rejected gate with no routing edge fails the run with approval_rejected",
|
|
27861
27861
|
"explicitId": null,
|
|
27862
27862
|
"citations": [
|
|
@@ -27879,7 +27879,7 @@
|
|
|
27879
27879
|
{
|
|
27880
27880
|
"id": "openwop.it.v2-approval-reject-disposition.interrupt-resolved-records-action-reject-and-decision-rejected",
|
|
27881
27881
|
"file": "v2-approval-reject-disposition.test.ts",
|
|
27882
|
-
"line":
|
|
27882
|
+
"line": 152,
|
|
27883
27883
|
"title": "interrupt.resolved records action reject and decision rejected",
|
|
27884
27884
|
"explicitId": null,
|
|
27885
27885
|
"citations": [
|
|
@@ -27898,7 +27898,7 @@
|
|
|
27898
27898
|
{
|
|
27899
27899
|
"id": "openwop.it.v2-approval-reject-disposition.the-gate-node-fails-with-approval-rejected-not-retryable-and-run-failed-names-it",
|
|
27900
27900
|
"file": "v2-approval-reject-disposition.test.ts",
|
|
27901
|
-
"line":
|
|
27901
|
+
"line": 165,
|
|
27902
27902
|
"title": "the gate node fails with approval_rejected, not retryable, and run.failed names it",
|
|
27903
27903
|
"explicitId": null,
|
|
27904
27904
|
"citations": [
|
|
@@ -27919,7 +27919,7 @@
|
|
|
27919
27919
|
{
|
|
27920
27920
|
"id": "openwop.it.v2-approval-reject-disposition.a-majority-reject-decides-the-quorum-gate-on-the-deciding-vote-and-fails-the-run",
|
|
27921
27921
|
"file": "v2-approval-reject-disposition.test.ts",
|
|
27922
|
-
"line":
|
|
27922
|
+
"line": 178,
|
|
27923
27923
|
"title": "a majority reject decides the quorum gate on the deciding vote and fails the run with approval_rejected",
|
|
27924
27924
|
"explicitId": null,
|
|
27925
27925
|
"citations": [
|
|
@@ -27945,7 +27945,7 @@
|
|
|
27945
27945
|
{
|
|
27946
27946
|
"id": "openwop.it.v2-approval-reject-disposition.a-rejected-gate-routed-over-an-any-failed-edge-fails-as-a-node-its-all-success-s",
|
|
27947
27947
|
"file": "v2-approval-reject-disposition.test.ts",
|
|
27948
|
-
"line":
|
|
27948
|
+
"line": 201,
|
|
27949
27949
|
"title": "a rejected gate routed over an any_failed edge fails as a node, its all_success sibling never runs, and the run completes",
|
|
27950
27950
|
"explicitId": null,
|
|
27951
27951
|
"citations": [
|
|
@@ -27971,7 +27971,7 @@
|
|
|
27971
27971
|
{
|
|
27972
27972
|
"id": "openwop.it.v2-approval-reject-disposition.an-unresolved-gate-past-its-timeoutms-with-no-ontimeout-is-resolved-rejected-by",
|
|
27973
27973
|
"file": "v2-approval-reject-disposition.test.ts",
|
|
27974
|
-
"line":
|
|
27974
|
+
"line": 220,
|
|
27975
27975
|
"title": "an unresolved gate past its timeoutMs with no onTimeout is resolved rejected by the host and fails the run naming the gate",
|
|
27976
27976
|
"explicitId": null,
|
|
27977
27977
|
"citations": [
|
|
@@ -27992,7 +27992,7 @@
|
|
|
27992
27992
|
{
|
|
27993
27993
|
"id": "openwop.it.v2-approval-reject-disposition.a-gate-whose-ontimeout-is-approve-still-resolves-rejected-on-timeout-and-fails-t",
|
|
27994
27994
|
"file": "v2-approval-reject-disposition.test.ts",
|
|
27995
|
-
"line":
|
|
27995
|
+
"line": 239,
|
|
27996
27996
|
"title": "a gate whose onTimeout is approve still resolves rejected on timeout and fails the run: a timeout never grants",
|
|
27997
27997
|
"explicitId": null,
|
|
27998
27998
|
"citations": [
|
|
@@ -28014,7 +28014,7 @@
|
|
|
28014
28014
|
{
|
|
28015
28015
|
"id": "openwop.it.v2-approval-reject-disposition.a-rejected-gate-looped-back-to-itself-is-asked-again-under-a-new-key-and-the-fir",
|
|
28016
28016
|
"file": "v2-approval-reject-disposition.test.ts",
|
|
28017
|
-
"line":
|
|
28017
|
+
"line": 257,
|
|
28018
28018
|
"title": "a rejected gate looped back to itself is asked again under a new key, and the first rejection is not replayed",
|
|
28019
28019
|
"explicitId": null,
|
|
28020
28020
|
"citations": [
|
|
@@ -28037,6 +28037,33 @@
|
|
|
28037
28037
|
}
|
|
28038
28038
|
]
|
|
28039
28039
|
},
|
|
28040
|
+
{
|
|
28041
|
+
"id": "openwop.it.v2-approval-reject-disposition.a-replay-fork-of-a-decided-reject-derives-the-rejection-from-the-log-and-never-r",
|
|
28042
|
+
"file": "v2-approval-reject-disposition.test.ts",
|
|
28043
|
+
"line": 333,
|
|
28044
|
+
"title": "a replay fork of a decided reject derives the rejection from the log and never re-decides it (leg A)",
|
|
28045
|
+
"explicitId": null,
|
|
28046
|
+
"citations": [
|
|
28047
|
+
{
|
|
28048
|
+
"section": "spec/v2/core/errors.md §One code per state",
|
|
28049
|
+
"requirement": "a resolve against the replayed gate MUST answer 409 interrupt_already_resolved — the fork holds no open decision"
|
|
28050
|
+
}
|
|
28051
|
+
]
|
|
28052
|
+
},
|
|
28053
|
+
{
|
|
28054
|
+
"id": "openwop.it.v2-approval-reject-disposition.a-replay-fork-of-a-timed-out-gate-derives-the-timeout-rejection-from-the-log-and",
|
|
28055
|
+
"file": "v2-approval-reject-disposition.test.ts",
|
|
28056
|
+
"line": 348,
|
|
28057
|
+
"title": "a replay fork of a timed-out gate derives the timeout rejection from the log and never re-arms it (leg B)",
|
|
28058
|
+
"explicitId": null,
|
|
28059
|
+
"citations": [
|
|
28060
|
+
{
|
|
28061
|
+
"section": null,
|
|
28062
|
+
"requirement": "leg B: the derived resolution is the recorded timeout — action timeout, reason timeout, and the source's resolvedBy (a host subject or absent), never a caller",
|
|
28063
|
+
"interpolated": true
|
|
28064
|
+
}
|
|
28065
|
+
]
|
|
28066
|
+
},
|
|
28040
28067
|
{
|
|
28041
28068
|
"id": "openwop.it.v2-approver-enforced.a-resolver-outside-approverslist-is-refused-at-resolve-time",
|
|
28042
28069
|
"file": "v2-approver-enforced.test.ts",
|
|
@@ -33078,7 +33105,7 @@
|
|
|
33078
33105
|
{
|
|
33079
33106
|
"id": "openwop.it.v2-replay-suppression-ordinal.a-replay-fork-resolves-each-execution-of-a-looped-side-effecting-node-by-its-own",
|
|
33080
33107
|
"file": "v2-replay-suppression-ordinal.test.ts",
|
|
33081
|
-
"line":
|
|
33108
|
+
"line": 74,
|
|
33082
33109
|
"title": "a replay fork resolves each execution of a looped side-effecting node by its own ordinal, failing closed where none was recorded",
|
|
33083
33110
|
"explicitId": "openwop.requirement.replay.suppression-execution-ordinal",
|
|
33084
33111
|
"citations": [
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "Provenance of @openwop/spec-artifacts (RFC 0168 §D.2). files: SHA-256 per file; the conformance suite compares the installed peer against dist/spec-artifacts.lock.json at start.",
|
|
3
3
|
"package": "@openwop/spec-artifacts",
|
|
4
|
-
"version": "2.44.
|
|
5
|
-
"corpusTag": "v2.44.
|
|
4
|
+
"version": "2.44.5",
|
|
5
|
+
"corpusTag": "v2.44.5",
|
|
6
6
|
"files": {
|
|
7
7
|
"api/.redocly.lint-ignore.yaml": "bf5a8350b88a72fa43f59605ed8d903ed24b6cfccda5e45509c9f6ed9ee4e712",
|
|
8
8
|
"api/asyncapi.yaml": "d5ecb9ee6114582be3b1f662c84bfac9ae96dae7bacb853e461168f70a8e1c7d",
|
|
9
9
|
"api/grpc/openwop.proto": "c3e72bb17cba514ee98feb6434e6c9b6ea6795bfd086489ec69fd882dd1ad977",
|
|
10
10
|
"api/openapi.yaml": "69464bc0e1e71ef3b543a73d4fa4f67202343d2603c80af8033f6c3619ef1b30",
|
|
11
11
|
"api/redocly.yaml": "b0604c89b2ca6d5076ec25725c539dad44a741a811fe524439ee6daef8baa09f",
|
|
12
|
-
"api/seams-v2.yaml": "
|
|
13
|
-
"api/v2/asyncapi.yaml": "
|
|
14
|
-
"api/v2/openapi.yaml": "
|
|
12
|
+
"api/seams-v2.yaml": "27a2f887597684e7a1ceeb275970e7c7079650881f238be26bdd8d940dfe5a5e",
|
|
13
|
+
"api/v2/asyncapi.yaml": "9ed411f174d9235d47fae7edee86edb2b4ea51cefffd17179e727b00d2f5fe79",
|
|
14
|
+
"api/v2/openapi.yaml": "b479b1cb2319a60b5d48c471b793646cce843564a447a136eebf12636ec1a75d",
|
|
15
15
|
"api/v2/redocly.yaml": "1e66b60e6118ad11a823bb620678be464d99dfe50a40e3e6f93ec9429b88b34c",
|
|
16
16
|
"schemas/README.md": "0c0b737ffcf8f30e7d2809cec8a498232de710f41443212922ad8337cdde0b51",
|
|
17
17
|
"schemas/a2a-task-state.schema.json": "75d5049dea8bd873ff0e7546f1c60c8d36c219bec7084264360be54a8be30ae1",
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
"schemas/v2/ai-envelope.schema.json": "0dc7dfbf032a37239ffd81fc2d67f999a40782ff5074866a56e414164cdee809",
|
|
113
113
|
"schemas/v2/annotation-create.schema.json": "ec32c22a243b343eeb6956a23be2d76ba5b50c8018ec3ec9d6efa5a1d9b0c792",
|
|
114
114
|
"schemas/v2/annotation.schema.json": "eb4813cdaa8e325412cfcaeeee5c8a28f3751c946dec64e546c7ba0ad3aad8d4",
|
|
115
|
-
"schemas/v2/artifact-type-pack-manifest.schema.json": "
|
|
115
|
+
"schemas/v2/artifact-type-pack-manifest.schema.json": "acbbf14d32da4e0cb81254bbd6cc48fc10083890572044ddef71c59a0f65c185",
|
|
116
116
|
"schemas/v2/artifact.schema.json": "85b9dc284d39b99d2a66fa8cbf47f2dd3d8526d3bd6cc411ad315b372de38975",
|
|
117
117
|
"schemas/v2/audit-verify-result.schema.json": "14af8e9283510f0a40e48a2059c929ee44e0fc9fa3cc66a95bfbe90b17c48297",
|
|
118
118
|
"schemas/v2/budget-policy.schema.json": "c7449daeb6e1d95e7a047b8c2814d3d54748697c27046972834874867d9b5f4c",
|
|
@@ -193,53 +193,54 @@
|
|
|
193
193
|
"schemas/v2/webhook-delivery.schema.json": "4e8ab3b722ad1731adf0548c1217f0de62da40de14e8abf77533aee4ace4c079",
|
|
194
194
|
"schemas/v2/webhook-verification.schema.json": "76e4e0d81143ffd2649bc7adba2aa34f2525e2c4e9f0300a425d908b3163196b",
|
|
195
195
|
"schemas/v2/workflow-chain-pack-manifest.schema.json": "0816c6c59a91452d6fa1931394b766780eaf90949c9f7cd0ced522dadd32ff81",
|
|
196
|
-
"schemas/v2/workflow-definition.schema.json": "
|
|
196
|
+
"schemas/v2/workflow-definition.schema.json": "3f031c0cb1644ac21f203945a0860e04dcea4bd1ab722f280bdcdc788edd0a22",
|
|
197
197
|
"schemas/v2/workload-identity.schema.json": "1f589debfac6d70e0908da511550ec69a27b4bcc8314551cd843b6d923605efc",
|
|
198
198
|
"schemas/v2/workspace-file-create.schema.json": "b6b5260857318c3b67de0231ed15e3a746f525880a763e7b36a7854178dd10d0",
|
|
199
199
|
"schemas/v2/workspace-file.schema.json": "d2ed39e8c229dde33b56905c8df6f8b2ff056c97e1c5f615e3124baa54a6254f",
|
|
200
200
|
"schemas/workflow-chain-pack-manifest.schema.json": "c027571913c44d8d9678750c72701a710a02fa0df55e86849da03e13e95c333a",
|
|
201
|
-
"schemas/workflow-definition.schema.json": "
|
|
201
|
+
"schemas/workflow-definition.schema.json": "fbab3787707fc0e00ecf2adea8de8aba582e6057af9cde893b1b0eb969adea83",
|
|
202
202
|
"schemas/workload-identity.schema.json": "64ed93e12cc9f0657e56ff9b8f045e5d68d397df5276d103c07f42c7f34f0579",
|
|
203
203
|
"schemas/workspace-file-create.schema.json": "70a23b92c2958c0165d7d29aea9020a045dc7816af82c80c5f50d0aac19c4856",
|
|
204
204
|
"schemas/workspace-file.schema.json": "464de85c2a068243084ee9c1d969bc7cd5d8f7948574e58450d6493c38a0e1e4",
|
|
205
205
|
"spec/v1/alias-detectors.json": "29f21e0fb2c7a232a92a5c19b4cba3a86bc3b5e16f06780452f0421c66c6ec38",
|
|
206
206
|
"spec/v1/capability-declaration-classes.json": "e7729aed5c4b4e1dd02abab0530f14cc95f5d4070fe51fb139e7f5cccefa00c6",
|
|
207
|
-
"spec/v1/core-standard-manifest.json": "
|
|
207
|
+
"spec/v1/core-standard-manifest.json": "2a7f88488571ffee4073ce1b7fc77abbfd539b9c493d60a7cd4887317ede9b17",
|
|
208
208
|
"spec/v1/deprecations.json": "46df807fb4adf0aae24671c20f69975cf3496e3bc3a521cb7e6ea556bdce563c",
|
|
209
209
|
"spec/v1/deprecations.schema.json": "3e393c405d2a41b467d8c5e3c468549078df1ce6a6d2588fc488097b95d9b55b",
|
|
210
210
|
"spec/v1/event-codemap.json": "3da60d884157793a360da532a9fcbbfb5285636db325a74cec94b34622186d97",
|
|
211
211
|
"spec/v1/event-codemap.schema.json": "d05933b2e88103aff51a2f774df97b9bda065e0fc88dbd9b114cc47a32189174",
|
|
212
212
|
"spec/v1/extensions.json": "79a60754aa16cbdbdb604f8e5c00038af535a1690a4d07bd5ff7c38f0256a7bc",
|
|
213
|
-
"spec/v1/gaps.json": "
|
|
213
|
+
"spec/v1/gaps.json": "11c35393ad666899fe8a668c45fa69f79d20361ab57e84949dce3bc2f528d5c3",
|
|
214
214
|
"spec/v1/gaps.schema.json": "8fd83259f556553c9df0f53e7a82ca8c2a4771d471197f69b4e2ae0ceeacfd66",
|
|
215
|
-
"spec/v1/migrations.json": "
|
|
215
|
+
"spec/v1/migrations.json": "ca57b7ce335a681aaa78573ce229e9c3d7d75859f03e087ae93bbfc889359127",
|
|
216
216
|
"spec/v1/migrations.schema.json": "886779aa6c22e646db097f5df210adb018a4dd14a7b815465a18c8a7056c8f72",
|
|
217
217
|
"spec/v1/operation-path-manifest.json": "fada0342815fb129c1ae61f1829bd6e34417717408ce86fcc54280f373c19e74",
|
|
218
218
|
"spec/v1/spec-gaps.json": "ea9612af55bea9dae3e82223d4f714243f8f590b0193f1188257b13491a38afc",
|
|
219
|
-
"spec/v2/README.md": "
|
|
219
|
+
"spec/v2/README.md": "ddea90d7cf14aefffe1a43eeeecd4222dadad1d1e5ac64dceb073990eb93d9ef",
|
|
220
|
+
"spec/v2/core/artifact-type-packs.md": "e143bfd6f98d4867855bb57090a6b854e98d4b0a6202b9b3734cb45366f99854",
|
|
220
221
|
"spec/v2/core/capabilities.md": "ad728e8a761153b55d036ece55043ff62d0982121e2e629e5e7cdf560fca3c03",
|
|
221
222
|
"spec/v2/core/conformance.md": "0cfceb926eed87055550a5c23e640210c795fc8cf9dd1b2544db0e2b3842c564",
|
|
222
223
|
"spec/v2/core/connection-packs.md": "1ee129d792c537bfe82393ccb92555d8e6ddc342b1aa781a13f101b27ec38171",
|
|
223
224
|
"spec/v2/core/conversation.md": "354fa57c73d11062e523a44aa75a6353c5c81aefaf306d30c42416a228c82c86",
|
|
224
225
|
"spec/v2/core/errors.md": "6d5a67389de84556f81e2558a30d83959c9283c8c5481ce7c759021e486d84fd",
|
|
225
226
|
"spec/v2/core/events.md": "8a2767691f604b2adf76399edeeaccfc151c697fc9717dc17cf7dd57372a8c22",
|
|
226
|
-
"spec/v2/core/execution.md": "
|
|
227
|
+
"spec/v2/core/execution.md": "299c4d548e16e5a0809957a47cbd34b56a38de82b8c9b3a5d036b2c7abca09c6",
|
|
227
228
|
"spec/v2/core/form-content-packs.md": "02931227899676fd4581a27017be2a6c4953904717be9b81c29d12f87ab3917d",
|
|
228
229
|
"spec/v2/core/headers.md": "5cb1624fa63acc81992b6a6d842ec392cf7271aaf2f65d1f83c8c4fa286e6730",
|
|
229
|
-
"spec/v2/core/host-services.md": "
|
|
230
|
+
"spec/v2/core/host-services.md": "aa8b269e6348bdcb797da1a23c82f9df95c2133a7b947253799f6eaf88ccd3f1",
|
|
230
231
|
"spec/v2/core/i18n.md": "2d5a67bd3b9b956d1144acab2fbc3646cdb50b3621f97d772087eaa953798286",
|
|
231
232
|
"spec/v2/core/idempotency.md": "2084d756789c1faf5121593fb39e67ca36a4c227178e175004742e403178ea49",
|
|
232
233
|
"spec/v2/core/identity.md": "582eddbf2e1bad41911e886bd08524683837045404f7b8f6bceccf61792b2765",
|
|
233
234
|
"spec/v2/core/interop.md": "e72e25dff864c83b2980d7366b34054cffa072af0225821199f260f6228d260e",
|
|
234
|
-
"spec/v2/core/interrupt.md": "
|
|
235
|
+
"spec/v2/core/interrupt.md": "c1e13179e792bf8099b2d1c7891542646bb1d4cdee1757590f190833cae6047c",
|
|
235
236
|
"spec/v2/core/node-pack-runtimes.md": "e1088cb69b8c3c67256c494063328916c2980ab1aa97a7063de25bab82fdb1e5",
|
|
236
237
|
"spec/v2/core/oauth.md": "c96483a44b62978c596945fbb7d7c863cf6056cd980fdb4cf4e97555e3e10b72",
|
|
237
|
-
"spec/v2/core/overview.md": "
|
|
238
|
-
"spec/v2/core/packs.md": "
|
|
238
|
+
"spec/v2/core/overview.md": "d7000fbcd2f59280870fc592f5d5ea28a5195b9eb209029f09c5de59e0f2bf9c",
|
|
239
|
+
"spec/v2/core/packs.md": "935fc2f633c68fa98ad6c9cdb083dd556e955b1a15eff32f4773d13329a57238",
|
|
239
240
|
"spec/v2/core/persistence.md": "711874f74cb08b03cd118d683056db10a063517762fcea73ec70feb5d983e6a4",
|
|
240
241
|
"spec/v2/core/portability.md": "87c4327f674f714fda6217fd79b99d2ba219ad47dc02f9751bb56c200fb2c17a",
|
|
241
|
-
"spec/v2/core/replay.md": "
|
|
242
|
-
"spec/v2/core/runs.md": "
|
|
242
|
+
"spec/v2/core/replay.md": "e7f935c215cae84d58da9e6babcb37ce0afe6217c453d1f50199d5ebd288a5cf",
|
|
243
|
+
"spec/v2/core/runs.md": "8a1d71fb9305fd0df31150bfbbd892f96b8aab553400a36196212f7d0a24906d",
|
|
243
244
|
"spec/v2/core/security-defaults.md": "153884da7a2d54c30c81b5e649cfdef31e151b3a2a19dee1bc5a16c35d60a3aa",
|
|
244
245
|
"spec/v2/core/storage.md": "598ad4f3775c297c698a20daf72a7d15a6836c1231ddbca5961865c704f765b5",
|
|
245
246
|
"spec/v2/core/tool-catalog.md": "d16a9004418719c48301804aefa4d7ab6f41112a4ed83727f552aa1b92b418b0",
|
|
@@ -248,7 +249,7 @@
|
|
|
248
249
|
"spec/v2/core/workflow-chain-packs.md": "fdce7a8477e269950a2bac05ee6eddbd66eb0b670b464d00054f7196ec05d29c",
|
|
249
250
|
"spec/v2/corrections.json": "59f647f4d22477f6c2b06dcf913b682dde5d51dd5e607a5d0cf648ab60864d6e",
|
|
250
251
|
"spec/v2/corrections.schema.json": "46bbffb954d13bd9995aebc912ceeb2544f7064331abf25fd6ab85c81a5cec94",
|
|
251
|
-
"spec/v2/declaration.json": "
|
|
252
|
+
"spec/v2/declaration.json": "edace8c4ec7e32aba44828f459e2ae8933dc88ec8129f16549bca6279e3ca24c",
|
|
252
253
|
"spec/v2/declaration.schema.json": "ef951633e4070899f64828f5cf1b5f4870270a0360f81dbb49b5b4d6cd407550",
|
|
253
254
|
"spec/v2/errors.json": "fa650cbbdf25a140fdaf82ec5269f95b4585ac4bdde1fa6b0b3ffe48a3f7943c",
|
|
254
255
|
"spec/v2/errors.schema.json": "47404ad53e6147cf9017b813483712466573e419fb33e6c4b6c12d3e7deba1bc",
|
|
@@ -266,11 +267,11 @@
|
|
|
266
267
|
"spec/v2/ext/kanban/README.md": "bfc386dc0f874a4afa1e3ec6a2f99084b547dcce9bd363da3b8ebd8786b9c184",
|
|
267
268
|
"spec/v2/ext/knowledge/README.md": "cbe1f945a757791dedac0fd542a68c86c5b457778b831d58d9ae44a2baa270f2",
|
|
268
269
|
"spec/v2/ext/launchStudio/README.md": "a941ef1d2401438a412eaea48d2d6ca807a032b44ea08719fea33120daf088a0",
|
|
269
|
-
"spec/v2/ext/messaging/README.md": "
|
|
270
|
+
"spec/v2/ext/messaging/README.md": "ee2d19fb6f6c2de11d889e966a41e6d1ff34272771689a81d7de039105a5be03",
|
|
270
271
|
"spec/v2/ext/portability/README.md": "c59423ee446638b748a911b9e97e803c4786157be6120f656f0a060586bc7b82",
|
|
271
272
|
"spec/v2/ext/provider-idempotency/README.md": "9d1bccef0ec16a19a3370ae79c2f5a1726ce0d1d25a51352907f288a2ccb15d7",
|
|
272
273
|
"spec/v2/ext/provider-idempotency/registry.json": "5b5fecad604fb4ab38da39f5abe697a4e57b05406c398f8afeb377331b2bd965",
|
|
273
|
-
"spec/v2/ext/restTransport/README.md": "
|
|
274
|
+
"spec/v2/ext/restTransport/README.md": "bd7207683b2fbcead78bdb7d474fa723e08adad500185ea10804fd4e99541c0c",
|
|
274
275
|
"spec/v2/ext/sandbox-runtime-notes/README.md": "0d1290fb1e20b2e35c0b15051541ab6546e4e2a6f693b4f912cb3f7d6a65e92c",
|
|
275
276
|
"spec/v2/ext/webResearch/README.md": "b675e6f6646128d781b661f754c2393eed86e5935d16c3a22df582a5ce025b9b",
|
|
276
277
|
"spec/v2/facets/a2a.schema.json": "b68bf5e29e455e46c2ad7cfcccf5963730ec2310ff9db81884550d8df30fd6ef",
|
|
@@ -300,9 +301,9 @@
|
|
|
300
301
|
"spec/v2/path-manifest.json": "17298a245c5da9a70aa7ab0b7f2db4277e06209d6fcdd7187a8b4cea179c08f3",
|
|
301
302
|
"spec/v2/peer-dependency-aliases.json": "d10299280abee08258502925bc327293ee413e0108cd6e6ec75ff6110653308d",
|
|
302
303
|
"spec/v2/profiles.json": "180beb9ef2d161766fa23b2987e7e829851e3d316273147cf6a4a3f324befd6b",
|
|
303
|
-
"spec/v2/release.json": "
|
|
304
|
+
"spec/v2/release.json": "ec7e5ac7845e1c7ff67f0f522f5897db65bf840c7787768ac0324095baad2e2f",
|
|
304
305
|
"spec/v2/retention-floors.json": "eaf3722d95c79947af1d4269ef85117e126518c588cfcf1a2b21b97269f51624",
|
|
305
|
-
"spec/v2/surface-baseline.json": "
|
|
306
|
+
"spec/v2/surface-baseline.json": "ca7310e7b970f4cd5629e20718d77ee64427a04c7aba2e963bbfd43500ba1521"
|
|
306
307
|
},
|
|
307
|
-
"corpusCommit": "
|
|
308
|
+
"corpusCommit": "e84e633368000323a54ddd6913cadcabafb268c6"
|
|
308
309
|
}
|
|
@@ -34,7 +34,9 @@
|
|
|
34
34
|
*
|
|
35
35
|
* ## Construction
|
|
36
36
|
*
|
|
37
|
-
* `conformance-replay-side-effect` is `core.
|
|
37
|
+
* `conformance-replay-side-effect` is `core.conformance.hold` (a reserved PURE
|
|
38
|
+
* wait; it was `core.delay`, which the registry declares side-effecting,
|
|
39
|
+
* openwop#1769) → a side-effecting node. We
|
|
38
40
|
* start it with a long delay, cancel mid-flight so the effect node never
|
|
39
41
|
* records a terminal outcome, then fork `mode:"replay"`. Requirement 3 of
|
|
40
42
|
* `replay.md` §"Side-effect suppression in replay" then applies to that node.
|
|
@@ -145,7 +147,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('replay-side-effect-suppression: a replay does
|
|
|
145
147
|
// side-effecting node downstream never reaches a terminal outcome.
|
|
146
148
|
//
|
|
147
149
|
// Kept SHORT on purpose: a `replay` fork re-executes from seq 0, so it
|
|
148
|
-
// re-runs this
|
|
150
|
+
// re-runs this hold too (`core.conformance.hold` is pure). A 30s delay
|
|
149
151
|
// here would make the fork outlive any sane poll budget — measured, not
|
|
150
152
|
// theorised: the first version of this scenario used 30s and timed out
|
|
151
153
|
// at step 3 with the fork still `running`.
|
|
@@ -182,7 +184,7 @@ describe.skipIf(SKIP_NO_FIXTURE)('replay-side-effect-suppression: a replay does
|
|
|
182
184
|
}
|
|
183
185
|
expect(fork.status, req('openwop.it.replay-side-effect-suppression.fails-a-side-effecting-node-closed-with-replay-source-missing-when-the-source-ne', 'replay.md §"Side-effect suppression in replay" requirement 1', 'fork should be accepted')).toBe(201);
|
|
184
186
|
const forkRunId = (fork.json as { runId: string }).runId;
|
|
185
|
-
await pollUntilTerminal(forkRunId, { timeoutMs: 45_000 }); // re-runs the
|
|
187
|
+
await pollUntilTerminal(forkRunId, { timeoutMs: 45_000 }); // re-runs the pure hold before reaching `effect`
|
|
186
188
|
|
|
187
189
|
// 3. THE assertion. A non-suppressing host executes the node; a suppressing
|
|
188
190
|
// host cannot, and has no recorded outcome to serve, so it must fail
|
|
@@ -33,6 +33,21 @@
|
|
|
33
33
|
* rejection is not replayed). `inapplicable` on a host that does not
|
|
34
34
|
* advertise the fixture, i.e. one that does not run cycles.
|
|
35
35
|
*
|
|
36
|
+
* 9. `openwop.requirement.0223.replay-derives-rejection` (openwop#1756, suite
|
|
37
|
+
* 2.44.4; gated on `replay` advertising mode `replay` and on each leg's
|
|
38
|
+
* fixture): a `replay` fork taken at the gate's recorded `interrupt.resolved` (the
|
|
39
|
+
* prefix holds its request and suspension) derives
|
|
40
|
+
* the rejection from the recorded `interrupt.resolved` and never
|
|
41
|
+
* re-decides it. The fork raises no new `interrupt.requested` for the
|
|
42
|
+
* gate (the discriminator: a re-deciding host re-raises and waits), records
|
|
43
|
+
* an `interrupt.resolved` whose `decision` / `action` / `reason` equal the
|
|
44
|
+
* source's, fails the same way (`approval_rejected`, `failedNodeId`, a
|
|
45
|
+
* non-retryable `node.failed`), and answers a resolve with `409
|
|
46
|
+
* interrupt_already_resolved`. Leg A replays a decided reject
|
|
47
|
+
* (`conformance-approval`); leg B replays a timeout
|
|
48
|
+
* (`conformance-approval-timeout-approve`), whose `resolvedBy` must match the source's (a host subject or absent).
|
|
49
|
+
* `interruptId` equality is not asserted: a host may re-mint ids.
|
|
50
|
+
*
|
|
36
51
|
* The legs are separate rows so a host that misses one field fails that row
|
|
37
52
|
* only. Legs 5 and 6 each need their own fixture, because `conformance-approval`
|
|
38
53
|
* has no edges and no timer and widening a fixture every host serves would force
|
|
@@ -98,8 +113,8 @@ const errorCode = (o: unknown): string | undefined => (o as { code?: unknown } |
|
|
|
98
113
|
const resolve = (runId: string, resumeValue: unknown): Promise<OpenWOPResponse | null> => http(() => driver.post(`/runs/${enc(runId)}/interrupts/${enc(NODE_ID)}`, { resumeValue }));
|
|
99
114
|
|
|
100
115
|
/** Create a run of `fixture` and wait for it to suspend on its gate. */
|
|
101
|
-
async function suspended(fixture: string): Promise<{ runId: string } | { reason: string }> {
|
|
102
|
-
const res = await http(() => driver.post('/runs', { workflowId: fixture }));
|
|
116
|
+
async function suspended(fixture: string, extra: Record<string, unknown> = {}): Promise<{ runId: string } | { reason: string }> {
|
|
117
|
+
const res = await http(() => driver.post('/runs', { workflowId: fixture, ...extra }));
|
|
103
118
|
if (res === null) return { reason: 'POST /runs unreachable (fetch failed)' };
|
|
104
119
|
const runId = (res.json as { runId?: unknown } | null)?.runId;
|
|
105
120
|
if (res.status !== 201 || typeof runId !== 'string') return { reason: `POST /runs (${fixture}) answered ${res.status} ${readErrorCode(res.json) ?? ''}`.trim() };
|
|
@@ -242,7 +257,8 @@ describe('RFC 0223 — v2-approval-reject-disposition (gated on interrupt + conf
|
|
|
242
257
|
it('a rejected gate looped back to itself is asked again under a new key, and the first rejection is not replayed', async () => {
|
|
243
258
|
if (!(await gateFamily('interrupt'))) return softSkip('inapplicable', 'interrupt family not advertised (gate recorded under openwop.family.interrupt)');
|
|
244
259
|
if (!isFixtureAdvertised(LOOPBACK)) return softSkip('inapplicable', `fixture ${LOOPBACK} is not advertised — the host does not run cycles`);
|
|
245
|
-
|
|
260
|
+
// The loop is bounded by the scenario (one reject), not by the host; recursionLimit is a runaway guard (openwop#1748).
|
|
261
|
+
const s = await suspended(LOOPBACK, { configurable: { version: 1, run: { recursionLimit: 20 } } });
|
|
246
262
|
if ('reason' in s) return softSkip('blocked', s.reason);
|
|
247
263
|
const res = await resolve(s.runId, { action: 'reject' });
|
|
248
264
|
if (res === null || res.status < 200 || res.status >= 300) return softSkip('blocked', `the reject resolve answered ${res?.status ?? 'nothing'} ${readErrorCode(res?.json) ?? ''} — interrupt resolution owns that contract`);
|
|
@@ -259,4 +275,91 @@ describe('RFC 0223 — v2-approval-reject-disposition (gated on interrupt + conf
|
|
|
259
275
|
expect(evs.filter((e) => e.type === 'interrupt.resolved').length, req(id, 'spec/v2/core/interrupt.md §Re-entry and resume values', 'an earlier visit\'s resumeValue MUST NOT be returned: only the one caller resolve is recorded')).toBe(1);
|
|
260
276
|
await http(() => driver.post(`/runs/${enc(s.runId)}/cancel`, {}));
|
|
261
277
|
}, 45_000);
|
|
278
|
+
|
|
279
|
+
// ── openwop#1756: replay derives the rejection, never re-decides it ──────────
|
|
280
|
+
const REPLAY_ID = R('replay-derives-rejection');
|
|
281
|
+
const REPLAY_DOC = `${DOC}, last bullet; spec/v2/core/replay.md §Determinism caveats rule 2 (short-circuit, raising no new interrupt.requested)`;
|
|
282
|
+
interface SeqEv extends Ev { readonly sequence?: number }
|
|
283
|
+
async function debugLog(runId: string): Promise<SeqEv[]> {
|
|
284
|
+
const res = await http(() => driver.get(`/runs/${enc(runId)}/events/poll?timeout=1&streamMode=debug`));
|
|
285
|
+
const evs = ((res?.json as { events?: SeqEv[] } | null)?.events ?? []).filter((e) => typeof e.sequence === 'number');
|
|
286
|
+
return evs.sort((a, b) => (a.sequence as number) - (b.sequence as number));
|
|
287
|
+
}
|
|
288
|
+
const onGate = (e: SeqEv): boolean => (e.payload?.['nodeId'] ?? e.nodeId) === NODE_ID;
|
|
289
|
+
async function replayGate(): Promise<string | null> {
|
|
290
|
+
const fam = await gateFamily('replay');
|
|
291
|
+
if (!fam) return 'replay family not advertised (gate recorded under openwop.family.replay)';
|
|
292
|
+
const modes = Array.isArray(fam['modes']) ? (fam['modes'] as unknown[]) : [];
|
|
293
|
+
return modes.includes('replay') ? null : `replay.modes ${JSON.stringify(modes)} does not include replay`;
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Run → the source's own disposition → a `replay` fork at the gate's recorded
|
|
297
|
+
* `interrupt.resolved`: the prefix holds the gate's request and suspension, and the
|
|
298
|
+
* fork must supply the resolution. A deriving host takes the recorded one (replay.md
|
|
299
|
+
* §Determinism 2); a re-deciding host raises a NEW `interrupt.requested` and waits.
|
|
300
|
+
* Forking at the gate's `node.started` instead would let a host re-emit the recorded
|
|
301
|
+
* request as history, which cannot be told apart from a new one.
|
|
302
|
+
*/
|
|
303
|
+
async function replayedRejection(fixture: string, decide: ((runId: string) => Promise<string | null>) | null, settleMs: number): Promise<{ source: SeqEv[]; fromSeq: number; forkId: string; forkStatus: string | null; fork: SeqEv[] } | { reason: string }> {
|
|
304
|
+
const s = await suspended(fixture);
|
|
305
|
+
if ('reason' in s) return s;
|
|
306
|
+
if (decide !== null) { const why = await decide(s.runId); if (why !== null) return { reason: why }; }
|
|
307
|
+
const terminal = await waitStatus(s.runId, TERMINAL, settleMs);
|
|
308
|
+
if (terminal?.['status'] !== 'failed') return { reason: `the source run did not end failed (status ${String(terminal?.['status'])}) — the reject rows own that contract` };
|
|
309
|
+
const source = await debugLog(s.runId);
|
|
310
|
+
const at = source.find((e) => e.type === 'interrupt.resolved' && onGate(e));
|
|
311
|
+
if (at?.sequence === undefined) return { reason: 'the source log has no interrupt.resolved for the gate to fork at' };
|
|
312
|
+
const fork = await http(() => driver.post(`/runs/${enc(s.runId)}:fork`, { mode: 'replay', fromSeq: at.sequence }));
|
|
313
|
+
const forkId = (fork?.json as { runId?: unknown } | null)?.runId;
|
|
314
|
+
if (fork === null || fork.status !== 201 || typeof forkId !== 'string') return { reason: `POST /runs/{runId}:fork answered ${fork?.status ?? 'nothing'} ${readErrorCode(fork?.json) ?? ''} — forkRun owns that contract`.trim() };
|
|
315
|
+
const settled = await waitStatus(forkId, new Set([...TERMINAL, 'waiting-approval']), scaledTimeoutMs(20_000));
|
|
316
|
+
return { source, fromSeq: at.sequence, forkId, forkStatus: settled === null ? null : String(settled['status']), fork: await debugLog(forkId) };
|
|
317
|
+
}
|
|
318
|
+
function assertDerived(r: { source: SeqEv[]; fromSeq: number; forkId: string; forkStatus: string | null; fork: SeqEv[] }, leg: string): void {
|
|
319
|
+
const reRaised = r.fork.filter((e) => (e.sequence as number) >= r.fromSeq && (e.type === 'interrupt.requested' || e.type === 'approval.requested') && onGate(e));
|
|
320
|
+
expect(reRaised.length, req(REPLAY_ID, REPLAY_DOC, `${leg}: the replay fork MUST NOT raise a new interrupt for the gate — the rejection is derived from the recorded interrupt.resolved, never re-decided (status ${String(r.forkStatus)})`)).toBe(0);
|
|
321
|
+
const pick = (e: SeqEv | undefined): Record<string, unknown> => ({ decision: e?.payload?.['decision'], action: e?.payload?.['action'], reason: e?.payload?.['reason'] });
|
|
322
|
+
const srcResolved = r.source.find((e) => e.type === 'interrupt.resolved' && onGate(e));
|
|
323
|
+
const forkResolved = r.fork.find((e) => e.type === 'interrupt.resolved' && onGate(e));
|
|
324
|
+
expect(pick(forkResolved), req(REPLAY_ID, REPLAY_DOC, `${leg}: the fork's interrupt.resolved for the gate MUST carry the source's decision, action and reason`)).toEqual(pick(srcResolved));
|
|
325
|
+
const runFailed = r.fork.find((e) => e.type === 'run.failed');
|
|
326
|
+
const nodeFailed = r.fork.find((e) => e.type === 'node.failed' && onGate(e));
|
|
327
|
+
expect(
|
|
328
|
+
{ status: r.forkStatus, code: errorCode(runFailed?.payload?.['error']), failedNodeId: runFailed?.payload?.['failedNodeId'], retryable: (nodeFailed?.payload?.['error'] as { retryable?: unknown } | undefined)?.retryable },
|
|
329
|
+
req(REPLAY_ID, REPLAY_DOC, `${leg}: the fork MUST fail as the source did — failed, run.failed approval_rejected naming the gate, and a non-retryable node.failed`),
|
|
330
|
+
).toEqual({ status: 'failed', code: CODE, failedNodeId: NODE_ID, retryable: false });
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
it('a replay fork of a decided reject derives the rejection from the log and never re-decides it (leg A)', async () => {
|
|
334
|
+
if (!(await gateFamily('interrupt'))) return softSkip('inapplicable', 'interrupt family not advertised (gate recorded under openwop.family.interrupt)');
|
|
335
|
+
const noReplay = await replayGate();
|
|
336
|
+
if (noReplay !== null) return softSkip('inapplicable', noReplay);
|
|
337
|
+
if (!isFixtureAdvertised(FIXTURE)) return softSkip('inapplicable', `fixture ${FIXTURE} is not advertised`);
|
|
338
|
+
const r = await replayedRejection(FIXTURE, async (runId) => {
|
|
339
|
+
const res = await resolve(runId, { action: 'reject' });
|
|
340
|
+
return res === null || res.status < 200 || res.status >= 300 ? `the reject resolve answered ${res?.status ?? 'nothing'} ${readErrorCode(res?.json) ?? ''}` : null;
|
|
341
|
+
}, 10_000);
|
|
342
|
+
if ('reason' in r) return softSkip('blocked', r.reason);
|
|
343
|
+
const again = await resolve(r.forkId, { action: 'accept' });
|
|
344
|
+
assertDerived(r, 'leg A (decided reject)');
|
|
345
|
+
expect({ status: again?.status, code: readErrorCode(again?.json) }, req(REPLAY_ID, 'spec/v2/core/errors.md §One code per state', 'a resolve against the replayed gate MUST answer 409 interrupt_already_resolved — the fork holds no open decision')).toEqual({ status: 409, code: 'interrupt_already_resolved' });
|
|
346
|
+
}, 90_000);
|
|
347
|
+
|
|
348
|
+
it('a replay fork of a timed-out gate derives the timeout rejection from the log and never re-arms it (leg B)', async () => {
|
|
349
|
+
if (!(await gateFamily('interrupt'))) return softSkip('inapplicable', 'interrupt family not advertised (gate recorded under openwop.family.interrupt)');
|
|
350
|
+
const noReplay = await replayGate();
|
|
351
|
+
if (noReplay !== null) return softSkip('inapplicable', noReplay);
|
|
352
|
+
if (!isFixtureAdvertised(TIMEOUT_APPROVE)) return softSkip('inapplicable', `fixture ${TIMEOUT_APPROVE} is not advertised`);
|
|
353
|
+
const r = await replayedRejection(TIMEOUT_APPROVE, null, scaledTimeoutMs(20_000));
|
|
354
|
+
if ('reason' in r) return softSkip('blocked', r.reason);
|
|
355
|
+
assertDerived(r, 'leg B (timeout)');
|
|
356
|
+
// resolvedBy on a timeout row is the host's subject or absent (RFC 0183 §Unresolved 1), so
|
|
357
|
+
// the leg asserts the fork carries the SOURCE's value, not that it is absent.
|
|
358
|
+
const forkResolved = r.fork.find((e) => e.type === 'interrupt.resolved' && onGate(e));
|
|
359
|
+
const srcResolved = r.source.find((e) => e.type === 'interrupt.resolved' && onGate(e));
|
|
360
|
+
expect(
|
|
361
|
+
{ action: forkResolved?.payload?.['action'], reason: forkResolved?.payload?.['reason'], resolvedBy: JSON.stringify(forkResolved?.payload?.['resolvedBy'] ?? null) },
|
|
362
|
+
req(REPLAY_ID, REPLAY_DOC, 'leg B: the derived resolution is the recorded timeout — action timeout, reason timeout, and the source\'s resolvedBy (a host subject or absent), never a caller'),
|
|
363
|
+
).toEqual({ action: 'timeout', reason: 'timeout', resolvedBy: JSON.stringify(srcResolved?.payload?.['resolvedBy'] ?? null) });
|
|
364
|
+
}, 120_000);
|
|
262
365
|
});
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `spec/v2/core/replay.md` §Suppression rule 2 — a replay fork resolves a
|
|
3
|
-
* side-effecting node's outcome keyed on `(sourceRunId, nodeId, n)`, where `n`
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* side-effecting node's outcome keyed on `(sourceRunId, nodeId, n)`, where `n` is
|
|
4
|
+
* one more than the node's recorded terminals (`node.completed`, `node.failed`)
|
|
5
|
+
* before this execution, the fork's inherited prefix included (openwop#1718,
|
|
6
|
+
* re-corrected by openwop#1769 from a count of `node.started`, which a pause or a
|
|
7
|
+
* suspend re-emits; suite 2.43.2, target major 2).
|
|
7
8
|
*
|
|
8
9
|
* The rule said `(nodeId, attempt)`. In a run where a node executes more than
|
|
9
10
|
* once (a loop back over an edge), that key names no single execution: a host
|
|
@@ -11,8 +12,8 @@
|
|
|
11
12
|
* outcome, and one selecting the latest recorded outcome does the same when
|
|
12
13
|
* only one exists.
|
|
13
14
|
*
|
|
14
|
-
* Construction (`conformance-replay-ordinal-loop`): start → effect → wait
|
|
15
|
-
* effect, so `effect` executes twice. The suite cancels the source inside
|
|
15
|
+
* Construction (`conformance-replay-ordinal-loop`): start → effect → wait
|
|
16
|
+
* (`core.conformance.hold`, a reserved PURE node that re-executes live) → effect, so `effect` executes twice. The suite cancels the source inside
|
|
16
17
|
* `wait`, after `effect`'s first execution completed and before its second, and
|
|
17
18
|
* forks `mode: "replay"` at `effect`'s first `node.started`. In the fork:
|
|
18
19
|
* - `effect` execution 1 (n = 1) has a recorded outcome and resolves from it;
|
|
@@ -74,7 +75,7 @@ describe('v2 replay-suppression-ordinal (replay.md §Suppression rule 2, openwop
|
|
|
74
75
|
if (!(await gateFamily('replay'))) return softSkip('inapplicable', 'replay family not advertised (gate recorded under openwop.family.replay)');
|
|
75
76
|
if (!isFixtureAdvertised(FIXTURE)) return softSkip('inapplicable', `fixture ${FIXTURE} is not advertised — the host does not run cycles with a side-effecting node`);
|
|
76
77
|
|
|
77
|
-
const created = await http(() => driver.post('/runs', { workflowId: FIXTURE, inputs: { delayMs: 3000 } }));
|
|
78
|
+
const created = await http(() => driver.post('/runs', { workflowId: FIXTURE, inputs: { delayMs: 3000 }, configurable: { version: 1, run: { recursionLimit: 20 } } }));
|
|
78
79
|
const runId = (created?.json as { runId?: unknown } | null)?.runId;
|
|
79
80
|
if (created === null || created.status !== 201 || typeof runId !== 'string') return softSkip('blocked', `POST /runs (${FIXTURE}) answered ${created?.status ?? 'nothing'} ${readErrorCode(created?.json) ?? ''}`.trim());
|
|
80
81
|
|
|
@@ -453,8 +453,18 @@ describe('webhook-signed-delivery: end-to-end HMAC v1', () => {
|
|
|
453
453
|
activeServer = receiver;
|
|
454
454
|
|
|
455
455
|
const ownedTenant = await discoverOwnedTenant(driver);
|
|
456
|
+
// The LOOPBACK address, over https so the refusal can only come from the
|
|
457
|
+
// loopback deny list and not from a scheme check. `receiver.url` is the
|
|
458
|
+
// wrong value here: since 2.37.0 `startScopedReceiver` returns the PUBLIC
|
|
459
|
+
// FRONT as `url` whenever OPENWOP_WEBHOOK_RECEIVER_URL is set (the tunnel,
|
|
460
|
+
// plus this exercise's nonce). So this control registered the tunnel, a
|
|
461
|
+
// host with a working guard correctly accepted it, and the control failed
|
|
462
|
+
// on a conformant host. Measured on openwop-app production 2026-09-28: the
|
|
463
|
+
// same host answers `https://127.0.0.1:…` and `https://localhost:…` with
|
|
464
|
+
// 400 webhook_url_rejected ("denied (loopback / link-local / private-IP)").
|
|
465
|
+
const loopback = receiver.localUrl.replace(/^http:/, 'https:');
|
|
456
466
|
const reg = await driver.post('/v1/webhooks', {
|
|
457
|
-
url:
|
|
467
|
+
url: loopback, // deliberately the raw loopback listener, NOT the tunnel
|
|
458
468
|
events: ['run.completed'],
|
|
459
469
|
...(ownedTenant ? { tenantId: ownedTenant } : {}),
|
|
460
470
|
});
|