@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,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workspace-cross-tenant-isolation — RFC 0059 §E WCT-1.
|
|
3
|
+
*
|
|
4
|
+
* Status: ACTIVE (advertisement + behavioral). Public test for the
|
|
5
|
+
* `workspace-cross-tenant-isolation` SECURITY invariant: a workspace file
|
|
6
|
+
* owned by `{tenant, workspace}` MUST NOT be readable (get or list) under a
|
|
7
|
+
* different `{tenant′, workspace′}`, regardless of the caller's permissions
|
|
8
|
+
* elsewhere. Mirrors `kv-cross-tenant-isolation` / `agent-memory-cti-1`.
|
|
9
|
+
*
|
|
10
|
+
* The two owners are driven through the documented test seam
|
|
11
|
+
* `POST /v1/host/sample/workspace/op` (host-sample-test-seams.md §9), which
|
|
12
|
+
* lets a single-credential host exercise distinct owners. Hosts without the
|
|
13
|
+
* seam soft-skip the behavioral probe; the advertisement-shape assertion still
|
|
14
|
+
* runs whenever `capabilities.workspace.supported` is advertised.
|
|
15
|
+
*
|
|
16
|
+
* @see RFCS/0059-agent-workspace.md §E WCT-1
|
|
17
|
+
* @see spec/v1/agent-workspace.md §"§E — Invariants"
|
|
18
|
+
* @see SECURITY/invariants.yaml workspace-cross-tenant-isolation
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, it, expect } from 'vitest';
|
|
22
|
+
import { driver } from '../lib/driver.js';
|
|
23
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
24
|
+
|
|
25
|
+
interface DiscoveryDoc {
|
|
26
|
+
capabilities?: { workspace?: { supported?: boolean } };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function workspaceSupported(): Promise<boolean> {
|
|
30
|
+
const res = await driver.get('/.well-known/openwop');
|
|
31
|
+
const body = res.json as DiscoveryDoc | undefined;
|
|
32
|
+
return capabilityFamily(body, 'workspace')?.supported === true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function seam(args: Record<string, unknown>) {
|
|
36
|
+
return driver.post('/v1/host/sample/workspace/op', args);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const SEAM_PATH = 'WCT1-SECRET.md';
|
|
40
|
+
|
|
41
|
+
describe('workspace-cross-tenant-isolation: a workspace file MUST NOT leak across owners (RFC 0059 §E WCT-1)', () => {
|
|
42
|
+
it('a file written under {tenant A, workspace A} is not readable under a different owner', async () => {
|
|
43
|
+
if (!(await workspaceSupported())) return; // capability not advertised — skip
|
|
44
|
+
|
|
45
|
+
// Owner A writes a file.
|
|
46
|
+
const put = await seam({ tenant: 'wct1-tenant-a', workspace: 'ws-a', op: 'put', path: SEAM_PATH, content: 'A-only secret body' });
|
|
47
|
+
if (put.status === 404) return; // seam unwired — soft-skip the behavioral probe
|
|
48
|
+
expect(put.status, driver.describe('agent-workspace.md §C PUT', 'seam put MUST succeed for the owning workspace')).toBe(200);
|
|
49
|
+
|
|
50
|
+
// A DIFFERENT workspace (same tenant) MUST NOT read it.
|
|
51
|
+
const crossWs = await seam({ tenant: 'wct1-tenant-a', workspace: 'ws-b', op: 'get', path: SEAM_PATH });
|
|
52
|
+
expect(
|
|
53
|
+
crossWs.status === 404 || crossWs.status === 403,
|
|
54
|
+
driver.describe('agent-workspace.md §E WCT-1', `a cross-workspace get MUST fail closed (404/403, no existence leak), got ${crossWs.status}`),
|
|
55
|
+
).toBe(true);
|
|
56
|
+
const crossWsBody = JSON.stringify(crossWs.json ?? '');
|
|
57
|
+
expect(
|
|
58
|
+
!crossWsBody.includes('A-only secret body'),
|
|
59
|
+
driver.describe('agent-workspace.md §E WCT-1', 'a cross-workspace read MUST NOT surface the other owner\'s content'),
|
|
60
|
+
).toBe(true);
|
|
61
|
+
|
|
62
|
+
// A DIFFERENT tenant MUST NOT read it either.
|
|
63
|
+
const crossTenant = await seam({ tenant: 'wct1-tenant-b', workspace: 'ws-a', op: 'get', path: SEAM_PATH });
|
|
64
|
+
expect(
|
|
65
|
+
crossTenant.status === 404 || crossTenant.status === 403,
|
|
66
|
+
driver.describe('agent-workspace.md §E WCT-1', `a cross-tenant get MUST fail closed, got ${crossTenant.status}`),
|
|
67
|
+
).toBe(true);
|
|
68
|
+
|
|
69
|
+
// And list MUST NOT enumerate the other owner's path.
|
|
70
|
+
const crossList = await seam({ tenant: 'wct1-tenant-a', workspace: 'ws-b', op: 'list' });
|
|
71
|
+
const listed = JSON.stringify((crossList.json as { files?: unknown })?.files ?? []);
|
|
72
|
+
expect(
|
|
73
|
+
!listed.includes(SEAM_PATH),
|
|
74
|
+
driver.describe('agent-workspace.md §E WCT-1', 'a cross-workspace list MUST NOT enumerate another owner\'s file'),
|
|
75
|
+
).toBe(true);
|
|
76
|
+
|
|
77
|
+
// Sanity: the owner itself still reads its file (isolation, not loss).
|
|
78
|
+
const ownerRead = await seam({ tenant: 'wct1-tenant-a', workspace: 'ws-a', op: 'get', path: SEAM_PATH });
|
|
79
|
+
expect(
|
|
80
|
+
(ownerRead.json as { content?: string } | undefined)?.content,
|
|
81
|
+
driver.describe('agent-workspace.md §C GET', 'the owning workspace MUST still read its own file'),
|
|
82
|
+
).toBe('A-only secret body');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `x-openwop-form` vendor-extension shape validation — `node-packs.md`
|
|
3
|
+
* §"`x-openwop-form` UX hints" (RFC 0066).
|
|
4
|
+
*
|
|
5
|
+
* Server-free, no host-advertisement gate: `x-openwop-form` is a
|
|
6
|
+
* CONSUMER-SIDE rendering hint that a pack author places on `configSchema`
|
|
7
|
+
* properties. Hosts do not advertise it; the contract is purely the shape a
|
|
8
|
+
* pack author targets. This scenario asserts the two shape-level guarantees
|
|
9
|
+
* the RFC's §Conformance pins:
|
|
10
|
+
*
|
|
11
|
+
* 1. A pack `configSchema` carrying `x-openwop-form` annotations remains a
|
|
12
|
+
* VALID JSON Schema 2020-12 document — the annotation is an ignored
|
|
13
|
+
* vendor keyword, so a schema validator (Ajv2020) compiles it without
|
|
14
|
+
* error. This is the load-bearing additive promise: a consumer that
|
|
15
|
+
* doesn't understand the keyword still validates config against the
|
|
16
|
+
* schema unchanged.
|
|
17
|
+
* 2. The annotation OBJECT itself matches the §A vocabulary shape: `kind`
|
|
18
|
+
* is REQUIRED and a string; the documented sub-fields (`dependsOn`,
|
|
19
|
+
* `promptKind`, `provider`, `credentialProvider`) are optional strings.
|
|
20
|
+
*
|
|
21
|
+
* Forward-compat (RFC §A + §B, both `MUST`): an UNKNOWN `kind` value is still
|
|
22
|
+
* a structurally valid annotation — a renderer MUST treat it as if the hint
|
|
23
|
+
* were absent rather than reject it. So the shape schema accepts any string
|
|
24
|
+
* `kind`, NOT a closed enum; the four reserved kinds
|
|
25
|
+
* (`prompt-picker`/`provider-picker`/`model-picker`/`credential-picker`) are
|
|
26
|
+
* a renderer-routing vocabulary, not a validation constraint.
|
|
27
|
+
*
|
|
28
|
+
* NOTE: the renderer behavior matrix (which picker each `kind` produces, the
|
|
29
|
+
* `dependsOn` sibling-resolution + graceful fallback) is a reference-FRONTEND
|
|
30
|
+
* concern unit-tested in the workflow-engine sample, NOT a protocol wire
|
|
31
|
+
* shape — it is intentionally out of scope for this server-free scenario.
|
|
32
|
+
*
|
|
33
|
+
* @see spec/v1/node-packs.md §"`x-openwop-form` UX hints"
|
|
34
|
+
* @see RFCS/0066-x-openwop-form-vendor-extension.md
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
import { describe, it, expect } from 'vitest';
|
|
38
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
39
|
+
import addFormats from 'ajv-formats';
|
|
40
|
+
import type { ErrorObject } from 'ajv';
|
|
41
|
+
|
|
42
|
+
/** The §A `x-openwop-form` annotation shape. `kind` is the only required
|
|
43
|
+
* field; it is an OPEN string (unknown kinds are valid per the forward-compat
|
|
44
|
+
* MUST), with the documented sub-fields typed as optional strings. */
|
|
45
|
+
const X_OPENWOP_FORM_SHAPE = {
|
|
46
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
47
|
+
type: 'object',
|
|
48
|
+
required: ['kind'],
|
|
49
|
+
properties: {
|
|
50
|
+
kind: { type: 'string' },
|
|
51
|
+
dependsOn: { type: 'string' },
|
|
52
|
+
promptKind: { type: 'string' },
|
|
53
|
+
provider: { type: 'string' },
|
|
54
|
+
credentialProvider: { type: 'string' },
|
|
55
|
+
},
|
|
56
|
+
additionalProperties: false,
|
|
57
|
+
} as const;
|
|
58
|
+
|
|
59
|
+
/** A pack `configSchema` annotated for picker UX — the RFC §A positive
|
|
60
|
+
* example (`core.ai.chatCompletion`-style): provider/model/credential/prompt
|
|
61
|
+
* pickers with a `dependsOn` cascade. */
|
|
62
|
+
function annotatedConfigSchema() {
|
|
63
|
+
return {
|
|
64
|
+
$schema: 'https://json-schema.org/draft/2020-12/schema',
|
|
65
|
+
type: 'object',
|
|
66
|
+
properties: {
|
|
67
|
+
provider: {
|
|
68
|
+
type: 'string',
|
|
69
|
+
'x-openwop-form': { kind: 'provider-picker' },
|
|
70
|
+
},
|
|
71
|
+
model: {
|
|
72
|
+
type: 'string',
|
|
73
|
+
'x-openwop-form': { kind: 'model-picker', dependsOn: 'provider' },
|
|
74
|
+
},
|
|
75
|
+
credential: {
|
|
76
|
+
type: 'string',
|
|
77
|
+
'x-openwop-form': { kind: 'credential-picker', dependsOn: 'provider' },
|
|
78
|
+
},
|
|
79
|
+
systemPrompt: {
|
|
80
|
+
type: 'string',
|
|
81
|
+
'x-openwop-form': { kind: 'prompt-picker', promptKind: 'system' },
|
|
82
|
+
},
|
|
83
|
+
temperature: { type: 'number', minimum: 0, maximum: 2 },
|
|
84
|
+
},
|
|
85
|
+
additionalProperties: false,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
describe('category: x-openwop-form pack-manifest shape (RFC 0066)', () => {
|
|
90
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
91
|
+
addFormats(ajv);
|
|
92
|
+
const validateShape = ajv.compile<Record<string, unknown>>(X_OPENWOP_FORM_SHAPE);
|
|
93
|
+
|
|
94
|
+
const shapeFailsWith = (annotation: unknown, keyword: string): ErrorObject[] => {
|
|
95
|
+
const ok = validateShape(annotation);
|
|
96
|
+
expect(ok).toBe(false);
|
|
97
|
+
return (validateShape.errors ?? []).filter((e) => e.keyword === keyword);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
it('positive: a configSchema carrying x-openwop-form remains a valid JSON Schema 2020-12 document', () => {
|
|
101
|
+
// The annotation is an ignored vendor keyword — compiling MUST NOT throw,
|
|
102
|
+
// and the schema MUST still validate/reject instance config normally.
|
|
103
|
+
let validateConfig: ReturnType<typeof ajv.compile> | undefined;
|
|
104
|
+
expect(() => {
|
|
105
|
+
validateConfig = ajv.compile(annotatedConfigSchema());
|
|
106
|
+
}, 'node-packs.md §x-openwop-form: an annotated configSchema MUST remain a valid 2020-12 schema').not.toThrow();
|
|
107
|
+
// The annotations do not alter schema semantics: valid config passes…
|
|
108
|
+
expect(
|
|
109
|
+
validateConfig!({ provider: 'anthropic', model: 'claude', temperature: 1 }),
|
|
110
|
+
'x-openwop-form is advisory — it MUST NOT change what the schema accepts',
|
|
111
|
+
).toBe(true);
|
|
112
|
+
// …and a type violation on an annotated field still rejects.
|
|
113
|
+
expect(validateConfig!({ provider: 123 })).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('positive: each documented x-openwop-form annotation matches the §A shape', () => {
|
|
117
|
+
const cfg = annotatedConfigSchema();
|
|
118
|
+
for (const [name, prop] of Object.entries(cfg.properties)) {
|
|
119
|
+
const ann = (prop as Record<string, unknown>)['x-openwop-form'];
|
|
120
|
+
if (ann === undefined) continue;
|
|
121
|
+
expect(
|
|
122
|
+
validateShape(ann),
|
|
123
|
+
`node-packs.md §x-openwop-form: the annotation on "${name}" MUST match the §A shape. Errors: ${JSON.stringify(validateShape.errors)}`,
|
|
124
|
+
).toBe(true);
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('forward-compat: an unknown kind is a structurally valid annotation (renderer falls back per the §A MUST)', () => {
|
|
129
|
+
expect(
|
|
130
|
+
validateShape({ kind: 'unknown-future-picker' }),
|
|
131
|
+
'node-packs.md §x-openwop-form: an unknown kind MUST validate (kind is an open string, not a closed enum) so future vocabulary is forward-compatible',
|
|
132
|
+
).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('negative: an annotation missing kind is rejected', () => {
|
|
136
|
+
expect(
|
|
137
|
+
shapeFailsWith({ dependsOn: 'provider' }, 'required').length,
|
|
138
|
+
'node-packs.md §x-openwop-form: kind is the one REQUIRED sub-field',
|
|
139
|
+
).toBeGreaterThan(0);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('negative: a non-string kind is rejected', () => {
|
|
143
|
+
expect(
|
|
144
|
+
shapeFailsWith({ kind: 42 }, 'type').length,
|
|
145
|
+
'node-packs.md §x-openwop-form: kind MUST be a string',
|
|
146
|
+
).toBeGreaterThan(0);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('negative: a non-string dependsOn is rejected', () => {
|
|
150
|
+
expect(
|
|
151
|
+
shapeFailsWith({ kind: 'model-picker', dependsOn: ['provider'] }, 'type').length,
|
|
152
|
+
'node-packs.md §x-openwop-form: dependsOn names a sibling property — it MUST be a string',
|
|
153
|
+
).toBeGreaterThan(0);
|
|
154
|
+
});
|
|
155
|
+
});
|