@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,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat card pack manifest validation — `chat-card-packs.md` §"Manifest format"
|
|
3
|
+
* + `schemas/chat-card-pack-manifest.schema.json` (RFC 0071 Phase 2).
|
|
4
|
+
*
|
|
5
|
+
* Server-free schema-validation scenario for `kind: "card"` packs:
|
|
6
|
+
* 1. Positive: a valid card manifest validates.
|
|
7
|
+
* 2. Negative — kind/contents mismatch: cards[] + a foreign artifactTypes[]
|
|
8
|
+
* is rejected (additionalProperties -> pack_kind_invalid at the registry).
|
|
9
|
+
* 3. Negative — empty cards[] (minItems).
|
|
10
|
+
* 4. Negative — invalid cardTypeId (uppercase scope -> pattern).
|
|
11
|
+
* 5. Negative — a card missing prompt (required).
|
|
12
|
+
* 6. Negative — a non-portable inputs[].type that is neither in the closed
|
|
13
|
+
* enum nor a vendor-prefixed extension (`canvas-reference` -> pattern).
|
|
14
|
+
* 7. Positive — a vendor.*-prefixed inputs[].type extension is tolerated.
|
|
15
|
+
*
|
|
16
|
+
* Behavioral execution (`chat-card-pack-execution.test.ts` — prompt routed
|
|
17
|
+
* through ctx.aiEnvelope.generate, output validated against the linked
|
|
18
|
+
* outputArtifactType, untrusted-input trust-tag propagation) is the Phase-2
|
|
19
|
+
* `Active` gate (R2) and lands with a host advertising `host.chat.cardPacks`.
|
|
20
|
+
*
|
|
21
|
+
* @see spec/v1/chat-card-packs.md
|
|
22
|
+
* @see schemas/chat-card-pack-manifest.schema.json
|
|
23
|
+
* @see RFCS/0071-artifact-type-and-chat-card-packs.md
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { describe, it, expect } from 'vitest';
|
|
27
|
+
import { readFileSync } from 'node:fs';
|
|
28
|
+
import { join } from 'node:path';
|
|
29
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
30
|
+
import addFormats from 'ajv-formats';
|
|
31
|
+
import type { ErrorObject } from 'ajv';
|
|
32
|
+
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
33
|
+
|
|
34
|
+
const SCHEMA_PATH = join(SCHEMAS_DIR, 'chat-card-pack-manifest.schema.json');
|
|
35
|
+
|
|
36
|
+
function validManifest() {
|
|
37
|
+
return {
|
|
38
|
+
kind: 'card',
|
|
39
|
+
name: 'vendor.acme.cad-cards',
|
|
40
|
+
version: '1.0.0',
|
|
41
|
+
engines: { openwop: '>=1.1' },
|
|
42
|
+
cards: [
|
|
43
|
+
{
|
|
44
|
+
cardTypeId: 'vendor.acme.cad.model.create',
|
|
45
|
+
prompt: {
|
|
46
|
+
template: 'Design a model for: {{spec}}',
|
|
47
|
+
placeholderMapping: { spec: 'inputs.spec' },
|
|
48
|
+
temperature: 0.2,
|
|
49
|
+
},
|
|
50
|
+
inputs: [{ id: 'spec', type: 'text', label: 'Part spec', required: true }],
|
|
51
|
+
outputArtifactType: 'vendor.acme.cad.model',
|
|
52
|
+
outputSchemaRef: 'schemas/cad-model.schema.json',
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('category: chat-card-pack manifest validation', () => {
|
|
59
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
60
|
+
addFormats(ajv);
|
|
61
|
+
const validate = ajv.compile(JSON.parse(readFileSync(SCHEMA_PATH, 'utf8')));
|
|
62
|
+
|
|
63
|
+
const failsWith = (manifest: unknown, keyword: string): ErrorObject[] => {
|
|
64
|
+
expect(validate(manifest)).toBe(false);
|
|
65
|
+
return (validate.errors ?? []).filter((e) => e.keyword === keyword);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
it('positive: a valid chat card pack manifest validates cleanly', () => {
|
|
69
|
+
expect(
|
|
70
|
+
validate(validManifest()),
|
|
71
|
+
`chat-card-packs.md §"Manifest format": a well-formed kind:"card" manifest MUST validate. Errors: ${JSON.stringify(validate.errors)}`,
|
|
72
|
+
).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('negative: a manifest mixing cards[] and artifactTypes[] is rejected', () => {
|
|
76
|
+
const manifest = { ...validManifest(), artifactTypes: [{ artifactTypeId: 'vendor.acme.x', schemaRef: 'x.json' }] };
|
|
77
|
+
const errs = failsWith(manifest, 'additionalProperties');
|
|
78
|
+
expect(
|
|
79
|
+
errs.some((e) => (e.params as { additionalProperty?: string }).additionalProperty === 'artifactTypes'),
|
|
80
|
+
'chat-card-packs.md §"Pack kind": one kind per pack (additionalProperties:false)',
|
|
81
|
+
).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('negative: an empty cards[] is rejected', () => {
|
|
85
|
+
expect(failsWith({ ...validManifest(), cards: [] }, 'minItems').length).toBeGreaterThan(0);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('negative: an uppercase-scope cardTypeId is rejected', () => {
|
|
89
|
+
const m = validManifest();
|
|
90
|
+
m.cards[0]!.cardTypeId = 'Vendor.Acme.Card';
|
|
91
|
+
expect(failsWith(m, 'pattern').length).toBeGreaterThan(0);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('negative: a card missing prompt is rejected', () => {
|
|
95
|
+
const m = validManifest();
|
|
96
|
+
delete (m.cards[0] as { prompt?: unknown }).prompt;
|
|
97
|
+
expect(failsWith(m, 'required').length).toBeGreaterThan(0);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('negative: a non-portable inputs[].type (canvas-reference) is rejected', () => {
|
|
101
|
+
const m = validManifest();
|
|
102
|
+
m.cards[0]!.inputs[0]!.type = 'canvas-reference';
|
|
103
|
+
expect(
|
|
104
|
+
failsWith(m, 'pattern').length,
|
|
105
|
+
'chat-card-packs.md §"Input fields": type is the closed portable enum OR a vendor.*/x- extension',
|
|
106
|
+
).toBeGreaterThan(0);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('positive: a vendor.*-prefixed inputs[].type extension is tolerated', () => {
|
|
110
|
+
const m = validManifest();
|
|
111
|
+
m.cards[0]!.inputs[0]!.type = 'vendor.myndhyve.canvas-ref';
|
|
112
|
+
expect(
|
|
113
|
+
validate(m),
|
|
114
|
+
'chat-card-packs.md §"Input fields": a vendor.<org>.<kind> input type extension MUST validate (other hosts ignore it)',
|
|
115
|
+
).toBe(true);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('positive: the full portable inputs[].type subset validates (G9, incl. multiselect + file)', () => {
|
|
119
|
+
for (const t of ['text', 'longtext', 'number', 'boolean', 'select', 'multiselect', 'file', 'artifact-ref']) {
|
|
120
|
+
const m = validManifest();
|
|
121
|
+
m.cards[0]!.inputs[0]!.type = t;
|
|
122
|
+
expect(
|
|
123
|
+
validate(m),
|
|
124
|
+
`chat-card-packs.md §"Input fields": portable inputs[].type "${t}" MUST validate (G9 resolved 2026-05-27)`,
|
|
125
|
+
).toBe(true);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inferred standing commitment — fire-once + content-free (RFC 0068, `Draft`).
|
|
3
|
+
*
|
|
4
|
+
* Gated on `capabilities.agents.commitments.supported`. Drives the
|
|
5
|
+
* documented host seam `POST /v1/host/sample/commitment/fire` (staged per
|
|
6
|
+
* the RFC 0027 §G precedent — soft-skips on 404/501 until a reference host
|
|
7
|
+
* wires it). Asserts:
|
|
8
|
+
* - a fired commitment emits a content-free `commitment.fired` carrying
|
|
9
|
+
* `commitmentId` + `memoryRef` provenance + `condition` (RFC 0068 §C);
|
|
10
|
+
* - the event MUST NOT carry the inferred intention text (no-content);
|
|
11
|
+
* - the commitment fires at most once per satisfied condition.
|
|
12
|
+
*
|
|
13
|
+
* Hosts that omit the capability skip cleanly.
|
|
14
|
+
*
|
|
15
|
+
* Spec references:
|
|
16
|
+
* - https://github.com/openwop/openwop/blob/main/spec/v1/agent-memory.md §"Inferred commitments"
|
|
17
|
+
* - https://github.com/openwop/openwop/blob/main/RFCS/0068-memory-consolidation-and-standing-commitments.md
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, it, expect } from 'vitest';
|
|
21
|
+
import { driver } from '../lib/driver.js';
|
|
22
|
+
|
|
23
|
+
interface CommitmentCaps {
|
|
24
|
+
agents?: { commitments?: { supported?: boolean } };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface FireResult {
|
|
28
|
+
event?: {
|
|
29
|
+
commitmentId?: string;
|
|
30
|
+
memoryRef?: string;
|
|
31
|
+
condition?: string;
|
|
32
|
+
[k: string]: unknown;
|
|
33
|
+
};
|
|
34
|
+
fireCount?: number;
|
|
35
|
+
/** The plaintext intention the host inferred — used only to assert it does NOT appear on the event. */
|
|
36
|
+
intentionCanary?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function commitmentsSupported(): Promise<boolean> {
|
|
40
|
+
const res = await driver.get('/.well-known/openwop', { authenticated: false });
|
|
41
|
+
if (res.status !== 200) return false;
|
|
42
|
+
return Boolean((res.json as CommitmentCaps).agents?.commitments?.supported);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
describe('commitment-fired: fire contract (RFC 0068 §C, capability-gated)', () => {
|
|
46
|
+
it('a fired commitment emits a content-free event with memory provenance, exactly once', async () => {
|
|
47
|
+
if (!(await commitmentsSupported())) return; // capability absent — gated skip
|
|
48
|
+
|
|
49
|
+
const res = await driver.post('/v1/host/sample/commitment/fire', {
|
|
50
|
+
memoryRef: 'mem://conformance/commitments',
|
|
51
|
+
condition: 'predicate',
|
|
52
|
+
includeIntentionCanary: true,
|
|
53
|
+
});
|
|
54
|
+
if (res.status === 404 || res.status === 501) return; // seam not wired — soft-skip
|
|
55
|
+
|
|
56
|
+
expect(res.status, driver.describe('RFC 0068 §C', 'an advertised commitment seam MUST succeed')).toBe(200);
|
|
57
|
+
const r = res.json as FireResult;
|
|
58
|
+
|
|
59
|
+
// §C — required identifiers.
|
|
60
|
+
expect(r.event?.commitmentId, driver.describe('RFC 0068 §C', 'commitment.fired MUST carry commitmentId')).toBeTruthy();
|
|
61
|
+
expect(
|
|
62
|
+
r.event?.memoryRef,
|
|
63
|
+
driver.describe('RFC 0068 §C.1', 'commitment.fired MUST carry the source memoryRef (CTI-1 provenance)'),
|
|
64
|
+
).toBeTruthy();
|
|
65
|
+
|
|
66
|
+
// §C.3 — content-free: the inferred intention text MUST NOT appear on the event.
|
|
67
|
+
if (typeof r.intentionCanary === 'string' && r.intentionCanary.length > 0) {
|
|
68
|
+
const serialized = JSON.stringify(r.event ?? {});
|
|
69
|
+
expect(
|
|
70
|
+
serialized.includes(r.intentionCanary),
|
|
71
|
+
driver.describe('RFC 0068 §C.3', 'the inferred intention text MUST NOT appear on the commitment.fired payload'),
|
|
72
|
+
).toBe(false);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// §C.2 — fire-once-per-condition (when the seam reports a count).
|
|
76
|
+
if (typeof r.fireCount === 'number') {
|
|
77
|
+
expect(
|
|
78
|
+
r.fireCount,
|
|
79
|
+
driver.describe('RFC 0068 §C.2', 'a commitment MUST fire at most once per satisfied condition'),
|
|
80
|
+
).toBeLessThanOrEqual(1);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -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
|
interface DiscoveryCredentials {
|
|
31
32
|
supported?: boolean;
|
|
@@ -43,7 +44,7 @@ const CANARY = 'OPENWOP_CRED_CANARY_4f1c8a2e9b';
|
|
|
43
44
|
async function readCredentials(): Promise<DiscoveryCredentials | null> {
|
|
44
45
|
const res = await driver.get('/.well-known/openwop');
|
|
45
46
|
const body = res.json as DiscoveryDoc | undefined;
|
|
46
|
-
return body
|
|
47
|
+
return capabilityFamily(body, 'credentials') ?? null;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
50
|
describe('credential-payload-redaction: advertisement shape (RFC 0046 §A)', () => {
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
|
|
22
22
|
import { describe, it, expect } from 'vitest';
|
|
23
23
|
import { driver } from '../lib/driver.js';
|
|
24
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
24
25
|
|
|
25
26
|
interface DiscoveryCredentials {
|
|
26
27
|
supported?: boolean;
|
|
@@ -42,7 +43,7 @@ const VALID_ROTATION: ReadonlySet<string> = new Set(['none', 'two-key-overlap'])
|
|
|
42
43
|
async function readCredentials(): Promise<DiscoveryCredentials | null> {
|
|
43
44
|
const res = await driver.get('/.well-known/openwop');
|
|
44
45
|
const body = res.json as DiscoveryDoc | undefined;
|
|
45
|
-
return body
|
|
46
|
+
return capabilityFamily(body, 'credentials') ?? null;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
describe('credentials-capability-shape: advertisement shape (RFC 0046 §A)', () => {
|
|
@@ -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 HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
|
|
31
32
|
const ORDERING_MODELS = new Set(['lamport', 'vector-clock', 'global-sequencer']);
|
|
@@ -55,7 +56,7 @@ describe.skipIf(HTTP_SKIP)('cross-engine-append-ordering: advertisement shape (R
|
|
|
55
56
|
it('capabilities.eventLog.crossEngineOrdering (when present) conforms to RFC 0036 §B', async () => {
|
|
56
57
|
const d = await readDiscovery();
|
|
57
58
|
if (d === null) return;
|
|
58
|
-
const ceo = d
|
|
59
|
+
const ceo = capabilityFamily<{ crossEngineOrdering?: { supported?: unknown; orderingModel?: unknown } }>(d, 'eventLog')?.crossEngineOrdering;
|
|
59
60
|
if (ceo === undefined) return; // host doesn't advertise — soft-skip
|
|
60
61
|
|
|
61
62
|
expect(
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
|
|
36
36
|
import { describe, it, expect } from 'vitest';
|
|
37
37
|
import { driver } from '../lib/driver.js';
|
|
38
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
38
39
|
|
|
39
40
|
const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
|
|
40
41
|
|
|
@@ -65,7 +66,7 @@ async function readDiscovery(): Promise<DiscoveryDoc | null> {
|
|
|
65
66
|
describe.skipIf(HTTP_SKIP)('cross-host-ancestry-endpoint: behavioral (RFC 0040 §C)', () => {
|
|
66
67
|
it('hosts advertising ancestryEndpointSupported MUST serve GET /v1/runs/{runId}/ancestry with the documented shape on a top-level run', async (ctx) => {
|
|
67
68
|
const d = await readDiscovery();
|
|
68
|
-
const chc = d
|
|
69
|
+
const chc = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.crossHostCausation;
|
|
69
70
|
if (chc?.ancestryEndpointSupported !== true) {
|
|
70
71
|
ctx.skip();
|
|
71
72
|
return;
|
|
@@ -112,7 +113,7 @@ describe.skipIf(HTTP_SKIP)('cross-host-ancestry-endpoint: behavioral (RFC 0040
|
|
|
112
113
|
|
|
113
114
|
it('hosts advertising crossHostCausation.supported but NOT ancestryEndpointSupported MUST return 404 from the ancestry endpoint', async (ctx) => {
|
|
114
115
|
const d = await readDiscovery();
|
|
115
|
-
const chc = d
|
|
116
|
+
const chc = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.crossHostCausation;
|
|
116
117
|
if (chc?.supported !== true) {
|
|
117
118
|
ctx.skip();
|
|
118
119
|
return;
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
|
|
28
28
|
import { describe, it, expect } from 'vitest';
|
|
29
29
|
import { driver } from '../lib/driver.js';
|
|
30
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
30
31
|
|
|
31
32
|
const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
|
|
32
33
|
|
|
@@ -63,7 +64,7 @@ describe.skipIf(HTTP_SKIP)('cross-host-causation-shape: advertisement shape (RFC
|
|
|
63
64
|
ctx.skip();
|
|
64
65
|
return;
|
|
65
66
|
}
|
|
66
|
-
const chc = d
|
|
67
|
+
const chc = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.crossHostCausation;
|
|
67
68
|
if (chc === undefined) {
|
|
68
69
|
ctx.skip(); // host doesn't advertise — soft-skip
|
|
69
70
|
return;
|
|
@@ -78,7 +79,7 @@ describe.skipIf(HTTP_SKIP)('cross-host-causation-shape: advertisement shape (RFC
|
|
|
78
79
|
).toBe('boolean');
|
|
79
80
|
|
|
80
81
|
if (chc.supported === true) {
|
|
81
|
-
const version = d
|
|
82
|
+
const version = capabilityFamily<{ executionModel?: { [k: string]: unknown; crossHostCausation?: Record<string, unknown>; replayDeterminism?: Record<string, unknown> } }>(d, 'multiAgent')?.executionModel?.version as number | undefined;
|
|
82
83
|
expect(
|
|
83
84
|
typeof version === 'number' && version >= 3,
|
|
84
85
|
driver.describe(
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { describe, it, expect } from 'vitest';
|
|
21
21
|
import { driver } from '../lib/driver.js';
|
|
22
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
22
23
|
|
|
23
24
|
interface DiscoveryDeadLetter {
|
|
24
25
|
supported?: boolean;
|
|
@@ -32,7 +33,7 @@ interface DiscoveryDoc {
|
|
|
32
33
|
async function readDeadLetter(): Promise<DiscoveryDeadLetter | null> {
|
|
33
34
|
const res = await driver.get('/.well-known/openwop');
|
|
34
35
|
const body = res.json as DiscoveryDoc | undefined;
|
|
35
|
-
return body
|
|
36
|
+
return capabilityFamily(body, 'deadLetter') ?? null;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
describe('deadletter-capability-shape: advertisement shape (RFC 0053 §A)', () => {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
import { describe, it, expect } from 'vitest';
|
|
25
25
|
import { driver } from '../lib/driver.js';
|
|
26
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
26
27
|
|
|
27
28
|
interface DiscoveryDoc {
|
|
28
29
|
capabilities?: { deadLetter?: { supported?: boolean } };
|
|
@@ -30,7 +31,7 @@ interface DiscoveryDoc {
|
|
|
30
31
|
|
|
31
32
|
async function deadLetterSupported(): Promise<boolean> {
|
|
32
33
|
const res = await driver.get('/.well-known/openwop');
|
|
33
|
-
return (res.json as DiscoveryDoc | undefined)
|
|
34
|
+
return capabilityFamily((res.json as DiscoveryDoc | undefined), 'deadLetter')?.supported === true;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
describe('deadletter-retry-exhaustion: retry exhaustion → dead-lettered + fork-eligible (RFC 0053 §C)', () => {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* distillation-index-roundtrip — RFC 0062 §B(5). After distillation the
|
|
3
|
+
* memory-index workspace file (`MEMORY-INDEX.json`, RFC 0059) is retrievable and
|
|
4
|
+
* the run reported updating the index (rides `workspace.updated`, not a bespoke
|
|
5
|
+
* index event).
|
|
6
|
+
*
|
|
7
|
+
* Gated on `capabilities.memory.distillation.supported` + `indexEmitted` + the
|
|
8
|
+
* host memory-distillation seam; soft-skips when any is absent. (The seam echoes
|
|
9
|
+
* the index file, so this scenario does not separately require the workspace
|
|
10
|
+
* read endpoint to be wired.)
|
|
11
|
+
*
|
|
12
|
+
* @see RFCS/0062-scheduled-memory-distillation.md §B
|
|
13
|
+
* @see RFCS/0059-agent-workspace.md — the durable layer the index rides
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect } from 'vitest';
|
|
17
|
+
import { driver } from '../lib/driver.js';
|
|
18
|
+
import { readDistillationCap, invokeDistill } from '../lib/distillation.js';
|
|
19
|
+
|
|
20
|
+
describe('distillation-index-roundtrip (RFC 0062 §B)', () => {
|
|
21
|
+
it('an indexEmitted run updates a retrievable memory-index manifest', async () => {
|
|
22
|
+
const cap = await readDistillationCap();
|
|
23
|
+
if (cap?.supported !== true || cap?.indexEmitted !== true) return;
|
|
24
|
+
const res = await invokeDistill({ memoryRef: 'conformance-distill', tokenBudget: 8000, indexEmitted: true });
|
|
25
|
+
if (res === null) return; // seam absent — soft-skip
|
|
26
|
+
expect(
|
|
27
|
+
res.body.indexUpdated === true || res.body.event?.distillation?.indexUpdated === true,
|
|
28
|
+
driver.describe('RFC 0062 §B', 'an indexEmitted distillation MUST report updating the memory index'),
|
|
29
|
+
).toBe(true);
|
|
30
|
+
expect(
|
|
31
|
+
res.body.indexFile !== undefined && res.body.indexFile !== null,
|
|
32
|
+
driver.describe('RFC 0062 §B', 'the MEMORY-INDEX.json manifest MUST be retrievable after distillation'),
|
|
33
|
+
).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* distillation-secret-carryforward — RFC 0062 §B(3). A redacted secret in
|
|
3
|
+
* source memory stays redacted in the distilled archive — the SR-1 carry-forward
|
|
4
|
+
* invariant (RFC 0012 §D) holds through distillation; the raw value never appears
|
|
5
|
+
* in the archive or the emitted `memory.compacted` event.
|
|
6
|
+
*
|
|
7
|
+
* Gated on `capabilities.memory.distillation.supported` + the host memory-
|
|
8
|
+
* distillation seam; soft-skips when either is absent.
|
|
9
|
+
*
|
|
10
|
+
* @see RFCS/0062-scheduled-memory-distillation.md §B
|
|
11
|
+
* @see spec/v1/agent-memory.md §SR-1 — Secret-Redaction Invariant
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect } from 'vitest';
|
|
15
|
+
import { driver } from '../lib/driver.js';
|
|
16
|
+
import { readDistillationCap, invokeDistill } from '../lib/distillation.js';
|
|
17
|
+
|
|
18
|
+
const CANARY = 'sk-canary-rfc0062-do-not-leak-qrs456';
|
|
19
|
+
|
|
20
|
+
describe('distillation-secret-carryforward (RFC 0062 §B)', () => {
|
|
21
|
+
it('a redacted secret in source memory never appears in the distilled output', async () => {
|
|
22
|
+
if ((await readDistillationCap())?.supported !== true) return;
|
|
23
|
+
const res = await invokeDistill({
|
|
24
|
+
memoryRef: 'conformance-distill',
|
|
25
|
+
tokenBudget: 8000,
|
|
26
|
+
includeSecretCanary: true,
|
|
27
|
+
sources: [{ content: `notes with embedded secret ${CANARY}` }],
|
|
28
|
+
});
|
|
29
|
+
if (res === null) return; // seam absent — soft-skip
|
|
30
|
+
expect(
|
|
31
|
+
JSON.stringify(res.body).includes(CANARY),
|
|
32
|
+
driver.describe('RFC 0062 §B', 'SR-1 carry-forward: a redacted secret MUST NOT re-appear in the archive or memory.compacted event'),
|
|
33
|
+
).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* distillation-shape — RFC 0062 §A. The `capabilities.memory.distillation`
|
|
3
|
+
* advertisement block is either absent or a well-formed object (with a positive
|
|
4
|
+
* `maxTokenBudget` when present).
|
|
5
|
+
*
|
|
6
|
+
* Status: ACTIVE (advertisement-shape; always runs). Behavioral coverage lives
|
|
7
|
+
* in the sibling distillation-*.test.ts scenarios, gated on `supported` + the
|
|
8
|
+
* host memory-distillation seam.
|
|
9
|
+
*
|
|
10
|
+
* @see RFCS/0062-scheduled-memory-distillation.md §A
|
|
11
|
+
* @see spec/v1/agent-memory.md §"Scheduled distillation"
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect } from 'vitest';
|
|
15
|
+
import { driver } from '../lib/driver.js';
|
|
16
|
+
import { readDistillationCap } from '../lib/distillation.js';
|
|
17
|
+
|
|
18
|
+
describe('distillation-shape: advertisement (RFC 0062 §A)', () => {
|
|
19
|
+
it('capabilities.memory.distillation is absent or a well-formed object', async () => {
|
|
20
|
+
const cap = await readDistillationCap();
|
|
21
|
+
if (cap === null) return; // not advertised — valid
|
|
22
|
+
expect(
|
|
23
|
+
typeof cap.supported,
|
|
24
|
+
driver.describe('capabilities.schema.json §memory.distillation', 'distillation.supported MUST be a boolean when the block is present'),
|
|
25
|
+
).toBe('boolean');
|
|
26
|
+
if (cap.maxTokenBudget !== undefined) {
|
|
27
|
+
expect(
|
|
28
|
+
typeof cap.maxTokenBudget === 'number' && (cap.maxTokenBudget as number) >= 1,
|
|
29
|
+
driver.describe('capabilities.schema.json §memory.distillation', 'maxTokenBudget MUST be a positive integer when present'),
|
|
30
|
+
).toBe(true);
|
|
31
|
+
}
|
|
32
|
+
for (const k of ['scheduled', 'indexEmitted'] as const) {
|
|
33
|
+
if (cap[k] !== undefined) {
|
|
34
|
+
expect(
|
|
35
|
+
typeof cap[k],
|
|
36
|
+
driver.describe('capabilities.schema.json §memory.distillation', `distillation.${k} MUST be a boolean when present`),
|
|
37
|
+
).toBe('boolean');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
});
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* distillation-stable-archive — RFC 0062 §B(4). The distilled archive is an
|
|
3
|
+
* immutable, addressable artifact: the same source set + budget MUST yield a
|
|
4
|
+
* byte-stable archive checksum (reproducible + auditable).
|
|
5
|
+
*
|
|
6
|
+
* Gated on `capabilities.memory.distillation.supported` + the host memory-
|
|
7
|
+
* distillation seam; soft-skips when either is absent.
|
|
8
|
+
*
|
|
9
|
+
* @see RFCS/0062-scheduled-memory-distillation.md §B
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect } from 'vitest';
|
|
13
|
+
import { driver } from '../lib/driver.js';
|
|
14
|
+
import { readDistillationCap, invokeDistill } from '../lib/distillation.js';
|
|
15
|
+
|
|
16
|
+
describe('distillation-stable-archive (RFC 0062 §B)', () => {
|
|
17
|
+
it('identical sources + budget produce an identical archive checksum', async () => {
|
|
18
|
+
if ((await readDistillationCap())?.supported !== true) return;
|
|
19
|
+
const req = {
|
|
20
|
+
memoryRef: 'conformance-distill',
|
|
21
|
+
tokenBudget: 8000,
|
|
22
|
+
sources: ['s1', 's2', 's3'],
|
|
23
|
+
};
|
|
24
|
+
const a = await invokeDistill(req);
|
|
25
|
+
if (a === null) return; // seam absent — soft-skip
|
|
26
|
+
const b = await invokeDistill(req);
|
|
27
|
+
if (b === null) return;
|
|
28
|
+
expect(
|
|
29
|
+
typeof a.body.archiveChecksum === 'string' && (a.body.archiveChecksum as string).length > 0,
|
|
30
|
+
driver.describe('RFC 0062 §B', 'a distillation run MUST produce a non-empty archive checksum'),
|
|
31
|
+
).toBe(true);
|
|
32
|
+
expect(
|
|
33
|
+
b.body.archiveChecksum,
|
|
34
|
+
driver.describe('RFC 0062 §B', 'the same source set + budget MUST yield a byte-stable archive'),
|
|
35
|
+
).toBe(a.body.archiveChecksum);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* distillation-token-budget — RFC 0062 §B. A distillation run stays within its
|
|
3
|
+
* token budget (`memory.compacted.distillation.tokensUsed ≤ tokenBudget`); an
|
|
4
|
+
* un-meetable budget fails with `token_budget_exceeded` and writes no partial
|
|
5
|
+
* archive (atomic).
|
|
6
|
+
*
|
|
7
|
+
* Gated on `capabilities.memory.distillation.supported` + the host memory-
|
|
8
|
+
* distillation seam; soft-skips when either is absent.
|
|
9
|
+
*
|
|
10
|
+
* @see RFCS/0062-scheduled-memory-distillation.md §B
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, it, expect } from 'vitest';
|
|
14
|
+
import { driver } from '../lib/driver.js';
|
|
15
|
+
import { readDistillationCap, invokeDistill } from '../lib/distillation.js';
|
|
16
|
+
|
|
17
|
+
describe('distillation-token-budget (RFC 0062 §B)', () => {
|
|
18
|
+
it('within budget tokensUsed ≤ tokenBudget; an un-meetable budget fails atomically', async () => {
|
|
19
|
+
if ((await readDistillationCap())?.supported !== true) return;
|
|
20
|
+
|
|
21
|
+
const ok = await invokeDistill({ memoryRef: 'conformance-distill', tokenBudget: 8000 });
|
|
22
|
+
if (ok === null) return; // seam absent — soft-skip
|
|
23
|
+
const dist = ok.body.event?.distillation ?? {};
|
|
24
|
+
expect(
|
|
25
|
+
typeof dist.tokenBudget === 'number' && typeof dist.tokensUsed === 'number',
|
|
26
|
+
driver.describe('RFC 0062 §B', 'memory.compacted MUST carry distillation.tokenBudget + tokensUsed on a budgeted run'),
|
|
27
|
+
).toBe(true);
|
|
28
|
+
expect(
|
|
29
|
+
(dist.tokensUsed as number) <= (dist.tokenBudget as number),
|
|
30
|
+
driver.describe('RFC 0062 §B', 'a successful distillation MUST consume ≤ its tokenBudget'),
|
|
31
|
+
).toBe(true);
|
|
32
|
+
|
|
33
|
+
// A budget too small to distill the corpus MUST fail closed, no partial archive.
|
|
34
|
+
const tooSmall = await invokeDistill({ memoryRef: 'conformance-distill', tokenBudget: 1 });
|
|
35
|
+
if (tooSmall === null) return;
|
|
36
|
+
expect(
|
|
37
|
+
tooSmall.status >= 400 && tooSmall.body.error === 'token_budget_exceeded',
|
|
38
|
+
driver.describe('RFC 0062 §B', 'an un-meetable budget MUST fail with token_budget_exceeded'),
|
|
39
|
+
).toBe(true);
|
|
40
|
+
expect(
|
|
41
|
+
tooSmall.body.archiveChecksum,
|
|
42
|
+
driver.describe('RFC 0062 §B', 'a token_budget_exceeded run MUST write no partial archive (atomic)'),
|
|
43
|
+
).toBeUndefined();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -31,6 +31,7 @@ import { describe, it, expect } from 'vitest';
|
|
|
31
31
|
import { driver } from '../lib/driver.js';
|
|
32
32
|
import { pollUntilTerminal } from '../lib/polling.js';
|
|
33
33
|
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
34
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
34
35
|
|
|
35
36
|
const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
|
|
36
37
|
const NODE_ID = 'structured-call';
|
|
@@ -91,7 +92,7 @@ describe.skipIf(HTTP_SKIP)('envelope-completion-distinguishes-truncation: advert
|
|
|
91
92
|
it('capabilities.envelopes.reliability.completion (when present) conforms to RFC 0033 §E', async () => {
|
|
92
93
|
const d = await readDiscovery();
|
|
93
94
|
if (d === null) return;
|
|
94
|
-
const completion = d
|
|
95
|
+
const completion = capabilityFamily<{ reasoning?: Record<string, unknown>; tierOneSubsetCompliance?: unknown; reliability?: { completion?: Record<string, unknown> } & Record<string, unknown> }>(d, 'envelopes')?.reliability?.completion;
|
|
95
96
|
if (completion === undefined) return;
|
|
96
97
|
expect(
|
|
97
98
|
typeof completion.distinguishesTruncation,
|
|
@@ -114,7 +115,7 @@ describe.skipIf(HTTP_SKIP)('envelope-completion-distinguishes-truncation: trunca
|
|
|
114
115
|
it('truncation: emits envelope.truncated + envelope.retry.attempted with reason: "truncation"', async () => {
|
|
115
116
|
if (!isFixtureAdvertised(TRUNCATED_FIXTURE)) return;
|
|
116
117
|
const d = await readDiscovery();
|
|
117
|
-
if (d
|
|
118
|
+
if (capabilityFamily<{ reasoning?: Record<string, unknown>; tierOneSubsetCompliance?: unknown; reliability?: { completion?: Record<string, unknown> } & Record<string, unknown> }>(d, 'envelopes')?.reliability?.completion?.distinguishesTruncation !== true) return;
|
|
118
119
|
const seed = await programMock([
|
|
119
120
|
{ stopReason: 'max_tokens', content: '{"partial' },
|
|
120
121
|
{ stopReason: 'end_turn', content: '{"valid":true}' },
|
|
@@ -139,7 +140,7 @@ describe.skipIf(HTTP_SKIP)('envelope-completion-distinguishes-truncation: trunca
|
|
|
139
140
|
it('truncation: retry budget strictly greater than initial (RFC 0033 §B truncationBudgetMultiplier)', async () => {
|
|
140
141
|
if (!isFixtureAdvertised(TRUNCATED_FIXTURE)) return;
|
|
141
142
|
const d = await readDiscovery();
|
|
142
|
-
if (d
|
|
143
|
+
if (capabilityFamily<{ reasoning?: Record<string, unknown>; tierOneSubsetCompliance?: unknown; reliability?: { completion?: Record<string, unknown> } & Record<string, unknown> }>(d, 'envelopes')?.reliability?.completion?.distinguishesTruncation !== true) return;
|
|
143
144
|
const seed = await programMock([
|
|
144
145
|
{ stopReason: 'max_tokens', content: '{"partial' },
|
|
145
146
|
{ stopReason: 'end_turn', content: '{"valid":true}' },
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
|
|
36
36
|
import { describe, it, expect } from 'vitest';
|
|
37
37
|
import { driver } from '../lib/driver.js';
|
|
38
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
38
39
|
|
|
39
40
|
const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
|
|
40
41
|
|
|
@@ -97,8 +98,8 @@ describe.skipIf(HTTP_SKIP)('envelope-reasoning-secret-redaction: advertisement s
|
|
|
97
98
|
it('hosts advertising envelope reasoning + BYOK honor SR-1 carry-forward for the reasoning field', async () => {
|
|
98
99
|
const d = await readDiscovery();
|
|
99
100
|
if (d === null) return;
|
|
100
|
-
const reasoning = d
|
|
101
|
-
const secrets = d
|
|
101
|
+
const reasoning = capabilityFamily<{ reasoning?: Record<string, unknown>; tierOneSubsetCompliance?: unknown; reliability?: { completion?: Record<string, unknown> } & Record<string, unknown> }>(d, 'envelopes')?.reasoning?.supported;
|
|
102
|
+
const secrets = capabilityFamily<{ supported?: unknown }>(d, 'secrets')?.supported;
|
|
102
103
|
if (reasoning !== true || secrets !== true) return; // soft-skip when either is absent
|
|
103
104
|
// The contract is invariant-based, not capability-flag-based — the
|
|
104
105
|
// advertisement-shape check here just confirms both surfaces are claimed.
|
|
@@ -257,7 +258,7 @@ describe.skipIf(HTTP_SKIP)('envelope-reasoning-secret-redaction: downstream-proj
|
|
|
257
258
|
// RFC 0034 §B: gate on capabilities.observability.testSeams.otelScrape.
|
|
258
259
|
// Hosts that don't advertise it soft-skip; hosts that DO advertise MUST serve a valid response.
|
|
259
260
|
const d = await readDiscovery();
|
|
260
|
-
const otelScrapeAdvertised = d
|
|
261
|
+
const otelScrapeAdvertised = capabilityFamily<{ testSeams?: Record<string, unknown> }>(d, 'observability')?.testSeams?.otelScrape === true;
|
|
261
262
|
if (!otelScrapeAdvertised) return; // soft-skip — host honest about not implementing per RFC 0034 §A
|
|
262
263
|
|
|
263
264
|
const r = await acceptForRun(
|
|
@@ -291,7 +292,7 @@ describe.skipIf(HTTP_SKIP)('envelope-reasoning-secret-redaction: downstream-proj
|
|
|
291
292
|
it("debug-bundle export MUST NOT include plaintext `secret:`-prefixed substrings from envelope.reasoning", async () => {
|
|
292
293
|
// RFC 0034 §B: gate on capabilities.observability.testSeams.debugBundleExport.
|
|
293
294
|
const d = await readDiscovery();
|
|
294
|
-
const debugBundleAdvertised = d
|
|
295
|
+
const debugBundleAdvertised = capabilityFamily<{ testSeams?: Record<string, unknown> }>(d, 'observability')?.testSeams?.debugBundleExport === true;
|
|
295
296
|
if (!debugBundleAdvertised) return; // soft-skip — host honest about not implementing per RFC 0034 §A
|
|
296
297
|
|
|
297
298
|
const r = await acceptForRun(
|
|
@@ -32,6 +32,7 @@ import { readFileSync } from 'node:fs';
|
|
|
32
32
|
import { join } from 'node:path';
|
|
33
33
|
import { driver } from '../lib/driver.js';
|
|
34
34
|
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
35
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
35
36
|
|
|
36
37
|
const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
|
|
37
38
|
|
|
@@ -163,7 +164,7 @@ describe.skipIf(HTTP_SKIP)('envelope-reasoning-shape: capabilities.envelopes adv
|
|
|
163
164
|
it('capabilities.envelopes.reasoning (when present) conforms to RFC 0030 §C', async () => {
|
|
164
165
|
const d = await readDiscovery();
|
|
165
166
|
if (d === null) return;
|
|
166
|
-
const reasoning = d
|
|
167
|
+
const reasoning = capabilityFamily<{ reasoning?: Record<string, unknown>; tierOneSubsetCompliance?: unknown; reliability?: { completion?: Record<string, unknown> } & Record<string, unknown> }>(d, 'envelopes')?.reasoning;
|
|
167
168
|
if (reasoning === undefined) return; // optional block; host MAY omit
|
|
168
169
|
expect(
|
|
169
170
|
typeof reasoning.supported,
|
|
@@ -180,7 +181,7 @@ describe.skipIf(HTTP_SKIP)('envelope-reasoning-shape: capabilities.envelopes adv
|
|
|
180
181
|
it('capabilities.envelopes.tierOneSubsetCompliance (when present) conforms to RFC 0030 §B', async () => {
|
|
181
182
|
const d = await readDiscovery();
|
|
182
183
|
if (d === null) return;
|
|
183
|
-
const compliance = d
|
|
184
|
+
const compliance = capabilityFamily<{ reasoning?: Record<string, unknown>; tierOneSubsetCompliance?: unknown; reliability?: { completion?: Record<string, unknown> } & Record<string, unknown> }>(d, 'envelopes')?.tierOneSubsetCompliance;
|
|
184
185
|
if (compliance === undefined) return; // optional; host MAY omit
|
|
185
186
|
expect(
|
|
186
187
|
['strict', 'warn', 'off'],
|