@openwop/openwop-conformance 1.136.0 → 1.136.2
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/fixtures/conformance-agent-memory-cross-tenant.json +41 -8
- package/fixtures/conformance-agent-memory-ttl.json +35 -7
- package/fixtures.md +2 -2
- package/package.json +1 -1
- package/schemas/CORPUS-STAMP.json +2 -2
- package/schemas/workflow-chain-pack-manifest.schema.json +17 -3
- package/schemas/workflow-definition.schema.json +18 -4
- package/src/scenarios/agentMemoryCrossTenantIsolation.test.ts +29 -7
- package/src/scenarios/agentMemoryTtlExpiry.test.ts +18 -0
- package/src/scenarios/compensation-profile.test.ts +8 -0
|
@@ -1,28 +1,61 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "conformance-agent-memory-cross-tenant",
|
|
3
3
|
"name": "Conformance: Agent Memory Cross-Tenant Isolation (CTI-1)",
|
|
4
|
-
"version": "1.
|
|
5
|
-
"description": "Phase 3 / CTI-1.
|
|
4
|
+
"version": "1.1",
|
|
5
|
+
"description": "Phase 3 / CTI-1. TWO-SIDED since 2026-08-17 (S35): the `core.identity` node with `memoryAction: \"cross-tenant-probe\"` MUST (1) write one entry under the run's OWN tenant at `agent.memoryRef` and land its id in `ownerEntryId`, (2) list that owner memoryRef and land the result in `ownerProbe` — a NON-EMPTY array containing the written entry (the positive control: proves the adapter is really being exercised), and (3) issue the deliberately-cross-tenant probe at `config.probeMemoryRef` (a path owned by ANOTHER tenant) and land the result in `crossTenantProbe`, which MUST be exactly `[]` (or `null`) — never another tenant's entries, and never left unset. A host may instead fail the run with a security-related error at step 3. Before S35 the scenario accepted an unset `crossTenantProbe`, so a host that ignored `memoryAction` entirely passed a critical-tier invariant vacuously. See agentMemoryCrossTenantIsolation.test.ts.",
|
|
6
6
|
"nodes": [
|
|
7
7
|
{
|
|
8
8
|
"id": "cross-tenant-probe",
|
|
9
9
|
"typeId": "core.identity",
|
|
10
10
|
"name": "Cross-Tenant Memory Probe",
|
|
11
|
-
"position": {
|
|
11
|
+
"position": {
|
|
12
|
+
"x": 0,
|
|
13
|
+
"y": 0
|
|
14
|
+
},
|
|
12
15
|
"config": {
|
|
13
16
|
"memoryAction": "cross-tenant-probe",
|
|
14
17
|
"probeMemoryRef": "conformance/another-tenant/agent-memory"
|
|
15
18
|
},
|
|
16
|
-
"inputs": {}
|
|
19
|
+
"inputs": {},
|
|
20
|
+
"agent": {
|
|
21
|
+
"agentId": "core.conformance.cti-agent",
|
|
22
|
+
"memoryRef": "conformance/agent-memory-cti"
|
|
23
|
+
}
|
|
17
24
|
}
|
|
18
25
|
],
|
|
19
26
|
"edges": [],
|
|
20
27
|
"triggers": [
|
|
21
|
-
{
|
|
28
|
+
{
|
|
29
|
+
"id": "manual",
|
|
30
|
+
"type": "manual",
|
|
31
|
+
"enabled": true
|
|
32
|
+
}
|
|
22
33
|
],
|
|
23
34
|
"variables": [
|
|
24
|
-
{
|
|
35
|
+
{
|
|
36
|
+
"name": "ownerEntryId",
|
|
37
|
+
"type": "string"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"name": "ownerProbe",
|
|
41
|
+
"type": "array"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"name": "crossTenantProbe",
|
|
45
|
+
"type": "array"
|
|
46
|
+
}
|
|
25
47
|
],
|
|
26
|
-
"metadata": {
|
|
27
|
-
|
|
48
|
+
"metadata": {
|
|
49
|
+
"tags": [
|
|
50
|
+
"conformance",
|
|
51
|
+
"multi-agent",
|
|
52
|
+
"phase-3",
|
|
53
|
+
"memory",
|
|
54
|
+
"cti-1",
|
|
55
|
+
"security"
|
|
56
|
+
]
|
|
57
|
+
},
|
|
58
|
+
"settings": {
|
|
59
|
+
"timeout": 10000
|
|
60
|
+
}
|
|
28
61
|
}
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "conformance-agent-memory-ttl",
|
|
3
3
|
"name": "Conformance: Agent Memory TTL Expiry",
|
|
4
|
-
"version": "1.
|
|
5
|
-
"description": "Phase 3. Writes two memory entries — one with `expiresAt` in the past, one in the future — then lists
|
|
4
|
+
"version": "1.1",
|
|
5
|
+
"description": "Phase 3. Writes two memory entries under `agent.memoryRef` — one with `expiresAt` in the past, one in the future — lands their ids in `expiredId` / `freshId`, then lists into `memoryList`. TWO-SIDED since 2026-08-17 (S35): the list MUST contain the entry whose id is `freshId` and MUST NOT contain the entry whose id is `expiredId`; before S35 an empty list (a host that filtered everything, or wrote nothing) passed. See agentMemoryTtlExpiry.test.ts.",
|
|
6
6
|
"nodes": [
|
|
7
7
|
{
|
|
8
8
|
"id": "memory-ttl-probe",
|
|
9
9
|
"typeId": "core.identity",
|
|
10
10
|
"name": "Memory TTL Probe",
|
|
11
|
-
"position": {
|
|
11
|
+
"position": {
|
|
12
|
+
"x": 0,
|
|
13
|
+
"y": 0
|
|
14
|
+
},
|
|
12
15
|
"config": {
|
|
13
16
|
"memoryAction": "ttl-probe"
|
|
14
17
|
},
|
|
@@ -21,11 +24,36 @@
|
|
|
21
24
|
],
|
|
22
25
|
"edges": [],
|
|
23
26
|
"triggers": [
|
|
24
|
-
{
|
|
27
|
+
{
|
|
28
|
+
"id": "manual",
|
|
29
|
+
"type": "manual",
|
|
30
|
+
"enabled": true
|
|
31
|
+
}
|
|
25
32
|
],
|
|
26
33
|
"variables": [
|
|
27
|
-
{
|
|
34
|
+
{
|
|
35
|
+
"name": "freshId",
|
|
36
|
+
"type": "string"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "expiredId",
|
|
40
|
+
"type": "string"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "memoryList",
|
|
44
|
+
"type": "array"
|
|
45
|
+
}
|
|
28
46
|
],
|
|
29
|
-
"metadata": {
|
|
30
|
-
|
|
47
|
+
"metadata": {
|
|
48
|
+
"tags": [
|
|
49
|
+
"conformance",
|
|
50
|
+
"multi-agent",
|
|
51
|
+
"phase-3",
|
|
52
|
+
"memory",
|
|
53
|
+
"ttl"
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
"settings": {
|
|
57
|
+
"timeout": 10000
|
|
58
|
+
}
|
|
31
59
|
}
|
package/fixtures.md
CHANGED
|
@@ -90,9 +90,9 @@ All fixtures MUST advertise:
|
|
|
90
90
|
| Multi-Agent Handoff (child) | `conformance-multi-agent-handoff-child` | RFC 0037 (`version: 1`) — child for `conformance-multi-agent-handoff`. Declares `childOutcome.defaultValue='handoff-complete'`; the parent's outputMapping harvests it onto `parentResult`, triggering the `output.harvested` transition event. | `completed` | ≤ 5s |
|
|
91
91
|
| Multi-Agent Confidence Escalation | `conformance-multi-agent-confidence-escalation` | RFC 0039 §A (`version: 2`) — exercises the confidence-floor escalation contract. Supervisor's `mockDispatchPlan` carries ONE decision with `confidence: 0.3` (below the 0.5 spec floor). The host MUST emit `core.workflowChain.confidence-escalated` AND suspend with a clarification interrupt BEFORE any dispatch.began fires; conformance asserts zero `core.workflowChain.event` records (no dispatch). Capability-gated on `capabilities.multiAgent.executionModel.version >= 2`. | `waiting-clarification` | ≤ 30s |
|
|
92
92
|
| Agent Memory Round-Trip | `conformance-agent-memory-roundtrip` | Phase 3 — `MemoryAdapter.list/get` write → read | `completed` | ≤ 15s |
|
|
93
|
-
| Agent Memory Cross-Tenant | `conformance-agent-memory-cross-tenant` | Phase 3 / CTI-1 — cross-tenant probe MUST
|
|
93
|
+
| Agent Memory Cross-Tenant | `conformance-agent-memory-cross-tenant` | Phase 3 / CTI-1 — TWO-SIDED (S35, 2026-08-17): owner write+list lands `ownerEntryId` + non-empty `ownerProbe` (positive control); the cross-tenant probe at `config.probeMemoryRef` lands `crossTenantProbe`, which MUST be exactly `[]` / `null` and MUST be set | `completed` | ≤ 10s |
|
|
94
94
|
| Agent Memory Redaction | `conformance-agent-memory-redaction` | Phase 3 / SR-1 — BYOK plaintext surfaces as `[REDACTED:<id>]` on read | `completed` | ≤ 15s |
|
|
95
|
-
| Agent Memory TTL | `conformance-agent-memory-ttl` | Phase 3 — `expiresAt` excludes expired entries from list/get
|
|
95
|
+
| Agent Memory TTL | `conformance-agent-memory-ttl` | Phase 3 — `expiresAt` excludes expired entries from list/get; TWO-SIDED (S35, 2026-08-17): host lands `freshId` / `expiredId`, `memoryList` MUST include the fresh id and exclude the expired id | `completed` | ≤ 10s |
|
|
96
96
|
| Agent Memory Injection Budget | `conformance-agent-memory-injection-budget` | RFC 0113 — token-budgeted `MemoryAdapter.list` (`tokenBudget`/`rank`/`query`): cumulative tokens ≤ budget, over-budget single entry omitted, SR-1 + CTI-1 re-asserted on the budgeted path; `rank:'relevance'` delegates to `memory.search` semantic | `completed` | ≤ 15s |
|
|
97
97
|
| Context-Budget Multi-Turn | `conformance-context-budget-multiturn` | RFC 0111 — multi-iteration `core.orchestrator.supervisor` loop whose per-turn transcript grows; a host bounding it exposes `{ tokenCounter, tokenCount, eventIds, summarizedRanges }` via the `GET /v1/host/sample/agent/transcript-window` seam (`tokenCount ≤ transcriptTokenBudget`, internally consistent, keepLastTurns verbatim); the summarization leg replays and asserts `context.summarized` → `summaryRef` reuse. Reference hosts that do not advertise `contextBudget` soft-skip | `completed` | ≤ 60s |
|
|
98
98
|
| Conversation Lifecycle | `conformance-conversation-lifecycle` | Phase 4 — open → exchange → close event ordering | `completed` | ≤ 20s |
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_comment": "Provenance of this vendored schemas/ copy. See conformance/README.md \u00a7\"Resolving the contract\". Compare against the stamp in your installed @openwop/openwop-conformance to detect a stale hand-copied contract.",
|
|
3
|
-
"suiteVersion": "1.136.
|
|
4
|
-
"corpusCommit": "
|
|
3
|
+
"suiteVersion": "1.136.2",
|
|
4
|
+
"corpusCommit": "4123dfae532f80231763fc43f8d3df3022c58c3d"
|
|
5
5
|
}
|
|
@@ -488,6 +488,10 @@
|
|
|
488
488
|
"requiresApproval": {
|
|
489
489
|
"type": "boolean",
|
|
490
490
|
"description": "RFC 0151 §C — an inverse action can itself be harmful (RFC 0147 R9), so it may be gated behind the same approval surface as a forward effect."
|
|
491
|
+
},
|
|
492
|
+
"waiveRequiresApproval": {
|
|
493
|
+
"type": "boolean",
|
|
494
|
+
"description": "RFC 0151 §B/§E (S36, 2026-08-17) — OPTIONAL. When true, ABANDONING this inverse action (§E `skip with justification` or `terminate as uncompensated`) requires an RFC 0051 approval to have resolved `approved` first — if a human must authorize RUNNING an inverse, a human must authorize deciding it will NEVER run. DEFAULT = the obligation's EFFECTIVE `requiresApproval` (after the policy's `approvalScope` escalation, §B) — NOT false — so the field is purely additive: no existing document changes meaning. Stamped onto the obligation at MINT time like `requiresApproval` (a mid-flight redefinition cannot change who had to authorize). Does NOT apply to `substitute` (still an attempt to undo; a substitution is a new planVersion whose own `requiresApproval` governs it). No policy-level counterpart: because the default inherits the effective value, an `approvalScope` escalation escalates waives with it."
|
|
491
495
|
}
|
|
492
496
|
}
|
|
493
497
|
},
|
|
@@ -498,11 +502,21 @@
|
|
|
498
502
|
},
|
|
499
503
|
"additionalProperties": false,
|
|
500
504
|
"if": {
|
|
501
|
-
"properties": {
|
|
502
|
-
|
|
505
|
+
"properties": {
|
|
506
|
+
"irreversibleEffect": {
|
|
507
|
+
"const": true
|
|
508
|
+
}
|
|
509
|
+
},
|
|
510
|
+
"required": [
|
|
511
|
+
"irreversibleEffect"
|
|
512
|
+
]
|
|
503
513
|
},
|
|
504
514
|
"then": {
|
|
505
|
-
"not": {
|
|
515
|
+
"not": {
|
|
516
|
+
"required": [
|
|
517
|
+
"compensation"
|
|
518
|
+
]
|
|
519
|
+
}
|
|
506
520
|
}
|
|
507
521
|
},
|
|
508
522
|
"FragmentEdge": {
|
|
@@ -291,6 +291,10 @@
|
|
|
291
291
|
"requiresApproval": {
|
|
292
292
|
"type": "boolean",
|
|
293
293
|
"description": "RFC 0151 \u00a7C \u2014 an inverse action can itself be harmful (RFC 0147 R9), so it may be gated behind the same approval surface as a forward effect."
|
|
294
|
+
},
|
|
295
|
+
"waiveRequiresApproval": {
|
|
296
|
+
"type": "boolean",
|
|
297
|
+
"description": "RFC 0151 \u00a7B/\u00a7E (S36, 2026-08-17) \u2014 OPTIONAL. When true, ABANDONING this inverse action (\u00a7E `skip with justification` or `terminate as uncompensated`) requires an RFC 0051 approval to have resolved `approved` first \u2014 if a human must authorize RUNNING an inverse, a human must authorize deciding it will NEVER run. DEFAULT = the obligation's EFFECTIVE `requiresApproval` (after the policy's `approvalScope` escalation, \u00a7B) \u2014 NOT false \u2014 so the field is purely additive: no existing document changes meaning. Stamped onto the obligation at MINT time like `requiresApproval` (a mid-flight redefinition cannot change who had to authorize). Does NOT apply to `substitute` (still an attempt to undo; a substitution is a new planVersion whose own `requiresApproval` governs it). No policy-level counterpart: because the default inherits the effective value, an `approvalScope` escalation escalates waives with it."
|
|
294
298
|
}
|
|
295
299
|
}
|
|
296
300
|
},
|
|
@@ -301,11 +305,21 @@
|
|
|
301
305
|
},
|
|
302
306
|
"additionalProperties": false,
|
|
303
307
|
"if": {
|
|
304
|
-
"properties": {
|
|
305
|
-
|
|
308
|
+
"properties": {
|
|
309
|
+
"irreversibleEffect": {
|
|
310
|
+
"const": true
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
"required": [
|
|
314
|
+
"irreversibleEffect"
|
|
315
|
+
]
|
|
306
316
|
},
|
|
307
317
|
"then": {
|
|
308
|
-
"not": {
|
|
318
|
+
"not": {
|
|
319
|
+
"required": [
|
|
320
|
+
"compensation"
|
|
321
|
+
]
|
|
322
|
+
}
|
|
309
323
|
}
|
|
310
324
|
},
|
|
311
325
|
"WorkflowEdge": {
|
|
@@ -828,7 +842,7 @@
|
|
|
828
842
|
},
|
|
829
843
|
"compensation": {
|
|
830
844
|
"$ref": "compensation-policy.schema.json",
|
|
831
|
-
"description": "RFC 0151
|
|
845
|
+
"description": "RFC 0151 \u00a7B \u2014 the workflow-level compensation policy (WHEN an unwind starts and HOW it runs). OPTIONAL and capability-gated: a host that advertises `capabilities.compensation` MUST validate it at registration; a host that does not MUST refuse the workflow with `capability_required` rather than accept a policy it will never honour. Node-level `nodes[].compensation` declarations remain valid without it \u2014 they describe the inverse action; this key requests the unwind."
|
|
832
846
|
}
|
|
833
847
|
}
|
|
834
848
|
}
|
|
@@ -40,15 +40,37 @@ describe.skipIf(SKIP)('agentMemoryCrossTenantIsolation: CTI-1 invariant', () =>
|
|
|
40
40
|
if (terminal.status === 'completed') {
|
|
41
41
|
const snap = await driver.get(`/v1/runs/${encodeURIComponent(runId)}`);
|
|
42
42
|
const body = snap.json as { variables?: Record<string, unknown> };
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
//
|
|
43
|
+
const vars = body.variables ?? {};
|
|
44
|
+
|
|
45
|
+
// S35 (2026-08-17): POSITIVE CONTROL first. Before this, the only assertion
|
|
46
|
+
// was "crossTenantProbe is empty or falsy" — a host whose core.identity node
|
|
47
|
+
// ignored `config.memoryAction` entirely left the variable undefined and
|
|
48
|
+
// PASSED a critical-tier invariant vacuously (measured by openwop-app H49).
|
|
49
|
+
// The owner-side write+list proves the MemoryAdapter is really exercised.
|
|
50
|
+
const ownerProbe = vars.ownerProbe;
|
|
51
|
+
expect(
|
|
52
|
+
Array.isArray(ownerProbe) && ownerProbe.length > 0,
|
|
53
|
+
driver.describe('agent-memory.md §CTI-1 / fixtures.md conformance-agent-memory-cross-tenant', 'ownerProbe MUST be a NON-EMPTY array — the run\'s own tenant can read the entry it just wrote (positive control; an unset or empty ownerProbe means the probe never ran)'),
|
|
54
|
+
).toBe(true);
|
|
55
|
+
const ownerEntryId = vars.ownerEntryId;
|
|
56
|
+
if (typeof ownerEntryId === 'string' && ownerEntryId.length > 0) {
|
|
57
|
+
expect(
|
|
58
|
+
(ownerProbe as Array<{ id?: unknown }>).some((e) => e && typeof e === 'object' && e.id === ownerEntryId),
|
|
59
|
+
driver.describe('agent-memory.md §CTI-1', `ownerProbe MUST contain the entry the host reports writing (ownerEntryId=${ownerEntryId})`),
|
|
60
|
+
).toBe(true);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Cross-tenant list MUST return exactly [] (or null); never another
|
|
64
|
+
// tenant's entries, and never left unset.
|
|
65
|
+
const crossTenantResult = vars.crossTenantProbe;
|
|
66
|
+
expect(
|
|
67
|
+
crossTenantResult !== undefined,
|
|
68
|
+
driver.describe('agent-memory.md §CTI-1', 'crossTenantProbe MUST be set — an unset variable means the cross-tenant probe was never issued, which proves nothing'),
|
|
69
|
+
).toBe(true);
|
|
48
70
|
if (Array.isArray(crossTenantResult)) {
|
|
49
|
-
expect(crossTenantResult.length).toBe(0);
|
|
71
|
+
expect(crossTenantResult.length, driver.describe('agent-memory.md §CTI-1', 'a cross-tenant memoryRef probe MUST return [] — no entries of another tenant')).toBe(0);
|
|
50
72
|
} else {
|
|
51
|
-
expect(crossTenantResult).
|
|
73
|
+
expect(crossTenantResult, driver.describe('agent-memory.md §CTI-1', 'a non-array cross-tenant probe result MUST be null')).toBeNull();
|
|
52
74
|
}
|
|
53
75
|
}
|
|
54
76
|
});
|
|
@@ -44,5 +44,23 @@ describe.skipIf(SKIP)('agentMemoryTtlExpiry: expired entries are excluded from l
|
|
|
44
44
|
expect(new Date(e.expiresAt).getTime()).toBeGreaterThan(now);
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
// S35 (2026-08-17): TWO-SIDED. `[]` used to pass (a host that filtered
|
|
48
|
+
// everything, or wrote nothing and returned []). The fixture now lands the
|
|
49
|
+
// ids of both entries; the fresh one MUST be present, the expired one MUST
|
|
50
|
+
// be absent.
|
|
51
|
+
const freshId = body.variables?.freshId;
|
|
52
|
+
const expiredId = body.variables?.expiredId;
|
|
53
|
+
expect(
|
|
54
|
+
typeof freshId === 'string' && freshId.length > 0 && typeof expiredId === 'string' && expiredId.length > 0,
|
|
55
|
+
driver.describe('fixtures.md conformance-agent-memory-ttl', 'the host MUST land freshId and expiredId (the ids of the two entries it wrote) so the list can be checked on both sides'),
|
|
56
|
+
).toBe(true);
|
|
57
|
+
expect(
|
|
58
|
+
listResult!.some((e) => e.id === freshId),
|
|
59
|
+
driver.describe('agent-memory.md §TTL', `list() MUST include the fresh entry (freshId=${String(freshId)})`),
|
|
60
|
+
).toBe(true);
|
|
61
|
+
expect(
|
|
62
|
+
listResult!.some((e) => e.id === expiredId),
|
|
63
|
+
driver.describe('agent-memory.md §TTL', `list() MUST NOT include the expired entry (expiredId=${String(expiredId)})`),
|
|
64
|
+
).toBe(false);
|
|
47
65
|
});
|
|
48
66
|
});
|
|
@@ -124,6 +124,14 @@ describe('RFC 0151 §B — node compensation declaration', () => {
|
|
|
124
124
|
expect(ok, JSON.stringify(validate.errors)).toBe(true);
|
|
125
125
|
});
|
|
126
126
|
|
|
127
|
+
it('waiveRequiresApproval (S36) is an OPTIONAL boolean in the closed §B block — absent is valid (default = effective requiresApproval), non-boolean is refused', () => {
|
|
128
|
+
const decl = { nodeTypeId: 'vendor.shop.release', requiresApproval: true };
|
|
129
|
+
expect(validate({ ...base, compensation: { ...decl, waiveRequiresApproval: false } }), JSON.stringify(validate.errors)).toBe(true);
|
|
130
|
+
expect(validate({ ...base, compensation: { ...decl, waiveRequiresApproval: true } }), JSON.stringify(validate.errors)).toBe(true);
|
|
131
|
+
expect(validate({ ...base, compensation: { ...decl } }), 'absent MUST validate — the default is the obligation\'s effective requiresApproval, so no existing document changes meaning').toBe(true);
|
|
132
|
+
expect(validate({ ...base, compensation: { ...decl, waiveRequiresApproval: 'yes' } }), 'compensation.md §B: a plain boolean, not a policy language').toBe(false);
|
|
133
|
+
});
|
|
134
|
+
|
|
127
135
|
it('a node without compensation stays valid', () => {
|
|
128
136
|
// The profile is optional. Absent MUST NOT become a validation error, or
|
|
129
137
|
// every existing workflow in the corpus breaks.
|