@openwop/openwop-conformance 1.6.1 → 1.11.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 +44 -0
- package/README.md +2 -2
- package/api/asyncapi.yaml +127 -0
- package/api/openapi.yaml +518 -1
- package/coverage.md +44 -2
- package/fixtures/conformance-run-duration-breach.json +33 -0
- package/fixtures/oauth-providers/synthetic.json +38 -0
- package/fixtures.md +29 -0
- package/package.json +1 -1
- package/schemas/README.md +22 -0
- package/schemas/agent-deployment-transition.schema.json +49 -0
- package/schemas/agent-deployment.schema.json +54 -0
- package/schemas/agent-eval-suite.schema.json +140 -0
- package/schemas/agent-inventory-response.schema.json +115 -0
- package/schemas/agent-manifest.schema.json +5 -0
- package/schemas/agent-org-chart.schema.json +82 -0
- package/schemas/agent-ref.schema.json +12 -2
- package/schemas/agent-roster-entry.schema.json +81 -0
- package/schemas/agent-roster-response.schema.json +21 -0
- package/schemas/ai-envelope.schema.json +28 -0
- package/schemas/artifact-type-pack-manifest.schema.json +160 -0
- package/schemas/budget-policy.schema.json +18 -0
- package/schemas/capabilities.schema.json +448 -4
- package/schemas/chat-card-pack-manifest.schema.json +158 -0
- package/schemas/credential-provenance.schema.json +18 -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/eval-summary.schema.json +92 -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 +33 -1
- package/schemas/org-chart-responsibility-view.schema.json +26 -0
- package/schemas/run-event-payloads.schema.json +380 -6
- package/schemas/run-event.schema.json +23 -0
- package/schemas/tool-descriptor.schema.json +63 -0
- package/schemas/trigger-subscription.schema.json +26 -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/agentRoster.ts +76 -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/liveRuntime.ts +59 -0
- package/src/lib/memoryAttribution.ts +48 -0
- package/src/lib/profiles.ts +157 -0
- package/src/lib/runtimeRequires.ts +38 -0
- package/src/lib/safeFetch.ts +87 -0
- package/src/lib/subRunAttestation.ts +35 -0
- package/src/lib/toolHooks.ts +33 -0
- package/src/scenarios/agent-deployment-shape.test.ts +139 -0
- package/src/scenarios/agent-eval-suite-shape.test.ts +167 -0
- package/src/scenarios/agent-live-allowlist-enforced.test.ts +53 -0
- package/src/scenarios/agent-live-invocation-bracket.test.ts +98 -0
- package/src/scenarios/agent-live-runtime-shape.test.ts +98 -0
- package/src/scenarios/agent-live-structured-output.test.ts +58 -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/agent-org-chart-shape.test.ts +127 -0
- package/src/scenarios/agent-platform-profile.test.ts +158 -0
- package/src/scenarios/agent-roster-attribution.test.ts +179 -0
- package/src/scenarios/agent-roster-shape.test.ts +146 -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/budget-policy-shape.test.ts +136 -0
- 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/egress-provenance-shape.test.ts +137 -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-capability-model-shape.test.ts +186 -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-authorization-code-roundtrip.test.ts +145 -0
- 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/runtime-requires-install-gate.test.ts +92 -0
- package/src/scenarios/runtime-requires-shape.test.ts +134 -0
- package/src/scenarios/safefetch-behavior.test.ts +99 -0
- package/src/scenarios/safefetch-live-audit.test.ts +175 -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 +20 -4
- 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-descriptor-shape.test.ts +133 -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/trigger-bridge-shape.test.ts +135 -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
- package/src/scenarios/x-openwop-form-pack-manifest.test.ts +155 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helpers for the RFC 0057 memory write-attribution scenarios.
|
|
3
|
+
* Lives in lib/ (not a *.test.ts) so scenarios import it via `../lib/memoryAttribution.js`.
|
|
4
|
+
*/
|
|
5
|
+
import { driver } from './driver.js';
|
|
6
|
+
import { isFixtureAdvertised } from './fixtures.js';
|
|
7
|
+
|
|
8
|
+
interface DiscoveryDoc {
|
|
9
|
+
capabilities?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Reads `capabilities.memory.attribution` from discovery; null when unadvertised. */
|
|
13
|
+
export async function readMemoryAttributionCap(): Promise<Record<string, unknown> | null> {
|
|
14
|
+
const res = await driver.get('/.well-known/openwop');
|
|
15
|
+
const caps = (res.json as DiscoveryDoc | undefined)?.capabilities;
|
|
16
|
+
const mem = caps && typeof caps === 'object' ? (caps as Record<string, unknown>)['memory'] : undefined;
|
|
17
|
+
const attr = mem && typeof mem === 'object' ? (mem as Record<string, unknown>)['attribution'] : undefined;
|
|
18
|
+
return attr && typeof attr === 'object' ? (attr as Record<string, unknown>) : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** True when the host commits to emitting `memory.written`. */
|
|
22
|
+
export function emitsWriteEvents(cap: Record<string, unknown> | null): boolean {
|
|
23
|
+
return cap?.['supported'] === true && cap?.['emitsWriteEvents'] === true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const SEED_FIXTURE = 'conformance-noop';
|
|
27
|
+
|
|
28
|
+
/** Seeds a basic run (the host writes a run-summary on completion); null
|
|
29
|
+
* (soft-skip) when the fixture isn't advertised or creation fails. */
|
|
30
|
+
export async function seedRun(tenantId: string): Promise<string | null> {
|
|
31
|
+
if (!isFixtureAdvertised(SEED_FIXTURE)) return null;
|
|
32
|
+
const r = await driver.post('/v1/runs', { workflowId: SEED_FIXTURE, tenantId, inputs: {} });
|
|
33
|
+
if (r.status !== 200 && r.status !== 201) return null;
|
|
34
|
+
return (r.json as { runId?: string } | undefined)?.runId ?? null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface RunEventLike {
|
|
38
|
+
type: string;
|
|
39
|
+
runId?: string;
|
|
40
|
+
payload?: Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Fetches a run's events and returns only the `memory.written` ones. */
|
|
44
|
+
export async function memoryWrittenEvents(runId: string): Promise<RunEventLike[]> {
|
|
45
|
+
const res = await driver.get(`/v1/runs/${encodeURIComponent(runId)}/events`);
|
|
46
|
+
const events = (res.json as { events?: RunEventLike[] } | undefined)?.events ?? [];
|
|
47
|
+
return events.filter((e) => e.type === 'memory.written');
|
|
48
|
+
}
|
package/src/lib/profiles.ts
CHANGED
|
@@ -30,6 +30,8 @@ export const PROFILE_NAMES = [
|
|
|
30
30
|
'openwop-node-packs',
|
|
31
31
|
'openwop-replay-fork',
|
|
32
32
|
'openwop-fixtures',
|
|
33
|
+
'openwop-memory',
|
|
34
|
+
'openwop-trigger-bridge',
|
|
33
35
|
] as const;
|
|
34
36
|
|
|
35
37
|
export type ProfileName = (typeof PROFILE_NAMES)[number];
|
|
@@ -211,6 +213,155 @@ export function isFixtures(c: DiscoveryPayload): boolean {
|
|
|
211
213
|
return c.fixtures.every((id) => typeof id === 'string' && id.length > 0);
|
|
212
214
|
}
|
|
213
215
|
|
|
216
|
+
/**
|
|
217
|
+
* `openwop-memory` predicate (RFC 0080). Host implements the reconciled
|
|
218
|
+
* memory-capability model at the core tier: a read/write `MemoryAdapter`
|
|
219
|
+
* (`memory.supported: true` and `memory.writable !== false`) plus a cross-run
|
|
220
|
+
* durable store (`agents.memoryBackends` includes `'long-term'`). Capability
|
|
221
|
+
* families are document-root properties of the discovery payload (RFC 0073),
|
|
222
|
+
* so this reads `c.memory` / `c.agents`, matching `isReplayFork`.
|
|
223
|
+
*
|
|
224
|
+
* @see spec/v1/profiles.md §`openwop-memory`
|
|
225
|
+
* @see spec/v1/agent-memory.md §"Memory capability model"
|
|
226
|
+
*/
|
|
227
|
+
export function isMemory(c: DiscoveryPayload): boolean {
|
|
228
|
+
if (!isCore(c)) return false;
|
|
229
|
+
const memory = c.memory as { supported?: unknown; writable?: unknown } | undefined;
|
|
230
|
+
if (memory == null || typeof memory !== 'object') return false;
|
|
231
|
+
if (memory.supported !== true) return false;
|
|
232
|
+
if (memory.writable === false) return false;
|
|
233
|
+
const agents = c.agents as { memoryBackends?: unknown } | undefined;
|
|
234
|
+
if (agents == null || !isStringArray(agents.memoryBackends)) return false;
|
|
235
|
+
return agents.memoryBackends.includes('long-term');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* `openwop-trigger-bridge` predicate (RFC 0083). Host composes the durable
|
|
240
|
+
* inbound-work contract: advertises the `triggerBridge`, has a `deadLetter`
|
|
241
|
+
* sink for exhausted deliveries, and has at least one durable inbound source
|
|
242
|
+
* (queue bus, durable webhooks, or scheduling). Capability families are
|
|
243
|
+
* document-root properties (RFC 0073), so this reads `c.triggerBridge` /
|
|
244
|
+
* `c.deadLetter` / `c.queueBus` / `c.webhooks` / `c.scheduling`.
|
|
245
|
+
*
|
|
246
|
+
* @see spec/v1/profiles.md §`openwop-trigger-bridge`
|
|
247
|
+
* @see spec/v1/trigger-bridge.md
|
|
248
|
+
*/
|
|
249
|
+
export function isTriggerBridge(c: DiscoveryPayload): boolean {
|
|
250
|
+
if (!isCore(c)) return false;
|
|
251
|
+
const supported = (v: unknown): boolean =>
|
|
252
|
+
v != null && typeof v === 'object' && (v as { supported?: unknown }).supported === true;
|
|
253
|
+
if (!supported(c.triggerBridge)) return false;
|
|
254
|
+
if (!supported(c.deadLetter)) return false;
|
|
255
|
+
const webhooks = c.webhooks as { durable?: unknown } | undefined;
|
|
256
|
+
const durableSource =
|
|
257
|
+
supported(c.queueBus) ||
|
|
258
|
+
supported(c.scheduling) ||
|
|
259
|
+
(webhooks != null && typeof webhooks === 'object' && webhooks.durable === true);
|
|
260
|
+
return durableSource;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
264
|
+
// Operational annex: openwop-agent-platform (RFC 0085).
|
|
265
|
+
//
|
|
266
|
+
// NOT part of the closed `profiles.md` predicate catalog (PROFILE_NAMES /
|
|
267
|
+
// deriveProfiles above) — it is an operational ANNEX (the production-profile.md /
|
|
268
|
+
// auth-profiles.md pattern) combining a discovery predicate with required runtime
|
|
269
|
+
// conformance evidence + documentation + a badge. These helpers compute only the
|
|
270
|
+
// discovery-PREDICATE part; the live aggregate-evidence assertion (does every
|
|
271
|
+
// constituent scenario actually pass?) lives in agent-platform-profile.test.ts.
|
|
272
|
+
//
|
|
273
|
+
// @see spec/v1/agent-platform-profile.md
|
|
274
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
/** Narrow helper: a capability sub-block with `supported === true`. */
|
|
277
|
+
function blockSupported(v: unknown): boolean {
|
|
278
|
+
return v != null && typeof v === 'object' && (v as { supported?: unknown }).supported === true;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** The `openwop-agent-platform` FLOOR (`partial`) discovery predicate — RFC 0085 §B. */
|
|
282
|
+
export function isAgentPlatformPartial(c: DiscoveryPayload): boolean {
|
|
283
|
+
if (!isCore(c)) return false;
|
|
284
|
+
const agents = c.agents as { manifestRuntime?: unknown; liveRuntime?: unknown } | undefined;
|
|
285
|
+
const httpClient = c.httpClient as { safeFetch?: unknown; egressPolicy?: unknown } | undefined;
|
|
286
|
+
const replay = c.replay as { supported?: unknown } | undefined;
|
|
287
|
+
const nondet = c.nondeterminismPolicy as { declared?: unknown } | undefined;
|
|
288
|
+
return (
|
|
289
|
+
blockSupported(agents?.manifestRuntime) &&
|
|
290
|
+
blockSupported(agents?.liveRuntime) &&
|
|
291
|
+
blockSupported(c.toolCatalog) &&
|
|
292
|
+
blockSupported(c.toolHooks) &&
|
|
293
|
+
blockSupported(httpClient?.safeFetch) &&
|
|
294
|
+
blockSupported(c.providerUsage) &&
|
|
295
|
+
blockSupported(c.prompts) &&
|
|
296
|
+
blockSupported(c.memory) &&
|
|
297
|
+
blockSupported(c.feedback) &&
|
|
298
|
+
(replay?.supported === true || nondet?.declared === true)
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** The `openwop-agent-platform` `full` discovery predicate (floor + governance tier) — RFC 0085 §B. */
|
|
303
|
+
export function isAgentPlatformFull(c: DiscoveryPayload): boolean {
|
|
304
|
+
if (!isAgentPlatformPartial(c)) return false;
|
|
305
|
+
const agents = c.agents as { manifestRuntime?: { installScope?: unknown } } | undefined;
|
|
306
|
+
const memory = c.memory as { attribution?: unknown } | undefined;
|
|
307
|
+
// Debug bundle is advertised at `capabilities.debugBundle.supported` (debug-bundle.md /
|
|
308
|
+
// RFC 0009), NOT under `production.*` — the production block only adds stricter truncation MUSTs.
|
|
309
|
+
const httpClient = c.httpClient as { egressPolicy?: unknown } | undefined;
|
|
310
|
+
return (
|
|
311
|
+
blockSupported(c.authorization) &&
|
|
312
|
+
agents?.manifestRuntime?.installScope === 'tenant' &&
|
|
313
|
+
blockSupported(memory?.attribution) &&
|
|
314
|
+
blockSupported(c.debugBundle) &&
|
|
315
|
+
blockSupported(c.triggerBridge) &&
|
|
316
|
+
blockSupported(httpClient?.egressPolicy)
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** The host-reported annex status: `full` ⊃ `partial` ⊃ `none` (discovery-predicate only). */
|
|
321
|
+
export function agentPlatformStatus(c: DiscoveryPayload): 'none' | 'partial' | 'full' {
|
|
322
|
+
if (isAgentPlatformFull(c)) return 'full';
|
|
323
|
+
if (isAgentPlatformPartial(c)) return 'partial';
|
|
324
|
+
return 'none';
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* The per-term satisfaction breakdown (RFC 0085 §D) — the richer interop signal
|
|
329
|
+
* alongside the flat `none`/`partial`/`full` ladder. Adoption is NON-CONTIGUOUS:
|
|
330
|
+
* a real host built feature-by-feature can satisfy `full`-tier terms (RBAC,
|
|
331
|
+
* memory-attribution, tenant-scoping) while still failing `floor` terms, so the
|
|
332
|
+
* flat status would understate it (reads identical to a do-nothing host). This
|
|
333
|
+
* returns exactly the term ids a host satisfies, so a `none` host honoring 6/16
|
|
334
|
+
* terms is distinguishable from one honoring 0/16.
|
|
335
|
+
*/
|
|
336
|
+
export function agentPlatformSatisfiedTerms(c: DiscoveryPayload): readonly string[] {
|
|
337
|
+
const agents = c.agents as { manifestRuntime?: { installScope?: unknown }; liveRuntime?: unknown } | undefined;
|
|
338
|
+
const httpClient = c.httpClient as { safeFetch?: unknown; egressPolicy?: unknown } | undefined;
|
|
339
|
+
const memory = c.memory as { attribution?: unknown } | undefined;
|
|
340
|
+
const replay = c.replay as { supported?: unknown } | undefined;
|
|
341
|
+
const nondet = c.nondeterminismPolicy as { declared?: unknown } | undefined;
|
|
342
|
+
const checks: ReadonlyArray<readonly [string, boolean]> = [
|
|
343
|
+
// floor
|
|
344
|
+
['floor:agents.manifestRuntime', blockSupported(agents?.manifestRuntime)],
|
|
345
|
+
['floor:agents.liveRuntime', blockSupported(agents?.liveRuntime)],
|
|
346
|
+
['floor:toolCatalog', blockSupported(c.toolCatalog)],
|
|
347
|
+
['floor:toolHooks', blockSupported(c.toolHooks)],
|
|
348
|
+
['floor:httpClient.safeFetch', blockSupported(httpClient?.safeFetch)],
|
|
349
|
+
['floor:providerUsage', blockSupported(c.providerUsage)],
|
|
350
|
+
['floor:prompts', blockSupported(c.prompts)],
|
|
351
|
+
['floor:memory', blockSupported(c.memory)],
|
|
352
|
+
['floor:feedback', blockSupported(c.feedback)],
|
|
353
|
+
['floor:replay-or-nondeterminism', replay?.supported === true || nondet?.declared === true],
|
|
354
|
+
// full (governance)
|
|
355
|
+
['full:authorization', blockSupported(c.authorization)],
|
|
356
|
+
['full:tenant-installScope', agents?.manifestRuntime?.installScope === 'tenant'],
|
|
357
|
+
['full:memory.attribution', blockSupported(memory?.attribution)],
|
|
358
|
+
['full:debugBundle', blockSupported(c.debugBundle)],
|
|
359
|
+
['full:triggerBridge', blockSupported(c.triggerBridge)],
|
|
360
|
+
['full:egressPolicy', blockSupported(httpClient?.egressPolicy)],
|
|
361
|
+
];
|
|
362
|
+
return checks.filter(([, ok]) => ok).map(([id]) => id);
|
|
363
|
+
}
|
|
364
|
+
|
|
214
365
|
/**
|
|
215
366
|
* Derive the full profile set from a discovery payload.
|
|
216
367
|
*
|
|
@@ -228,6 +379,8 @@ export function deriveProfiles(c: DiscoveryPayload): readonly ProfileName[] {
|
|
|
228
379
|
if (isNodePacksDiscovery(c)) result.push('openwop-node-packs');
|
|
229
380
|
if (isReplayFork(c)) result.push('openwop-replay-fork');
|
|
230
381
|
if (isFixtures(c)) result.push('openwop-fixtures');
|
|
382
|
+
if (isMemory(c)) result.push('openwop-memory');
|
|
383
|
+
if (isTriggerBridge(c)) result.push('openwop-trigger-bridge');
|
|
231
384
|
return result;
|
|
232
385
|
}
|
|
233
386
|
|
|
@@ -254,5 +407,9 @@ export function hasProfile(c: DiscoveryPayload, profile: ProfileName): boolean {
|
|
|
254
407
|
return isReplayFork(c);
|
|
255
408
|
case 'openwop-fixtures':
|
|
256
409
|
return isFixtures(c);
|
|
410
|
+
case 'openwop-memory':
|
|
411
|
+
return isMemory(c);
|
|
412
|
+
case 'openwop-trigger-bridge':
|
|
413
|
+
return isTriggerBridge(c);
|
|
257
414
|
}
|
|
258
415
|
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper for the RFC 0076 §A `runtime.requires[]` install-gate
|
|
3
|
+
* conformance scenarios. Lives in lib/ (not a *.test.ts) so scenarios import it
|
|
4
|
+
* via `../lib/runtimeRequires.js`.
|
|
5
|
+
*
|
|
6
|
+
* Drives the conformance-only host seam specified in host-sample-test-seams.md
|
|
7
|
+
* §"Open seams": `POST /v1/host/sample/packs/install-gate`. The seam evaluates a
|
|
8
|
+
* manifest's `runtime.requires[]` against a simulated host grant-set and returns
|
|
9
|
+
* the install-time outcome the host would produce — letting a single seam
|
|
10
|
+
* exercise the grant / refuse / non-sandbox-projection behaviors deterministically.
|
|
11
|
+
*/
|
|
12
|
+
import { driver } from './driver.js';
|
|
13
|
+
|
|
14
|
+
export interface InstallGateRequest {
|
|
15
|
+
/** The candidate pack manifest (carrying runtime.requires[]). */
|
|
16
|
+
manifest: Record<string, unknown>;
|
|
17
|
+
/** Primitives the simulated sandbox grants. Ignored when `gating === false`. */
|
|
18
|
+
grantSet?: string[];
|
|
19
|
+
/** Whether the simulated host gates platform access. Default true (sandbox host). */
|
|
20
|
+
gating?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface InstallGateResponse {
|
|
24
|
+
/** HTTP status the seam returned (200 install, 400 refuse). */
|
|
25
|
+
status: number;
|
|
26
|
+
/** Parsed response body. */
|
|
27
|
+
body: Record<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Drives one install-gate evaluation via the host-sample seam, or null
|
|
32
|
+
* (soft-skip) when the host doesn't expose it.
|
|
33
|
+
*/
|
|
34
|
+
export async function installGate(req: InstallGateRequest): Promise<InstallGateResponse | null> {
|
|
35
|
+
const res = await driver.post('/v1/host/sample/packs/install-gate', req as unknown as Record<string, unknown>);
|
|
36
|
+
if (res.status === 404 || res.status === 405) return null; // seam absent — soft-skip
|
|
37
|
+
return { status: res.status, body: (res.json as Record<string, unknown> | undefined) ?? {} };
|
|
38
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper for the RFC 0076 §B `ctx.http.safeFetch` conformance scenarios.
|
|
3
|
+
* Lives in lib/ (not a *.test.ts) so scenarios import it via `../lib/safeFetch.js`.
|
|
4
|
+
*
|
|
5
|
+
* Reads `capabilities.httpClient.safeFetch` (root-first, wrapper-fallback) and
|
|
6
|
+
* drives the conformance-only host seam `POST /v1/host/sample/http/safe-fetch`
|
|
7
|
+
* (host-sample-test-seams.md §"Open seams").
|
|
8
|
+
*/
|
|
9
|
+
import { driver } from './driver.js';
|
|
10
|
+
import { capabilityFamily } from './discovery-capabilities.js';
|
|
11
|
+
|
|
12
|
+
interface HttpClientCap {
|
|
13
|
+
supported?: boolean;
|
|
14
|
+
safeFetch?: { supported?: boolean };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** True when the host advertises `capabilities.httpClient.safeFetch.supported`. */
|
|
18
|
+
export async function isSafeFetchSupported(): Promise<boolean> {
|
|
19
|
+
const disco = await driver.get('/.well-known/openwop');
|
|
20
|
+
return capabilityFamily<HttpClientCap>(disco.json, 'httpClient')?.safeFetch?.supported === true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** True when the host also advertises `capabilities.toolHooks.prePostEvents`. */
|
|
24
|
+
export async function isToolHookAuditOn(): Promise<boolean> {
|
|
25
|
+
const disco = await driver.get('/.well-known/openwop');
|
|
26
|
+
return capabilityFamily<{ prePostEvents?: boolean }>(disco.json, 'toolHooks')?.prePostEvents === true;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface SafeFetchResult {
|
|
30
|
+
outcome?: 'fetched' | 'blocked';
|
|
31
|
+
status?: number;
|
|
32
|
+
blocked?: 'ssrf' | 'upgrade' | string;
|
|
33
|
+
toolCalled?: Record<string, unknown>;
|
|
34
|
+
toolReturned?: Record<string, unknown>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Drives one safeFetch evaluation via the host-sample seam, or null (soft-skip)
|
|
39
|
+
* when the host doesn't expose it.
|
|
40
|
+
*/
|
|
41
|
+
export async function safeFetch(body: Record<string, unknown>): Promise<SafeFetchResult | null> {
|
|
42
|
+
const res = await driver.post('/v1/host/sample/http/safe-fetch', body);
|
|
43
|
+
if (res.status === 404 || res.status === 405) return null; // seam absent — soft-skip
|
|
44
|
+
return (res.json as SafeFetchResult | undefined) ?? {};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* True when the host advertises BOTH `httpClient.safeFetch.supported` AND
|
|
49
|
+
* `toolHooks.prePostEvents` — the co-advertisement that, per
|
|
50
|
+
* `host-capabilities.md` §host.http + RFC 0076 §B, makes live audit-pair
|
|
51
|
+
* emission a MUST. One discovery fetch (the two single-flag helpers above each
|
|
52
|
+
* fetch; this avoids the double round-trip for the live-audit gate).
|
|
53
|
+
*/
|
|
54
|
+
export async function isSafeFetchLiveAuditAdvertised(): Promise<boolean> {
|
|
55
|
+
const disco = await driver.get('/.well-known/openwop');
|
|
56
|
+
const safeFetchOn =
|
|
57
|
+
capabilityFamily<HttpClientCap>(disco.json, 'httpClient')?.safeFetch?.supported === true;
|
|
58
|
+
const auditOn =
|
|
59
|
+
capabilityFamily<{ prePostEvents?: boolean }>(disco.json, 'toolHooks')?.prePostEvents === true;
|
|
60
|
+
return safeFetchOn && auditOn;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Result of the live-run safe-fetch seam: the host executed one
|
|
64
|
+
* `ctx.http.safeFetch` call inside a real run via the production injection
|
|
65
|
+
* path, and returns the run's id so the caller can read the durable event
|
|
66
|
+
* log. `null` ⇒ the run seam is unwired (soft-skip, host-pending). */
|
|
67
|
+
export interface SafeFetchRunResult {
|
|
68
|
+
runId?: string;
|
|
69
|
+
outcome?: 'fetched' | 'blocked';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Drives one `ctx.http.safeFetch` call **inside a real run** via the open seam
|
|
74
|
+
* `POST /v1/host/sample/http/safe-fetch-run`, returning `{ runId, outcome }`,
|
|
75
|
+
* or null (soft-skip) when the run seam isn't wired. Distinct from `safeFetch`
|
|
76
|
+
* (which returns the audit pair INLINE from the seam): this exercises the
|
|
77
|
+
* production per-ctx `ctx.http.safeFetch` path so the caller can assert the
|
|
78
|
+
* `agent.toolCalled`/`agent.toolReturned` pair landed in the DURABLE run event
|
|
79
|
+
* log — closing the seam-vs-production gap in `safefetch-behavior.test.ts`.
|
|
80
|
+
*/
|
|
81
|
+
export async function safeFetchViaRun(
|
|
82
|
+
body: Record<string, unknown>,
|
|
83
|
+
): Promise<SafeFetchRunResult | null> {
|
|
84
|
+
const res = await driver.post('/v1/host/sample/http/safe-fetch-run', body);
|
|
85
|
+
if (res.status === 404 || res.status === 405) return null; // run seam unwired — soft-skip
|
|
86
|
+
return (res.json as SafeFetchRunResult | undefined) ?? {};
|
|
87
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper for the RFC 0063 sub-run output-attestation conformance
|
|
3
|
+
* scenarios. Lives in lib/ (not a *.test.ts) so scenarios import it via
|
|
4
|
+
* `../lib/subRunAttestation.js`.
|
|
5
|
+
*/
|
|
6
|
+
import { driver } from './driver.js';
|
|
7
|
+
import { readCapabilityFamily } from './discovery-capabilities.js';
|
|
8
|
+
|
|
9
|
+
/** Reads `agents.subRunAttestation` from discovery (root-first per RFC 0073);
|
|
10
|
+
* null when the host advertises no `agents` block (treated as no support). */
|
|
11
|
+
export async function readSubRunAttestationCap(): Promise<boolean | null> {
|
|
12
|
+
const agents = await readCapabilityFamily<Record<string, unknown>>('agents');
|
|
13
|
+
if (!agents || typeof agents !== 'object') return null;
|
|
14
|
+
const flag = agents['subRunAttestation'];
|
|
15
|
+
return flag === undefined ? null : flag === true;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface AttestResult {
|
|
19
|
+
attestation?: { checksum?: unknown; algorithm?: unknown };
|
|
20
|
+
harvestedEvent?: Record<string, unknown>;
|
|
21
|
+
merged?: unknown;
|
|
22
|
+
mergedValues?: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Drives one sub-run harvest-then-merge via the host-sample seam, or null
|
|
26
|
+
* (soft-skip) when the host doesn't expose it. The seam is host-extension
|
|
27
|
+
* surface specified in host-sample-test-seams.md §"Open seams":
|
|
28
|
+
* `POST /v1/host/sample/subrun/attest` returns the `attestation` the host
|
|
29
|
+
* would surface on `core.workflowChain.event { phase: 'output.harvested' }`,
|
|
30
|
+
* plus whether the merge proceeded. */
|
|
31
|
+
export async function invokeSubRunAttest(body: Record<string, unknown>): Promise<AttestResult | null> {
|
|
32
|
+
const res = await driver.post('/v1/host/sample/subrun/attest', body);
|
|
33
|
+
if (res.status === 404 || res.status === 405) return null; // seam absent — soft-skip
|
|
34
|
+
return (res.json as AttestResult | undefined) ?? {};
|
|
35
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper for the RFC 0064 host.toolHooks conformance scenarios.
|
|
3
|
+
* Lives in lib/ (not a *.test.ts) so scenarios import it via `../lib/toolHooks.js`.
|
|
4
|
+
*/
|
|
5
|
+
import { driver } from './driver.js';
|
|
6
|
+
|
|
7
|
+
interface DiscoveryDoc {
|
|
8
|
+
capabilities?: Record<string, unknown>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** Reads `capabilities.toolHooks` from discovery; null when unadvertised. */
|
|
12
|
+
export async function readToolHooksCap(): Promise<Record<string, unknown> | null> {
|
|
13
|
+
const res = await driver.get('/.well-known/openwop');
|
|
14
|
+
const caps = (res.json as DiscoveryDoc | undefined)?.capabilities;
|
|
15
|
+
const th = caps && typeof caps === 'object' ? (caps as Record<string, unknown>)['toolHooks'] : undefined;
|
|
16
|
+
return th && typeof th === 'object' ? (th as Record<string, unknown>) : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
interface ToolHookResult {
|
|
20
|
+
toolCalled?: Record<string, unknown>;
|
|
21
|
+
toolReturned?: Record<string, unknown>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Drives one gated tool invocation via the host-sample seam, or null
|
|
25
|
+
* (soft-skip) when the host doesn't expose it. The seam is host-extension
|
|
26
|
+
* surface specified in host-sample-test-seams.md §"Open seams":
|
|
27
|
+
* `POST /v1/host/sample/toolhooks/invoke` returns the `agent.toolCalled` /
|
|
28
|
+
* `agent.toolReturned` payload pair the host would emit for the call. */
|
|
29
|
+
export async function invokeToolHook(body: Record<string, unknown>): Promise<ToolHookResult | null> {
|
|
30
|
+
const res = await driver.post('/v1/host/sample/toolhooks/invoke', body);
|
|
31
|
+
if (res.status === 404 || res.status === 405) return null; // seam absent — soft-skip
|
|
32
|
+
return (res.json as ToolHookResult | undefined) ?? {};
|
|
33
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent deployment lifecycle — record + binding + event shapes (RFC 0082).
|
|
3
|
+
*
|
|
4
|
+
* Always-on, server-free schema-shape probe. Verifies that:
|
|
5
|
+
* - `capabilities.agents.deployment` is declared with its `supported` /
|
|
6
|
+
* `channels` / `canary` / `rollback` / `states` sub-flags.
|
|
7
|
+
* - `agent-deployment.schema.json` compiles and round-trips a conforming
|
|
8
|
+
* deployment record, and rejects malformed ones (an out-of-enum `state`;
|
|
9
|
+
* `canaryPercent` out of 0..100).
|
|
10
|
+
* - the `AgentRef` `channel` XOR `version` rule holds: each alone (and
|
|
11
|
+
* neither) validates; both together is rejected (the `not` clause).
|
|
12
|
+
* - the four `deployment.*` payload $defs validate conforming content-free
|
|
13
|
+
* payloads and reject malformed ones.
|
|
14
|
+
* - the four `deployment.*` payloads are CONTENT-FREE: a `deployment.promoted`
|
|
15
|
+
* carrying a `manifestBody`, and a `deployment.state.changed` carrying a
|
|
16
|
+
* `prompt`, are rejected (`additionalProperties:false`). This is the public
|
|
17
|
+
* test for the protocol-tier SECURITY invariant `deployment-event-no-content-leak`.
|
|
18
|
+
* - `agent.invocation.started` carries the additive recorded-fact
|
|
19
|
+
* `resolvedAgentVersion` / `resolvedChannel` fields (RFC 0082 §B).
|
|
20
|
+
* - all four event names appear in the RunEventType enum.
|
|
21
|
+
*
|
|
22
|
+
* Behavioral assertions (the authz → approvalGate → eval-verify → promotion path,
|
|
23
|
+
* the fail-closed denial, the §B replay re-read of `resolvedAgentVersion`) are
|
|
24
|
+
* gated on `capabilities.agents.deployment.supported` and land in
|
|
25
|
+
* `agent-deployment-lifecycle.test.ts` (deferred per RFC 0082 §Conformance —
|
|
26
|
+
* reference host deferred). This scenario asserts the wire contract, not host
|
|
27
|
+
* behavior.
|
|
28
|
+
*
|
|
29
|
+
* Spec references:
|
|
30
|
+
* - https://github.com/openwop/openwop/blob/main/spec/v1/agent-deployment.md
|
|
31
|
+
* - https://github.com/openwop/openwop/blob/main/RFCS/0082-agent-deployment-lifecycle.md
|
|
32
|
+
* - https://github.com/openwop/openwop/blob/main/SECURITY/invariants.yaml (deployment-event-no-content-leak)
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { describe, it, expect } from 'vitest';
|
|
36
|
+
import { readFileSync } from 'node:fs';
|
|
37
|
+
import { join } from 'node:path';
|
|
38
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
39
|
+
import addFormats from 'ajv-formats';
|
|
40
|
+
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
41
|
+
|
|
42
|
+
/** Server-free assertion-message helper. */
|
|
43
|
+
const why = (specRef: string, requirement: string): string => `${specRef} — ${requirement}`;
|
|
44
|
+
|
|
45
|
+
function loadSchema(name: string): Record<string, unknown> {
|
|
46
|
+
return JSON.parse(readFileSync(join(SCHEMAS_DIR, name), 'utf8')) as Record<string, unknown>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
describe('agent-deployment-shape: capability advertisement (RFC 0082, server-free)', () => {
|
|
50
|
+
it('the capabilities schema declares agents.deployment with its sub-flags', () => {
|
|
51
|
+
const caps = loadSchema('capabilities.schema.json');
|
|
52
|
+
const agents = (caps.properties as Record<string, { properties?: Record<string, { properties?: Record<string, unknown> }> }>).agents;
|
|
53
|
+
const deployment = agents?.properties?.deployment;
|
|
54
|
+
expect(deployment, why('capabilities.md §agents', 'agents.deployment MUST be declared')).toBeDefined();
|
|
55
|
+
for (const flag of ['supported', 'channels', 'canary', 'rollback', 'states']) {
|
|
56
|
+
expect(
|
|
57
|
+
deployment?.properties?.[flag],
|
|
58
|
+
why('agent-deployment.md §F', `agents.deployment.${flag} MUST be declared`),
|
|
59
|
+
).toBeDefined();
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('agent-deployment-shape: deployment record + AgentRef binding (RFC 0082, server-free)', () => {
|
|
65
|
+
const ajv = new Ajv2020({ strict: false, allErrors: true });
|
|
66
|
+
addFormats(ajv);
|
|
67
|
+
const record = ajv.compile(loadSchema('agent-deployment.schema.json'));
|
|
68
|
+
const agentRef = ajv.compile(loadSchema('agent-ref.schema.json'));
|
|
69
|
+
|
|
70
|
+
it('AgentDeployment validates a conforming record and rejects a bad state / out-of-range canary', () => {
|
|
71
|
+
const good = { agentId: 'core.openwop.agents.support-resolver', version: '2.4.0', state: 'active', canaryPercent: 10, channels: ['stable'] };
|
|
72
|
+
expect(record(good), why('RFC 0082 §C', 'a conforming deployment record MUST validate')).toBe(true);
|
|
73
|
+
expect(record({ ...good, state: 'live' }), why('RFC 0082 §C', 'an out-of-enum state MUST be rejected')).toBe(false);
|
|
74
|
+
expect(record({ ...good, canaryPercent: 150 }), why('RFC 0082 §C', 'canaryPercent > 100 MUST be rejected')).toBe(false);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('AgentRef channel XOR version: each alone and neither validate; both is rejected (RFC 0082 §A)', () => {
|
|
78
|
+
expect(agentRef({ agentId: 'core.x.y.z', version: '1.0.0' }), why('RFC 0082 §A', 'version-only AgentRef MUST validate')).toBe(true);
|
|
79
|
+
expect(agentRef({ agentId: 'core.x.y.z', channel: 'stable' }), why('RFC 0082 §A', 'channel-only AgentRef MUST validate')).toBe(true);
|
|
80
|
+
expect(agentRef({ agentId: 'core.x.y.z' }), why('RFC 0082 §A', 'a ref with neither version nor channel MUST validate (host default)')).toBe(true);
|
|
81
|
+
expect(agentRef({ agentId: 'core.x.y.z', version: '1.0.0', channel: 'stable' }), why('RFC 0082 §A', 'a ref with BOTH version and channel MUST be rejected')).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
describe('agent-deployment-shape: deployment.* event payloads (RFC 0082, server-free)', () => {
|
|
86
|
+
const payloads = loadSchema('run-event-payloads.schema.json');
|
|
87
|
+
const ajv = new Ajv2020({ strict: false, allErrors: true });
|
|
88
|
+
addFormats(ajv);
|
|
89
|
+
ajv.addSchema(payloads, 'payloads');
|
|
90
|
+
|
|
91
|
+
const promoted = ajv.getSchema('payloads#/$defs/deploymentPromoted');
|
|
92
|
+
const rolledBack = ajv.getSchema('payloads#/$defs/deploymentRolledBack');
|
|
93
|
+
const canary = ajv.getSchema('payloads#/$defs/deploymentCanaryAdjusted');
|
|
94
|
+
const stateChanged = ajv.getSchema('payloads#/$defs/deploymentStateChanged');
|
|
95
|
+
|
|
96
|
+
it('deployment.promoted validates a content-free promotion record and requires toVersion + toState', () => {
|
|
97
|
+
expect(promoted, 'the deploymentPromoted $def MUST exist').toBeTruthy();
|
|
98
|
+
expect(
|
|
99
|
+
promoted!({ agentId: 'core.openwop.agents.support-resolver', toVersion: '2.4.0', toState: 'active', channel: 'stable', canaryPercent: 10, evalRunId: 'run_abc' }),
|
|
100
|
+
why('RFC 0082 §D', 'a conforming deployment.promoted payload MUST validate'),
|
|
101
|
+
).toBe(true);
|
|
102
|
+
expect(promoted!({ agentId: 'a' }), why('RFC 0082 §D', 'deployment.promoted without toVersion/toState MUST be rejected')).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('deployment.rolled-back / canary.adjusted / state.changed validate conforming records', () => {
|
|
106
|
+
expect(rolledBack!({ agentId: 'a', fromVersion: '2.4.0', toVersion: '2.3.1', rollbackPointer: '2.3.1' }), why('RFC 0082 §D', 'a conforming deployment.rolled-back MUST validate')).toBe(true);
|
|
107
|
+
expect(canary!({ agentId: 'a', version: '2.4.0', fromPercent: 10, toPercent: 50 }), why('RFC 0082 §D', 'a conforming deployment.canary.adjusted MUST validate')).toBe(true);
|
|
108
|
+
expect(stateChanged!({ agentId: 'a', version: '2.4.0', fromState: 'active', toState: 'paused' }), why('RFC 0082 §D', 'a conforming deployment.state.changed MUST validate')).toBe(true);
|
|
109
|
+
expect(stateChanged!({ agentId: 'a', version: '2.4.0', fromState: 'active', toState: 'live' }), why('RFC 0082 §D', 'an out-of-enum toState MUST be rejected')).toBe(false);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('deployment.* events are content-free — a manifest body and a prompt are rejected (deployment-event-no-content-leak)', () => {
|
|
113
|
+
expect(
|
|
114
|
+
promoted!({ agentId: 'a', toVersion: '2.4.0', toState: 'active', manifestBody: '{...}' }),
|
|
115
|
+
why('SECURITY invariant deployment-event-no-content-leak', 'a deployment.promoted MUST NOT carry a manifest body'),
|
|
116
|
+
).toBe(false);
|
|
117
|
+
expect(
|
|
118
|
+
stateChanged!({ agentId: 'a', version: '2.4.0', fromState: 'active', toState: 'paused', prompt: 'system: …' }),
|
|
119
|
+
why('SECURITY invariant deployment-event-no-content-leak', 'a deployment.state.changed MUST NOT carry prompt content'),
|
|
120
|
+
).toBe(false);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe('agent-deployment-shape: §B recorded-fact pin + enum (RFC 0082, server-free)', () => {
|
|
125
|
+
it('agent.invocation.started carries the additive recorded-fact resolvedAgentVersion / resolvedChannel', () => {
|
|
126
|
+
const payloads = loadSchema('run-event-payloads.schema.json');
|
|
127
|
+
const started = ((payloads.$defs as Record<string, { properties?: Record<string, unknown> }>).agentInvocationStarted)?.properties ?? {};
|
|
128
|
+
expect(started.resolvedAgentVersion, why('RFC 0082 §B', 'agent.invocation.started.resolvedAgentVersion MUST be declared (the channel pin)')).toBeDefined();
|
|
129
|
+
expect(started.resolvedChannel, why('RFC 0082 §B', 'agent.invocation.started.resolvedChannel MUST be declared')).toBeDefined();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('all four deployment event names appear in the RunEventType enum', () => {
|
|
133
|
+
const runEvent = loadSchema('run-event.schema.json');
|
|
134
|
+
const enumVals = (runEvent.$defs as Record<string, { enum?: string[] }>).RunEventType?.enum ?? [];
|
|
135
|
+
for (const e of ['deployment.promoted', 'deployment.rolled-back', 'deployment.canary.adjusted', 'deployment.state.changed']) {
|
|
136
|
+
expect(enumVals).toContain(e);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
});
|