@openwop/openwop-conformance 1.3.0 → 1.5.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 +132 -1
- package/README.md +3 -2
- package/api/asyncapi.yaml +8 -0
- package/api/openapi.yaml +371 -1
- package/coverage.md +26 -6
- package/fixtures/conformance-envelope-nl-to-format-engaged.json +41 -0
- package/fixtures/conformance-envelope-recovery-applied.json +39 -0
- package/fixtures/conformance-envelope-refusal.json +38 -0
- package/fixtures/conformance-envelope-retry-attempted.json +39 -0
- package/fixtures/conformance-envelope-retry-exhausted.json +38 -0
- package/fixtures/conformance-envelope-truncated.json +39 -0
- package/fixtures/conformance-envelope-truncation-cap-exhaustion.json +39 -0
- package/fixtures/conformance-model-capability-insufficient.json +25 -0
- package/fixtures/conformance-multi-agent-confidence-escalation.json +49 -0
- package/fixtures/conformance-multi-agent-handoff-child.json +27 -0
- package/fixtures/conformance-multi-agent-handoff.json +49 -0
- package/fixtures/conformance-prompt-all-four-kinds.json +39 -0
- package/fixtures/conformance-prompt-end-to-end.json +33 -0
- package/fixtures/conformance-subworkflow-mid-run-mutation-child.json +31 -0
- package/fixtures/conformance-subworkflow-mid-run-mutation.json +33 -0
- package/fixtures/openwop-smoke-cost-emit.json +37 -0
- package/fixtures/prompt-templates/conformance-prompt-few-shot-2.json +14 -0
- package/fixtures/prompt-templates/conformance-prompt-few-shot.json +14 -0
- package/fixtures/prompt-templates/conformance-prompt-schema-hint.json +14 -0
- package/fixtures/prompt-templates/conformance-prompt-secret-redaction.json +23 -0
- package/fixtures/prompt-templates/conformance-prompt-trust-marker.json +23 -0
- package/fixtures/prompt-templates/conformance-prompt-writer-system.json +15 -0
- package/fixtures/prompt-templates/conformance-prompt-writer-user.json +15 -0
- package/fixtures.md +39 -0
- package/package.json +1 -1
- package/schemas/README.md +5 -0
- package/schemas/agent-manifest.schema.json +16 -0
- package/schemas/capabilities.schema.json +384 -1
- package/schemas/envelopes/clarification.request.schema.json +9 -0
- package/schemas/envelopes/error.schema.json +4 -0
- package/schemas/envelopes/schema.request.schema.json +4 -0
- package/schemas/envelopes/schema.response.schema.json +1 -1
- package/schemas/node-pack-manifest.schema.json +28 -0
- package/schemas/orchestrator-decision.schema.json +12 -0
- package/schemas/prompt-kind.schema.json +8 -0
- package/schemas/prompt-pack-manifest.schema.json +80 -0
- package/schemas/prompt-ref.schema.json +40 -0
- package/schemas/prompt-template.schema.json +149 -0
- package/schemas/registry-version-manifest.schema.json +5 -0
- package/schemas/run-ancestry-response.schema.json +54 -0
- package/schemas/run-event-payloads.schema.json +479 -11
- package/schemas/run-event.schema.json +15 -1
- package/schemas/run-snapshot.schema.json +3 -2
- package/schemas/workflow-definition.schema.json +19 -1
- package/src/lib/llm-cache-key-recipe.ts +68 -0
- package/src/scenarios/aiEnvelope.contractRefusal.test.ts +104 -13
- package/src/scenarios/aiEnvelope.correlationReplay.test.ts +32 -15
- package/src/scenarios/aiEnvelope.redaction.test.ts +6 -5
- package/src/scenarios/aiEnvelope.schemaDrift.test.ts +5 -5
- package/src/scenarios/aiEnvelope.trustBoundaryPropagation.test.ts +211 -12
- package/src/scenarios/aiEnvelope.universalKinds.test.ts +7 -7
- package/src/scenarios/blob-presign-expiry.test.ts +7 -7
- package/src/scenarios/cache-ttl-expiry.test.ts +6 -6
- package/src/scenarios/cost-attribution.test.ts +124 -11
- package/src/scenarios/cross-engine-append-ordering.test.ts +99 -0
- package/src/scenarios/cross-host-ancestry-endpoint.test.ts +136 -0
- package/src/scenarios/cross-host-causation-shape.test.ts +117 -0
- package/src/scenarios/cross-host-traceparent-propagation.test.ts +60 -0
- package/src/scenarios/envelope-completion-distinguishes-truncation.test.ts +223 -0
- package/src/scenarios/envelope-nl-to-format-engaged.test.ts +152 -0
- package/src/scenarios/envelope-reasoning-secret-redaction.test.ts +343 -0
- package/src/scenarios/envelope-reasoning-shape.test.ts +190 -0
- package/src/scenarios/envelope-recovery-applied.test.ts +229 -0
- package/src/scenarios/envelope-refusal-shape.test.ts +289 -0
- package/src/scenarios/envelope-retry-attempted.test.ts +258 -0
- package/src/scenarios/envelope-retry-exhausted.test.ts +168 -0
- package/src/scenarios/envelope-tier-one-subset-static.test.ts +229 -0
- package/src/scenarios/envelope-truncated.test.ts +136 -0
- package/src/scenarios/envelope-truncation-cap-exhaustion.test.ts +144 -0
- package/src/scenarios/envelope-variant-discriminator-static.test.ts +152 -0
- package/src/scenarios/fixtures-valid.test.ts +123 -15
- package/src/scenarios/kv-ttl-expiry.test.ts +7 -7
- package/src/scenarios/model-capability-insufficient.test.ts +221 -0
- package/src/scenarios/model-capability-substituted.test.ts +203 -0
- package/src/scenarios/multi-agent-confidence-escalation.test.ts +201 -0
- package/src/scenarios/multi-agent-handoff-state-machine.test.ts +167 -0
- package/src/scenarios/multi-agent-memory-lifecycle.test.ts +124 -0
- package/src/scenarios/multi-region-idempotency.test.ts +58 -0
- package/src/scenarios/node-module-required-capabilities-shape.test.ts +185 -0
- package/src/scenarios/prompt-all-four-kinds-events.test.ts +198 -0
- package/src/scenarios/prompt-composed-secret-redaction.test.ts +178 -0
- package/src/scenarios/prompt-composed-trust-marker.test.ts +165 -0
- package/src/scenarios/prompt-end-to-end-events.test.ts +202 -0
- package/src/scenarios/prompt-list-and-fetch.test.ts +207 -0
- package/src/scenarios/prompt-mutable-lifecycle.test.ts +216 -0
- package/src/scenarios/prompt-pack-install.test.ts +187 -0
- package/src/scenarios/prompt-render-deterministic.test.ts +240 -0
- package/src/scenarios/prompt-resolution-chain-agent-intrinsic.test.ts +140 -0
- package/src/scenarios/prompt-resolution-chain-fallback-cascade.test.ts +172 -0
- package/src/scenarios/prompt-resolution-chain-node-wins.test.ts +144 -0
- package/src/scenarios/prompt-template-shape.test.ts +359 -0
- package/src/scenarios/queue-ack-nack-dlq.test.ts +7 -7
- package/src/scenarios/queue-publish-consume-roundtrip.test.ts +7 -7
- package/src/scenarios/replay-divergence-at-refusal.test.ts +134 -0
- package/src/scenarios/replay-llm-cache-key-portable.test.ts +197 -0
- package/src/scenarios/replay-llm-cache-key.test.ts +1 -40
- package/src/scenarios/replay-observable-sequence-determinism.test.ts +80 -0
- package/src/scenarios/sandbox-capability-gate-respected.test.ts +27 -0
- package/src/scenarios/sandbox-memory-cap.test.ts +58 -0
- package/src/scenarios/sandbox-no-cross-pack-mutation.test.ts +30 -0
- package/src/scenarios/sandbox-no-host-env-leak.test.ts +27 -0
- package/src/scenarios/sandbox-no-host-fs-escape.test.ts +88 -0
- package/src/scenarios/sandbox-no-host-process-escape.test.ts +31 -0
- package/src/scenarios/sandbox-no-network-escape.test.ts +28 -0
- package/src/scenarios/sandbox-timeout-cap.test.ts +58 -0
- package/src/scenarios/search-bm25-roundtrip.test.ts +7 -7
- package/src/scenarios/spec-corpus-validity.test.ts +34 -6
- package/src/scenarios/sql-transaction-atomicity.test.ts +6 -6
- package/src/scenarios/stream-subscribe-from-beginning.test.ts +7 -7
- package/src/scenarios/subworkflow-input-mapping.test.ts +70 -4
- package/src/scenarios/table-cursor-pagination.test.ts +7 -7
- package/src/scenarios/table-schema-enforcement.test.ts +7 -7
- package/src/scenarios/vector-knn-roundtrip.test.ts +7 -7
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prompt-pack-install — RFC 0028 §B boot-time pack-install proof.
|
|
3
|
+
*
|
|
4
|
+
* Asserts: when the host advertises
|
|
5
|
+
* `capabilities.prompts.endpointsSupported: true` AND the in-tree
|
|
6
|
+
* reference prompt pack (`vendor.openwop.prompt-sample`) was
|
|
7
|
+
* installed at boot, the pack's two templates (`writer-system`,
|
|
8
|
+
* `critic-system`) surface in `GET /v1/prompts` carrying
|
|
9
|
+
* `meta.source: "pack"` + `meta.packName: "vendor.openwop.prompt-sample"`
|
|
10
|
+
* + `meta.packVersion: "1.0.0"`.
|
|
11
|
+
*
|
|
12
|
+
* This is the install-flow regression pin. If the boot-time loader
|
|
13
|
+
* stops walking `examples/packs/*` or stops calling
|
|
14
|
+
* `installPackTemplates()`, this scenario fails first — before any
|
|
15
|
+
* downstream scenario notices missing templates.
|
|
16
|
+
*
|
|
17
|
+
* The scenario does NOT mutate state — it relies on the host having
|
|
18
|
+
* installed at least one prompt pack at startup. RFC 0028 §B does
|
|
19
|
+
* NOT require a host advertising `endpointsSupported: true` to have
|
|
20
|
+
* any pack installed (a fresh production host with no pack
|
|
21
|
+
* subscriptions is conformant); when zero pack-source templates
|
|
22
|
+
* are listed, the structural assertions on sub-tests 2-3 still run
|
|
23
|
+
* but the existence claim is treated as a soft skip.
|
|
24
|
+
*
|
|
25
|
+
* `OPENWOP_TEST_PROMPT_PACK_INSTALLED=true` is a conformance-runner
|
|
26
|
+
* (client-side) flag — the operator running the suite sets it when
|
|
27
|
+
* they know the target host has at least one prompt pack installed,
|
|
28
|
+
* which promotes the existence claim from soft-skip to hard
|
|
29
|
+
* assertion. The flag is NOT set by the host itself. When running
|
|
30
|
+
* against the in-tree workflow-engine sample (which auto-installs
|
|
31
|
+
* `vendor.openwop.prompt-sample` via `promptPackLoader`), the
|
|
32
|
+
* operator should set it so the existence path IS exercised.
|
|
33
|
+
*
|
|
34
|
+
* Capability-gated: skips when the host doesn't advertise
|
|
35
|
+
* `capabilities.prompts.endpointsSupported: true`. Under
|
|
36
|
+
* `OPENWOP_REQUIRE_BEHAVIOR=true`, the gate hardens from SKIP to
|
|
37
|
+
* FAIL via `behaviorGate('prompts-endpoints', ...)`.
|
|
38
|
+
*
|
|
39
|
+
* HTTP-driven: skips when no `OPENWOP_BASE_URL` is configured.
|
|
40
|
+
*
|
|
41
|
+
* @see RFCS/0028-prompt-library-endpoints.md §B
|
|
42
|
+
* @see spec/v1/prompts.md §"Discovery & distribution"
|
|
43
|
+
* @see examples/packs/prompt-sample/pack.json
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { describe, it, expect } from 'vitest';
|
|
47
|
+
import { driver } from '../lib/driver.js';
|
|
48
|
+
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
49
|
+
|
|
50
|
+
interface DiscoveryDoc {
|
|
51
|
+
capabilities?: {
|
|
52
|
+
prompts?: {
|
|
53
|
+
supported?: unknown;
|
|
54
|
+
endpointsSupported?: unknown;
|
|
55
|
+
};
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface PromptTemplate {
|
|
60
|
+
templateId: string;
|
|
61
|
+
version: string;
|
|
62
|
+
kind: string;
|
|
63
|
+
meta?: {
|
|
64
|
+
source?: 'host' | 'pack' | 'user';
|
|
65
|
+
packName?: string;
|
|
66
|
+
packVersion?: string;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface ListResponse {
|
|
71
|
+
items: PromptTemplate[];
|
|
72
|
+
nextCursor?: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function readDiscovery(): Promise<DiscoveryDoc | null> {
|
|
76
|
+
const res = await driver.get('/.well-known/openwop');
|
|
77
|
+
if (res.status !== 200) return null;
|
|
78
|
+
return res.json as DiscoveryDoc;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function endpointsSupported(d: DiscoveryDoc | null): boolean {
|
|
82
|
+
return d?.capabilities?.prompts?.endpointsSupported === true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
|
|
86
|
+
const REQUIRE_PACK_INSTALLED = process.env.OPENWOP_TEST_PROMPT_PACK_INSTALLED === 'true';
|
|
87
|
+
|
|
88
|
+
describe.skipIf(HTTP_SKIP)('prompt-pack-install: boot-time loader surfaces pack templates (RFC 0028 §B)', () => {
|
|
89
|
+
it('GET /v1/prompts?source=pack returns 200 + an array of PromptTemplate objects when endpointsSupported is advertised', async () => {
|
|
90
|
+
const d = await readDiscovery();
|
|
91
|
+
if (!behaviorGate('prompts-endpoints', endpointsSupported(d))) return;
|
|
92
|
+
|
|
93
|
+
const res = await driver.get('/v1/prompts?source=pack');
|
|
94
|
+
expect(
|
|
95
|
+
res.status,
|
|
96
|
+
driver.describe(
|
|
97
|
+
'spec/v1/prompts.md §"Discovery & distribution"',
|
|
98
|
+
'GET /v1/prompts MUST return 200 when prompts.endpointsSupported is advertised',
|
|
99
|
+
),
|
|
100
|
+
).toBe(200);
|
|
101
|
+
|
|
102
|
+
const body = res.json as ListResponse;
|
|
103
|
+
expect(
|
|
104
|
+
Array.isArray(body.items),
|
|
105
|
+
driver.describe(
|
|
106
|
+
'RFCS/0028-prompt-library-endpoints.md §A',
|
|
107
|
+
'`items` MUST be an array of PromptTemplate objects',
|
|
108
|
+
),
|
|
109
|
+
).toBe(true);
|
|
110
|
+
|
|
111
|
+
// Existence claim — only fail when the host explicitly opts in
|
|
112
|
+
// via OPENWOP_TEST_PROMPT_PACK_INSTALLED. RFC 0028 §B treats
|
|
113
|
+
// "zero installed packs" as a conformant state for any host that
|
|
114
|
+
// hasn't subscribed to a pack source.
|
|
115
|
+
if (REQUIRE_PACK_INSTALLED) {
|
|
116
|
+
const packItems = body.items.filter((t) => t.meta?.source === 'pack');
|
|
117
|
+
expect(
|
|
118
|
+
packItems.length,
|
|
119
|
+
driver.describe(
|
|
120
|
+
'RFCS/0028-prompt-library-endpoints.md §B',
|
|
121
|
+
'OPENWOP_TEST_PROMPT_PACK_INSTALLED=true asserts the boot-time loader installed at least one pack',
|
|
122
|
+
),
|
|
123
|
+
).toBeGreaterThan(0);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('each pack-source template carries meta.source/packName/packVersion stamps per RFC 0028 §B', async () => {
|
|
128
|
+
const d = await readDiscovery();
|
|
129
|
+
if (!behaviorGate('prompts-endpoints', endpointsSupported(d))) return;
|
|
130
|
+
|
|
131
|
+
const res = await driver.get('/v1/prompts?source=pack');
|
|
132
|
+
if (res.status !== 200) return;
|
|
133
|
+
const body = res.json as ListResponse;
|
|
134
|
+
const packItems = body.items.filter((t) => t.meta?.source === 'pack');
|
|
135
|
+
if (packItems.length === 0) return; // gated above
|
|
136
|
+
|
|
137
|
+
for (const t of packItems) {
|
|
138
|
+
expect(
|
|
139
|
+
t.meta?.source,
|
|
140
|
+
driver.describe(
|
|
141
|
+
'schemas/prompt-template.schema.json §meta.source',
|
|
142
|
+
'pack-installed templates MUST stamp `meta.source: "pack"`',
|
|
143
|
+
),
|
|
144
|
+
).toBe('pack');
|
|
145
|
+
expect(
|
|
146
|
+
typeof t.meta?.packName === 'string' && (t.meta?.packName?.length ?? 0) > 0,
|
|
147
|
+
driver.describe(
|
|
148
|
+
'RFCS/0028-prompt-library-endpoints.md §B',
|
|
149
|
+
'pack-installed templates MUST stamp `meta.packName`',
|
|
150
|
+
),
|
|
151
|
+
).toBe(true);
|
|
152
|
+
expect(
|
|
153
|
+
typeof t.meta?.packVersion === 'string' && /^\d+\.\d+\.\d+/.test(t.meta?.packVersion ?? ''),
|
|
154
|
+
driver.describe(
|
|
155
|
+
'RFCS/0028-prompt-library-endpoints.md §B',
|
|
156
|
+
'pack-installed templates MUST stamp a semver `meta.packVersion`',
|
|
157
|
+
),
|
|
158
|
+
).toBe(true);
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it('GET /v1/prompts/{templateId} returns a pack-source template by id (reference pack: writer-system)', async () => {
|
|
163
|
+
const d = await readDiscovery();
|
|
164
|
+
if (!behaviorGate('prompts-endpoints', endpointsSupported(d))) return;
|
|
165
|
+
|
|
166
|
+
const list = await driver.get('/v1/prompts?source=pack');
|
|
167
|
+
if (list.status !== 200) return;
|
|
168
|
+
const body = list.json as ListResponse;
|
|
169
|
+
const writer = body.items.find((t) => t.templateId === 'writer-system' && t.meta?.source === 'pack');
|
|
170
|
+
if (!writer) return; // host may have installed a different reference pack; skip silently
|
|
171
|
+
|
|
172
|
+
const fetched = await driver.get(`/v1/prompts/${encodeURIComponent('writer-system')}`);
|
|
173
|
+
expect(
|
|
174
|
+
fetched.status,
|
|
175
|
+
driver.describe(
|
|
176
|
+
'RFCS/0028-prompt-library-endpoints.md §A',
|
|
177
|
+
'GET /v1/prompts/{templateId} MUST return 200 for a known pack-source template id',
|
|
178
|
+
),
|
|
179
|
+
).toBe(200);
|
|
180
|
+
const t = fetched.json as PromptTemplate;
|
|
181
|
+
expect(t.templateId).toBe('writer-system');
|
|
182
|
+
expect(
|
|
183
|
+
t.meta?.source,
|
|
184
|
+
'fetched template MUST preserve `meta.source: "pack"` provenance',
|
|
185
|
+
).toBe('pack');
|
|
186
|
+
});
|
|
187
|
+
});
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prompt-render-deterministic — RFC 0028 §A deterministic-hash invariant.
|
|
3
|
+
*
|
|
4
|
+
* Asserts: two calls to `POST /v1/prompts:render` with identical
|
|
5
|
+
* `(ref, variables, contentTrust)` inputs MUST produce identical
|
|
6
|
+
* `hash` AND identical `variableHashes`. Different variables MUST
|
|
7
|
+
* produce different `variableHashes` for the changed keys (and a
|
|
8
|
+
* different overall `hash`). The deterministic-render invariant
|
|
9
|
+
* mirrors the `prompt.composed` replay invariant per RFC 0027 §F.
|
|
10
|
+
*
|
|
11
|
+
* Capability-gated: skips when the host doesn't advertise
|
|
12
|
+
* `capabilities.prompts.endpointsSupported: true`.
|
|
13
|
+
*
|
|
14
|
+
* HTTP-driven: skips when no `OPENWOP_BASE_URL` is configured.
|
|
15
|
+
*
|
|
16
|
+
* Under `OPENWOP_REQUIRE_BEHAVIOR=true`, the capability gate hardens
|
|
17
|
+
* from SKIP to FAIL.
|
|
18
|
+
*
|
|
19
|
+
* @see spec/v1/prompts.md §"Discovery & distribution" — Deterministic-render invariant
|
|
20
|
+
* @see RFCS/0028-prompt-library-endpoints.md §A
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { describe, it, expect } from 'vitest';
|
|
24
|
+
import { driver } from '../lib/driver.js';
|
|
25
|
+
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
26
|
+
|
|
27
|
+
interface DiscoveryDoc {
|
|
28
|
+
capabilities?: {
|
|
29
|
+
prompts?: {
|
|
30
|
+
supported?: unknown;
|
|
31
|
+
endpointsSupported?: unknown;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface RenderResponse {
|
|
37
|
+
composed?: string;
|
|
38
|
+
hash: string;
|
|
39
|
+
refs: string[];
|
|
40
|
+
variableHashes: Record<string, string>;
|
|
41
|
+
contentTrust?: 'trusted' | 'untrusted';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface PromptTemplate {
|
|
45
|
+
templateId: string;
|
|
46
|
+
version: string;
|
|
47
|
+
kind: string;
|
|
48
|
+
text: string;
|
|
49
|
+
variables?: Array<{ name: string; required?: boolean; source?: string }>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface ListResponse {
|
|
53
|
+
items: PromptTemplate[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function readDiscovery(): Promise<DiscoveryDoc | null> {
|
|
57
|
+
const res = await driver.get('/.well-known/openwop');
|
|
58
|
+
if (res.status !== 200) return null;
|
|
59
|
+
return res.json as DiscoveryDoc;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function endpointsSupported(d: DiscoveryDoc | null): boolean {
|
|
63
|
+
return d?.capabilities?.prompts?.endpointsSupported === true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Pick a template that has at least one input-source variable
|
|
67
|
+
* (so we can vary the binding). Prefer host-source so we don't
|
|
68
|
+
* depend on user-created templates from prior runs. Skip
|
|
69
|
+
* secret-source variables — those need BYOK provisioning. */
|
|
70
|
+
async function pickTemplateWithInputVar(): Promise<PromptTemplate | null> {
|
|
71
|
+
const res = await driver.get('/v1/prompts?source=host&limit=200');
|
|
72
|
+
if (res.status !== 200) return null;
|
|
73
|
+
const body = res.json as ListResponse;
|
|
74
|
+
for (const t of body.items) {
|
|
75
|
+
const hasInputVar = (t.variables ?? []).some(
|
|
76
|
+
(v) => v.source !== 'secret' && v.required === true,
|
|
77
|
+
);
|
|
78
|
+
if (hasInputVar) return t;
|
|
79
|
+
}
|
|
80
|
+
// Fall back: any template (even with no required vars works for the
|
|
81
|
+
// identity-of-hash assertion; just no negative-control sub-test).
|
|
82
|
+
return body.items[0] ?? null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
|
|
86
|
+
|
|
87
|
+
describe.skipIf(HTTP_SKIP)('prompt-render-deterministic: hash stable across identical inputs (RFC 0028 §A)', () => {
|
|
88
|
+
it('identical (ref, variables) inputs produce identical hash + variableHashes', async () => {
|
|
89
|
+
const d = await readDiscovery();
|
|
90
|
+
if (!behaviorGate('prompts-endpoints', endpointsSupported(d))) return;
|
|
91
|
+
|
|
92
|
+
const template = await pickTemplateWithInputVar();
|
|
93
|
+
if (!template) return;
|
|
94
|
+
|
|
95
|
+
const variableNames = (template.variables ?? [])
|
|
96
|
+
.filter((v) => v.source !== 'secret')
|
|
97
|
+
.map((v) => v.name);
|
|
98
|
+
|
|
99
|
+
// Build a binding set that satisfies every non-secret variable.
|
|
100
|
+
const variables: Record<string, unknown> = {};
|
|
101
|
+
for (const name of variableNames) variables[name] = `conformance-${name}-value`;
|
|
102
|
+
|
|
103
|
+
const ref = `prompt:${template.templateId}@${template.version}`;
|
|
104
|
+
const first = await driver.post('/v1/prompts:render', { ref, variables });
|
|
105
|
+
if (first.status !== 200) return;
|
|
106
|
+
const second = await driver.post('/v1/prompts:render', { ref, variables });
|
|
107
|
+
expect(second.status).toBe(200);
|
|
108
|
+
|
|
109
|
+
const a = first.json as RenderResponse;
|
|
110
|
+
const b = second.json as RenderResponse;
|
|
111
|
+
|
|
112
|
+
expect(
|
|
113
|
+
a.hash,
|
|
114
|
+
driver.describe(
|
|
115
|
+
'spec/v1/prompts.md §Discovery & distribution',
|
|
116
|
+
'render hash MUST be stable across identical (ref, variables) inputs',
|
|
117
|
+
),
|
|
118
|
+
).toBe(b.hash);
|
|
119
|
+
expect(
|
|
120
|
+
Object.keys(a.variableHashes).sort(),
|
|
121
|
+
driver.describe(
|
|
122
|
+
'spec/v1/prompts.md §Discovery & distribution',
|
|
123
|
+
'variableHashes key set MUST be stable',
|
|
124
|
+
),
|
|
125
|
+
).toEqual(Object.keys(b.variableHashes).sort());
|
|
126
|
+
for (const k of Object.keys(a.variableHashes)) {
|
|
127
|
+
expect(a.variableHashes[k]).toBe(b.variableHashes[k]);
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('different variable values produce different hash + at least one different variableHash', async () => {
|
|
132
|
+
const d = await readDiscovery();
|
|
133
|
+
if (!behaviorGate('prompts-endpoints', endpointsSupported(d))) return;
|
|
134
|
+
|
|
135
|
+
const template = await pickTemplateWithInputVar();
|
|
136
|
+
if (!template) return;
|
|
137
|
+
const requiredVars = (template.variables ?? []).filter(
|
|
138
|
+
(v) => v.source !== 'secret' && v.required === true,
|
|
139
|
+
);
|
|
140
|
+
if (requiredVars.length === 0) return; // no required var to toggle
|
|
141
|
+
|
|
142
|
+
const variables: Record<string, unknown> = {};
|
|
143
|
+
for (const v of template.variables ?? []) {
|
|
144
|
+
if (v.source === 'secret') continue;
|
|
145
|
+
variables[v.name] = `conformance-${v.name}-baseline`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const ref = `prompt:${template.templateId}@${template.version}`;
|
|
149
|
+
const baseline = await driver.post('/v1/prompts:render', { ref, variables });
|
|
150
|
+
if (baseline.status !== 200) return;
|
|
151
|
+
|
|
152
|
+
// Toggle one required variable.
|
|
153
|
+
const toggled = { ...variables, [requiredVars[0]!.name]: 'conformance-toggled-value' };
|
|
154
|
+
const altered = await driver.post('/v1/prompts:render', { ref, variables: toggled });
|
|
155
|
+
expect(altered.status).toBe(200);
|
|
156
|
+
|
|
157
|
+
const a = baseline.json as RenderResponse;
|
|
158
|
+
const b = altered.json as RenderResponse;
|
|
159
|
+
|
|
160
|
+
expect(
|
|
161
|
+
a.hash,
|
|
162
|
+
driver.describe(
|
|
163
|
+
'spec/v1/prompts.md §Discovery & distribution',
|
|
164
|
+
'render hash MUST differ when any variable binding differs',
|
|
165
|
+
),
|
|
166
|
+
).not.toBe(b.hash);
|
|
167
|
+
expect(
|
|
168
|
+
a.variableHashes[requiredVars[0]!.name],
|
|
169
|
+
driver.describe(
|
|
170
|
+
'spec/v1/prompts.md §Discovery & distribution',
|
|
171
|
+
'variableHashes[name] MUST differ when name binding differs',
|
|
172
|
+
),
|
|
173
|
+
).not.toBe(b.variableHashes[requiredVars[0]!.name]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it('hash + variableHashes MUST match sha256:<hex64> pattern', async () => {
|
|
177
|
+
const d = await readDiscovery();
|
|
178
|
+
if (!behaviorGate('prompts-endpoints', endpointsSupported(d))) return;
|
|
179
|
+
|
|
180
|
+
const template = await pickTemplateWithInputVar();
|
|
181
|
+
if (!template) return;
|
|
182
|
+
const variables: Record<string, unknown> = {};
|
|
183
|
+
for (const v of template.variables ?? []) {
|
|
184
|
+
if (v.source === 'secret') continue;
|
|
185
|
+
variables[v.name] = `conformance-${v.name}-shape`;
|
|
186
|
+
}
|
|
187
|
+
const ref = `prompt:${template.templateId}@${template.version}`;
|
|
188
|
+
const res = await driver.post('/v1/prompts:render', { ref, variables });
|
|
189
|
+
if (res.status !== 200) return;
|
|
190
|
+
const r = res.json as RenderResponse;
|
|
191
|
+
|
|
192
|
+
expect(
|
|
193
|
+
/^sha256:[0-9a-f]{64}$/.test(r.hash),
|
|
194
|
+
driver.describe(
|
|
195
|
+
'schemas/run-event-payloads.schema.json §promptComposed.hash',
|
|
196
|
+
'hash MUST match `^sha256:[0-9a-f]{64}$`',
|
|
197
|
+
),
|
|
198
|
+
).toBe(true);
|
|
199
|
+
for (const [name, h] of Object.entries(r.variableHashes)) {
|
|
200
|
+
expect(
|
|
201
|
+
/^sha256:[0-9a-f]{64}$/.test(h),
|
|
202
|
+
`variableHashes[${name}] MUST match sha256:<hex64>; got ${h}`,
|
|
203
|
+
).toBe(true);
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('renders few-shot + schema-hint kinds with non-empty `composed` body', async () => {
|
|
208
|
+
// RFC 0028 §A says `composed` is the full body regardless of kind.
|
|
209
|
+
// Regression pin for the rendering-bug fix: few-shot and
|
|
210
|
+
// schema-hint templates SHOULD surface a body, not the empty
|
|
211
|
+
// string (which the kind-specific systemPrompt/userPrompt fields
|
|
212
|
+
// would yield by themselves for these kinds).
|
|
213
|
+
const d = await readDiscovery();
|
|
214
|
+
if (!behaviorGate('prompts-endpoints', endpointsSupported(d))) return;
|
|
215
|
+
const list = await driver.get('/v1/prompts?source=host&limit=200');
|
|
216
|
+
if (list.status !== 200) return;
|
|
217
|
+
const body = list.json as ListResponse;
|
|
218
|
+
const nonSystemUser = body.items.find(
|
|
219
|
+
(t) => t.kind === 'few-shot' || t.kind === 'schema-hint',
|
|
220
|
+
);
|
|
221
|
+
if (!nonSystemUser) return; // host doesn't ship one — soft-skip
|
|
222
|
+
|
|
223
|
+
const variables: Record<string, unknown> = {};
|
|
224
|
+
for (const v of nonSystemUser.variables ?? []) {
|
|
225
|
+
if (v.source === 'secret') continue;
|
|
226
|
+
variables[v.name] = 'conformance-value';
|
|
227
|
+
}
|
|
228
|
+
const ref = `prompt:${nonSystemUser.templateId}@${nonSystemUser.version}`;
|
|
229
|
+
const res = await driver.post('/v1/prompts:render', { ref, variables });
|
|
230
|
+
if (res.status !== 200) return;
|
|
231
|
+
const r = res.json as RenderResponse;
|
|
232
|
+
expect(
|
|
233
|
+
typeof r.composed === 'string' && r.composed.length > 0,
|
|
234
|
+
driver.describe(
|
|
235
|
+
'spec/v1/prompts.md §Discovery & distribution',
|
|
236
|
+
'`composed` body MUST populate for every PromptKind under observability: full',
|
|
237
|
+
),
|
|
238
|
+
).toBe(true);
|
|
239
|
+
});
|
|
240
|
+
});
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prompt-resolution-chain-agent-intrinsic — RFC 0029 §A layer-2
|
|
3
|
+
* agent-intrinsic precedence.
|
|
4
|
+
*
|
|
5
|
+
* Asserts: when a workflow node has no layer-1 systemPromptRef but is
|
|
6
|
+
* bound to an agent whose `AgentManifest.systemPromptRef` (or
|
|
7
|
+
* `systemPrompt`) is set, the agent's intrinsic prompt wins. The
|
|
8
|
+
* emitted `agent.promptResolved.chain` MUST show
|
|
9
|
+
* `layer: "agent-intrinsic"` with `applied: true`, and `resolved` MUST
|
|
10
|
+
* be a synthetic PromptRef projected from the manifest's intrinsic
|
|
11
|
+
* surface.
|
|
12
|
+
*
|
|
13
|
+
* Capability-gated: skips when the host doesn't advertise BOTH
|
|
14
|
+
* `capabilities.prompts.supported: true` AND
|
|
15
|
+
* `capabilities.prompts.agentBindings: true`.
|
|
16
|
+
*
|
|
17
|
+
* HTTP-driven: skips when no `OPENWOP_BASE_URL` is configured.
|
|
18
|
+
*
|
|
19
|
+
*
|
|
20
|
+
* Under `OPENWOP_REQUIRE_BEHAVIOR=true` the capability gate hardens
|
|
21
|
+
* from SKIP to FAIL — a host that advertises the gating capability
|
|
22
|
+
* but doesn't emit the asserted contract fails the scenario instead
|
|
23
|
+
* of silently skipping. See `conformance/coverage.md` §"Capability-
|
|
24
|
+
* gated scenarios."
|
|
25
|
+
*
|
|
26
|
+
* @see spec/v1/prompts.md §"Resolution chain (normative)" — Layer 2
|
|
27
|
+
* @see RFCS/0029-prompt-override-hierarchy.md §A
|
|
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
|
+
|
|
34
|
+
interface DiscoveryDoc {
|
|
35
|
+
capabilities?: {
|
|
36
|
+
prompts?: {
|
|
37
|
+
supported?: unknown;
|
|
38
|
+
agentBindings?: unknown;
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface AgentPromptResolvedPayload {
|
|
44
|
+
nodeId: string;
|
|
45
|
+
kind: string;
|
|
46
|
+
agentId?: string;
|
|
47
|
+
chain: Array<{
|
|
48
|
+
layer: string;
|
|
49
|
+
source?: string;
|
|
50
|
+
applied: boolean;
|
|
51
|
+
reason?: string;
|
|
52
|
+
}>;
|
|
53
|
+
resolved: string | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function readDiscovery(): Promise<DiscoveryDoc | null> {
|
|
57
|
+
const res = await driver.get('/.well-known/openwop');
|
|
58
|
+
if (res.status !== 200) return null;
|
|
59
|
+
return res.json as DiscoveryDoc;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function promptsAgentBindings(d: DiscoveryDoc | null): boolean {
|
|
63
|
+
const p = d?.capabilities?.prompts;
|
|
64
|
+
if (!p) return false;
|
|
65
|
+
return p.supported === true && p.agentBindings === true;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
|
|
69
|
+
|
|
70
|
+
describe.skipIf(HTTP_SKIP)('prompt-resolution-chain-agent-intrinsic: layer-2 agent intrinsic wins when node has no override (RFC 0029 §A)', () => {
|
|
71
|
+
it('agent intrinsic systemPromptRef wins over workflow defaults + host defaults when node has no layer-1 ref', async () => {
|
|
72
|
+
const d = await readDiscovery();
|
|
73
|
+
if (!behaviorGate('prompts-agent-bindings', promptsAgentBindings(d))) return;
|
|
74
|
+
|
|
75
|
+
const res = await driver.post('/v1/host/sample/prompt/resolve', {
|
|
76
|
+
kind: 'system',
|
|
77
|
+
node: {
|
|
78
|
+
nodeId: 'writer',
|
|
79
|
+
config: {
|
|
80
|
+
// Layer 1 absent — no systemPromptRef on node.
|
|
81
|
+
agentId: 'vendor.acme.writer-agent',
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
agentManifest: {
|
|
85
|
+
agentId: 'vendor.acme.writer-agent',
|
|
86
|
+
// Layer 2 intrinsic — should win.
|
|
87
|
+
systemPromptRef: 'prompts/intrinsic.md',
|
|
88
|
+
// Layer 2 overrides also set — for `system` kind, intrinsic
|
|
89
|
+
// takes precedence over overrides per RFC 0029 §A.
|
|
90
|
+
promptOverrides: {
|
|
91
|
+
system: 'prompt:editorial-house-style@1.0.0',
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
workflowDefaults: {
|
|
95
|
+
promptRefs: {
|
|
96
|
+
system: 'prompt:workflow-default@1.0.0',
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
hostDefaults: {
|
|
100
|
+
system: 'prompt:host-default@1.0.0',
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
if (res.status === 404) return;
|
|
104
|
+
expect(res.status).toBe(200);
|
|
105
|
+
|
|
106
|
+
const payload = res.json as AgentPromptResolvedPayload;
|
|
107
|
+
|
|
108
|
+
const appliedEntries = payload.chain.filter((c) => c.applied);
|
|
109
|
+
expect(
|
|
110
|
+
appliedEntries.length,
|
|
111
|
+
driver.describe(
|
|
112
|
+
'spec/v1/prompts.md §Resolution chain (normative)',
|
|
113
|
+
'exactly one chain entry MUST carry applied: true',
|
|
114
|
+
),
|
|
115
|
+
).toBe(1);
|
|
116
|
+
expect(
|
|
117
|
+
appliedEntries[0]?.layer,
|
|
118
|
+
driver.describe(
|
|
119
|
+
'spec/v1/prompts.md §Resolution chain (normative) — Layer 2',
|
|
120
|
+
'system-kind resolution MUST prefer agent intrinsic (systemPromptRef) over agent-overrides when both are set',
|
|
121
|
+
),
|
|
122
|
+
).toBe('agent-intrinsic');
|
|
123
|
+
|
|
124
|
+
expect(
|
|
125
|
+
payload.resolved,
|
|
126
|
+
driver.describe(
|
|
127
|
+
'spec/v1/prompts.md §Resolution chain (normative) — Layer 2',
|
|
128
|
+
'resolved MUST mirror the winning chain entry source',
|
|
129
|
+
),
|
|
130
|
+
).toBe(appliedEntries[0]?.source ?? null);
|
|
131
|
+
|
|
132
|
+
expect(
|
|
133
|
+
payload.agentId,
|
|
134
|
+
driver.describe(
|
|
135
|
+
'spec/v1/prompts.md §Resolution chain (normative)',
|
|
136
|
+
'agent.promptResolved.agentId MUST be set when config.agentId resolves to a known agent',
|
|
137
|
+
),
|
|
138
|
+
).toBe('vendor.acme.writer-agent');
|
|
139
|
+
});
|
|
140
|
+
});
|