@openwop/openwop-conformance 1.12.0 → 1.14.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 +23 -0
- package/README.md +2 -2
- package/api/openapi.yaml +60 -0
- package/coverage.md +18 -6
- package/fixtures/wasm-sandbox/isolation-global.wasm +0 -0
- package/fixtures/wasm-sandbox/isolation-global.wat +6 -0
- package/fixtures/wasm-sandbox/misbehaving-capability-gate.wasm +0 -0
- package/fixtures/wasm-sandbox/misbehaving-capability-gate.wat +4 -0
- package/fixtures/wasm-sandbox/misbehaving-env.wasm +0 -0
- package/fixtures/wasm-sandbox/misbehaving-env.wat +4 -0
- package/fixtures/wasm-sandbox/misbehaving-fs.wasm +0 -0
- package/fixtures/wasm-sandbox/misbehaving-fs.wat +4 -0
- package/fixtures/wasm-sandbox/misbehaving-memory.wasm +0 -0
- package/fixtures/wasm-sandbox/misbehaving-memory.wat +5 -0
- package/fixtures/wasm-sandbox/misbehaving-network.wasm +0 -0
- package/fixtures/wasm-sandbox/misbehaving-network.wat +4 -0
- package/fixtures/wasm-sandbox/misbehaving-process.wasm +0 -0
- package/fixtures/wasm-sandbox/misbehaving-process.wat +4 -0
- package/fixtures/wasm-sandbox/misbehaving-timeout.wasm +0 -0
- package/fixtures/wasm-sandbox/misbehaving-timeout.wat +4 -0
- package/fixtures/wasm-sandbox/well-behaved-echo.wasm +0 -0
- package/fixtures/wasm-sandbox/well-behaved-echo.wat +2 -0
- package/fixtures/wasm-sandbox/well-behaved-host-fetch.wasm +0 -0
- package/fixtures/wasm-sandbox/well-behaved-host-fetch.wat +3 -0
- package/package.json +1 -1
- package/src/lib/agentDeployment.ts +117 -0
- package/src/lib/agentEval.ts +83 -0
- package/src/lib/discovery-capabilities.ts +18 -19
- package/src/lib/egressPolicy.ts +76 -0
- package/src/lib/profiles.ts +15 -0
- package/src/lib/sandbox-timeout-worker.mjs +31 -0
- package/src/lib/toolCatalog.ts +81 -0
- package/src/lib/wasm-sandbox-probe.ts +168 -0
- package/src/scenarios/agent-deployment-lifecycle.test.ts +147 -0
- package/src/scenarios/agent-eval-run.test.ts +145 -0
- package/src/scenarios/core-standard-profile.test.ts +75 -0
- package/src/scenarios/egress-audience-binding.test.ts +81 -0
- package/src/scenarios/egress-decision-content-free.test.ts +57 -0
- package/src/scenarios/multi-agent-confidence-escalation.test.ts +12 -7
- package/src/scenarios/prompt-resolution-chain-event.test.ts +113 -0
- package/src/scenarios/sandbox-wasm-isolation.test.ts +98 -0
- package/src/scenarios/sandbox-wasm-timeout.test.ts +40 -0
- package/src/scenarios/tool-catalog-projection.test.ts +120 -0
- package/src/scenarios/tool-session-lifecycle.test.ts +105 -0
- package/src/scenarios/workspace-cross-tenant-isolation-blackbox.test.ts +89 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent deployment lifecycle — the §E promotion contract + §B channel pin
|
|
3
|
+
* (RFC 0082) — behavioral.
|
|
4
|
+
*
|
|
5
|
+
* Capability-gated on `agents.deployment.supported` (root-first per RFC 0073).
|
|
6
|
+
* Soft-skips when unadvertised (default) / hard-fails under
|
|
7
|
+
* `OPENWOP_REQUIRE_BEHAVIOR=true`. The always-on wire-shape coverage lives in
|
|
8
|
+
* `agent-deployment-shape.test.ts`; this asserts host BEHAVIOR via the
|
|
9
|
+
* `POST /v1/host/sample/agents/deployment-transition` seam + the test event-log
|
|
10
|
+
* seam + the NORMATIVE `GET /v1/agents/{agentId}/deployments` read:
|
|
11
|
+
*
|
|
12
|
+
* 1. PROMOTE (§E) — authorize → approvalGate → eval-verify → a content-free
|
|
13
|
+
* `deployment.promoted` with `toState` in the seven-state vocabulary; the
|
|
14
|
+
* returned record validates against `agent-deployment.schema.json`.
|
|
15
|
+
* 2. FAIL-CLOSED (§E-1, `deployment-promotion-fail-closed`) — a principal
|
|
16
|
+
* lacking `deploy:promote` is denied (`allowed !== true`) and emits NO
|
|
17
|
+
* `deployment.promoted`.
|
|
18
|
+
* 3. EVAL-GATE-UNMET (§E-3) — a promote whose `evalRunId` has `passed:false`
|
|
19
|
+
* is denied with `eval_gate_unmet` and emits NO `deployment.promoted`.
|
|
20
|
+
* 4. CHANNEL PIN (§B) — a `@channel`-bound run records the resolved version as
|
|
21
|
+
* `resolvedAgentVersion` on `agent.invocation.started` (the recorded fact a
|
|
22
|
+
* replay re-reads rather than re-resolving).
|
|
23
|
+
*
|
|
24
|
+
* Each leg soft-skips independently (seam absent / event-log seam absent).
|
|
25
|
+
*
|
|
26
|
+
* Spec references:
|
|
27
|
+
* - https://github.com/openwop/openwop/blob/main/spec/v1/agent-deployment.md (§B/§E)
|
|
28
|
+
* - https://github.com/openwop/openwop/blob/main/RFCS/0082-agent-deployment-lifecycle.md
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
import { describe, it, expect } from 'vitest';
|
|
32
|
+
import { readFileSync } from 'node:fs';
|
|
33
|
+
import { join } from 'node:path';
|
|
34
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
35
|
+
import addFormats from 'ajv-formats';
|
|
36
|
+
import { driver } from '../lib/driver.js';
|
|
37
|
+
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
38
|
+
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
39
|
+
import {
|
|
40
|
+
readDeploymentCap,
|
|
41
|
+
driveDeploymentTransition,
|
|
42
|
+
DEPLOYMENT_STATES,
|
|
43
|
+
DEPLOYMENT_CONTENT_FORBIDDEN,
|
|
44
|
+
} from '../lib/agentDeployment.js';
|
|
45
|
+
import { queryTestEvents, isEventLogSeamAvailable, resetTestSeam } from '../lib/event-log-query.js';
|
|
46
|
+
|
|
47
|
+
function loadSchema(name: string): Record<string, unknown> {
|
|
48
|
+
return JSON.parse(readFileSync(join(SCHEMAS_DIR, name), 'utf8')) as Record<string, unknown>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function expectContentFree(payload: Record<string, unknown>, where: string): void {
|
|
52
|
+
for (const f of DEPLOYMENT_CONTENT_FORBIDDEN) {
|
|
53
|
+
expect(
|
|
54
|
+
!(f in payload),
|
|
55
|
+
driver.describe('RFC 0082 §D (deployment-event-no-content-leak)', `${where} MUST be content-free (no ${f})`),
|
|
56
|
+
).toBe(true);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('agent-deployment-lifecycle (RFC 0082 §B/§E)', () => {
|
|
61
|
+
it('promotes via the eval+RBAC+approval gate, fails closed without scope/eval, and pins the channel version', async () => {
|
|
62
|
+
const cap = await readDeploymentCap();
|
|
63
|
+
if (!behaviorGate('openwop-deployment-lifecycle', cap?.supported === true)) return;
|
|
64
|
+
if (!(await isEventLogSeamAvailable())) return; // event-log seam absent — soft-skip
|
|
65
|
+
|
|
66
|
+
const ajv = new Ajv2020({ strict: false, allErrors: true });
|
|
67
|
+
addFormats(ajv);
|
|
68
|
+
const validateRecord = ajv.compile(loadSchema('agent-deployment.schema.json'));
|
|
69
|
+
|
|
70
|
+
// ---- Leg 1: eval+RBAC+approval-gated promotion (§E) ------------------
|
|
71
|
+
const promote = await driveDeploymentTransition({ scenario: 'promote' });
|
|
72
|
+
if (promote === null) return; // deployment seam unwired — soft-skip the whole behavioral suite
|
|
73
|
+
|
|
74
|
+
if (promote.record) {
|
|
75
|
+
expect(
|
|
76
|
+
validateRecord(promote.record),
|
|
77
|
+
driver.describe(
|
|
78
|
+
'agent-deployment.schema.json',
|
|
79
|
+
`a promoted deployment record MUST validate (${ajv.errorsText(validateRecord.errors)})`,
|
|
80
|
+
),
|
|
81
|
+
).toBe(true);
|
|
82
|
+
}
|
|
83
|
+
if (promote.runId) {
|
|
84
|
+
const pq = await queryTestEvents(promote.runId, { type: 'deployment.promoted' });
|
|
85
|
+
if (pq.ok) {
|
|
86
|
+
for (const e of pq.events) {
|
|
87
|
+
expectContentFree(e.payload, 'deployment.promoted');
|
|
88
|
+
expect(
|
|
89
|
+
typeof e.payload.toState === 'string' && DEPLOYMENT_STATES.includes(e.payload.toState as string),
|
|
90
|
+
driver.describe('run-event-payloads.schema.json#/$defs/deploymentPromoted', 'toState MUST be in the seven-state vocabulary'),
|
|
91
|
+
).toBe(true);
|
|
92
|
+
expect(
|
|
93
|
+
typeof e.payload.toVersion === 'string' && (e.payload.toVersion as string).length > 0,
|
|
94
|
+
driver.describe('agent-deployment.md §D', 'deployment.promoted MUST carry the promoted toVersion'),
|
|
95
|
+
).toBe(true);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// ---- Leg 2: fail-closed authz (§E-1; deployment-promotion-fail-closed) -
|
|
101
|
+
const unauth = await driveDeploymentTransition({ scenario: 'unauthorized' });
|
|
102
|
+
if (unauth && unauth.runId) {
|
|
103
|
+
expect(
|
|
104
|
+
unauth.allowed !== true,
|
|
105
|
+
driver.describe('agent-deployment.md §E-1', 'a principal without deploy:promote MUST be denied (fail-closed)'),
|
|
106
|
+
).toBe(true);
|
|
107
|
+
const uq = await queryTestEvents(unauth.runId, { type: 'deployment.promoted' });
|
|
108
|
+
if (uq.ok) {
|
|
109
|
+
expect(
|
|
110
|
+
uq.events.length === 0,
|
|
111
|
+
driver.describe('SECURITY invariant deployment-promotion-fail-closed', 'a denied transition MUST emit NO deployment.promoted'),
|
|
112
|
+
).toBe(true);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ---- Leg 3: eval-gate-unmet denial (§E-3) ----------------------------
|
|
117
|
+
const evalUnmet = await driveDeploymentTransition({ scenario: 'eval-gate-unmet' });
|
|
118
|
+
if (evalUnmet && evalUnmet.runId) {
|
|
119
|
+
expect(
|
|
120
|
+
evalUnmet.error === 'eval_gate_unmet' || evalUnmet.allowed !== true,
|
|
121
|
+
driver.describe('agent-deployment.md §E-3', 'a promote whose eval evidence has passed:false MUST be denied (eval_gate_unmet)'),
|
|
122
|
+
).toBe(true);
|
|
123
|
+
const eq = await queryTestEvents(evalUnmet.runId, { type: 'deployment.promoted' });
|
|
124
|
+
if (eq.ok) {
|
|
125
|
+
expect(
|
|
126
|
+
eq.events.length === 0,
|
|
127
|
+
driver.describe('agent-deployment.md §E-3', 'an unmet eval gate MUST emit NO deployment.promoted'),
|
|
128
|
+
).toBe(true);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---- Leg 4: channel-resolution pin (§B) ------------------------------
|
|
133
|
+
const pin = await driveDeploymentTransition({ scenario: 'channel-pin', channel: 'stable' });
|
|
134
|
+
if (pin && pin.runId) {
|
|
135
|
+
const iq = await queryTestEvents(pin.runId, { type: 'agent.invocation.started' });
|
|
136
|
+
if (iq.ok && iq.events.length > 0) {
|
|
137
|
+
const started = iq.events.sort((a, b) => a.sequence - b.sequence)[0]!;
|
|
138
|
+
expect(
|
|
139
|
+
typeof started.payload.resolvedAgentVersion === 'string' && (started.payload.resolvedAgentVersion as string).length > 0,
|
|
140
|
+
driver.describe('agent-deployment.md §B', 'a @channel-bound run MUST record resolvedAgentVersion on agent.invocation.started (the recorded fact a replay re-reads)'),
|
|
141
|
+
).toBe(true);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
await resetTestSeam();
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent eval-run — the `mode:"eval"` projection (RFC 0081 §B/§C) — behavioral.
|
|
3
|
+
*
|
|
4
|
+
* Capability-gated on `agents.evalSuite.supported` (root-first per RFC 0073).
|
|
5
|
+
* Soft-skips when unadvertised (default) / hard-fails under
|
|
6
|
+
* `OPENWOP_REQUIRE_BEHAVIOR=true`. The always-on wire-shape coverage lives in
|
|
7
|
+
* `agent-eval-suite-shape.test.ts`; this asserts host BEHAVIOR via the
|
|
8
|
+
* `POST /v1/host/sample/agents/eval-run` seam + the test event-log seam + the
|
|
9
|
+
* NORMATIVE `GET /v1/runs/{runId}/eval-summary` read:
|
|
10
|
+
*
|
|
11
|
+
* 1. ORDERING (§C) — an eval run emits `eval.started` FIRST, one `eval.scored`
|
|
12
|
+
* per task, then `eval.completed` once (count == eval.completed.taskCount).
|
|
13
|
+
* 2. CONTENT-FREE (SR-1 / `eval-summary-no-content-leak`) — every `eval.scored`
|
|
14
|
+
* carries scores / ids / scalars ONLY (never task output / rubric / prose);
|
|
15
|
+
* `score` ∈ 0..1; `passed` is a boolean.
|
|
16
|
+
* 3. NORMATIVE SUMMARY (§C) — `GET /v1/runs/{runId}/eval-summary` returns a
|
|
17
|
+
* schema-valid `EvalSummary` whose `passedCount <= taskCount` and whose
|
|
18
|
+
* task entries carry no output body.
|
|
19
|
+
*
|
|
20
|
+
* Each leg soft-skips independently (seam absent / event-log seam absent).
|
|
21
|
+
*
|
|
22
|
+
* Spec references:
|
|
23
|
+
* - https://github.com/openwop/openwop/blob/main/spec/v1/agent-evaluation.md (§B/§C)
|
|
24
|
+
* - https://github.com/openwop/openwop/blob/main/RFCS/0081-agent-evaluation-and-scorecards.md
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { describe, it, expect } from 'vitest';
|
|
28
|
+
import { readFileSync } from 'node:fs';
|
|
29
|
+
import { join } from 'node:path';
|
|
30
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
31
|
+
import addFormats from 'ajv-formats';
|
|
32
|
+
import { driver } from '../lib/driver.js';
|
|
33
|
+
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
34
|
+
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
35
|
+
import {
|
|
36
|
+
readEvalSuiteCap,
|
|
37
|
+
driveEvalRun,
|
|
38
|
+
getEvalSummary,
|
|
39
|
+
EVAL_CONTENT_FORBIDDEN,
|
|
40
|
+
} from '../lib/agentEval.js';
|
|
41
|
+
import { queryTestEvents, isEventLogSeamAvailable, resetTestSeam } from '../lib/event-log-query.js';
|
|
42
|
+
|
|
43
|
+
function loadSchema(name: string): Record<string, unknown> {
|
|
44
|
+
return JSON.parse(readFileSync(join(SCHEMAS_DIR, name), 'utf8')) as Record<string, unknown>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function expectContentFree(payload: Record<string, unknown>, where: string): void {
|
|
48
|
+
for (const f of EVAL_CONTENT_FORBIDDEN) {
|
|
49
|
+
expect(
|
|
50
|
+
!(f in payload),
|
|
51
|
+
driver.describe('RFC 0081 §C (eval-summary-no-content-leak)', `${where} MUST be content-free (no ${f})`),
|
|
52
|
+
).toBe(true);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('agent-eval-run (RFC 0081 §B/§C)', () => {
|
|
57
|
+
it('emits eval.started → per-task eval.scored → eval.completed and serves a content-free EvalSummary', async () => {
|
|
58
|
+
const cap = await readEvalSuiteCap();
|
|
59
|
+
if (!behaviorGate('openwop-eval-run', cap?.supported === true)) return;
|
|
60
|
+
if (!(await isEventLogSeamAvailable())) return; // event-log seam absent — soft-skip
|
|
61
|
+
|
|
62
|
+
const run = await driveEvalRun({ modes: ['golden'] });
|
|
63
|
+
if (run === null) return; // eval-run seam unwired — soft-skip the whole behavioral suite
|
|
64
|
+
if (!run.runId) return;
|
|
65
|
+
|
|
66
|
+
// ---- Legs 1+2: eval.* ordering + content-free (§C) -------------------
|
|
67
|
+
const startedQ = await queryTestEvents(run.runId, { type: 'eval.started' });
|
|
68
|
+
const scoredQ = await queryTestEvents(run.runId, { type: 'eval.scored' });
|
|
69
|
+
const completedQ = await queryTestEvents(run.runId, { type: 'eval.completed' });
|
|
70
|
+
|
|
71
|
+
if (startedQ.ok && scoredQ.ok && startedQ.events.length > 0) {
|
|
72
|
+
const started = startedQ.events.sort((a, b) => a.sequence - b.sequence)[0]!;
|
|
73
|
+
|
|
74
|
+
// eval.started precedes every eval.scored (§C ordering).
|
|
75
|
+
for (const s of scoredQ.events) {
|
|
76
|
+
expect(
|
|
77
|
+
started.sequence < s.sequence,
|
|
78
|
+
driver.describe('agent-evaluation.md §C', 'eval.started MUST precede every eval.scored'),
|
|
79
|
+
).toBe(true);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (completedQ.ok && completedQ.events.length > 0) {
|
|
83
|
+
const completed = completedQ.events.sort((a, b) => a.sequence - b.sequence)[completedQ.events.length - 1]!;
|
|
84
|
+
for (const s of scoredQ.events) {
|
|
85
|
+
expect(
|
|
86
|
+
s.sequence < completed.sequence,
|
|
87
|
+
driver.describe('agent-evaluation.md §C', 'every eval.scored MUST precede eval.completed'),
|
|
88
|
+
).toBe(true);
|
|
89
|
+
}
|
|
90
|
+
// eval.scored is emitted once per task (count == eval.completed.taskCount).
|
|
91
|
+
if (typeof completed.payload.taskCount === 'number') {
|
|
92
|
+
expect(
|
|
93
|
+
scoredQ.events.length === completed.payload.taskCount,
|
|
94
|
+
driver.describe('agent-evaluation.md §C', 'one eval.scored per task (count == eval.completed.taskCount)'),
|
|
95
|
+
).toBe(true);
|
|
96
|
+
}
|
|
97
|
+
expectContentFree(completed.payload, 'eval.completed');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// each eval.scored content-free + score ∈ 0..1, passed boolean.
|
|
101
|
+
for (const s of scoredQ.events) {
|
|
102
|
+
expectContentFree(s.payload, 'eval.scored');
|
|
103
|
+
expect(
|
|
104
|
+
typeof s.payload.score === 'number' && (s.payload.score as number) >= 0 && (s.payload.score as number) <= 1,
|
|
105
|
+
driver.describe('run-event-payloads.schema.json#/$defs/evalScored', 'eval.scored.score MUST be in 0..1'),
|
|
106
|
+
).toBe(true);
|
|
107
|
+
expect(
|
|
108
|
+
typeof s.payload.passed === 'boolean',
|
|
109
|
+
driver.describe('run-event-payloads.schema.json#/$defs/evalScored', 'eval.scored.passed MUST be a boolean'),
|
|
110
|
+
).toBe(true);
|
|
111
|
+
}
|
|
112
|
+
expectContentFree(started.payload, 'eval.started');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---- Leg 3: NORMATIVE EvalSummary read (§C) --------------------------
|
|
116
|
+
const { status, summary } = await getEvalSummary(run.runId);
|
|
117
|
+
if (status === 200 && summary) {
|
|
118
|
+
const ajv = new Ajv2020({ strict: false, allErrors: true });
|
|
119
|
+
addFormats(ajv);
|
|
120
|
+
const validate = ajv.compile(loadSchema('eval-summary.schema.json'));
|
|
121
|
+
expect(
|
|
122
|
+
validate(summary),
|
|
123
|
+
driver.describe(
|
|
124
|
+
'eval-summary.schema.json',
|
|
125
|
+
`GET /v1/runs/{runId}/eval-summary MUST return a schema-valid EvalSummary (${ajv.errorsText(validate.errors)})`,
|
|
126
|
+
),
|
|
127
|
+
).toBe(true);
|
|
128
|
+
|
|
129
|
+
const tasks = (summary.tasks as Array<Record<string, unknown>> | undefined) ?? [];
|
|
130
|
+
const passedCount = summary.passedCount as number | undefined;
|
|
131
|
+
const taskCount = summary.taskCount as number | undefined;
|
|
132
|
+
if (typeof passedCount === 'number' && typeof taskCount === 'number') {
|
|
133
|
+
expect(
|
|
134
|
+
passedCount <= taskCount,
|
|
135
|
+
driver.describe('agent-evaluation.md §C', 'EvalSummary.passedCount MUST NOT exceed taskCount'),
|
|
136
|
+
).toBe(true);
|
|
137
|
+
}
|
|
138
|
+
for (const t of tasks) {
|
|
139
|
+
expectContentFree(t, 'EvalSummary.tasks[]');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
await resetTestSeam();
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* openwop-core-standard — operational-annex predicate derivation (RFC 0088).
|
|
3
|
+
*
|
|
4
|
+
* Always-on, server-free derivation probe. Verifies that `isCoreStandard`
|
|
5
|
+
* derives the Core Standard Profile floor correctly from representative
|
|
6
|
+
* discovery payloads (RFC 0088 §B / core-standard-profile.md §B):
|
|
7
|
+
* - a host meeting openwop-core + openwop-interrupts + a transport is core-standard;
|
|
8
|
+
* - a bare openwop-core host (no interrupts) is NOT core-standard — the floor is
|
|
9
|
+
* deliberately stricter than the v1 minimum;
|
|
10
|
+
* - a host with no event transport (supportedTransports: []) fails the floor;
|
|
11
|
+
* - the floor is the AND of three existing closed-catalog predicates (it composes,
|
|
12
|
+
* it does not redefine — so it is absent from deriveProfiles()).
|
|
13
|
+
*
|
|
14
|
+
* The LIVE aggregate-evidence assertion (does every §C floor scenario actually
|
|
15
|
+
* pass against a host claiming the profile?) is the `Active → Accepted` step per
|
|
16
|
+
* RFC 0088 §C — already satisfied by MyndHyve + all reference hosts, asserted via
|
|
17
|
+
* each constituent scenario, and deferred here. This scenario asserts the
|
|
18
|
+
* discovery-predicate derivation only.
|
|
19
|
+
*
|
|
20
|
+
* Spec references:
|
|
21
|
+
* - https://github.com/openwop/openwop/blob/main/spec/v1/core-standard-profile.md
|
|
22
|
+
* - https://github.com/openwop/openwop/blob/main/RFCS/0088-core-standard-profile.md
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { describe, it, expect } from 'vitest';
|
|
26
|
+
import { isCoreStandard, isCore, deriveProfiles } from '../lib/profiles.js';
|
|
27
|
+
|
|
28
|
+
const why = (specRef: string, requirement: string): string => `${specRef} — ${requirement}`;
|
|
29
|
+
|
|
30
|
+
const CORE = {
|
|
31
|
+
protocolVersion: '1.0',
|
|
32
|
+
supportedEnvelopes: ['clarification.request'],
|
|
33
|
+
schemaVersions: {},
|
|
34
|
+
limits: { clarificationRounds: 1, schemaRounds: 1, envelopesPerTurn: 1 },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
describe('core-standard-profile: floor predicate (RFC 0088 §B, server-free)', () => {
|
|
38
|
+
it('a host meeting core + interrupts + a default transport is core-standard', () => {
|
|
39
|
+
// No supportedTransports ⇒ both stream predicates default-true (profiles.md).
|
|
40
|
+
const c = { ...CORE };
|
|
41
|
+
expect(isCoreStandard(c), why('core-standard-profile.md §B', 'core + interrupts + transport ⇒ core-standard')).toBe(true);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('a bare openwop-core host without interrupts is NOT core-standard', () => {
|
|
45
|
+
// openwop-core minimum, but no clarification.request ⇒ fails openwop-interrupts.
|
|
46
|
+
const c = { ...CORE, supportedEnvelopes: ['schema.request'] };
|
|
47
|
+
expect(isCore(c), why('profiles.md §openwop-core', 'still a valid openwop-core host')).toBe(true);
|
|
48
|
+
expect(isCoreStandard(c), why('core-standard-profile.md §B', 'the floor is stricter than the v1 minimum')).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('a host advertising no event transport fails the floor', () => {
|
|
52
|
+
const c = { ...CORE, supportedTransports: [] as string[] };
|
|
53
|
+
expect(isCoreStandard(c), why('core-standard-profile.md §B', 'at least one event transport is required')).toBe(false);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('a host advertising the rest transport satisfies the transport term', () => {
|
|
57
|
+
const c = { ...CORE, supportedTransports: ['rest'] };
|
|
58
|
+
expect(isCoreStandard(c), why('core-standard-profile.md §B', 'rest transport ⇒ stream term satisfied')).toBe(true);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('a non-1.x host is not core-standard', () => {
|
|
62
|
+
const c = { ...CORE, protocolVersion: '2.0' };
|
|
63
|
+
expect(isCoreStandard(c), why('profiles.md §openwop-core', 'core-standard implies openwop-core (1.x)')).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('core-standard-profile: composes, does not redefine (RFC 0088 §A, server-free)', () => {
|
|
68
|
+
it('openwop-core-standard is an annex, NOT a closed-catalog profile (absent from deriveProfiles)', () => {
|
|
69
|
+
const c = { ...CORE };
|
|
70
|
+
expect(
|
|
71
|
+
(deriveProfiles(c) as readonly string[]).includes('openwop-core-standard'),
|
|
72
|
+
why('core-standard-profile.md §A', 'the annex is not a closed-catalog predicate'),
|
|
73
|
+
).toBe(false);
|
|
74
|
+
});
|
|
75
|
+
});
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential-audience-bound egress (RFC 0079 §C) — behavioral KEYSTONE.
|
|
3
|
+
*
|
|
4
|
+
* Gated on `httpClient.egressPolicy.supported` (root-first per RFC 0073).
|
|
5
|
+
* Soft-skips when unadvertised (default) / hard-fails under
|
|
6
|
+
* `OPENWOP_REQUIRE_BEHAVIOR=true`. The always-on wire-shape coverage lives in
|
|
7
|
+
* `egress-provenance-shape.test.ts`; this asserts host BEHAVIOR — the §C
|
|
8
|
+
* confused-deputy MUST that backs the `egress-credential-audience-bound`
|
|
9
|
+
* SECURITY invariant:
|
|
10
|
+
*
|
|
11
|
+
* 1. OUT-OF-AUDIENCE — a host-issued credential bound to audience A, used for
|
|
12
|
+
* an egress to destination B (B ∉ A), MUST be `denied` or `downgraded`
|
|
13
|
+
* with `reason: "out-of-audience"`, and the credential MUST NOT be attached
|
|
14
|
+
* to the egress (`credentialAttached !== true`).
|
|
15
|
+
* 2. PROVENANCE-UNEVALUABLE — an egress whose credential provenance cannot be
|
|
16
|
+
* evaluated MUST be `denied` with `reason: "provenance-unevaluable"`
|
|
17
|
+
* (fail-closed, not fail-open).
|
|
18
|
+
*
|
|
19
|
+
* The decision is driven through the OPTIONAL host-sample egress seam
|
|
20
|
+
* (`POST /v1/host/sample/egress/decide`) — soft-skip on 404/405. The decision
|
|
21
|
+
* reason is a CLOSED enum so a host cannot spill a blocked URL/host into a
|
|
22
|
+
* free-form string (SR-1, asserted in `egress-decision-content-free.test.ts`).
|
|
23
|
+
*
|
|
24
|
+
* Spec references:
|
|
25
|
+
* - https://github.com/openwop/openwop/blob/main/spec/v1/host-capabilities.md (§"Credential provenance + egress policy")
|
|
26
|
+
* - https://github.com/openwop/openwop/blob/main/RFCS/0079-credential-provenance-and-egress-policy.md
|
|
27
|
+
* - https://github.com/openwop/openwop/blob/main/SECURITY/invariants.yaml (egress-credential-audience-bound)
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { describe, it, expect } from 'vitest';
|
|
31
|
+
import { driver } from '../lib/driver.js';
|
|
32
|
+
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
33
|
+
import { readEgressPolicyCap, driveEgress, EGRESS_DECISIONS, EGRESS_REASONS } from '../lib/egressPolicy.js';
|
|
34
|
+
|
|
35
|
+
describe('egress-audience-binding (RFC 0079 §C)', () => {
|
|
36
|
+
it('denies/downgrades an out-of-audience egress without attaching the credential, and fails closed on unevaluable provenance', async () => {
|
|
37
|
+
const cap = await readEgressPolicyCap();
|
|
38
|
+
if (!behaviorGate('openwop-egress-audience-binding', cap?.supported === true)) return;
|
|
39
|
+
|
|
40
|
+
// ---- Leg 1: out-of-audience — deny|downgrade + credential NOT attached --
|
|
41
|
+
const oob = await driveEgress({ scenario: 'out-of-audience' });
|
|
42
|
+
if (oob === null) return; // egress seam absent — soft-skip the whole behavior
|
|
43
|
+
expect(
|
|
44
|
+
oob.decision === 'denied' || oob.decision === 'downgraded',
|
|
45
|
+
driver.describe('host-capabilities.md §"Credential provenance + egress policy"', 'an out-of-audience egress MUST be denied or downgraded'),
|
|
46
|
+
).toBe(true);
|
|
47
|
+
expect(
|
|
48
|
+
typeof oob.decision === 'string' && EGRESS_DECISIONS.includes(oob.decision),
|
|
49
|
+
driver.describe('run-event-payloads.schema.json#egressDecided', 'decision MUST be in the closed enum'),
|
|
50
|
+
).toBe(true);
|
|
51
|
+
expect(
|
|
52
|
+
oob.reason === 'out-of-audience',
|
|
53
|
+
driver.describe('RFC 0079 §C', 'an out-of-audience denial MUST carry reason "out-of-audience"'),
|
|
54
|
+
).toBe(true);
|
|
55
|
+
expect(
|
|
56
|
+
oob.credentialAttached !== true,
|
|
57
|
+
driver.describe('SECURITY/invariants.yaml egress-credential-audience-bound', 'the host MUST NOT attach a credential whose audience excludes the destination (confused-deputy)'),
|
|
58
|
+
).toBe(true);
|
|
59
|
+
|
|
60
|
+
// ---- Leg 2: provenance-unevaluable — fail closed (deny) ----------------
|
|
61
|
+
const uneval = await driveEgress({ scenario: 'provenance-unevaluable' });
|
|
62
|
+
if (uneval !== null) {
|
|
63
|
+
expect(
|
|
64
|
+
uneval.decision === 'denied',
|
|
65
|
+
driver.describe('RFC 0079 §C', 'an egress with unevaluable provenance MUST fail closed (denied)'),
|
|
66
|
+
).toBe(true);
|
|
67
|
+
expect(
|
|
68
|
+
uneval.reason === 'provenance-unevaluable',
|
|
69
|
+
driver.describe('RFC 0079 §C', 'a provenance-unevaluable denial MUST carry reason "provenance-unevaluable"'),
|
|
70
|
+
).toBe(true);
|
|
71
|
+
expect(
|
|
72
|
+
typeof uneval.reason === 'string' && EGRESS_REASONS.includes(uneval.reason),
|
|
73
|
+
driver.describe('run-event-payloads.schema.json#egressDecided', 'reason MUST be in the closed enum'),
|
|
74
|
+
).toBe(true);
|
|
75
|
+
expect(
|
|
76
|
+
uneval.credentialAttached !== true,
|
|
77
|
+
driver.describe('SECURITY/invariants.yaml egress-credential-audience-bound', 'a fail-closed egress MUST NOT attach the credential'),
|
|
78
|
+
).toBe(true);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Egress-decision secret non-leak (RFC 0079 §F / SR-1) — behavioral.
|
|
3
|
+
*
|
|
4
|
+
* Gated on `httpClient.egressPolicy.supported` (root-first per RFC 0073).
|
|
5
|
+
* Soft-skips when unadvertised (default) / hard-fails under
|
|
6
|
+
* `OPENWOP_REQUIRE_BEHAVIOR=true`. Backs the `egress-decision-no-secret-leak`
|
|
7
|
+
* guarantee: an `egress.decided` payload is metadata-only — it MUST NOT carry
|
|
8
|
+
* the credential value, nor spill the blocked URL/host/header/body into a
|
|
9
|
+
* free-form field, and its `reason` MUST be drawn from the CLOSED vocabulary
|
|
10
|
+
* (so a host cannot smuggle a blocked destination into the reason string).
|
|
11
|
+
*
|
|
12
|
+
* Drives the host-sample seam with a `canary` credential whose value is a known
|
|
13
|
+
* sentinel and asserts the sentinel never surfaces in the decision
|
|
14
|
+
* (`canaryLeaked !== true`) and that the payload carries none of the forbidden
|
|
15
|
+
* content keys. Soft-skips on 404/405.
|
|
16
|
+
*
|
|
17
|
+
* Spec references:
|
|
18
|
+
* - https://github.com/openwop/openwop/blob/main/spec/v1/host-capabilities.md (§"Credential provenance + egress policy")
|
|
19
|
+
* - https://github.com/openwop/openwop/blob/main/RFCS/0079-credential-provenance-and-egress-policy.md
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { describe, it, expect } from 'vitest';
|
|
23
|
+
import { driver } from '../lib/driver.js';
|
|
24
|
+
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
25
|
+
import { readEgressPolicyCap, driveEgress, EGRESS_REASONS, EGRESS_CONTENT_FORBIDDEN } from '../lib/egressPolicy.js';
|
|
26
|
+
|
|
27
|
+
describe('egress-decision-content-free (RFC 0079 §F / SR-1)', () => {
|
|
28
|
+
it('never leaks the credential value or the blocked destination into the egress.decided payload', async () => {
|
|
29
|
+
const cap = await readEgressPolicyCap();
|
|
30
|
+
if (!behaviorGate('openwop-egress-decision-content-free', cap?.supported === true)) return;
|
|
31
|
+
|
|
32
|
+
const res = await driveEgress({ scenario: 'canary' });
|
|
33
|
+
if (res === null) return; // seam absent — soft-skip
|
|
34
|
+
|
|
35
|
+
// The canary sentinel MUST NOT appear anywhere observable.
|
|
36
|
+
expect(
|
|
37
|
+
res.canaryLeaked !== true,
|
|
38
|
+
driver.describe('RFC 0079 §F (SR-1)', 'the credential value (canary) MUST NOT leak into any observable surface'),
|
|
39
|
+
).toBe(true);
|
|
40
|
+
|
|
41
|
+
// No forbidden content keys on the decision payload.
|
|
42
|
+
for (const forbidden of EGRESS_CONTENT_FORBIDDEN) {
|
|
43
|
+
expect(
|
|
44
|
+
!(forbidden in res),
|
|
45
|
+
driver.describe('RFC 0079 §F (SR-1)', `egress.decided MUST be content-free (no ${forbidden})`),
|
|
46
|
+
).toBe(true);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// The reason stays in the closed vocabulary — no free-form destination spill.
|
|
50
|
+
if (res.reason !== undefined) {
|
|
51
|
+
expect(
|
|
52
|
+
typeof res.reason === 'string' && EGRESS_REASONS.includes(res.reason),
|
|
53
|
+
driver.describe('run-event-payloads.schema.json#egressDecided', 'reason MUST be in the closed enum (no free-form spill)'),
|
|
54
|
+
).toBe(true);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -49,7 +49,7 @@
|
|
|
49
49
|
import { describe, it, expect } from 'vitest';
|
|
50
50
|
import { driver } from '../lib/driver.js';
|
|
51
51
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
52
|
-
import {
|
|
52
|
+
import { pollUntil } from '../lib/polling.js';
|
|
53
53
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
54
54
|
|
|
55
55
|
const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
|
|
@@ -111,12 +111,17 @@ describe.skipIf(BEHAVIORAL_SKIP)('multi-agent-confidence-escalation: behavioral
|
|
|
111
111
|
expect(create.status).toBe(201);
|
|
112
112
|
const runId = (create.json as { runId: string }).runId;
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
//
|
|
116
|
-
//
|
|
117
|
-
//
|
|
118
|
-
//
|
|
119
|
-
|
|
114
|
+
// RFC 0039 confidence escalation SUSPENDS the parent (a `waiting-*` status)
|
|
115
|
+
// — it is NOT a terminal `completed`/`failed`/`cancelled`. So poll until the
|
|
116
|
+
// run either suspends or settles; polling only for terminal statuses
|
|
117
|
+
// (`pollUntilTerminal`, whose set is {completed,failed,cancelled}) would time
|
|
118
|
+
// out before the suspension is ever observed — the cause of the prior flake.
|
|
119
|
+
const terminal = await pollUntil(runId, (s) => {
|
|
120
|
+
const st = s.status as string;
|
|
121
|
+
return st.startsWith('waiting-') || st === 'completed' || st === 'failed' || st === 'cancelled';
|
|
122
|
+
});
|
|
123
|
+
// RFC 0039 §A gives hosts a choice: clarify-kind escalation
|
|
124
|
+
// (→ waiting-clarification) OR escalate-kind approval (→ waiting-approval).
|
|
120
125
|
//
|
|
121
126
|
// RFC 0044 routing: when the host advertises
|
|
122
127
|
// `capabilities.multiAgent.executionModel.confidenceEscalationInterruptKind`
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prompt-resolution-chain-event — RFC 0029 layer precedence on the PRODUCTION wire.
|
|
3
|
+
*
|
|
4
|
+
* The black-box, production-path counterpart to the three seam-driven
|
|
5
|
+
* `prompt-resolution-chain-{node-wins,agent-intrinsic,fallback-cascade}.test.ts`
|
|
6
|
+
* scenarios. Instead of the synchronous `POST /v1/host/sample/prompt/resolve`
|
|
7
|
+
* seam, this creates a real run from a prompt-exercising fixture, reads the
|
|
8
|
+
* run's DURABLE event log via the NORMATIVE `GET /v1/runs/{runId}/events/poll`
|
|
9
|
+
* endpoint, and asserts the `agent.promptResolved` event carries the full
|
|
10
|
+
* layer-by-layer precedence record (`spec/v1/prompts.md` §"Resolution chain") —
|
|
11
|
+
* no `/v1/host/sample/*` seam.
|
|
12
|
+
*
|
|
13
|
+
* The `agentPromptResolved` payload (`schemas/run-event-payloads.schema.json`)
|
|
14
|
+
* already REQUIRES `chain[]` with one `applied: true` entry + the full-traversal
|
|
15
|
+
* MUST, so the wire is already capable of conveying precedence without the seam.
|
|
16
|
+
* This is the "replace seam-gated proofs with black-box production-path
|
|
17
|
+
* conformance" step (independent-audit acceptance-bar item 3) for RFC 0029: once
|
|
18
|
+
* a host emits `agent.promptResolved`, prompt-chain precedence is proven on the
|
|
19
|
+
* production wire and the surface graduates INTO the `openwop-core-standard`
|
|
20
|
+
* floor (RFC 0088 §D Lever-2 → floor).
|
|
21
|
+
*
|
|
22
|
+
* Gating: soft-skips unless `capabilities.prompts.supported` AND the host
|
|
23
|
+
* actually emits `agent.promptResolved` for the run (emission is staged per
|
|
24
|
+
* RFC 0029 / RFC 0021 — a host advertising prompts MAY not yet emit the event).
|
|
25
|
+
*
|
|
26
|
+
* @see RFCS/0029-prompt-override-hierarchy.md
|
|
27
|
+
* @see spec/v1/prompts.md §"Resolution chain (normative)"
|
|
28
|
+
*/
|
|
29
|
+
import { describe, it, expect } from 'vitest';
|
|
30
|
+
import { driver } from '../lib/driver.js';
|
|
31
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
32
|
+
import { pollUntilTerminal } from '../lib/polling.js';
|
|
33
|
+
|
|
34
|
+
const PROMPT_FIXTURE_ID = 'conformance-prompt-end-to-end';
|
|
35
|
+
const VALID_LAYERS = new Set([
|
|
36
|
+
'run-configurable', 'node', 'agent-intrinsic', 'agent-overrides',
|
|
37
|
+
'agent-library-default', 'workflow-defaults', 'host-defaults',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
interface ChainEntry { layer?: unknown; source?: unknown; applied?: unknown }
|
|
41
|
+
interface PromptResolvedPayload { chain?: ChainEntry[]; resolved?: unknown }
|
|
42
|
+
interface RawEvent { type?: string; payload?: PromptResolvedPayload }
|
|
43
|
+
|
|
44
|
+
async function promptsSupported(): Promise<boolean> {
|
|
45
|
+
const res = await driver.get('/.well-known/openwop');
|
|
46
|
+
return capabilityFamily(res.json as Record<string, unknown> | undefined, 'prompts')?.supported === true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('prompt-resolution-chain-event (black-box): agent.promptResolved carries the precedence record (RFC 0029)', () => {
|
|
50
|
+
it('the production agent.promptResolved event records the full four-layer resolution chain', async () => {
|
|
51
|
+
if (!(await promptsSupported())) return; // capability not advertised — skip
|
|
52
|
+
|
|
53
|
+
const create = await driver.post('/v1/runs', { workflowId: PROMPT_FIXTURE_ID });
|
|
54
|
+
if (create.status !== 201) {
|
|
55
|
+
// Fixture not seeded / run not accepted — not a prompt-chain failure.
|
|
56
|
+
// eslint-disable-next-line no-console
|
|
57
|
+
console.warn(`[prompt-resolution-chain-event] POST /v1/runs for ${PROMPT_FIXTURE_ID} returned ${create.status}; skipping the production-path assertion`);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const runId = (create.json as { runId?: string }).runId;
|
|
61
|
+
if (!runId) return;
|
|
62
|
+
await pollUntilTerminal(runId);
|
|
63
|
+
|
|
64
|
+
const poll = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/events/poll`);
|
|
65
|
+
const events = (poll.json as { events?: RawEvent[] } | undefined)?.events ?? [];
|
|
66
|
+
const resolved = events.filter((e) => e.type === 'agent.promptResolved');
|
|
67
|
+
if (resolved.length === 0) {
|
|
68
|
+
// Host advertises prompts but does not yet emit agent.promptResolved
|
|
69
|
+
// (RFC 0029 emission is staged) — soft-skip the behavioral assertion.
|
|
70
|
+
// eslint-disable-next-line no-console
|
|
71
|
+
console.warn('[prompt-resolution-chain-event] host emitted no agent.promptResolved event; skipping (RFC 0029 emission staged)');
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const ev of resolved) {
|
|
76
|
+
const chain = ev.payload?.chain;
|
|
77
|
+
expect(
|
|
78
|
+
Array.isArray(chain) && chain.length > 0,
|
|
79
|
+
driver.describe('prompts.md §Resolution chain', 'agent.promptResolved MUST carry a non-empty chain[] of attempted layers'),
|
|
80
|
+
).toBe(true);
|
|
81
|
+
const entries = chain as ChainEntry[];
|
|
82
|
+
|
|
83
|
+
// Every entry is a well-formed layer record (the full-traversal shape).
|
|
84
|
+
for (const e of entries) {
|
|
85
|
+
expect(
|
|
86
|
+
typeof e.layer === 'string' && VALID_LAYERS.has(e.layer),
|
|
87
|
+
driver.describe('prompts.md §Resolution chain', `each chain entry MUST name a valid layer, got ${String(e.layer)}`),
|
|
88
|
+
).toBe(true);
|
|
89
|
+
expect(typeof e.applied, driver.describe('prompts.md §Resolution chain', 'each chain entry MUST carry a boolean `applied`')).toBe('boolean');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Exactly one layer wins (or none, when resolved is null).
|
|
93
|
+
const applied = entries.filter((e) => e.applied === true);
|
|
94
|
+
expect(
|
|
95
|
+
applied.length <= 1,
|
|
96
|
+
driver.describe('prompts.md §Resolution chain', 'AT MOST one chain entry MAY be applied: true (the winning layer)'),
|
|
97
|
+
).toBe(true);
|
|
98
|
+
|
|
99
|
+
// resolved mirrors the applied entry's source (RFC 0029 §B).
|
|
100
|
+
if (applied.length === 1) {
|
|
101
|
+
expect(
|
|
102
|
+
ev.payload?.resolved,
|
|
103
|
+
driver.describe('run-event-payloads.schema.json agentPromptResolved', '`resolved` MUST mirror the applied chain entry\'s `source`'),
|
|
104
|
+
).toBe(applied[0]?.source);
|
|
105
|
+
} else {
|
|
106
|
+
expect(
|
|
107
|
+
ev.payload?.resolved === null || ev.payload?.resolved === undefined,
|
|
108
|
+
driver.describe('run-event-payloads.schema.json agentPromptResolved', 'with no applied layer, `resolved` MUST be null'),
|
|
109
|
+
).toBe(true);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
});
|