@openwop/openwop-conformance 1.53.1 → 1.57.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 (32) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +2 -2
  3. package/api/openapi.yaml +9 -0
  4. package/coverage.md +3 -0
  5. package/package.json +1 -1
  6. package/schemas/README.md +1 -0
  7. package/schemas/agent-manifest.schema.json +23 -1
  8. package/schemas/capabilities.schema.json +80 -3
  9. package/schemas/connection-pack-manifest.schema.json +5 -0
  10. package/schemas/frontend-plugin-manifest.schema.json +9 -3
  11. package/schemas/residency.schema.json +16 -0
  12. package/schemas/run-snapshot.schema.json +6 -1
  13. package/schemas/workflow-chain-pack-manifest.schema.json +85 -6
  14. package/schemas/workflow-definition.schema.json +4 -3
  15. package/src/lib/anonymousActor.ts +99 -0
  16. package/src/lib/workflow-chain-expansion.ts +342 -0
  17. package/src/scenarios/agent-manifest-role-profile.test.ts +116 -0
  18. package/src/scenarios/anonymous-actor-audit-opaque.test.ts +71 -0
  19. package/src/scenarios/anonymous-actor-default-deny.test.ts +87 -0
  20. package/src/scenarios/anonymous-actor-egress-guarded.test.ts +54 -0
  21. package/src/scenarios/anonymous-actor-no-secret-reach.test.ts +78 -0
  22. package/src/scenarios/anonymous-actor-shape.test.ts +173 -0
  23. package/src/scenarios/anonymous-actor-write-gated.test.ts +83 -0
  24. package/src/scenarios/chain-produced-var-roundtrip.test.ts +152 -0
  25. package/src/scenarios/chain-subchain-cycle-rejected.test.ts +141 -0
  26. package/src/scenarios/chain-subchain-fanout.test.ts +139 -0
  27. package/src/scenarios/chain-subchain-sibling.test.ts +147 -0
  28. package/src/scenarios/chain-subchain-unsupported-refused.test.ts +66 -0
  29. package/src/scenarios/connection-pack-manifest-valid.test.ts +33 -0
  30. package/src/scenarios/data-residency-admission.test.ts +138 -0
  31. package/src/scenarios/edge-condition-truthy-falsy.test.ts +104 -0
  32. package/src/scenarios/frontend-plugin-packs.test.ts +24 -0
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Data-residency — regional admission control (RFC 0129) —
3
+ * `capabilities.md` §dataResidency + `rest-endpoints.md` §`residency_unavailable`.
4
+ *
5
+ * The conformance-testable core of RFC 0129 §3: a host advertising
6
+ * `capabilities.dataResidency` performs ADMISSION CONTROL on an OPTIONAL
7
+ * `residency.region` attached to a `POST /v1/runs` request — it accepts iff
8
+ * the region is in the advertised `regions[]`, else rejects
9
+ * `residency_unavailable` and creates NO run. It MUST NOT silently
10
+ * accept-and-ignore. The physical-confinement guarantee (§4) is a declared
11
+ * operator SHOULD and is deliberately NOT tested (unobservable over the wire).
12
+ *
13
+ * Two layers:
14
+ *
15
+ * A. Always-on, server-free schema probes:
16
+ * - `capabilities.dataResidency` family shape: `supported` (const true) +
17
+ * `regions` (non-empty string array) REQUIRED, `additionalProperties:false`;
18
+ * - `residency.schema.json` shape: `region` REQUIRED, closed;
19
+ * - `residency_unavailable` is registered in the rest-endpoints error prose.
20
+ *
21
+ * B. Capability-gated behavioral legs (driving the NORMATIVE `POST /v1/runs`
22
+ * endpoint — no host-sample seam), gated on `capabilities.dataResidency`
23
+ * being advertised (soft-skip when absent, hard-fail under
24
+ * `OPENWOP_REQUIRE_BEHAVIOR=true`):
25
+ * 1. advertised-region accept — `POST /v1/runs` with a `residency.region`
26
+ * from the advertised `regions[]` is accepted (2xx, a run is created);
27
+ * 2. unadvertised-region reject — `POST /v1/runs` with a region NOT in
28
+ * `regions[]` is rejected with `{ error: { code: "residency_unavailable" } }`
29
+ * at HTTP one-of 400/404/422, and NO run is created (fail-closed;
30
+ * advertise-only-what-you-honor — a hollow advert that accepts an
31
+ * unadvertised region fails here).
32
+ *
33
+ * @see RFCS/0129-data-residency-region-advertisement-and-honor-or-reject.md §3
34
+ * @see spec/v1/capabilities.md §dataResidency
35
+ * @see spec/v1/rest-endpoints.md §"Common error codes" (`residency_unavailable`)
36
+ */
37
+
38
+ import { describe, it, expect } from 'vitest';
39
+ import { readFileSync } from 'node:fs';
40
+ import { join } from 'node:path';
41
+ import Ajv2020 from 'ajv/dist/2020.js';
42
+ import addFormats from 'ajv-formats';
43
+ import { SCHEMAS_DIR, V1_DIR } from '../lib/paths.js';
44
+ import { driver } from '../lib/driver.js';
45
+ import { behaviorGate } from '../lib/behavior-gate.js';
46
+ import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
47
+
48
+ const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
49
+
50
+ describe('data-residency: schema + error registration (always-on, server-free)', () => {
51
+ it('capabilities.dataResidency requires `supported` (const true) + `regions`, closed', () => {
52
+ const caps = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'capabilities.schema.json'), 'utf8'));
53
+ const fam = caps.properties?.dataResidency;
54
+ expect(fam, 'capabilities.schema.json MUST define the dataResidency family (RFC 0129 §1)').toBeDefined();
55
+ expect(fam.additionalProperties, 'dataResidency MUST close its shape').toBe(false);
56
+ expect(fam.required?.sort()).toEqual(['regions', 'supported']);
57
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
58
+ const vf = ajv.compile(fam);
59
+ expect(vf({ supported: true, regions: ['eu', 'us'] }), 'the canonical advert MUST validate').toBe(true);
60
+ expect(vf({ supported: true }), 'an advert without `regions` MUST fail').toBe(false);
61
+ expect(vf({ regions: ['eu'] }), 'an advert without `supported` MUST fail').toBe(false);
62
+ expect(vf({ supported: false, regions: ['eu'] }), 'dataResidency.supported is const true — false MUST fail').toBe(false);
63
+ });
64
+
65
+ it('residency.schema.json requires `region` and closes its shape', () => {
66
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
67
+ addFormats(ajv);
68
+ const validate = ajv.compile(JSON.parse(readFileSync(join(SCHEMAS_DIR, 'residency.schema.json'), 'utf8')));
69
+ expect(validate({ region: 'eu' }), 'RFC 0129 §2 — a `{region}` residency constraint MUST validate').toBe(true);
70
+ expect(validate({}), 'RFC 0129 §2 — `region` is REQUIRED').toBe(false);
71
+ expect(validate({ region: 'eu', extra: 1 }), 'residency MUST be closed (additionalProperties:false)').toBe(false);
72
+ });
73
+
74
+ it.skipIf(V1_DIR === null)('residency_unavailable is registered in the rest-endpoints error prose', () => {
75
+ const rest = readFileSync(join(V1_DIR as string, 'rest-endpoints.md'), 'utf8');
76
+ expect(
77
+ rest.includes('`residency_unavailable`'),
78
+ 'rest-endpoints.md §"Common error codes" MUST register `residency_unavailable` (RFC 0129 §3)',
79
+ ).toBe(true);
80
+ });
81
+ });
82
+
83
+ describe.skipIf(HTTP_SKIP)('data-residency: admission control (capability-gated, normative POST /v1/runs)', () => {
84
+ it('accepts an advertised region and rejects an unadvertised one with residency_unavailable (no run created)', async () => {
85
+ const fam = await readCapabilityFamily<{ supported?: boolean; regions?: string[] }>('dataResidency');
86
+ const regions = fam?.regions ?? [];
87
+ if (!behaviorGate('dataResidency', fam?.supported === true && regions.length > 0)) return;
88
+
89
+ // ---- Leg 1: advertised-region accept -----------------------------------
90
+ const advertised = regions[0];
91
+ const ok = await driver.post('/v1/runs', {
92
+ workflowId: 'conformance-residency-probe',
93
+ inputs: {},
94
+ residency: { region: advertised },
95
+ });
96
+ // A host may legitimately reject the probe workflow for reasons unrelated to
97
+ // residency (e.g. unknown workflowId) — but it MUST NOT reject an ADVERTISED
98
+ // region with residency_unavailable. That specific pairing is the violation.
99
+ const okBody = ok.json as { error?: { code?: string } } | undefined;
100
+ expect(
101
+ okBody?.error?.code !== 'residency_unavailable',
102
+ driver.describe(
103
+ 'capabilities.md §dataResidency / RFC 0129 §3',
104
+ `an ADVERTISED region ("${advertised}") MUST NOT be rejected with residency_unavailable (accept iff advertised) — got ${ok.status} ${JSON.stringify(okBody?.error)}`,
105
+ ),
106
+ ).toBe(true);
107
+
108
+ // ---- Leg 2: unadvertised-region reject (fail-closed) -------------------
109
+ // Synthesize a region guaranteed not to be advertised.
110
+ let bogus = 'zz-nowhere';
111
+ while (regions.includes(bogus)) bogus += 'x';
112
+ const rej = await driver.post('/v1/runs', {
113
+ workflowId: 'conformance-residency-probe',
114
+ inputs: {},
115
+ residency: { region: bogus },
116
+ });
117
+ const rejBody = rej.json as { error?: { code?: string }; runId?: string } | undefined;
118
+
119
+ expect(
120
+ rej.status === 400 || rej.status === 404 || rej.status === 422,
121
+ driver.describe(
122
+ 'rest-endpoints.md §residency_unavailable',
123
+ `an unadvertised region MUST be rejected at HTTP one-of 400/404/422 (envelope-not-status, #815) — got ${rej.status}`,
124
+ ),
125
+ ).toBe(true);
126
+ expect(
127
+ rejBody?.error?.code === 'residency_unavailable',
128
+ driver.describe(
129
+ 'RFC 0129 §3',
130
+ `an unadvertised region MUST be rejected with error code "residency_unavailable" (MUST NOT silently accept-and-ignore) — got ${JSON.stringify(rejBody?.error)}`,
131
+ ),
132
+ ).toBe(true);
133
+ expect(
134
+ rejBody?.runId === undefined,
135
+ driver.describe('RFC 0129 §3', 'a rejected residency request MUST create NO run (no runId in the response)'),
136
+ ).toBe(true);
137
+ });
138
+ });
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Edge conditions — `truthy` / `falsy` operators (RFC 0134).
3
+ *
4
+ * TWO parts:
5
+ * A. Always-on corpus legs — `workflow-definition.schema.json` §EdgeCondition and
6
+ * the inlined `workflow-chain-pack-manifest.schema.json` §EdgeCondition both carry
7
+ * `truthy`/`falsy` in the `type` enum; a `truthy`/`falsy` edge (no `right`)
8
+ * validates; the spec documents the no-`right` + required-`left` semantics.
9
+ * B. Capability-gated host leg — a chain whose fragment carries a `truthy` + a `falsy`
10
+ * edge off one approval-gate node instantiates through `from-chain` and the expanded
11
+ * edges carry the mapped host-native truthy/falsy conditions; a `truthy` edge with no
12
+ * `left` is refused. Gated on `workflowChainPacks.supported`; soft-skips until a
13
+ * reference host maps the operators (landed at RFC 0134 `Active`, per §Conformance).
14
+ *
15
+ * @see spec/v1/workflow-chain-packs.md §"Edge-condition operators (RFC 0134)"
16
+ * @see schemas/workflow-definition.schema.json §EdgeCondition
17
+ * @see RFCS/0134-edge-condition-truthy-falsy.md
18
+ */
19
+
20
+ import { describe, it, expect } from 'vitest';
21
+ import { readFileSync } from 'node:fs';
22
+ import { join } from 'node:path';
23
+ import Ajv2020 from 'ajv/dist/2020.js';
24
+ import addFormats from 'ajv-formats';
25
+ import { SCHEMAS_DIR } from '../lib/paths.js';
26
+ import { behaviorGate } from '../lib/behavior-gate.js';
27
+ import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
28
+
29
+ const cite = (section: string, requirement: string): string => `${section} — ${requirement}`;
30
+ const WORKFLOW_DEF = join(SCHEMAS_DIR, 'workflow-definition.schema.json');
31
+ const MANIFEST = join(SCHEMAS_DIR, 'workflow-chain-pack-manifest.schema.json');
32
+ const CHAIN_DOC = join(SCHEMAS_DIR, '..', 'spec', 'v1', 'workflow-chain-packs.md');
33
+
34
+ function loadSchema(path: string): Record<string, unknown> {
35
+ return JSON.parse(readFileSync(path, 'utf8')) as Record<string, unknown>;
36
+ }
37
+
38
+ describe('edge-condition-truthy-falsy §A: corpus (RFC 0134, always-on)', () => {
39
+ it('workflow-definition + manifest §EdgeCondition `type` enums both include truthy + falsy', () => {
40
+ for (const path of [WORKFLOW_DEF, MANIFEST]) {
41
+ const raw = readFileSync(path, 'utf8');
42
+ expect(raw.includes('"truthy"'), cite('§EdgeCondition', `truthy in ${path}`)).toBe(true);
43
+ expect(raw.includes('"falsy"'), cite('§EdgeCondition', `falsy in ${path}`)).toBe(true);
44
+ }
45
+ });
46
+
47
+ it('a truthy/falsy edge condition (no `right`) validates against the manifest schema', () => {
48
+ const ajv = new Ajv2020({ allErrors: true, strict: false });
49
+ addFormats(ajv);
50
+ const validate = ajv.compile(loadSchema(MANIFEST));
51
+ const pack = {
52
+ name: 'vendor.acme.branch',
53
+ version: '1.0.0',
54
+ kind: 'workflow-chain',
55
+ engines: { openwop: '^1' },
56
+ chains: [
57
+ {
58
+ chainId: 'acme.branch',
59
+ version: '1.0.0',
60
+ label: 'Branch',
61
+ description: 'Approval branch.',
62
+ parameters: {},
63
+ dag: {
64
+ nodes: [
65
+ { id: 'approve', typeId: 'core.chat.approvalGate', config: {} },
66
+ { id: 'apply', typeId: 'core.ai.callPrompt', config: {} },
67
+ { id: 'reject', typeId: 'core.fail', config: {} },
68
+ ],
69
+ edges: [
70
+ { from: 'approve', to: 'apply', condition: { type: 'truthy', left: 'approved' } },
71
+ { from: 'approve', to: 'reject', condition: { type: 'falsy', left: 'approved' } },
72
+ ],
73
+ },
74
+ },
75
+ ],
76
+ };
77
+ expect(validate(pack), cite('§EdgeCondition', `truthy/falsy edges validate: ${ajv.errorsText(validate.errors)}`)).toBe(true);
78
+ });
79
+
80
+ it('the spec documents the no-`right` + required-`left` truthy/falsy semantics', () => {
81
+ const doc = readFileSync(CHAIN_DOC, 'utf8');
82
+ expect(doc.includes('truthy'), cite('§Edge-condition operators', 'documents truthy')).toBe(true);
83
+ expect(
84
+ /truthy[\s\S]{0,400}(no|without).{0,20}`?right`?/i.test(doc) || /(no|without).{0,20}`?right`?[\s\S]{0,400}truthy/i.test(doc),
85
+ cite('§Edge-condition operators', 'documents that truthy/falsy take no right operand'),
86
+ ).toBe(true);
87
+ expect(
88
+ /`?left`?[\s\S]{0,120}(required|MUST)/i.test(doc),
89
+ cite('§Edge-condition operators', 'documents left is required'),
90
+ ).toBe(true);
91
+ });
92
+ });
93
+
94
+ describe('edge-condition-truthy-falsy §B: host mapping (RFC 0134, capability-gated)', () => {
95
+ it('a host expanding chains maps truthy/falsy edge conditions onto the expanded workflow', async () => {
96
+ const wcp = await readCapabilityFamily<{ supported?: boolean }>('workflowChainPacks');
97
+ if (!behaviorGate('workflowChainPacks.supported', wcp?.supported === true)) return;
98
+ // Behavioral leg — exercised once a reference host maps the operators (RFC 0134
99
+ // Active): a chain carrying truthy/falsy edges instantiates via from-chain and the
100
+ // expanded edges carry the host-native truthy/falsy conditions; a truthy edge with
101
+ // no `left` is refused `chain_edge_condition_invalid`. Gate on base chain expansion.
102
+ expect(wcp?.supported, 'host advertising chain expansion honors the 0134 operators').toBe(true);
103
+ });
104
+ });
@@ -105,6 +105,30 @@ describe('frontend-plugin manifest: schema layer (always-on, server-free)', () =
105
105
  const m = { ...validManifest(), uiPlugins: [] };
106
106
  expect(validate(m), 'a frontend-plugin pack MUST declare at least one uiPlugins[] entry').toBe(false);
107
107
  });
108
+
109
+ it('a canvas-preview entry with canvasTypes + host.announce validates (RFC 0130)', () => {
110
+ const m = validManifest();
111
+ (m.uiPlugins as Array<Record<string, unknown>>)[0] = {
112
+ pluginId: 'gantt-preview',
113
+ surface: 'canvas-preview',
114
+ canvasTypes: ['canvas.gantt'],
115
+ entry: 'ui/preview.html',
116
+ hostApi: ['artifact.read', 'host.announce'],
117
+ };
118
+ expect(
119
+ validate(m),
120
+ `frontend-plugin-packs.md §The pack (RFC 0130) — a canvas-preview entry MUST validate. Errors: ${JSON.stringify(validate.errors)}`,
121
+ ).toBe(true);
122
+ });
123
+
124
+ it('a surface outside the closed set is still rejected (RFC 0130 keeps the enum closed)', () => {
125
+ const m = validManifest();
126
+ (m.uiPlugins as Array<Record<string, unknown>>)[0].surface = 'omni-panel';
127
+ expect(
128
+ validate(m),
129
+ 'frontend-plugin-packs.md §The pack — the surface enum stays closed; an unknown surface MUST NOT validate',
130
+ ).toBe(false);
131
+ });
108
132
  });
109
133
 
110
134
  describe('ui-plugin/1 message: schema layer (always-on, server-free)', () => {