@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.
- package/CHANGELOG.md +20 -0
- package/README.md +2 -2
- package/api/openapi.yaml +9 -0
- package/coverage.md +3 -0
- package/package.json +1 -1
- package/schemas/README.md +1 -0
- package/schemas/agent-manifest.schema.json +23 -1
- package/schemas/capabilities.schema.json +80 -3
- package/schemas/connection-pack-manifest.schema.json +5 -0
- package/schemas/frontend-plugin-manifest.schema.json +9 -3
- package/schemas/residency.schema.json +16 -0
- package/schemas/run-snapshot.schema.json +6 -1
- package/schemas/workflow-chain-pack-manifest.schema.json +85 -6
- package/schemas/workflow-definition.schema.json +4 -3
- package/src/lib/anonymousActor.ts +99 -0
- package/src/lib/workflow-chain-expansion.ts +342 -0
- package/src/scenarios/agent-manifest-role-profile.test.ts +116 -0
- package/src/scenarios/anonymous-actor-audit-opaque.test.ts +71 -0
- package/src/scenarios/anonymous-actor-default-deny.test.ts +87 -0
- package/src/scenarios/anonymous-actor-egress-guarded.test.ts +54 -0
- package/src/scenarios/anonymous-actor-no-secret-reach.test.ts +78 -0
- package/src/scenarios/anonymous-actor-shape.test.ts +173 -0
- package/src/scenarios/anonymous-actor-write-gated.test.ts +83 -0
- package/src/scenarios/chain-produced-var-roundtrip.test.ts +152 -0
- package/src/scenarios/chain-subchain-cycle-rejected.test.ts +141 -0
- package/src/scenarios/chain-subchain-fanout.test.ts +139 -0
- package/src/scenarios/chain-subchain-sibling.test.ts +147 -0
- package/src/scenarios/chain-subchain-unsupported-refused.test.ts +66 -0
- package/src/scenarios/connection-pack-manifest-valid.test.ts +33 -0
- package/src/scenarios/data-residency-admission.test.ts +138 -0
- package/src/scenarios/edge-condition-truthy-falsy.test.ts +104 -0
- 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)', () => {
|