@openwop/openwop-conformance 1.10.0 → 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.
Files changed (55) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +2 -2
  3. package/api/asyncapi.yaml +70 -0
  4. package/api/openapi.yaml +268 -1
  5. package/coverage.md +30 -2
  6. package/fixtures/oauth-providers/synthetic.json +38 -0
  7. package/fixtures.md +10 -0
  8. package/package.json +1 -1
  9. package/schemas/README.md +12 -0
  10. package/schemas/agent-deployment-transition.schema.json +49 -0
  11. package/schemas/agent-deployment.schema.json +54 -0
  12. package/schemas/agent-eval-suite.schema.json +140 -0
  13. package/schemas/agent-inventory-response.schema.json +25 -0
  14. package/schemas/agent-manifest.schema.json +5 -0
  15. package/schemas/agent-org-chart.schema.json +82 -0
  16. package/schemas/agent-ref.schema.json +12 -2
  17. package/schemas/agent-roster-entry.schema.json +81 -0
  18. package/schemas/agent-roster-response.schema.json +21 -0
  19. package/schemas/budget-policy.schema.json +18 -0
  20. package/schemas/capabilities.schema.json +277 -0
  21. package/schemas/credential-provenance.schema.json +18 -0
  22. package/schemas/eval-summary.schema.json +92 -0
  23. package/schemas/node-pack-manifest.schema.json +17 -0
  24. package/schemas/org-chart-responsibility-view.schema.json +26 -0
  25. package/schemas/run-event-payloads.schema.json +286 -3
  26. package/schemas/run-event.schema.json +19 -0
  27. package/schemas/tool-descriptor.schema.json +63 -0
  28. package/schemas/trigger-subscription.schema.json +26 -0
  29. package/src/lib/agentRoster.ts +76 -0
  30. package/src/lib/liveRuntime.ts +59 -0
  31. package/src/lib/profiles.ts +157 -0
  32. package/src/lib/runtimeRequires.ts +38 -0
  33. package/src/lib/safeFetch.ts +87 -0
  34. package/src/scenarios/agent-deployment-shape.test.ts +139 -0
  35. package/src/scenarios/agent-eval-suite-shape.test.ts +167 -0
  36. package/src/scenarios/agent-live-allowlist-enforced.test.ts +53 -0
  37. package/src/scenarios/agent-live-invocation-bracket.test.ts +98 -0
  38. package/src/scenarios/agent-live-runtime-shape.test.ts +98 -0
  39. package/src/scenarios/agent-live-structured-output.test.ts +58 -0
  40. package/src/scenarios/agent-org-chart-shape.test.ts +127 -0
  41. package/src/scenarios/agent-platform-profile.test.ts +158 -0
  42. package/src/scenarios/agent-roster-attribution.test.ts +179 -0
  43. package/src/scenarios/agent-roster-shape.test.ts +146 -0
  44. package/src/scenarios/budget-policy-shape.test.ts +136 -0
  45. package/src/scenarios/egress-provenance-shape.test.ts +137 -0
  46. package/src/scenarios/memory-capability-model-shape.test.ts +186 -0
  47. package/src/scenarios/oauth-authorization-code-roundtrip.test.ts +145 -0
  48. package/src/scenarios/runtime-requires-install-gate.test.ts +92 -0
  49. package/src/scenarios/runtime-requires-shape.test.ts +134 -0
  50. package/src/scenarios/safefetch-behavior.test.ts +99 -0
  51. package/src/scenarios/safefetch-live-audit.test.ts +175 -0
  52. package/src/scenarios/spec-corpus-validity.test.ts +19 -3
  53. package/src/scenarios/tool-descriptor-shape.test.ts +133 -0
  54. package/src/scenarios/trigger-bridge-shape.test.ts +135 -0
  55. package/src/scenarios/x-openwop-form-pack-manifest.test.ts +155 -0
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Durable trigger + channel bridge — subscription + events + profile shapes (RFC 0083).
3
+ *
4
+ * Always-on, server-free schema-shape probe. Verifies that:
5
+ * - `trigger-subscription.schema.json` round-trips a conforming
6
+ * `TriggerSubscription` and rejects the malformed (missing REQUIRED `state`;
7
+ * an out-of-enum `source`; an unknown property under
8
+ * `additionalProperties:false`).
9
+ * - the four-state vocabulary (`active`/`paused`/`failed`/`dead-lettered`) is
10
+ * stable on the subscription `state` + the event `fromState`/`toState`.
11
+ * - the `trigger.subscription.state.changed` + `trigger.delivery.attempted`
12
+ * payload $defs validate conforming content-free records and reject malformed
13
+ * ones (a missing `outcome`; an out-of-enum `outcome`), and both event names
14
+ * appear in the RunEventType enum.
15
+ * - `capabilities.triggerBridge` (+ `webhooks.durable`) is declared.
16
+ * - `deriveProfiles` surfaces `openwop-trigger-bridge` for a host advertising
17
+ * the bridge + a dead-letter sink + a durable source, and withholds it when
18
+ * the dead-letter sink is absent (the §D predicate's OR + sink requirement).
19
+ *
20
+ * Behavioral assertions (the dedup → retry → dead-letter → causation delivery
21
+ * loop) are gated on the `openwop-trigger-bridge` profile and land in
22
+ * `trigger-bridge-delivery.test.ts` (deferred per RFC 0083 §Conformance —
23
+ * reference host deferred). This scenario asserts the wire contract, not host
24
+ * behavior.
25
+ *
26
+ * Spec references:
27
+ * - https://github.com/openwop/openwop/blob/main/spec/v1/trigger-bridge.md
28
+ * - https://github.com/openwop/openwop/blob/main/spec/v1/profiles.md (§`openwop-trigger-bridge`)
29
+ * - https://github.com/openwop/openwop/blob/main/RFCS/0083-durable-trigger-and-channel-bridge-profile.md
30
+ */
31
+
32
+ import { describe, it, expect } from 'vitest';
33
+ import { readFileSync } from 'node:fs';
34
+ import { join } from 'node:path';
35
+ import Ajv2020 from 'ajv/dist/2020.js';
36
+ import addFormats from 'ajv-formats';
37
+ import { SCHEMAS_DIR } from '../lib/paths.js';
38
+ import { deriveProfiles } from '../lib/profiles.js';
39
+
40
+ const why = (specRef: string, requirement: string): string => `${specRef} — ${requirement}`;
41
+
42
+ function loadSchema(name: string): Record<string, unknown> {
43
+ return JSON.parse(readFileSync(join(SCHEMAS_DIR, name), 'utf8')) as Record<string, unknown>;
44
+ }
45
+
46
+ const STATES = ['active', 'paused', 'failed', 'dead-lettered'] as const;
47
+
48
+ describe('trigger-bridge-shape: TriggerSubscription (RFC 0083 §B, server-free)', () => {
49
+ const ajv = addFormats(new Ajv2020({ strict: false }));
50
+ const sub = loadSchema('trigger-subscription.schema.json');
51
+ const validate = ajv.compile(sub);
52
+
53
+ it('a conforming subscription validates', () => {
54
+ expect(
55
+ validate({ subscriptionId: 'sub-1', source: 'webhook', state: 'active', dedupEnabled: true, retryPolicy: { maxAttempts: 8, backoff: 'exponential' }, webhookId: 'wh-1', secretFingerprint: 'fp-abc' }),
56
+ why('trigger-bridge.md §B', 'a conforming TriggerSubscription MUST validate'),
57
+ ).toBe(true);
58
+ });
59
+
60
+ it('rejects a missing REQUIRED state, an out-of-enum source, and an unknown property', () => {
61
+ expect(validate({ subscriptionId: 's', source: 'webhook' }), why('trigger-bridge.md §B', 'state is REQUIRED')).toBe(false);
62
+ expect(validate({ subscriptionId: 's', source: 'carrier-pigeon', state: 'active' }), why('trigger-bridge.md §B', 'source is a closed enum')).toBe(false);
63
+ expect(validate({ subscriptionId: 's', source: 'webhook', state: 'active', body: 'inbound' }), why('trigger-bridge.md §B', 'TriggerSubscription MUST be additionalProperties:false')).toBe(false);
64
+ });
65
+
66
+ it('secretFingerprint MUST be bounded — a full 64-hex (unsalted-hash-smelling) digest is rejected', () => {
67
+ const truncated = 'a1b2c3d4e5f6a7b8'; // 16 hex — a truncated host-keyed fingerprint
68
+ const fullDigest = 'a'.repeat(64); // 64 hex — smells like an unsalted SHA256(secret)
69
+ expect(validate({ subscriptionId: 's', source: 'webhook', state: 'active', secretFingerprint: truncated }), why('trigger-bridge.md §B', 'a truncated fingerprint MUST validate')).toBe(true);
70
+ expect(validate({ subscriptionId: 's', source: 'webhook', state: 'active', secretFingerprint: fullDigest }), why('SR-1', 'a full 64-hex digest MUST be rejected (brute-force oracle)')).toBe(false);
71
+ });
72
+
73
+ it('the state enum is exactly the four §B states', () => {
74
+ const stateEnum = ((sub.properties as Record<string, { enum?: string[] }>).state?.enum) ?? [];
75
+ expect([...stateEnum].sort(), why('trigger-bridge.md §B', 'the four-state vocabulary MUST be stable')).toEqual([...STATES].sort());
76
+ });
77
+ });
78
+
79
+ describe('trigger-bridge-shape: trigger.* events (RFC 0083 §C, server-free)', () => {
80
+ const payloads = loadSchema('run-event-payloads.schema.json');
81
+ const ajv = addFormats(new Ajv2020({ strict: false }));
82
+ const compile = (defName: string) => ajv.compile({
83
+ $schema: 'https://json-schema.org/draft/2020-12/schema',
84
+ $defs: (payloads as { $defs: Record<string, unknown> }).$defs,
85
+ $ref: `#/$defs/${defName}`,
86
+ } as Record<string, unknown>);
87
+
88
+ it('trigger.subscription.state.changed validates + reason is a CLOSED enum (no URL-bearing free text)', () => {
89
+ const v = compile('triggerSubscriptionStateChanged');
90
+ expect(v({ subscriptionId: 's', source: 'webhook', fromState: 'active', toState: 'dead-lettered', reason: 'retry-exhausted' }), why('trigger-bridge.md §C', 'state-changed MUST validate')).toBe(true);
91
+ expect(v({ subscriptionId: 's', source: 'webhook', fromState: 'active' }), why('trigger-bridge.md §C', 'toState is REQUIRED')).toBe(false);
92
+ expect(v({ subscriptionId: 's', source: 'webhook', fromState: 'active', toState: 'failed', reason: 'https://attacker.example/leak?token=sk' }), why('SR-1', 'a free-form / URL-bearing reason MUST be rejected')).toBe(false);
93
+ });
94
+
95
+ it('trigger.delivery.attempted validates + enforces the outcome enum', () => {
96
+ const v = compile('triggerDeliveryAttempted');
97
+ expect(v({ subscriptionId: 's', dedupKey: 'evt-9f3', attempt: 1, outcome: 'delivered', runId: 'run_x' }), why('trigger-bridge.md §C', 'delivery-attempted MUST validate')).toBe(true);
98
+ expect(v({ subscriptionId: 's', dedupKey: 'evt-9f3', attempt: 1 }), why('trigger-bridge.md §C', 'outcome is REQUIRED')).toBe(false);
99
+ expect(v({ subscriptionId: 's', dedupKey: 'evt-9f3', attempt: 1, outcome: 'exploded' }), why('trigger-bridge.md §C', 'outcome is a closed enum')).toBe(false);
100
+ });
101
+
102
+ it('both trigger event names appear in the RunEventType enum', () => {
103
+ const runEvent = loadSchema('run-event.schema.json');
104
+ const enumVals = ((runEvent.$defs as Record<string, { enum?: string[] }>).RunEventType?.enum) ?? [];
105
+ for (const name of ['trigger.subscription.state.changed', 'trigger.delivery.attempted']) {
106
+ expect(enumVals.includes(name), why('run-event.schema.json', `${name} MUST be in the RunEventType enum`)).toBe(true);
107
+ }
108
+ });
109
+ });
110
+
111
+ describe('trigger-bridge-shape: capability + profile derivation (RFC 0083 §A/§D, server-free)', () => {
112
+ it('capabilities.triggerBridge + webhooks.durable are declared', () => {
113
+ const caps = loadSchema('capabilities.schema.json');
114
+ const props = caps.properties as Record<string, { properties?: Record<string, unknown> }>;
115
+ expect(props.triggerBridge?.properties?.supported, why('trigger-bridge.md §A', 'triggerBridge.supported MUST be declared')).toBeDefined();
116
+ expect(props.webhooks?.properties?.durable, why('trigger-bridge.md §A', 'webhooks.durable MUST be declared')).toBeDefined();
117
+ });
118
+
119
+ const coreBase = {
120
+ protocolVersion: '1.0',
121
+ supportedEnvelopes: ['clarification.request'],
122
+ schemaVersions: {},
123
+ limits: { clarificationRounds: 1, schemaRounds: 1, envelopesPerTurn: 1 },
124
+ };
125
+
126
+ it('deriveProfiles surfaces openwop-trigger-bridge for bridge + deadLetter + a durable source', () => {
127
+ const c = { ...coreBase, triggerBridge: { supported: true }, deadLetter: { supported: true }, queueBus: { supported: true } } as Record<string, unknown>;
128
+ expect(deriveProfiles(c).includes('openwop-trigger-bridge'), why('profiles.md §openwop-trigger-bridge', 'bridge + sink + durable source MUST derive the profile')).toBe(true);
129
+ });
130
+
131
+ it('deriveProfiles withholds openwop-trigger-bridge when the dead-letter sink is absent', () => {
132
+ const c = { ...coreBase, triggerBridge: { supported: true }, webhooks: { durable: true } } as Record<string, unknown>;
133
+ expect(deriveProfiles(c).includes('openwop-trigger-bridge'), why('profiles.md §openwop-trigger-bridge', 'no deadLetter sink ⇒ MUST NOT derive the profile')).toBe(false);
134
+ });
135
+ });
@@ -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
+ });