@openwop/openwop-conformance 1.58.0 → 1.64.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/README.md +2 -2
- package/coverage.md +24 -0
- package/package.json +1 -1
- package/schemas/README.md +1 -0
- package/schemas/artifact-type-pack-manifest.schema.json +108 -24
- package/schemas/chat-card-pack-manifest.schema.json +125 -30
- package/schemas/connection-pack-manifest.schema.json +211 -38
- package/schemas/form-content-pack-manifest.schema.json +243 -0
- package/schemas/frontend-plugin-manifest.schema.json +10 -0
- package/schemas/node-pack-manifest.schema.json +311 -68
- package/schemas/prompt-pack-manifest.schema.json +48 -11
- package/schemas/registry-version-manifest.schema.json +16 -5
- package/schemas/workflow-chain-pack-manifest.schema.json +176 -40
- package/src/lib/artifactTypes.ts +76 -3
- package/src/lib/behavior-gate.ts +17 -0
- package/src/lib/cardPacks.ts +10 -3
- package/src/lib/formContentPacks.ts +133 -0
- package/src/lib/workflow-chain-expansion.ts +27 -0
- package/src/scenarios/artifact-type-pack-install.test.ts +17 -6
- package/src/scenarios/artifact-type-store-without-render.test.ts +17 -4
- package/src/scenarios/chat-card-pack-execution.test.ts +17 -5
- package/src/scenarios/form-content-instantiation.test.ts +168 -0
- package/src/scenarios/form-content-packs.test.ts +415 -0
- package/src/scenarios/pack-manifest-extension-opacity.test.ts +179 -0
- package/src/scenarios/pack-manifest-extensions.test.ts +203 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pack-manifest vendor extensions — RFC 0138.
|
|
3
|
+
*
|
|
4
|
+
* `host-extensions.md` §"Vendor-prefixed namespaces" carries a normative MUST:
|
|
5
|
+
* *"A client receiving an unknown vendor-prefixed field MUST treat it as
|
|
6
|
+
* opaque."* Every pack manifest, however, set `additionalProperties: false`
|
|
7
|
+
* with no pattern escape — so a vendor-prefixed field could not legally exist
|
|
8
|
+
* on a pack manifest at all. **The corpus mandated a behavior for a case it
|
|
9
|
+
* structurally forbade**, and a host with a working extension had to choose
|
|
10
|
+
* between shipping it and publishing a conformant pack. RFC 0138 adds an
|
|
11
|
+
* `^(x-|vendor\.)` escape hatch on each manifest root and each kind's per-item
|
|
12
|
+
* entry object.
|
|
13
|
+
*
|
|
14
|
+
* Always-on + server-free. Three parts:
|
|
15
|
+
*
|
|
16
|
+
* PART 1 — every pack manifest admits the hatch, INCLUDING the registry
|
|
17
|
+
* publication contract. Without the hatch on `registry-version-manifest`, a
|
|
18
|
+
* pack carrying a root-level extension validates against its source manifest
|
|
19
|
+
* and is then rejected at registry `PUT` — the same split-brain, one layer
|
|
20
|
+
* down. Enumerated by an explicit in-scope count, not a naming glob, so a new
|
|
21
|
+
* pack kind that forgets the hatch fails rather than being skipped.
|
|
22
|
+
*
|
|
23
|
+
* PART 2 — the hatch is NARROW. A misspelled canonical field
|
|
24
|
+
* (`dispalyName`) is still rejected. This is the regression guard that keeps
|
|
25
|
+
* `additionalProperties: false` doing its real job: the hatch admits
|
|
26
|
+
* DECLARED extensions, not arbitrary keys. A change that widened the pattern
|
|
27
|
+
* to `^.*` would pass PART 1 and fail here.
|
|
28
|
+
*
|
|
29
|
+
* PART 3 — the corpus states the opacity + trust rules normatively. The
|
|
30
|
+
* schema alone cannot express "MUST ignore"; without the prose the hatch is
|
|
31
|
+
* just a hole. Guards the `pack-manifest-extension-opaque` invariant text.
|
|
32
|
+
*
|
|
33
|
+
* @see spec/v1/node-packs.md §"Vendor extensions on pack manifests"
|
|
34
|
+
* @see spec/v1/host-extensions.md §"Vendor-prefixed namespaces"
|
|
35
|
+
* @see RFCS/0138-pack-manifest-vendor-extensions.md
|
|
36
|
+
* @see SECURITY/invariants.yaml `pack-manifest-extension-opaque`
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
import { describe, it, expect } from 'vitest';
|
|
40
|
+
import { readdirSync, readFileSync } from 'node:fs';
|
|
41
|
+
import { join } from 'node:path';
|
|
42
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
43
|
+
import addFormats from 'ajv-formats';
|
|
44
|
+
import { SCHEMAS_DIR, V1_DIR } from '../lib/paths.js';
|
|
45
|
+
|
|
46
|
+
const why = (specRef: string, requirement: string): string => `${specRef} — ${requirement}`;
|
|
47
|
+
|
|
48
|
+
/** The canonical hatch pattern. Kept as a literal so a drift in any schema is caught. */
|
|
49
|
+
const HATCH = '^(x-|vendor\\.)';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Manifest schemas RFC 0138 deliberately does NOT cover. Anything else matching
|
|
53
|
+
* `*manifest*.schema.json` MUST carry the hatch — so a new pack kind is caught
|
|
54
|
+
* rather than silently omitted.
|
|
55
|
+
*
|
|
56
|
+
* The first draft of this file globbed `*-pack-manifest.schema.json`, which
|
|
57
|
+
* silently skipped `frontend-plugin-manifest.schema.json` (RFC 0117,
|
|
58
|
+
* `kind: "frontend-plugin"`) purely because it does not carry `-pack-` in its
|
|
59
|
+
* name. Enumerating by naming convention is how a coverage hole hides; the
|
|
60
|
+
* exclusions below are a stated list, not an accident of a glob.
|
|
61
|
+
*/
|
|
62
|
+
const OUT_OF_SCOPE: Record<string, string> = {
|
|
63
|
+
// Not pack-manifest structure — their own contracts with their own
|
|
64
|
+
// compatibility surface. Stated in node-packs.md §Vendor extensions.
|
|
65
|
+
'agent-manifest.schema.json': "a node pack's agents[] entries — separate contract",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const manifestFiles = readdirSync(SCHEMAS_DIR)
|
|
69
|
+
.filter((f) => f.includes('manifest') && f.endsWith('.schema.json'))
|
|
70
|
+
.filter((f) => !(f in OUT_OF_SCOPE))
|
|
71
|
+
.sort();
|
|
72
|
+
|
|
73
|
+
const load = (f: string): Record<string, unknown> =>
|
|
74
|
+
JSON.parse(readFileSync(join(SCHEMAS_DIR, f), 'utf8'));
|
|
75
|
+
|
|
76
|
+
/** Walks a schema and returns every object node that declares `additionalProperties: false`. */
|
|
77
|
+
function closedObjects(root: unknown): Array<Record<string, unknown>> {
|
|
78
|
+
const out: Array<Record<string, unknown>> = [];
|
|
79
|
+
(function walk(node: unknown): void {
|
|
80
|
+
if (node === null || typeof node !== 'object') return;
|
|
81
|
+
const o = node as Record<string, unknown>;
|
|
82
|
+
if (o['additionalProperties'] === false) out.push(o);
|
|
83
|
+
for (const k of Object.keys(o)) walk(o[k]);
|
|
84
|
+
})(root);
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
describe('pack-manifest-extensions: every manifest admits the hatch (RFC 0138, server-free)', () => {
|
|
89
|
+
it('every pack kind is covered — the enumeration is not a naming-convention glob', () => {
|
|
90
|
+
// 8 source manifests (node, workflow-chain, prompt, artifact-type, card,
|
|
91
|
+
// connection, form-content, frontend-plugin) + the registry publication contract.
|
|
92
|
+
expect(
|
|
93
|
+
manifestFiles.length,
|
|
94
|
+
why('RFC 0138', `expected 9 in-scope manifests, found ${manifestFiles.length}: ${manifestFiles.join(', ')}. A new pack kind MUST be added here, or listed in OUT_OF_SCOPE with a reason.`),
|
|
95
|
+
).toBe(9);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
for (const f of manifestFiles) {
|
|
99
|
+
it(`${f.replace(/-?(pack-)?manifest\.schema\.json$/, '')} — manifest ROOT carries the extension hatch`, () => {
|
|
100
|
+
const s = load(f);
|
|
101
|
+
const pp = s['patternProperties'] as Record<string, unknown> | undefined;
|
|
102
|
+
expect(
|
|
103
|
+
pp !== undefined && Object.keys(pp).includes(HATCH),
|
|
104
|
+
why('node-packs.md §Vendor extensions on pack manifests', `${f} root MUST admit ^(x-|vendor\\.) so the host-extensions.md opacity MUST is satisfiable`),
|
|
105
|
+
).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
describe('pack-manifest-extensions: the hatch is NARROW — typos still rejected (RFC 0138)', () => {
|
|
111
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
112
|
+
addFormats(ajv);
|
|
113
|
+
|
|
114
|
+
// artifact-type is the motivating kind (a real host carried `x-openwop-app.canvas`
|
|
115
|
+
// on an artifactTypes[] entry and could not publish the pack).
|
|
116
|
+
const validate = ajv.compile(load('artifact-type-pack-manifest.schema.json'));
|
|
117
|
+
const pack = (extra: Record<string, unknown>): Record<string, unknown> => ({
|
|
118
|
+
name: 'community.openwop.canvas-checklist',
|
|
119
|
+
version: '1.0.0',
|
|
120
|
+
kind: 'artifact-type',
|
|
121
|
+
engines: { openwop: '>=1.1.0 <2.0.0' },
|
|
122
|
+
artifactTypes: [
|
|
123
|
+
{ artifactTypeId: 'community.openwop.doc.checklist', schemaRef: 'schemas/checklist.json', ...extra },
|
|
124
|
+
],
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('an `x-` extension on an entry VALIDATES (the motivating case)', () => {
|
|
128
|
+
expect(
|
|
129
|
+
validate(pack({ 'x-openwop-app.canvas': { components: ['checklist'] } })),
|
|
130
|
+
why('node-packs.md §Vendor extensions on pack manifests', 'a host extension MUST NOT make the pack unpublishable'),
|
|
131
|
+
).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('a `vendor.` extension on an entry VALIDATES', () => {
|
|
135
|
+
expect(
|
|
136
|
+
validate(pack({ 'vendor.acme.rating': 5 })),
|
|
137
|
+
why('host-extensions.md §Vendor-prefixed namespaces', 'vendor-prefixed fields are legitimate'),
|
|
138
|
+
).toBe(true);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('a MISSPELLED canonical field is STILL REJECTED', () => {
|
|
142
|
+
expect(
|
|
143
|
+
validate(pack({ dispalyName: 'Checklist' })),
|
|
144
|
+
why(
|
|
145
|
+
'node-packs.md §Vendor extensions on pack manifests',
|
|
146
|
+
'the hatch admits DECLARED extensions, not arbitrary keys — additionalProperties:false still catches typos. A pattern widened to ^.* would pass the other legs and fail here.',
|
|
147
|
+
),
|
|
148
|
+
).toBe(false);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('an unextended pack still validates (RFC 0138 is additive)', () => {
|
|
152
|
+
expect(validate(pack({})), why('COMPATIBILITY.md §2.1', 'existing manifests validate unchanged')).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('the hatch pattern is not accidentally permissive', () => {
|
|
156
|
+
for (const f of manifestFiles) {
|
|
157
|
+
for (const node of closedObjects(load(f))) {
|
|
158
|
+
for (const pat of Object.keys((node['patternProperties'] as Record<string, unknown>) ?? {})) {
|
|
159
|
+
const re = new RegExp(pat);
|
|
160
|
+
expect(re.test('dispalyName'), why('RFC 0138', `${f}: pattern ${pat} MUST NOT match a bare canonical-looking key`)).toBe(false);
|
|
161
|
+
expect(re.test('x-anything'), why('RFC 0138', `${f}: pattern ${pat} admits x- extensions`)).toBe(true);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
describe('pack-manifest-extensions: the corpus states opacity + trust normatively (RFC 0138)', () => {
|
|
169
|
+
const packsDoc = V1_DIR ? readFileSync(join(V1_DIR, 'node-packs.md'), 'utf8') : '';
|
|
170
|
+
const extDoc = V1_DIR ? readFileSync(join(V1_DIR, 'host-extensions.md'), 'utf8') : '';
|
|
171
|
+
|
|
172
|
+
it.skipIf(V1_DIR === null)('an unrecognized extension MUST be ignored, not rejected', () => {
|
|
173
|
+
expect(
|
|
174
|
+
/MUST ignore it\W{0,4}\s*and\W{0,4}\s*MUST NOT\W{0,4}\s*reject the pack/i.test(packsDoc),
|
|
175
|
+
why('node-packs.md §Vendor extensions on pack manifests', 'unrecognized extensions MUST be ignored, never a rejection reason'),
|
|
176
|
+
).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it.skipIf(V1_DIR === null)('"ignore" is defined — not render, execute, interpret, or persist-for-later', () => {
|
|
180
|
+
expect(
|
|
181
|
+
/MUST NOT render it, execute it, interpret it as markup/i.test(packsDoc),
|
|
182
|
+
why('node-packs.md §Vendor extensions on pack manifests', '"ignore" means ignore — the hatch MUST NOT become a rendering or execution channel'),
|
|
183
|
+
).toBe(true);
|
|
184
|
+
expect(
|
|
185
|
+
/pack-authored content/i.test(packsDoc) && /untrusted/i.test(packsDoc),
|
|
186
|
+
why('node-packs.md §Vendor extensions on pack manifests', 'an extension value is pack-authored, therefore untrusted'),
|
|
187
|
+
).toBe(true);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it.skipIf(V1_DIR === null)('extensions are NOT a capability-negotiation channel', () => {
|
|
191
|
+
expect(
|
|
192
|
+
/NOT\W{0,4}\s*a versioning or capability-negotiation channel/i.test(packsDoc),
|
|
193
|
+
why('node-packs.md §Vendor extensions on pack manifests', 'a host MUST NOT infer support from an extension property'),
|
|
194
|
+
).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it.skipIf(V1_DIR === null)('host-extensions.md still carries the opacity MUST the hatch exists to satisfy', () => {
|
|
198
|
+
expect(
|
|
199
|
+
/unknown vendor-prefixed field MUST treat it as opaque/i.test(extDoc),
|
|
200
|
+
why('host-extensions.md §Vendor-prefixed namespaces', 'the MUST that motivated RFC 0138 is still present'),
|
|
201
|
+
).toBe(true);
|
|
202
|
+
});
|
|
203
|
+
});
|