@openwop/openwop-conformance 1.6.1 → 1.10.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 +10 -0
- package/README.md +2 -2
- package/api/asyncapi.yaml +57 -0
- package/api/openapi.yaml +250 -0
- package/coverage.md +14 -0
- package/fixtures/conformance-run-duration-breach.json +33 -0
- package/fixtures.md +19 -0
- package/package.json +1 -1
- package/schemas/README.md +10 -0
- package/schemas/agent-inventory-response.schema.json +90 -0
- package/schemas/ai-envelope.schema.json +28 -0
- package/schemas/artifact-type-pack-manifest.schema.json +160 -0
- package/schemas/capabilities.schema.json +171 -4
- package/schemas/chat-card-pack-manifest.schema.json +158 -0
- package/schemas/envelopes/media.audio.schema.json +38 -0
- package/schemas/envelopes/media.file.schema.json +37 -0
- package/schemas/envelopes/media.image.schema.json +33 -0
- package/schemas/heartbeat-evaluated.schema.json +14 -0
- package/schemas/heartbeat-state-changed.schema.json +14 -0
- package/schemas/node-pack-manifest.schema.json +16 -1
- package/schemas/run-event-payloads.schema.json +96 -5
- package/schemas/run-event.schema.json +4 -0
- package/schemas/workflow-definition.schema.json +5 -0
- package/schemas/workspace-file-create.schema.json +20 -0
- package/schemas/workspace-file.schema.json +39 -0
- package/src/lib/agentLoop.ts +44 -0
- package/src/lib/agentRuntime.ts +45 -0
- package/src/lib/artifactTypes.ts +96 -0
- package/src/lib/cardPacks.ts +52 -0
- package/src/lib/discovery-capabilities.ts +50 -0
- package/src/lib/distillation.ts +38 -0
- package/src/lib/feedback.ts +3 -3
- package/src/lib/heartbeat.ts +31 -0
- package/src/lib/memoryAttribution.ts +48 -0
- package/src/lib/subRunAttestation.ts +35 -0
- package/src/lib/toolHooks.ts +33 -0
- package/src/scenarios/agent-loop-iteration-monotonic.test.ts +33 -0
- package/src/scenarios/agent-loop-stateful-resume.test.ts +28 -0
- package/src/scenarios/agent-loop-version5-shape.test.ts +41 -0
- package/src/scenarios/agent-loop-workspace-snapshot.test.ts +33 -0
- package/src/scenarios/agent-manifest-runtime.test.ts +85 -0
- package/src/scenarios/ai-envelope-shape.test.ts +14 -18
- package/src/scenarios/aiEnvelope.capBreached.test.ts +2 -1
- package/src/scenarios/aiEnvelope.schemaDrift.test.ts +2 -1
- package/src/scenarios/aiEnvelope.universalKinds.test.ts +2 -1
- package/src/scenarios/approval-gate-flow.test.ts +4 -6
- package/src/scenarios/artifact-schema-compile-bounded.test.ts +126 -0
- package/src/scenarios/artifact-type-pack-install.test.ts +78 -0
- package/src/scenarios/artifact-type-pack-manifest-validation.test.ts +140 -0
- package/src/scenarios/artifact-type-store-without-render.test.ts +54 -0
- package/src/scenarios/audit-log-integrity.test.ts +3 -2
- package/src/scenarios/auth-api-key-rotation.test.ts +2 -1
- package/src/scenarios/auth-mtls.test.ts +2 -1
- package/src/scenarios/auth-oauth2-client-credentials.test.ts +2 -1
- package/src/scenarios/auth-oidc-user-bearer.test.ts +2 -1
- package/src/scenarios/auth-saml-profile.test.ts +2 -1
- package/src/scenarios/auth-scim-profile.test.ts +2 -1
- package/src/scenarios/authorization-fail-closed.test.ts +2 -1
- package/src/scenarios/authorization-roles-shape.test.ts +2 -1
- package/src/scenarios/byok-auth-modes.test.ts +141 -0
- package/src/scenarios/chat-card-pack-execution.test.ts +56 -0
- package/src/scenarios/chat-card-pack-manifest-validation.test.ts +128 -0
- package/src/scenarios/commitment-fired.test.ts +83 -0
- package/src/scenarios/credential-payload-redaction.test.ts +2 -1
- package/src/scenarios/credentials-capability-shape.test.ts +2 -1
- package/src/scenarios/cross-engine-append-ordering.test.ts +2 -1
- package/src/scenarios/cross-host-ancestry-endpoint.test.ts +3 -2
- package/src/scenarios/cross-host-causation-shape.test.ts +3 -2
- package/src/scenarios/deadletter-capability-shape.test.ts +2 -1
- package/src/scenarios/deadletter-retry-exhaustion.test.ts +2 -1
- package/src/scenarios/distillation-index-roundtrip.test.ts +35 -0
- package/src/scenarios/distillation-secret-carryforward.test.ts +35 -0
- package/src/scenarios/distillation-shape.test.ts +41 -0
- package/src/scenarios/distillation-stable-archive.test.ts +37 -0
- package/src/scenarios/distillation-token-budget.test.ts +45 -0
- package/src/scenarios/envelope-completion-distinguishes-truncation.test.ts +4 -3
- package/src/scenarios/envelope-reasoning-secret-redaction.test.ts +5 -4
- package/src/scenarios/envelope-reasoning-shape.test.ts +3 -2
- package/src/scenarios/envelope-refusal-shape.test.ts +3 -2
- package/src/scenarios/envelope-rendering-hint.test.ts +95 -0
- package/src/scenarios/envelope-retry-attempted.test.ts +2 -1
- package/src/scenarios/envelope-tier-one-subset-static.test.ts +3 -2
- package/src/scenarios/exec-not-protocol-tier.test.ts +137 -0
- package/src/scenarios/experimental-tier-shape.test.ts +5 -4
- package/src/scenarios/fs-path-traversal.test.ts +2 -1
- package/src/scenarios/heartbeat-capability-shape.test.ts +35 -0
- package/src/scenarios/heartbeat-fires-once-per-tick.test.ts +28 -0
- package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +43 -0
- package/src/scenarios/heartbeat-runtime-bound.test.ts +30 -0
- package/src/scenarios/http-client-ssrf.test.ts +10 -13
- package/src/scenarios/mcp-toolcall-redaction.test.ts +3 -2
- package/src/scenarios/media-url-inline-cap.test.ts +167 -0
- package/src/scenarios/memory-attribution-emits-on-write.test.ts +54 -0
- package/src/scenarios/memory-attribution-no-content.test.ts +45 -0
- package/src/scenarios/memory-attribution-replay-stable.test.ts +60 -0
- package/src/scenarios/memory-attribution-shape.test.ts +28 -0
- package/src/scenarios/memory-attribution-tenant-scoped.test.ts +44 -0
- package/src/scenarios/memory-compaction-event-emitted.test.ts +2 -1
- package/src/scenarios/memory-compaction-provenance-tag.test.ts +2 -1
- package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +2 -1
- package/src/scenarios/memory-consolidation-idempotent.test.ts +77 -0
- package/src/scenarios/memory-consolidation-shape.test.ts +90 -0
- package/src/scenarios/model-capability-substituted.test.ts +2 -1
- package/src/scenarios/multi-agent-confidence-escalation.test.ts +5 -4
- package/src/scenarios/multi-agent-handoff-state-machine.test.ts +6 -5
- package/src/scenarios/multi-agent-memory-lifecycle.test.ts +4 -3
- package/src/scenarios/multi-region-idempotency.test.ts +10 -10
- package/src/scenarios/oauth-capability-shape.test.ts +2 -1
- package/src/scenarios/oauth-connector-redaction.test.ts +2 -1
- package/src/scenarios/pause-resume.test.ts +3 -3
- package/src/scenarios/production-backpressure.test.ts +2 -2
- package/src/scenarios/production-retention-expiry.test.ts +2 -2
- package/src/scenarios/prompt-all-four-kinds-events.test.ts +2 -1
- package/src/scenarios/prompt-composed-secret-redaction.test.ts +2 -1
- package/src/scenarios/prompt-composed-trust-marker.test.ts +2 -1
- package/src/scenarios/prompt-end-to-end-events.test.ts +2 -1
- package/src/scenarios/prompt-list-and-fetch.test.ts +2 -1
- package/src/scenarios/prompt-mutable-lifecycle.test.ts +2 -1
- package/src/scenarios/prompt-mutation-workspace-membership-enforced.test.ts +2 -1
- package/src/scenarios/prompt-pack-install.test.ts +2 -1
- package/src/scenarios/prompt-read-workspace-membership-enforced.test.ts +2 -1
- package/src/scenarios/prompt-render-deterministic.test.ts +2 -1
- package/src/scenarios/prompt-resolution-chain-agent-intrinsic.test.ts +2 -1
- package/src/scenarios/prompt-resolution-chain-fallback-cascade.test.ts +2 -1
- package/src/scenarios/prompt-resolution-chain-node-wins.test.ts +2 -1
- package/src/scenarios/prompt-template-shape.test.ts +2 -1
- package/src/scenarios/provider-usage.test.ts +2 -1
- package/src/scenarios/replay-divergence-at-refusal.test.ts +4 -3
- package/src/scenarios/replay-fork-arbitrary.test.ts +3 -1
- package/src/scenarios/replay-llm-cache-key-portable.test.ts +2 -1
- package/src/scenarios/replayDeterminism.test.ts +3 -1
- package/src/scenarios/run-execution-bounds-shape.test.ts +133 -0
- package/src/scenarios/sandbox-memory-cap.test.ts +2 -1
- package/src/scenarios/sandbox-mvp-behavior.test.ts +2 -1
- package/src/scenarios/sandbox-no-host-fs-escape.test.ts +2 -1
- package/src/scenarios/sandbox-timeout-cap.test.ts +2 -1
- package/src/scenarios/scheduling-capability-shape.test.ts +2 -1
- package/src/scenarios/scheduling-cron-fires-once.test.ts +2 -1
- package/src/scenarios/secret-leakage-otel-attribute.test.ts +7 -6
- package/src/scenarios/spec-corpus-validity.test.ts +1 -1
- package/src/scenarios/subrun-approval-fail-closed.test.ts +33 -0
- package/src/scenarios/subrun-approval-gate.test.ts +35 -0
- package/src/scenarios/subrun-attestation-shape.test.ts +30 -0
- package/src/scenarios/subrun-checksum-stable.test.ts +43 -0
- package/src/scenarios/tool-hooks-authorization-fail-closed.test.ts +39 -0
- package/src/scenarios/tool-hooks-content-free.test.ts +40 -0
- package/src/scenarios/tool-hooks-rate-limit.test.ts +32 -0
- package/src/scenarios/tool-hooks-secret-redaction.test.ts +34 -0
- package/src/scenarios/tool-hooks-shape.test.ts +34 -0
- package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +3 -10
- package/src/scenarios/wasm-pack-invoke-completed.test.ts +2 -2
- package/src/scenarios/wasm-pack-invoke-suspended.test.ts +2 -2
- package/src/scenarios/wasm-pack-load.test.ts +2 -2
- package/src/scenarios/wasm-pack-memory-cap.test.ts +3 -6
- package/src/scenarios/wasm-pack-replay-determinism.test.ts +2 -2
- package/src/scenarios/workflow-primary-output-annotation.test.ts +142 -0
- package/src/scenarios/workspace-behavior.test.ts +134 -0
- package/src/scenarios/workspace-capability-shape.test.ts +73 -0
- package/src/scenarios/workspace-cross-tenant-isolation.test.ts +84 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* media-url-inline-cap — RFC 0055 §C media envelope kinds + asset-URL discipline.
|
|
3
|
+
*
|
|
4
|
+
* SECURITY invariant: `media-asset-url-tenant-scoped` (RFC 0055 §C rule 1 + 4).
|
|
5
|
+
*
|
|
6
|
+
* Always-on (server-free):
|
|
7
|
+
* 1. The three `media.{image,audio,file}` payload schemas compile (Ajv2020).
|
|
8
|
+
* 2. Positive round-trip: a URL-reference payload and an inline-base64
|
|
9
|
+
* payload each validate.
|
|
10
|
+
* 3. Negative: a payload missing the required `bytes` is rejected; an
|
|
11
|
+
* unknown property is rejected (additionalProperties:false).
|
|
12
|
+
*
|
|
13
|
+
* Advertisement-shape (HTTP, soft-skip offline):
|
|
14
|
+
* 4. When a host advertises `aiProviders.maxInlineMediaBytes`, it MUST be a
|
|
15
|
+
* non-negative integer.
|
|
16
|
+
*
|
|
17
|
+
* Behavioral (cross-tenant scoping + cap enforcement) is staged via `it.todo`
|
|
18
|
+
* until a reference host wires tenant-scoped asset serving (greenfield;
|
|
19
|
+
* RFC 0027 §G precedent — advertisement + schema land first).
|
|
20
|
+
*
|
|
21
|
+
* @see RFCS/0055-multimodal-envelope-variants-and-rendering-hints.md §C
|
|
22
|
+
* @see spec/v1/ai-envelope.md §"Media reference payloads"
|
|
23
|
+
* @see SECURITY/invariants.yaml#media-asset-url-tenant-scoped
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { describe, it, expect } from 'vitest';
|
|
27
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
28
|
+
import addFormats from 'ajv-formats';
|
|
29
|
+
import { readFileSync } from 'node:fs';
|
|
30
|
+
import { join } from 'node:path';
|
|
31
|
+
import { driver } from '../lib/driver.js';
|
|
32
|
+
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
33
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
34
|
+
|
|
35
|
+
const MEDIA_KINDS = ['media.image', 'media.audio', 'media.file'] as const;
|
|
36
|
+
const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
|
|
37
|
+
|
|
38
|
+
function compile(kind: string): ReturnType<Ajv2020['compile']> {
|
|
39
|
+
const ajv = new Ajv2020({ strict: false, allErrors: true });
|
|
40
|
+
addFormats(ajv);
|
|
41
|
+
const schema = JSON.parse(
|
|
42
|
+
readFileSync(join(SCHEMAS_DIR, `envelopes/${kind}.schema.json`), 'utf8'),
|
|
43
|
+
) as Record<string, unknown>;
|
|
44
|
+
return ajv.compile(schema);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('media-url-inline-cap: media payload schemas compile + round-trip (RFC 0055 §C)', () => {
|
|
48
|
+
for (const kind of MEDIA_KINDS) {
|
|
49
|
+
it(`envelopes/${kind}.schema.json compiles under Ajv2020`, () => {
|
|
50
|
+
expect(
|
|
51
|
+
compile(kind),
|
|
52
|
+
`ai-envelope.md §"Media reference payloads": ${kind} payload schema MUST compile`,
|
|
53
|
+
).toBeTypeOf('function');
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
it('accepts a URL-reference image payload', () => {
|
|
58
|
+
const ok = compile('media.image')({
|
|
59
|
+
url: 'https://host.example/v1/runs/run_1/assets/img_9.png',
|
|
60
|
+
bytes: 184320,
|
|
61
|
+
mimeType: 'image/png',
|
|
62
|
+
});
|
|
63
|
+
expect(ok, 'URL-reference media payload MUST validate').toBe(true);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('accepts an inline-base64 audio payload', () => {
|
|
67
|
+
const ok = compile('media.audio')({ base64: 'AAAA', bytes: 3, mimeType: 'audio/ogg', durationSeconds: 1.2 });
|
|
68
|
+
expect(ok, 'inline-base64 media payload MUST validate').toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('rejects a payload missing required bytes', () => {
|
|
72
|
+
const ok = compile('media.file')({ url: 'https://host.example/v1/runs/run_1/assets/report.pdf' });
|
|
73
|
+
expect(ok, 'ai-envelope.md §"Media reference payloads": `bytes` is required').toBe(false);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('rejects an unknown property (additionalProperties:false)', () => {
|
|
77
|
+
const ok = compile('media.image')({ bytes: 1, wat: true });
|
|
78
|
+
expect(ok, 'media payload is additionalProperties:false').toBe(false);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
interface DiscoveryDoc {
|
|
83
|
+
capabilities?: { aiProviders?: { maxInlineMediaBytes?: unknown } };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
describe.skipIf(HTTP_SKIP)('media-url-inline-cap: advertisement shape (RFC 0055 §C rule 2)', () => {
|
|
87
|
+
it('aiProviders.maxInlineMediaBytes is a non-negative integer when advertised', async () => {
|
|
88
|
+
const res = await driver.get('/.well-known/openwop');
|
|
89
|
+
if (res.status !== 200) return;
|
|
90
|
+
const cap = capabilityFamily((res.json as DiscoveryDoc), 'aiProviders')?.maxInlineMediaBytes;
|
|
91
|
+
if (cap === undefined) return; // optional — soft-skip when absent
|
|
92
|
+
expect(
|
|
93
|
+
Number.isInteger(cap) && (cap as number) >= 0,
|
|
94
|
+
driver.describe('capabilities.md §aiProviders.maxInlineMediaBytes', 'cap MUST be a non-negative integer'),
|
|
95
|
+
).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Behavioral assertions for `media-asset-url-tenant-scoped`. Driven via the
|
|
99
|
+
// reference host's media-asset seam (store: POST /v1/host/sample/media/put,
|
|
100
|
+
// env-gated; serve: GET /v1/host/sample/assets/{token}, public token-auth).
|
|
101
|
+
// Soft-skip (return) when the host doesn't expose the store seam (404).
|
|
102
|
+
const PNG_1x1 =
|
|
103
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==';
|
|
104
|
+
|
|
105
|
+
it('a stored media asset is served by a tenant-scoped URL, not inlined', async () => {
|
|
106
|
+
const stored = await driver.post('/v1/host/sample/media/put', { contentBase64: PNG_1x1, contentType: 'image/png' });
|
|
107
|
+
if (stored.status === 404) return; // store seam disabled — soft-skip
|
|
108
|
+
expect(stored.status, 'media store MUST return 201').toBe(201);
|
|
109
|
+
const body = stored.json as { url?: string; bytes?: number };
|
|
110
|
+
expect(
|
|
111
|
+
typeof body.url === 'string' && /\/v1\/host\/sample\/assets\//.test(body.url!),
|
|
112
|
+
driver.describe('ai-envelope.md §"Media reference payloads"', 'asset MUST be served by a URL reference, not inlined'),
|
|
113
|
+
).toBe(true);
|
|
114
|
+
const served = await driver.get(body.url!);
|
|
115
|
+
expect(served.status, 'the asset URL MUST resolve').toBe(200);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('an unminted/guessed asset token does not resolve (media-asset-url-tenant-scoped)', async () => {
|
|
119
|
+
// Probe whether the serve route exists at all; soft-skip if not.
|
|
120
|
+
const probe = await driver.get('/v1/host/sample/assets/probe-never-minted-token');
|
|
121
|
+
if (probe.status === 404 && !process.env.OPENWOP_BASE_URL) return;
|
|
122
|
+
expect(
|
|
123
|
+
probe.status,
|
|
124
|
+
driver.describe('SECURITY/invariants.yaml#media-asset-url-tenant-scoped', 'a token not held by the caller (unguessable 256-bit) MUST NOT resolve'),
|
|
125
|
+
).toBe(404);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('a media.* payload in a run debug bundle is referenced by URL, not inlined (RFC 0055 §C rule 3)', async () => {
|
|
129
|
+
// RFC 0055 §C rule 3: asset URLs are part of a run's debug-bundle manifest
|
|
130
|
+
// BY REFERENCE, never by inlining the binary. Gated on a host that both
|
|
131
|
+
// serves media (advertises aiProviders.maxInlineMediaBytes) and exports
|
|
132
|
+
// debug bundles (capabilities.debugBundle.supported). Soft-skips otherwise
|
|
133
|
+
// — and on the reference host, which exports debug bundles but has no node
|
|
134
|
+
// that emits a media.* envelope into a run (so no media payload appears).
|
|
135
|
+
const disc = await driver.get('/.well-known/openwop');
|
|
136
|
+
if (disc.status !== 200) return;
|
|
137
|
+
const caps = (disc.json as {
|
|
138
|
+
capabilities?: { aiProviders?: { maxInlineMediaBytes?: unknown }; debugBundle?: { supported?: unknown } };
|
|
139
|
+
}).capabilities;
|
|
140
|
+
if (caps?.aiProviders?.maxInlineMediaBytes === undefined || caps.debugBundle?.supported !== true) {
|
|
141
|
+
return; // host doesn't serve media + export debug bundles — contract not exercisable
|
|
142
|
+
}
|
|
143
|
+
// Find a recent run and inspect its debug bundle for any media.* event.
|
|
144
|
+
const runs = await driver.get('/v1/runs?limit=20');
|
|
145
|
+
if (runs.status !== 200) return;
|
|
146
|
+
const runIds = ((runs.json as { runs?: { runId?: string }[] }).runs ?? [])
|
|
147
|
+
.map((r) => r.runId)
|
|
148
|
+
.filter((id): id is string => typeof id === 'string');
|
|
149
|
+
for (const runId of runIds) {
|
|
150
|
+
const bundle = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/debug-bundle`);
|
|
151
|
+
if (bundle.status !== 200) continue;
|
|
152
|
+
const events = (bundle.json as { events?: { type?: string; payload?: { url?: unknown; base64?: unknown } }[] }).events ?? [];
|
|
153
|
+
for (const ev of events) {
|
|
154
|
+
if (typeof ev.type === 'string' && ev.type.startsWith('media.')) {
|
|
155
|
+
// The §C rule-3 contract: served by URL, not inlined binary.
|
|
156
|
+
expect(
|
|
157
|
+
typeof ev.payload?.url === 'string' && ev.payload?.base64 === undefined,
|
|
158
|
+
driver.describe('ai-envelope.md §"Media reference payloads"', 'a media.* payload in a debug bundle MUST be a URL reference, never inlined binary'),
|
|
159
|
+
).toBe(true);
|
|
160
|
+
return; // asserted one — contract proven
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// No media.* payload surfaced in any recent run's debug bundle on this
|
|
165
|
+
// host — nothing to assert (the contract holds vacuously).
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-attribution-emits-on-write — RFC 0057 §A/§B. A host advertising
|
|
3
|
+
* `capabilities.memory.attribution.emitsWriteEvents: true` emits a
|
|
4
|
+
* `memory.written` event (with resolvable identifiers) for the memory its run
|
|
5
|
+
* writes. A host NOT advertising the capability emits none and still passes
|
|
6
|
+
* the locked core.
|
|
7
|
+
*
|
|
8
|
+
* @see RFCS/0057-memory-write-attribution-event.md §A
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect } from 'vitest';
|
|
12
|
+
import { driver } from '../lib/driver.js';
|
|
13
|
+
import { pollUntilTerminal } from '../lib/polling.js';
|
|
14
|
+
import { readMemoryAttributionCap, emitsWriteEvents, seedRun, memoryWrittenEvents } from '../lib/memoryAttribution.js';
|
|
15
|
+
|
|
16
|
+
describe('memory-attribution-emits-on-write (RFC 0057 §A/§B)', () => {
|
|
17
|
+
it('an advertised host emits memory.written carrying a stable memoryId', async () => {
|
|
18
|
+
const cap = await readMemoryAttributionCap();
|
|
19
|
+
if (!emitsWriteEvents(cap)) return;
|
|
20
|
+
const runId = await seedRun('mem-attr-emit');
|
|
21
|
+
if (!runId) return;
|
|
22
|
+
try {
|
|
23
|
+
await pollUntilTerminal(runId, { timeoutMs: 10_000 });
|
|
24
|
+
} catch {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const events = await memoryWrittenEvents(runId);
|
|
28
|
+
if (events.length === 0) return; // run wrote no memory — soft-skip
|
|
29
|
+
for (const e of events) {
|
|
30
|
+
const memoryId = (e.payload as { memoryId?: unknown } | undefined)?.memoryId;
|
|
31
|
+
expect(
|
|
32
|
+
typeof memoryId === 'string' && memoryId.length > 0,
|
|
33
|
+
driver.describe('RFC 0057 §B', 'memory.written.memoryId MUST be a stable, non-empty identifier'),
|
|
34
|
+
).toBe(true);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('a host without the capability emits no memory.written', async () => {
|
|
39
|
+
const cap = await readMemoryAttributionCap();
|
|
40
|
+
if (emitsWriteEvents(cap)) return; // advertised — N/A
|
|
41
|
+
const runId = await seedRun('mem-attr-absent');
|
|
42
|
+
if (!runId) return;
|
|
43
|
+
try {
|
|
44
|
+
await pollUntilTerminal(runId, { timeoutMs: 10_000 });
|
|
45
|
+
} catch {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const events = await memoryWrittenEvents(runId);
|
|
49
|
+
expect(
|
|
50
|
+
events.length,
|
|
51
|
+
driver.describe('RFC 0057 §A', 'a host not advertising memory.attribution MUST NOT emit memory.written'),
|
|
52
|
+
).toBe(0);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-attribution-no-content — RFC 0057 §C + SECURITY/invariants.yaml
|
|
3
|
+
* `memory-attribution-no-content`. A `memory.written` payload carries
|
|
4
|
+
* identifiers + non-secret tags only — never the memory entry content (the
|
|
5
|
+
* read-side serves that, already SR-1-redacted).
|
|
6
|
+
*
|
|
7
|
+
* Gated on `capabilities.memory.attribution.emitsWriteEvents`; soft-skips when
|
|
8
|
+
* unadvertised or when the seeded run wrote no memory.
|
|
9
|
+
*
|
|
10
|
+
* @see RFCS/0057-memory-write-attribution-event.md §C
|
|
11
|
+
* @see SECURITY/invariants.yaml — memory-attribution-no-content
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect } from 'vitest';
|
|
15
|
+
import { driver } from '../lib/driver.js';
|
|
16
|
+
import { pollUntilTerminal } from '../lib/polling.js';
|
|
17
|
+
import { readMemoryAttributionCap, emitsWriteEvents, seedRun, memoryWrittenEvents } from '../lib/memoryAttribution.js';
|
|
18
|
+
|
|
19
|
+
describe('memory-attribution-no-content (RFC 0057 §C)', () => {
|
|
20
|
+
it('memory.written payloads carry no entry content', async () => {
|
|
21
|
+
const cap = await readMemoryAttributionCap();
|
|
22
|
+
if (!emitsWriteEvents(cap)) return;
|
|
23
|
+
const runId = await seedRun('mem-attr-no-content');
|
|
24
|
+
if (!runId) return;
|
|
25
|
+
try {
|
|
26
|
+
await pollUntilTerminal(runId, { timeoutMs: 10_000 });
|
|
27
|
+
} catch {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const events = await memoryWrittenEvents(runId);
|
|
31
|
+
if (events.length === 0) return; // run wrote no memory — soft-skip
|
|
32
|
+
for (const e of events) {
|
|
33
|
+
const payload = e.payload ?? {};
|
|
34
|
+
expect(
|
|
35
|
+
'content' in payload,
|
|
36
|
+
driver.describe('RFC 0057 §C', 'memory.written MUST NOT carry the entry content field'),
|
|
37
|
+
).toBe(false);
|
|
38
|
+
expect(
|
|
39
|
+
typeof (payload as { memoryRef?: unknown }).memoryRef === 'string' &&
|
|
40
|
+
typeof (payload as { memoryId?: unknown }).memoryId === 'string',
|
|
41
|
+
driver.describe('RFC 0057 §B', 'memory.written MUST carry memoryRef + memoryId identifiers'),
|
|
42
|
+
).toBe(true);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-attribution-replay-stable — RFC 0057 §D. `memory.written` is an
|
|
3
|
+
* immutable recorded fact: a `replay`-mode fork MUST NOT mint a new
|
|
4
|
+
* `memoryId` for a write the source run already recorded. This asserts the
|
|
5
|
+
* "MUST NOT regenerate" half — every `memory.written` on a replayed run
|
|
6
|
+
* reuses a `memoryId` the source run recorded (a compliant host that
|
|
7
|
+
* suppresses re-mint on replay satisfies this vacuously with zero events).
|
|
8
|
+
*
|
|
9
|
+
* Gated on `capabilities.memory.attribution.emitsWriteEvents`; soft-skips
|
|
10
|
+
* when unadvertised, when the seeded run wrote no memory, or when the host
|
|
11
|
+
* doesn't support `:fork` in `replay` mode.
|
|
12
|
+
*
|
|
13
|
+
* @see RFCS/0057-memory-write-attribution-event.md §D
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect } from 'vitest';
|
|
17
|
+
import { driver } from '../lib/driver.js';
|
|
18
|
+
import { pollUntilTerminal } from '../lib/polling.js';
|
|
19
|
+
import { readMemoryAttributionCap, emitsWriteEvents, seedRun, memoryWrittenEvents } from '../lib/memoryAttribution.js';
|
|
20
|
+
|
|
21
|
+
function memoryIdOf(payload: Record<string, unknown> | undefined): string | null {
|
|
22
|
+
const id = (payload ?? {})['memoryId'];
|
|
23
|
+
return typeof id === 'string' ? id : null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('memory-attribution-replay-stable (RFC 0057 §D)', () => {
|
|
27
|
+
it('a replay-mode fork introduces no memory.written with a new memoryId', async () => {
|
|
28
|
+
const cap = await readMemoryAttributionCap();
|
|
29
|
+
if (!emitsWriteEvents(cap)) return;
|
|
30
|
+
const runId = await seedRun('mem-attr-replay');
|
|
31
|
+
if (!runId) return;
|
|
32
|
+
try {
|
|
33
|
+
await pollUntilTerminal(runId, { timeoutMs: 10_000 });
|
|
34
|
+
} catch {
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const original = await memoryWrittenEvents(runId);
|
|
38
|
+
if (original.length === 0) return; // run wrote no memory — nothing to test
|
|
39
|
+
const recordedIds = new Set(original.map((e) => memoryIdOf(e.payload)).filter((x): x is string => x !== null));
|
|
40
|
+
|
|
41
|
+
const fork = await driver.post(`/v1/runs/${runId}:fork`, { fromSeq: 0, mode: 'replay' });
|
|
42
|
+
if (fork.status !== 200 && fork.status !== 201) return; // replay fork unsupported — soft-skip
|
|
43
|
+
const forkId = (fork.json as { runId?: string } | undefined)?.runId;
|
|
44
|
+
if (!forkId) return;
|
|
45
|
+
try {
|
|
46
|
+
await pollUntilTerminal(forkId, { timeoutMs: 10_000 });
|
|
47
|
+
} catch {
|
|
48
|
+
/* still assert on whatever the replay emitted */
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const replayed = await memoryWrittenEvents(forkId);
|
|
52
|
+
for (const e of replayed) {
|
|
53
|
+
const id = memoryIdOf(e.payload);
|
|
54
|
+
expect(
|
|
55
|
+
id !== null && recordedIds.has(id),
|
|
56
|
+
driver.describe('RFC 0057 §D', 'a replay MUST NOT regenerate memoryId — every replayed memory.written reuses a recorded id'),
|
|
57
|
+
).toBe(true);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-attribution-shape — RFC 0057 §A. The `capabilities.memory.attribution`
|
|
3
|
+
* advertisement block is either absent or a well-formed object.
|
|
4
|
+
*
|
|
5
|
+
* Status: ACTIVE (advertisement-shape; always runs). Behavioral coverage lives
|
|
6
|
+
* in the sibling memory-attribution-*.test.ts scenarios, gated on
|
|
7
|
+
* `capabilities.memory.attribution.emitsWriteEvents`.
|
|
8
|
+
*
|
|
9
|
+
* @see RFCS/0057-memory-write-attribution-event.md §A
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect } from 'vitest';
|
|
13
|
+
import { driver } from '../lib/driver.js';
|
|
14
|
+
import { readMemoryAttributionCap } from '../lib/memoryAttribution.js';
|
|
15
|
+
|
|
16
|
+
describe('memory-attribution-shape: advertisement (RFC 0057 §A)', () => {
|
|
17
|
+
it('capabilities.memory.attribution is absent or a well-formed object', async () => {
|
|
18
|
+
const cap = await readMemoryAttributionCap();
|
|
19
|
+
if (cap === null) return; // not advertised — valid
|
|
20
|
+
expect(
|
|
21
|
+
cap.supported,
|
|
22
|
+
driver.describe('capabilities.schema.json §memory.attribution', 'memory.attribution.supported MUST be the literal true when the block is present'),
|
|
23
|
+
).toBe(true);
|
|
24
|
+
if (cap.emitsWriteEvents !== undefined) {
|
|
25
|
+
expect(typeof cap.emitsWriteEvents).toBe('boolean');
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
});
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* memory-attribution-tenant-scoped — RFC 0057 §C + SECURITY/invariants.yaml
|
|
3
|
+
* `memory-attribution-tenant-scoped`. A run's `memory.written` events appear
|
|
4
|
+
* only on that run's stream (mirrors CTI-1). The full cross-tenant proof
|
|
5
|
+
* (tenant B cannot read tenant A's run stream) needs a multi-tenant auth seam
|
|
6
|
+
* not standardized for this surface — that half soft-skips, mirroring
|
|
7
|
+
* `feedback-cross-tenant-isolation`.
|
|
8
|
+
*
|
|
9
|
+
* Gated on `capabilities.memory.attribution.emitsWriteEvents`.
|
|
10
|
+
*
|
|
11
|
+
* @see RFCS/0057-memory-write-attribution-event.md §C
|
|
12
|
+
* @see SECURITY/invariants.yaml — memory-attribution-tenant-scoped
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, it, expect } from 'vitest';
|
|
16
|
+
import { driver } from '../lib/driver.js';
|
|
17
|
+
import { pollUntilTerminal } from '../lib/polling.js';
|
|
18
|
+
import { readMemoryAttributionCap, emitsWriteEvents, seedRun, memoryWrittenEvents } from '../lib/memoryAttribution.js';
|
|
19
|
+
|
|
20
|
+
describe('memory-attribution-tenant-scoped (RFC 0057 §C)', () => {
|
|
21
|
+
it("a run's memory.written events appear only on that run's stream", async () => {
|
|
22
|
+
const cap = await readMemoryAttributionCap();
|
|
23
|
+
if (!emitsWriteEvents(cap)) return;
|
|
24
|
+
const runId = await seedRun('mem-attr-cti');
|
|
25
|
+
if (!runId) return;
|
|
26
|
+
try {
|
|
27
|
+
await pollUntilTerminal(runId, { timeoutMs: 10_000 });
|
|
28
|
+
} catch {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const events = await memoryWrittenEvents(runId);
|
|
32
|
+
if (events.length === 0) return;
|
|
33
|
+
// Every memory.written we read came from THIS run's /events stream; if the
|
|
34
|
+
// host echoes a runId in the event it MUST be this run's (no cross-run leak).
|
|
35
|
+
for (const e of events) {
|
|
36
|
+
if (typeof e.runId === 'string') {
|
|
37
|
+
expect(
|
|
38
|
+
e.runId,
|
|
39
|
+
driver.describe('RFC 0057 §C', "a memory.written event MUST belong to its own run's stream (CTI-1)"),
|
|
40
|
+
).toBe(runId);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
});
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
import { describe, it, expect } from 'vitest';
|
|
28
28
|
import { driver } from '../lib/driver.js';
|
|
29
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
29
30
|
|
|
30
31
|
const MEMORY_REF = 'mem_tenant:default_agent:conformance-rfc0012-event_longTerm';
|
|
31
32
|
|
|
@@ -35,7 +36,7 @@ interface MemoryCaps {
|
|
|
35
36
|
|
|
36
37
|
async function isCompactionAdvertised(): Promise<boolean> {
|
|
37
38
|
const disco = await driver.get('/.well-known/openwop');
|
|
38
|
-
const memory = (disco.json
|
|
39
|
+
const memory = capabilityFamily<MemoryCaps>(disco.json, 'memory');
|
|
39
40
|
return memory?.compaction?.supported === true;
|
|
40
41
|
}
|
|
41
42
|
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
|
|
21
21
|
import { describe, it, expect } from 'vitest';
|
|
22
22
|
import { driver } from '../lib/driver.js';
|
|
23
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
23
24
|
|
|
24
25
|
const MEMORY_REF = 'mem_tenant:default_agent:conformance-rfc0012-tag_longTerm';
|
|
25
26
|
const COMPACTED_FROM_RE = /^compacted-from:[^\s:][^\s]*$/;
|
|
@@ -34,7 +35,7 @@ interface MemoryListResponse {
|
|
|
34
35
|
|
|
35
36
|
async function isCompactionAdvertised(): Promise<boolean> {
|
|
36
37
|
const disco = await driver.get('/.well-known/openwop');
|
|
37
|
-
const memory = (disco.json
|
|
38
|
+
const memory = capabilityFamily<MemoryCaps>(disco.json, 'memory');
|
|
38
39
|
return memory?.compaction?.supported === true;
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
|
|
29
29
|
import { describe, it, expect } from 'vitest';
|
|
30
30
|
import { driver } from '../lib/driver.js';
|
|
31
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
31
32
|
|
|
32
33
|
const MEMORY_REF = 'mem_tenant:default_agent:conformance-rfc0012-sr1_longTerm';
|
|
33
34
|
|
|
@@ -37,7 +38,7 @@ interface MemoryCaps {
|
|
|
37
38
|
|
|
38
39
|
async function isCompactionAdvertised(): Promise<boolean> {
|
|
39
40
|
const disco = await driver.get('/.well-known/openwop');
|
|
40
|
-
const memory = (disco.json
|
|
41
|
+
const memory = capabilityFamily<MemoryCaps>(disco.json, 'memory');
|
|
41
42
|
return memory?.compaction?.supported === true;
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background memory consolidation — idempotence + SR-1 carry-forward
|
|
3
|
+
* (RFC 0068, `Draft`).
|
|
4
|
+
*
|
|
5
|
+
* Gated on `capabilities.agents.memoryConsolidation.supported`. Drives the
|
|
6
|
+
* documented host seam `POST /v1/host/sample/memory/consolidate` (staged
|
|
7
|
+
* per the RFC 0027 §G precedent — soft-skips on 404/501 until a reference
|
|
8
|
+
* host wires it). Asserts:
|
|
9
|
+
* - a consolidation pass emits `agent.memory.consolidated` with
|
|
10
|
+
* `outputCount <= inputCount` (RFC 0068 §D);
|
|
11
|
+
* - a second pass over the unchanged corpus is a no-op
|
|
12
|
+
* (`inputCount == outputCount`) — the idempotence MUST that bounds
|
|
13
|
+
* runaway consolidation;
|
|
14
|
+
* - SR-1 carry-forward — a redacted secret in a source entry stays
|
|
15
|
+
* redacted in a consolidated entry.
|
|
16
|
+
*
|
|
17
|
+
* Hosts that omit the capability skip cleanly.
|
|
18
|
+
*
|
|
19
|
+
* Spec references:
|
|
20
|
+
* - https://github.com/openwop/openwop/blob/main/spec/v1/agent-memory.md §"Background consolidation"
|
|
21
|
+
* - https://github.com/openwop/openwop/blob/main/RFCS/0068-memory-consolidation-and-standing-commitments.md
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { describe, it, expect } from 'vitest';
|
|
25
|
+
import { driver } from '../lib/driver.js';
|
|
26
|
+
|
|
27
|
+
interface ConsolidationCaps {
|
|
28
|
+
agents?: { memoryConsolidation?: { supported?: boolean } };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ConsolidateResult {
|
|
32
|
+
event?: { inputCount?: number; outputCount?: number };
|
|
33
|
+
secretLeaked?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function consolidationSupported(): Promise<boolean> {
|
|
37
|
+
const res = await driver.get('/.well-known/openwop', { authenticated: false });
|
|
38
|
+
if (res.status !== 200) return false;
|
|
39
|
+
return Boolean((res.json as ConsolidationCaps).agents?.memoryConsolidation?.supported);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('memory-consolidation-idempotent: pass contract (RFC 0068 §D, capability-gated)', () => {
|
|
43
|
+
it('a consolidation pass reduces or holds entry count and is idempotent on a stable corpus', async () => {
|
|
44
|
+
if (!(await consolidationSupported())) return; // capability absent — gated skip
|
|
45
|
+
|
|
46
|
+
const first = await driver.post('/v1/host/sample/memory/consolidate', {
|
|
47
|
+
memoryRef: 'mem://conformance/consolidation',
|
|
48
|
+
includeSecretCanary: true,
|
|
49
|
+
});
|
|
50
|
+
if (first.status === 404 || first.status === 501) return; // seam not wired — soft-skip
|
|
51
|
+
|
|
52
|
+
expect(first.status, driver.describe('RFC 0068 §D', 'an advertised consolidation seam MUST succeed')).toBe(200);
|
|
53
|
+
const r1 = first.json as ConsolidateResult;
|
|
54
|
+
const in1 = r1.event?.inputCount ?? 0;
|
|
55
|
+
const out1 = r1.event?.outputCount ?? 0;
|
|
56
|
+
expect(out1, driver.describe('RFC 0068 §D.1', 'outputCount MUST be <= inputCount for a merge/dedup pass')).toBeLessThanOrEqual(in1);
|
|
57
|
+
|
|
58
|
+
// §D.2 — a second pass over the unchanged corpus is a no-op.
|
|
59
|
+
const second = await driver.post('/v1/host/sample/memory/consolidate', {
|
|
60
|
+
memoryRef: 'mem://conformance/consolidation',
|
|
61
|
+
});
|
|
62
|
+
if (second.status === 404 || second.status === 501) return;
|
|
63
|
+
const r2 = second.json as ConsolidateResult;
|
|
64
|
+
expect(
|
|
65
|
+
r2.event?.inputCount,
|
|
66
|
+
driver.describe('RFC 0068 §D.2', 'a second pass over an unchanged corpus MUST be a no-op (inputCount == outputCount)'),
|
|
67
|
+
).toBe(r2.event?.outputCount);
|
|
68
|
+
|
|
69
|
+
// §D.3 — SR-1 carry-forward: a redacted secret stays redacted in the consolidated entry.
|
|
70
|
+
if (typeof r1.secretLeaked === 'boolean') {
|
|
71
|
+
expect(
|
|
72
|
+
r1.secretLeaked,
|
|
73
|
+
driver.describe('RFC 0068 §D.3 / agent-memory.md §SR-1', 'a redacted secret MUST NOT re-appear in a consolidated entry'),
|
|
74
|
+
).toBe(false);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory-consolidation + commitment event shapes (RFC 0068, `Draft`).
|
|
3
|
+
*
|
|
4
|
+
* Always-on, server-free schema-shape probe. Verifies that:
|
|
5
|
+
* - `capabilities.agents.memoryConsolidation` + `agents.commitments`
|
|
6
|
+
* sub-blocks are declared on the capabilities schema.
|
|
7
|
+
* - the `agent.memory.consolidated` + `commitment.fired` payload $defs
|
|
8
|
+
* validate conforming payloads and reject malformed ones (a
|
|
9
|
+
* `commitment.fired` missing `memoryRef` is rejected — a commitment
|
|
10
|
+
* with no memory provenance is not an *inferred* commitment).
|
|
11
|
+
* - both event names appear in the RunEventType enum.
|
|
12
|
+
*
|
|
13
|
+
* Distinct from RFC 0062 distillation (`memory.compacted`): consolidation
|
|
14
|
+
* reconciles long-term memory; this scenario asserts the new event
|
|
15
|
+
* contract, not host behavior.
|
|
16
|
+
*
|
|
17
|
+
* Spec references:
|
|
18
|
+
* - https://github.com/openwop/openwop/blob/main/spec/v1/agent-memory.md §"Background consolidation"
|
|
19
|
+
* - https://github.com/openwop/openwop/blob/main/spec/v1/agent-memory.md §"Inferred commitments"
|
|
20
|
+
* - https://github.com/openwop/openwop/blob/main/RFCS/0068-memory-consolidation-and-standing-commitments.md
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { describe, it, expect } from 'vitest';
|
|
24
|
+
import { readFileSync } from 'node:fs';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
27
|
+
import addFormats from 'ajv-formats';
|
|
28
|
+
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
29
|
+
|
|
30
|
+
/** Server-free assertion-message helper (mirrors driver.describe's "spec — requirement" shape without requiring OPENWOP_BASE_URL). */
|
|
31
|
+
const why = (specRef: string, requirement: string): string => `${specRef} — ${requirement}`;
|
|
32
|
+
|
|
33
|
+
function loadSchema(name: string): Record<string, unknown> {
|
|
34
|
+
return JSON.parse(readFileSync(join(SCHEMAS_DIR, name), 'utf8')) as Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe('memory-consolidation-shape: capability advertisement (RFC 0068, server-free)', () => {
|
|
38
|
+
it('the capabilities schema declares agents.memoryConsolidation + agents.commitments', () => {
|
|
39
|
+
const caps = loadSchema('capabilities.schema.json');
|
|
40
|
+
const agents = (caps.properties as Record<string, { properties?: Record<string, unknown> }>).agents;
|
|
41
|
+
expect(
|
|
42
|
+
agents?.properties?.memoryConsolidation,
|
|
43
|
+
why('capabilities.md §agents', 'agents.memoryConsolidation MUST be declared'),
|
|
44
|
+
).toBeDefined();
|
|
45
|
+
expect(
|
|
46
|
+
agents?.properties?.commitments,
|
|
47
|
+
why('capabilities.md §agents', 'agents.commitments MUST be declared'),
|
|
48
|
+
).toBeDefined();
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('memory-consolidation-shape: event payloads (RFC 0068, server-free)', () => {
|
|
53
|
+
const payloads = loadSchema('run-event-payloads.schema.json');
|
|
54
|
+
const ajv = new Ajv2020({ strict: false, allErrors: true });
|
|
55
|
+
addFormats(ajv);
|
|
56
|
+
ajv.addSchema(payloads, 'payloads');
|
|
57
|
+
|
|
58
|
+
const consolidated = ajv.getSchema('payloads#/$defs/agentMemoryConsolidated');
|
|
59
|
+
const fired = ajv.getSchema('payloads#/$defs/commitmentFired');
|
|
60
|
+
|
|
61
|
+
it('agent.memory.consolidated validates a content-free pass summary', () => {
|
|
62
|
+
expect(consolidated, 'the agentMemoryConsolidated $def MUST exist').toBeTruthy();
|
|
63
|
+
expect(
|
|
64
|
+
consolidated!({ memoryRef: 'mem://a/agent-1', inputCount: 240, outputCount: 201, trigger: 'host-managed' }),
|
|
65
|
+
why('RFC 0068 §B', 'a conforming agent.memory.consolidated payload MUST validate'),
|
|
66
|
+
).toBe(true);
|
|
67
|
+
// Negative: outputCount as string fails the integer type.
|
|
68
|
+
expect(consolidated!({ memoryRef: 'mem://a/agent-1', inputCount: 1, outputCount: 'x' })).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('commitment.fired validates a content-free fire record and requires memoryRef', () => {
|
|
72
|
+
expect(fired, 'the commitmentFired $def MUST exist').toBeTruthy();
|
|
73
|
+
expect(
|
|
74
|
+
fired!({ commitmentId: 'cmt-1', memoryRef: 'mem://a/agent-1', condition: 'predicate', enqueuedRunId: 'run-1' }),
|
|
75
|
+
why('RFC 0068 §C', 'a conforming commitment.fired payload MUST validate'),
|
|
76
|
+
).toBe(true);
|
|
77
|
+
// Negative: missing memoryRef — a commitment with no provenance breaks CTI-1 binding.
|
|
78
|
+
expect(
|
|
79
|
+
fired!({ commitmentId: 'cmt-1', condition: 'time' }),
|
|
80
|
+
why('RFC 0068 §C', 'commitment.fired without memoryRef MUST be rejected'),
|
|
81
|
+
).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('both event names appear in the RunEventType enum', () => {
|
|
85
|
+
const runEvent = loadSchema('run-event.schema.json');
|
|
86
|
+
const enumVals = (runEvent.$defs as Record<string, { enum?: string[] }>).RunEventType?.enum ?? [];
|
|
87
|
+
expect(enumVals).toContain('agent.memory.consolidated');
|
|
88
|
+
expect(enumVals).toContain('commitment.fired');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { describe, it, expect } from 'vitest';
|
|
19
19
|
import { driver } from '../lib/driver.js';
|
|
20
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
20
21
|
|
|
21
22
|
const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
|
|
22
23
|
|
|
@@ -64,7 +65,7 @@ describe.skipIf(HTTP_SKIP)('model-capability-substituted: advertisement shape (R
|
|
|
64
65
|
it('capabilities.modelCapabilities (when present) conforms to RFC 0031 §E', async () => {
|
|
65
66
|
const d = await readDiscovery();
|
|
66
67
|
if (d === null) return;
|
|
67
|
-
const mc = d
|
|
68
|
+
const mc = capabilityFamily(d, 'modelCapabilities');
|
|
68
69
|
if (mc === undefined) return;
|
|
69
70
|
expect(
|
|
70
71
|
typeof mc.supported,
|