@openwop/openwop-conformance 1.152.0 → 1.154.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 +17 -0
- package/README.md +9 -0
- package/dist/cli.js +7 -2
- package/dist/lib/requirement-ids.js +100 -0
- package/dist/lib/scenario-disposition.js +22 -0
- package/package.json +33 -2
- package/requirement-aliases.json +4 -0
- package/requirements.json +23554 -0
- package/schemas/CORPUS-STAMP.json +99 -3
- package/src/cli.ts +7 -5
- package/src/global-setup.ts +13 -0
- package/src/lib/corpus-stamp.ts +125 -0
- package/src/lib/requirement-ids.ts +111 -0
- package/src/lib/scenario-disposition.ts +22 -0
- package/src/setup.ts +84 -5
- package/src/lib/capabilities-auth-subject-link.test.ts +0 -103
- package/src/lib/fork-availability.test.ts +0 -69
- package/src/lib/global-setup.test.ts +0 -76
- package/src/lib/grpc-framing.test.ts +0 -96
- package/src/lib/oidc-issuer.test.ts +0 -328
- package/src/lib/otel-collector-grpc.test.ts +0 -191
- package/src/lib/otel-collector.test.ts +0 -303
- package/src/lib/otlp-protobuf.test.ts +0 -461
- package/src/lib/polling.test.ts +0 -80
- package/src/lib/requirement-ledger.test.ts +0 -75
- package/src/lib/risk-disposition.test.ts +0 -91
- package/src/lib/saml-idp.test.ts +0 -127
- package/src/lib/spec-coherence-registry.test.ts +0 -155
- package/src/lib/webhook-receiver.test.ts +0 -144
- package/src/scenarios/artifact-schema-compile-bounded.test.ts +0 -126
- package/src/scenarios/artifact-type-legacy-ids.test.ts +0 -124
- package/src/scenarios/capability-example-root-layout.test.ts +0 -272
- package/src/scenarios/certification-floor-enforcement.test.ts +0 -204
- package/src/scenarios/chain-subchain-unsupported-refused.test.ts +0 -70
- package/src/scenarios/compensation-profile.test.ts +0 -340
- package/src/scenarios/core-manifest-and-extension-registry.test.ts +0 -250
- package/src/scenarios/discovery-canonical-family-no-shadow.test.ts +0 -219
- package/src/scenarios/edge-condition-truthy-falsy.test.ts +0 -108
- package/src/scenarios/effect-identity-composition.test.ts +0 -129
- package/src/scenarios/effect-identity-cross-scope.test.ts +0 -82
- package/src/scenarios/error-envelope-canonical-shape.test.ts +0 -64
- package/src/scenarios/form-content-packs.test.ts +0 -415
- package/src/scenarios/multi-region-effect-vocabulary.test.ts +0 -175
- package/src/scenarios/normative-example-extraction.test.ts +0 -242
- package/src/scenarios/openapi-asyncapi-sdk-parity.test.ts +0 -309
- package/src/scenarios/pack-manifest-extensions.test.ts +0 -203
- package/src/scenarios/protocol-version-grammar.test.ts +0 -119
- package/src/scenarios/registry-declarative-kinds.test.ts +0 -121
- package/src/scenarios/rfc-0147-self-audit.test.ts +0 -104
- package/src/scenarios/rfc-lifecycle-coherence.test.ts +0 -215
- package/src/scenarios/semantic-digest-v2.test.ts +0 -128
- package/src/scenarios/spec-corpus-validity.test.ts +0 -1727
- package/src/scenarios/spec-section-citations.test.ts +0 -132
- package/src/scenarios/tool-result-trust-monotone.test.ts +0 -168
- package/src/scenarios/versioned-composition-profiles.test.ts +0 -201
- package/src/scenarios/workflow-chain-internal-flag.test.ts +0 -84
- package/src/scenarios/workload-identity-profile.test.ts +0 -184
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* spec-section-citations — server-free. A citation of the form
|
|
3
|
-
* `<doc>.md §"<Section>"` MUST resolve to a heading that exists in that doc.
|
|
4
|
-
*
|
|
5
|
-
* SP-04 (2026-08-18): `storage-adapters.md §"Claim acquisition"` was cited by
|
|
6
|
-
* FOUR artifacts — `production-profile.md` §Durability, RFC 0009's
|
|
7
|
-
* scenario-citation table, and the docstrings of `staleClaim.test.ts` and
|
|
8
|
-
* `restart-during-run.test.ts` — and the section did not exist. Nothing was
|
|
9
|
-
* checking, so a normative `MUST` ("Storage adapters MUST satisfy
|
|
10
|
-
* `storage-adapters.md` lease and event-log invariants") pointed at nothing for
|
|
11
|
-
* the life of RFC 0009.
|
|
12
|
-
*
|
|
13
|
-
* Scope is deliberately narrow. A corpus-wide sweep of this citation form finds
|
|
14
|
-
* ~1400 citations and ~260 that do not resolve under a heading-only matcher —
|
|
15
|
-
* a mix of genuinely dangling anchors and legitimate informal references to
|
|
16
|
-
* table rows and capability keys rather than headings. Triaging those is its own
|
|
17
|
-
* work item; gating the whole corpus on an untriaged sweep would either fail
|
|
18
|
-
* immediately or need an allowlist so large it would stop meaning anything.
|
|
19
|
-
* So this pins the docs whose section citations are load-bearing for the
|
|
20
|
-
* durability contract, and grows as other docs are triaged.
|
|
21
|
-
*
|
|
22
|
-
* @see spec/v1/storage-adapters.md §"Claim acquisition"
|
|
23
|
-
* @see spec/v1/production-profile.md §Durability
|
|
24
|
-
*/
|
|
25
|
-
|
|
26
|
-
import { describe, it, expect } from 'vitest';
|
|
27
|
-
import { readFileSync, readdirSync, existsSync } from 'node:fs';
|
|
28
|
-
import { join } from 'node:path';
|
|
29
|
-
import { V1_DIR, SCENARIOS_DIR } from '../lib/paths.js';
|
|
30
|
-
|
|
31
|
-
/** Docs whose `§"Section"` citations are checked. Add a doc once its citations are triaged. */
|
|
32
|
-
const CHECKED_DOCS = ['storage-adapters.md', 'production-profile.md'] as const;
|
|
33
|
-
|
|
34
|
-
/** `<doc>.md §"Quoted Section"` or `<doc>.md §BareToken`. */
|
|
35
|
-
const CITATION = /([A-Za-z0-9._-]+)\.md\s*§\s*(?:"([^"\n]+)"|([A-Za-z][A-Za-z0-9.-]*))/g;
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Strip decoration, a leading `§` (headings in this corpus carry their own), and
|
|
39
|
-
* trailing sentence punctuation — a docstring legitimately ends a sentence
|
|
40
|
-
* inside the quotes (`… §"Claim acquisition."`) and still names that section.
|
|
41
|
-
*/
|
|
42
|
-
const normalize = (t: string): string =>
|
|
43
|
-
t
|
|
44
|
-
.replace(/[`*_"]/g, '')
|
|
45
|
-
.replace(/^§\s*/, '')
|
|
46
|
-
.replace(/[.,;:]+$/, '')
|
|
47
|
-
.trim()
|
|
48
|
-
.toLowerCase();
|
|
49
|
-
|
|
50
|
-
/** A heading matches its full text, or its lead token before an em-dash / period. */
|
|
51
|
-
function headingKeys(raw: string): string[] {
|
|
52
|
-
const h = normalize(raw);
|
|
53
|
-
const keys = new Set<string>([h]);
|
|
54
|
-
const dash = h.split(/\s+[—–-]\s+/)[0]?.trim();
|
|
55
|
-
if (dash) keys.add(dash);
|
|
56
|
-
const dot = h.split(/\.\s+/)[0]?.trim();
|
|
57
|
-
if (dot) keys.add(dot);
|
|
58
|
-
return [...keys];
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function walk(dir: string, keep: (n: string) => boolean, out: string[] = []): string[] {
|
|
62
|
-
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
63
|
-
const p = join(dir, e.name);
|
|
64
|
-
if (e.isDirectory()) walk(p, keep, out);
|
|
65
|
-
else if (keep(e.name)) out.push(p);
|
|
66
|
-
}
|
|
67
|
-
return out;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
describe.skipIf(V1_DIR === null)('spec-section-citations (SP-04)', () => {
|
|
71
|
-
it('every cited section of a checked doc exists as a heading in that doc', () => {
|
|
72
|
-
const v1 = V1_DIR as string;
|
|
73
|
-
// `V1_DIR` is `<repo>/spec/v1` in a checkout; RFCS/ sits two levels up.
|
|
74
|
-
// Both are absent from the published tarball, which is why the whole file
|
|
75
|
-
// is `skipIf(V1_DIR === null)`.
|
|
76
|
-
const rfcsDir = join(v1, '..', '..', 'RFCS');
|
|
77
|
-
|
|
78
|
-
const sources = [
|
|
79
|
-
...walk(v1, (n) => n.endsWith('.md')),
|
|
80
|
-
...(existsSync(rfcsDir) ? walk(rfcsDir, (n) => n.endsWith('.md')) : []),
|
|
81
|
-
...(SCENARIOS_DIR !== null ? walk(SCENARIOS_DIR, (n) => n.endsWith('.test.ts')) : []),
|
|
82
|
-
];
|
|
83
|
-
|
|
84
|
-
const headings = new Map<string, Set<string>>();
|
|
85
|
-
for (const doc of CHECKED_DOCS) {
|
|
86
|
-
const text = readFileSync(join(v1, doc), 'utf8');
|
|
87
|
-
const keys = new Set<string>();
|
|
88
|
-
for (const m of text.matchAll(/^#{1,6}\s+(.+?)\s*$/gm)) {
|
|
89
|
-
for (const k of headingKeys(m[1] ?? '')) keys.add(k);
|
|
90
|
-
}
|
|
91
|
-
headings.set(doc, keys);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
const dangling: string[] = [];
|
|
95
|
-
let checked = 0;
|
|
96
|
-
for (const src of sources) {
|
|
97
|
-
const text = readFileSync(src, 'utf8');
|
|
98
|
-
for (const m of text.matchAll(CITATION)) {
|
|
99
|
-
const doc = `${m[1]}.md`;
|
|
100
|
-
const keys = headings.get(doc);
|
|
101
|
-
if (!keys) continue;
|
|
102
|
-
const section = (m[2] ?? m[3] ?? '').trim();
|
|
103
|
-
if (!section) continue;
|
|
104
|
-
checked++;
|
|
105
|
-
const want = normalize(section);
|
|
106
|
-
// A MULTI-WORD citation must match a heading key exactly: prefix
|
|
107
|
-
// matching would let `§"Claim acquisition"` be satisfied by a heading
|
|
108
|
-
// named `Claim acquisition considered harmful`, which is how a renamed
|
|
109
|
-
// section slips past a gate like this (caught while sabotage-testing
|
|
110
|
-
// this very leg). A single-token citation — `§B`, `§C.2`,
|
|
111
|
-
// `§host.aiProviders` — legitimately prefixes a longer heading
|
|
112
|
-
// (`## §B — Channel resolution …`), so prefix matching stays for those.
|
|
113
|
-
const hit = keys.has(want)
|
|
114
|
-
|| (!want.includes(' ') && [...keys].some((k) => k.startsWith(want)));
|
|
115
|
-
if (!hit) dangling.push(`${src.split('/').slice(-2).join('/')} → ${doc} §"${section}"`);
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Non-vacuity: these docs ARE cited. A zero here means the matcher stopped
|
|
120
|
-
// matching, not that the corpus got clean — the failure mode this file exists
|
|
121
|
-
// to prevent, one level up.
|
|
122
|
-
expect(
|
|
123
|
-
checked,
|
|
124
|
-
'spec-section-citations: no citations of the checked docs were found — the matcher is broken, not the corpus clean',
|
|
125
|
-
).toBeGreaterThan(0);
|
|
126
|
-
|
|
127
|
-
expect(
|
|
128
|
-
dangling,
|
|
129
|
-
`dangling section citations (the cited heading does not exist):\n ${dangling.join('\n ')}`,
|
|
130
|
-
).toEqual([]);
|
|
131
|
-
});
|
|
132
|
-
});
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* tool-result-trust-monotone — RFC 0143, the general rule the corpus's ~ten
|
|
3
|
-
* point-invariants instance.
|
|
4
|
-
*
|
|
5
|
-
* The threat model fences ~ten specific untrusted-content ingresses into the
|
|
6
|
-
* prompt (user input, RAG, artifacts, MCP, node-pack output, runner, card
|
|
7
|
-
* inputs, form-content, voice), each with its own MUST. It never stated the
|
|
8
|
-
* general rule: **a tool result is content entering model context, untrusted
|
|
9
|
-
* unless the host has a named basis for trust, and trust is the MEET of a
|
|
10
|
-
* composed segment's inputs — never raised by any transformation.** Absent that
|
|
11
|
-
* rule the FRMD-F1-1 laundering path (untrusted → tool → "result" → prompt,
|
|
12
|
-
* tag dropped in transit) is unforbidden in general, and nothing catches the
|
|
13
|
-
* NEXT ingress shipped without a fence.
|
|
14
|
-
*
|
|
15
|
-
* Always-on, server-free, three parts:
|
|
16
|
-
*
|
|
17
|
-
* PART 1 — COMPLETENESS (load-bearing). Every content-ingress surface the
|
|
18
|
-
* threat model's §4 STRIDE table names cites a fencing invariant that
|
|
19
|
-
* actually EXISTS in SECURITY/invariants.yaml — no dangling reference — AND a
|
|
20
|
-
* curated floor of prompt-ingress surfaces each resolves to a real invariant.
|
|
21
|
-
* A new ingress added to §4 without a trust invariant reds this. The
|
|
22
|
-
* general-rule analog of RFC 0138's "every manifest admits the hatch"; the
|
|
23
|
-
* thing the point-invariants alone cannot provide.
|
|
24
|
-
*
|
|
25
|
-
* PART 2 — the MEET is stated normatively. The new §2a states monotone
|
|
26
|
-
* composition, untrusted-by-default with "a tool ran" NOT a basis,
|
|
27
|
-
* no-laundering-through-storage, both blessed strategies, and the isolation
|
|
28
|
-
* carve-out. Prose-pinned: the schema cannot express "MUST be the meet."
|
|
29
|
-
*
|
|
30
|
-
* PART 3 — the instances name the general rule. ai-envelope.md §"Trust
|
|
31
|
-
* boundary" cross-references §2a, so the MCP/A2A special case is anchored to
|
|
32
|
-
* the general rule and cannot drift into contradicting it.
|
|
33
|
-
*
|
|
34
|
-
* WHAT THIS DOES NOT COVER — host behavior. Dynamic propagation through the
|
|
35
|
-
* durable-store hop (strategy a) is witnessed by the reference host (memory
|
|
36
|
-
* lane), not here; and the completeness leg is only as complete as §4 (an
|
|
37
|
-
* ingress that exists in a host but was never added to §4 is invisible —
|
|
38
|
-
* RFC 0143 unresolved-question 1).
|
|
39
|
-
*
|
|
40
|
-
* @see RFCS/0143-tool-result-trust-propagation.md
|
|
41
|
-
* @see SECURITY/threat-model-prompt-injection.md §2a, §4, §5
|
|
42
|
-
* @see spec/v1/ai-envelope.md §"Trust boundary"
|
|
43
|
-
*/
|
|
44
|
-
|
|
45
|
-
import { describe, it, expect } from 'vitest';
|
|
46
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
47
|
-
import { dirname, join } from 'node:path';
|
|
48
|
-
import { SCHEMAS_DIR, V1_DIR } from '../lib/paths.js';
|
|
49
|
-
|
|
50
|
-
// Repo root = parent of schemas/ (the canonical anchor other scenarios use).
|
|
51
|
-
// SECURITY/ is not vendored into the published package layout, so guard reads
|
|
52
|
-
// and skip when absent — the same posture as V1_DIR === null.
|
|
53
|
-
const REPO_ROOT: string | null = (() => {
|
|
54
|
-
const r = dirname(SCHEMAS_DIR);
|
|
55
|
-
return existsSync(join(r, 'SECURITY', 'invariants.yaml')) ? r : null;
|
|
56
|
-
})();
|
|
57
|
-
|
|
58
|
-
const why = (specRef: string, requirement: string): string => `${specRef} — ${requirement}`;
|
|
59
|
-
|
|
60
|
-
const TM = REPO_ROOT ? readFileSync(join(REPO_ROOT, 'SECURITY', 'threat-model-prompt-injection.md'), 'utf8') : '';
|
|
61
|
-
const INV = REPO_ROOT ? readFileSync(join(REPO_ROOT, 'SECURITY', 'invariants.yaml'), 'utf8') : '';
|
|
62
|
-
const ENV = V1_DIR ? readFileSync(join(V1_DIR, 'ai-envelope.md'), 'utf8') : '';
|
|
63
|
-
|
|
64
|
-
/** All invariant ids declared in SECURITY/invariants.yaml. */
|
|
65
|
-
function declaredInvariants(): Set<string> {
|
|
66
|
-
const ids = new Set<string>();
|
|
67
|
-
for (const m of INV.matchAll(/^\s*- id:\s*([a-z0-9-]+)\s*$/gim)) ids.add(m[1]);
|
|
68
|
-
return ids;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/** Every invariant id CITED in a §4 STRIDE `Invariant` column (backticked, kebab-case). */
|
|
72
|
-
function citedInSection4(): string[] {
|
|
73
|
-
const start = TM.indexOf('## 4.');
|
|
74
|
-
const s4 = start >= 0 ? TM.slice(start, TM.indexOf('\n## 5.')) : '';
|
|
75
|
-
const ids = new Set<string>();
|
|
76
|
-
// Backticked kebab identifiers that look like invariant ids (contain a hyphen,
|
|
77
|
-
// lowercase). Excludes prose backticks like `core.*` (has a dot) and `exec`.
|
|
78
|
-
for (const m of s4.matchAll(/`([a-z][a-z0-9]+(?:-[a-z0-9]+)+)`/g)) {
|
|
79
|
-
const id = m[1];
|
|
80
|
-
if (!id.includes('.') && !id.includes('_')) ids.add(id);
|
|
81
|
-
}
|
|
82
|
-
return [...ids];
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
describe('tool-result-trust-monotone: COMPLETENESS — no unfenced ingress (RFC 0143 part 1)', () => {
|
|
86
|
-
const declared = declaredInvariants();
|
|
87
|
-
|
|
88
|
-
it.skipIf(REPO_ROOT === null)('every invariant the §4 STRIDE table cites actually exists (no dangling fence)', () => {
|
|
89
|
-
const cited = citedInSection4();
|
|
90
|
-
expect(cited.length, why('threat-model §4', 'the STRIDE table cites fencing invariants')).toBeGreaterThan(5);
|
|
91
|
-
// Only assert for ids that ARE invariants (present in the file) OR are
|
|
92
|
-
// clearly invariant-shaped; a cited id that resolves to nothing is drift.
|
|
93
|
-
const dangling = cited.filter((id) => /injection|trust|untrusted|marker|sandbox|exec|a2ui|voice|runner|pack/.test(id) && !declared.has(id));
|
|
94
|
-
expect(
|
|
95
|
-
dangling,
|
|
96
|
-
why('threat-model §4', `every invariant-shaped id cited in §4 MUST exist in invariants.yaml — dangling: ${dangling.join(', ')}`),
|
|
97
|
-
).toEqual([]);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it.skipIf(REPO_ROOT === null)('the curated floor of prompt-ingress surfaces each has a real fencing invariant', () => {
|
|
101
|
-
// The core content-ingress-to-prompt surfaces. If a surface is de-fenced or
|
|
102
|
-
// its invariant renamed without updating this floor, the leg reds — this is
|
|
103
|
-
// the "next ingress shipped unfenced" guard, scoped to the known set.
|
|
104
|
-
const FLOOR: Record<string, string[]> = {
|
|
105
|
-
'user input': ['prompt-injection-input-marker'],
|
|
106
|
-
'RAG / knowledge base': ['prompt-injection-kb-marker'],
|
|
107
|
-
'prior artifact': ['prompt-injection-artifact-marker'],
|
|
108
|
-
'MCP tool result': ['prompt-injection-mcp-marker'],
|
|
109
|
-
'node-pack output': ['node-pack-output-untrusted'],
|
|
110
|
-
'self-hosted runner': ['runner-output-untrusted-transport'],
|
|
111
|
-
'chat-card input': ['chat-card-input-trust-boundary'],
|
|
112
|
-
'form-content pack strings': ['form-content-pack-string-trust-boundary'],
|
|
113
|
-
'voice transcript': ['voice-transcript-untrusted'],
|
|
114
|
-
'compose boundary (the meet)': ['prompt-composed-trust-marker'],
|
|
115
|
-
};
|
|
116
|
-
for (const [surface, inv] of Object.entries(FLOOR)) {
|
|
117
|
-
const present = inv.filter((i) => declared.has(i));
|
|
118
|
-
expect(
|
|
119
|
-
present,
|
|
120
|
-
why('threat-model §5 / invariants.yaml', `the "${surface}" ingress MUST retain a fencing invariant (${inv.join(' | ')})`),
|
|
121
|
-
).toEqual(inv);
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it.skipIf(REPO_ROOT === null)('the general rule itself is a declared invariant', () => {
|
|
126
|
-
expect(
|
|
127
|
-
declared.has('tool-result-trust-monotone'),
|
|
128
|
-
why('invariants.yaml', 'RFC 0143 mints the general monotone-composition invariant the point-invariants instance'),
|
|
129
|
-
).toBe(true);
|
|
130
|
-
});
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
describe('tool-result-trust-monotone: the MEET is stated normatively (RFC 0143 part 2)', () => {
|
|
134
|
-
it.skipIf(REPO_ROOT === null)('§2a exists and states monotone composition (the meet)', () => {
|
|
135
|
-
expect(/## 2a\. Trust is monotone through composition/i.test(TM), why('threat-model §2a', 'the general-rule section exists')).toBe(true);
|
|
136
|
-
expect(
|
|
137
|
-
/MUST be the \*\*meet\*\*|MUST be the meet of its inputs/i.test(TM),
|
|
138
|
-
why('threat-model §2a', 'a composed segment\'s trust MUST be the MEET of its inputs — one untrusted input taints the segment'),
|
|
139
|
-
).toBe(true);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it.skipIf(REPO_ROOT === null)('"a tool ran" is explicitly NOT a basis for trust', () => {
|
|
143
|
-
expect(
|
|
144
|
-
/tool having executed and returned is\W{0,4}\s*NOT a basis/i.test(TM),
|
|
145
|
-
why('threat-model §2a', 'untrusted-by-default — a tool having run is not a trust basis'),
|
|
146
|
-
).toBe(true);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
it.skipIf(REPO_ROOT === null)('no laundering through storage, and both strategies are blessed', () => {
|
|
150
|
-
expect(/does not launder it/i.test(TM), why('threat-model §2a', 'persist-then-recall does not launder (closes FRMD-F1-1)')).toBe(true);
|
|
151
|
-
expect(/dynamic coarse-grained propagation/i.test(TM) && /static reader classification/i.test(TM), why('threat-model §2a', 'both conforming strategies named')).toBe(true);
|
|
152
|
-
expect(/[Vv]alue-granular taint.{0,60}NOT required/i.test(TM), why('threat-model §2a', 'value-granular taint is NOT required — the floor is the coarse meet')).toBe(true);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it.skipIf(REPO_ROOT === null)('the structurally-isolated-reader carve-out is present and narrow', () => {
|
|
156
|
-
expect(/structurally isolated/i.test(TM), why('threat-model §2a', 'the isolation carve-out exists so correct sandboxing is not punished')).toBe(true);
|
|
157
|
-
expect(/missing.{0,20}`?"?trusted"?`?.{0,40}violat|absence of a trust decision is\W{0,4}\s*`?untrusted`?/i.test(TM), why('threat-model §2a', 'the fail-open (missing ⇒ trusted) default is the violation')).toBe(true);
|
|
158
|
-
});
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
describe('tool-result-trust-monotone: the instances name the general rule (RFC 0143 part 3)', () => {
|
|
162
|
-
it.skipIf(V1_DIR === null)('ai-envelope.md §"Trust boundary" cross-references §2a / RFC 0143', () => {
|
|
163
|
-
expect(
|
|
164
|
-
/named instance.{0,80}(§2a|RFC 0143|monotone-composition)/is.test(ENV),
|
|
165
|
-
why('ai-envelope.md §"Trust boundary"', 'the MCP/A2A rule cites the general rule as its instance, so it cannot drift into contradicting it'),
|
|
166
|
-
).toBe(true);
|
|
167
|
-
});
|
|
168
|
-
});
|
|
@@ -1,201 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* RFC 0152 + RFC 0153 — versioned A2A and MCP composition, shape only.
|
|
3
|
-
*
|
|
4
|
-
* **Stated first, because RFC 0147 §A.5 turns on it:** this file proves the
|
|
5
|
-
* discovery schema admits the versioned shapes §A describes and rejects the ones
|
|
6
|
-
* it forbids. It contacts no peer. It is therefore **not** evidence that this
|
|
7
|
-
* host interoperates with any A2A 1.0 or MCP 2026-07-28 implementation, and
|
|
8
|
-
* neither RFC can reach a defensible `Accepted` on it. Both additionally require
|
|
9
|
-
* a real upstream peer in CI, which is not a corpus deliverable.
|
|
10
|
-
*
|
|
11
|
-
* The defect both RFCs address is the same, which is why they land together:
|
|
12
|
-
* **`supported: true` with no version is a claim a peer cannot negotiate
|
|
13
|
-
* against.** It asserts the host speaks *some* A2A or *some* MCP. Two hosts can
|
|
14
|
-
* both advertise it, share no revision, and discover that only when a call
|
|
15
|
-
* fails — and the failure surfaces at the peer, not at the handshake that was
|
|
16
|
-
* supposed to prevent it. Versioning the advertisement moves the disagreement to
|
|
17
|
-
* the one place both sides are looking.
|
|
18
|
-
*
|
|
19
|
-
* Two shape choices worth keeping:
|
|
20
|
-
*
|
|
21
|
-
* - **MCP versions are date-patterned, not free strings.** MCP revisions *are*
|
|
22
|
-
* dates. Accepting `latest` or `2026-7-28` would let two hosts disagree
|
|
23
|
-
* about which revision they share while both validate.
|
|
24
|
-
* - **The MCP feature list is closed.** An unrecognized feature name is
|
|
25
|
-
* indistinguishable from a typo, and a peer that silently ignores one has
|
|
26
|
-
* negotiated a capability neither side implements.
|
|
27
|
-
*
|
|
28
|
-
* Server-free.
|
|
29
|
-
*/
|
|
30
|
-
|
|
31
|
-
import { describe, it, expect } from 'vitest';
|
|
32
|
-
import { readFileSync } from 'node:fs';
|
|
33
|
-
import { join, resolve as pathResolve } from 'node:path';
|
|
34
|
-
import Ajv2020 from 'ajv/dist/2020.js';
|
|
35
|
-
import { SCHEMAS_DIR, V1_DIR } from '../lib/paths.js';
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Schemas ship inside the package; RFC prose does not. Deriving both from
|
|
39
|
-
* `V1_DIR` — null in the published tarball — and casting the null away with
|
|
40
|
-
* `as string` is what took this file down at import when installed from npm.
|
|
41
|
-
*/
|
|
42
|
-
const RFCS_DIR = V1_DIR === null ? null : pathResolve(V1_DIR, '..', '..', 'RFCS');
|
|
43
|
-
|
|
44
|
-
function caps(): Record<string, unknown> {
|
|
45
|
-
return JSON.parse(
|
|
46
|
-
readFileSync(join(SCHEMAS_DIR, 'capabilities.schema.json'), 'utf8'),
|
|
47
|
-
) as Record<string, unknown>;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** Compile one capability family in isolation. */
|
|
51
|
-
function familyValidator(family: string) {
|
|
52
|
-
const schema = caps() as { properties: Record<string, object> };
|
|
53
|
-
const ajv = new Ajv2020({ strict: false, allErrors: true });
|
|
54
|
-
return ajv.compile(schema.properties[family] as object);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
describe('RFC 0152 §A — A2A versioned discovery', () => {
|
|
58
|
-
const validate = familyValidator('a2a');
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* The `a2a` family independently requires `agentCardUrl` (RFC 0100: it must
|
|
62
|
-
* GET-resolve credential-less). Every fixture below carries it.
|
|
63
|
-
*
|
|
64
|
-
* This is not tidiness. Without it the NEGATIVE legs would have failed
|
|
65
|
-
* validation for the missing card URL rather than for the version defect they
|
|
66
|
-
* name — passing for the wrong reason, which is the vacuity this whole
|
|
67
|
-
* program exists to close, committed inside a test written to prevent it.
|
|
68
|
-
*/
|
|
69
|
-
const a2a = (over: Record<string, unknown>) => ({
|
|
70
|
-
supported: true,
|
|
71
|
-
agentCardUrl: 'https://host.example/.well-known/agent-card.json',
|
|
72
|
-
...over,
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('a versioned advertisement validates', () => {
|
|
76
|
-
expect(
|
|
77
|
-
validate(a2a({
|
|
78
|
-
protocolVersions: ['1.0', '0.3'],
|
|
79
|
-
preferredVersion: '1.0',
|
|
80
|
-
profiles: ['a2a-1.0', 'a2a-0.3-legacy'],
|
|
81
|
-
durableTasks: true,
|
|
82
|
-
})),
|
|
83
|
-
JSON.stringify(validate.errors),
|
|
84
|
-
).toBe(true);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('a legacy-only host can say so explicitly', () => {
|
|
88
|
-
// The point of naming the legacy profile: a deprecation you can see is one
|
|
89
|
-
// you can time-bound. A bare `supported: true` hides the same fact.
|
|
90
|
-
expect(
|
|
91
|
-
validate(a2a({ protocolVersions: ['0.3'], preferredVersion: '0.3', profiles: ['a2a-0.3-legacy'] })),
|
|
92
|
-
JSON.stringify(validate.errors),
|
|
93
|
-
).toBe(true);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('an empty version list is rejected', () => {
|
|
97
|
-
expect(
|
|
98
|
-
validate(a2a({ protocolVersions: [] })),
|
|
99
|
-
'RFC 0152 §A: an A2A-capable host MUST advertise a NON-EMPTY `protocolVersions`. An empty ' +
|
|
100
|
-
'array is the bare `supported: true` problem wearing a schema — it validates and tells a ' +
|
|
101
|
-
'peer nothing it can negotiate against.',
|
|
102
|
-
).toBe(false);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('duplicate versions are rejected', () => {
|
|
106
|
-
expect(validate(a2a({ protocolVersions: ['1.0', '1.0'] }))).toBe(false);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('a malformed version is rejected', () => {
|
|
110
|
-
for (const v of ['1', 'v1.0', 'latest', '1.0.0']) {
|
|
111
|
-
expect(
|
|
112
|
-
validate(a2a({ protocolVersions: [v] })),
|
|
113
|
-
`RFC 0152 §A: '${v}' is not an A2A major.minor version`,
|
|
114
|
-
).toBe(false);
|
|
115
|
-
}
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it('a malformed profile id is rejected', () => {
|
|
119
|
-
expect(validate(a2a({ protocolVersions: ['1.0'], profiles: ['a2a-latest'] }))).toBe(false);
|
|
120
|
-
expect(validate(a2a({ protocolVersions: ['1.0'], profiles: ['a2a-1.0'] }))).toBe(true);
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
describe('RFC 0153 §A — MCP versioned discovery', () => {
|
|
125
|
-
const validate = familyValidator('mcp');
|
|
126
|
-
|
|
127
|
-
it('a versioned advertisement validates', () => {
|
|
128
|
-
expect(
|
|
129
|
-
validate({
|
|
130
|
-
supported: true,
|
|
131
|
-
protocolVersions: ['2026-07-28', '2025-06-18'],
|
|
132
|
-
preferredVersion: '2026-07-28',
|
|
133
|
-
profiles: ['mcp-2026-07-28', 'mcp-2025-06-18-legacy'],
|
|
134
|
-
features: ['server-discover', 'mrtr', 'cacheable-lists', 'extensions'],
|
|
135
|
-
}),
|
|
136
|
-
JSON.stringify(validate.errors),
|
|
137
|
-
).toBe(true);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('the v1.0 baseline `serverUrls` (required by mcp-discoverability when supported:true) validates — path or absolute URL, non-empty, no duplicates', () => {
|
|
141
|
-
// S27 (2026-08-17): the schema was `additionalProperties: false` and did not
|
|
142
|
-
// list `serverUrls`, while mcp-discoverability.test.ts REQUIRED it — a host
|
|
143
|
-
// could not satisfy the scenario and the schema at once. The second sibling
|
|
144
|
-
// host measured the contradiction on its own advert.
|
|
145
|
-
expect(
|
|
146
|
-
validate({ supported: true, serverUrls: ['/v1/host/openwop-app/mcp'], protocolVersions: ['2026-07-28'] }),
|
|
147
|
-
JSON.stringify(validate.errors),
|
|
148
|
-
).toBe(true);
|
|
149
|
-
expect(
|
|
150
|
-
validate({ supported: true, serverUrls: ['https://example.test/canvasApi/v1/mcp'], protocolVersions: ['2024-11-05'], preferredVersion: '2024-11-05' }),
|
|
151
|
-
JSON.stringify(validate.errors),
|
|
152
|
-
).toBe(true);
|
|
153
|
-
expect(validate({ supported: true, serverUrls: [] }), 'empty serverUrls is the bare `supported: true` problem').toBe(false);
|
|
154
|
-
expect(validate({ supported: true, serverUrls: ['/a', '/a'] }), 'duplicates').toBe(false);
|
|
155
|
-
expect(validate({ supported: true, serverUrls: [''] }), 'an empty string is not a mount').toBe(false);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
it('versions use MCP date form exactly', () => {
|
|
159
|
-
// MCP revisions ARE dates. `latest` or a non-padded month would let two
|
|
160
|
-
// hosts disagree about which revision they share while both validate.
|
|
161
|
-
for (const v of ['latest', '2026-7-28', '2026-07', 'v2026-07-28']) {
|
|
162
|
-
expect(
|
|
163
|
-
validate({ supported: true, protocolVersions: [v] }),
|
|
164
|
-
`RFC 0153 §A: '${v}' is not MCP's date form`,
|
|
165
|
-
).toBe(false);
|
|
166
|
-
}
|
|
167
|
-
expect(validate({ supported: true, protocolVersions: ['2026-07-28'] })).toBe(true);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('the feature list is closed', () => {
|
|
171
|
-
expect(
|
|
172
|
-
validate({ supported: true, protocolVersions: ['2026-07-28'], features: ['server-discover', 'telepathy'] }),
|
|
173
|
-
'RFC 0153 §A: an unrecognized feature name is indistinguishable from a typo, and a peer ' +
|
|
174
|
-
'that silently ignores one has negotiated a capability neither side implements.',
|
|
175
|
-
).toBe(false);
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it('an empty version list is rejected', () => {
|
|
179
|
-
expect(validate({ supported: true, protocolVersions: [] })).toBe(false);
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
describe.skipIf(RFCS_DIR === null)('RFC 0152 + 0153 — what these do NOT establish', () => {
|
|
184
|
-
it('both RFCs keep their upstream-peer acceptance items unticked and annotated', () => {
|
|
185
|
-
// RFC 0147 §A.5 forbids `Accepted` on shape-only evidence for a behavioral
|
|
186
|
-
// requirement. Schema validity is not interop: nothing here speaks to a
|
|
187
|
-
// peer, and both RFCs require a real upstream implementation in CI. This
|
|
188
|
-
// leg exists so the suite cannot read as more than it is.
|
|
189
|
-
for (const [file, needle] of [
|
|
190
|
-
['0152-a2a-1-0-versioned-composition.md', 'Real upstream A2A 1.0 peer passes in CI.'],
|
|
191
|
-
['0153-mcp-2026-07-28-versioned-composition.md', 'Pinned real MCP current peer passes in CI.'],
|
|
192
|
-
] as const) {
|
|
193
|
-
const rfc = readFileSync(join(RFCS_DIR as string, file), 'utf8');
|
|
194
|
-
expect(
|
|
195
|
-
new RegExp(`- \\[ \\] ${needle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*\\(`).test(rfc),
|
|
196
|
-
`${file}: the upstream-peer item MUST remain unticked and annotated — no peer is contacted ` +
|
|
197
|
-
'anywhere in this corpus, and interop is the one thing a schema cannot demonstrate.',
|
|
198
|
-
).toBe(true);
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
});
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Workflow-chain gallery visibility — `internal` chains (RFC 0135).
|
|
3
|
-
*
|
|
4
|
-
* Server-free corpus legs (always-on): the manifest schema's §WorkflowChain carries
|
|
5
|
-
* a boolean `internal` property; a chain declaring `internal: true` validates; a
|
|
6
|
-
* non-boolean `internal` is rejected; the spec documents the MUST-omit-from-default-
|
|
7
|
-
* gallery rule and the not-an-authorization-boundary rule.
|
|
8
|
-
*
|
|
9
|
-
* The gallery-omission behavior itself is host-catalog presentation with no
|
|
10
|
-
* normative wire listing endpoint, so it is witnessed at the reference host (host
|
|
11
|
-
* regression test), not over the wire — see RFC 0135 §Conformance.
|
|
12
|
-
*
|
|
13
|
-
* @see spec/v1/workflow-chain-packs.md §"Chain visibility (RFC 0135)"
|
|
14
|
-
* @see schemas/workflow-chain-pack-manifest.schema.json §WorkflowChain
|
|
15
|
-
* @see RFCS/0135-workflow-chain-internal-visibility.md
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
import { describe, it, expect } from 'vitest';
|
|
19
|
-
import { readFileSync } from 'node:fs';
|
|
20
|
-
import { join } from 'node:path';
|
|
21
|
-
import Ajv2020 from 'ajv/dist/2020.js';
|
|
22
|
-
import addFormats from 'ajv-formats';
|
|
23
|
-
import { SCHEMAS_DIR, V1_DIR } from '../lib/paths.js';
|
|
24
|
-
|
|
25
|
-
const cite = (section: string, requirement: string): string => `${section} — ${requirement}`;
|
|
26
|
-
const MANIFEST = join(SCHEMAS_DIR, 'workflow-chain-pack-manifest.schema.json');
|
|
27
|
-
// S38 (2026-08-17): `spec/` is NOT in the published package (`files`), so a path built
|
|
28
|
-
// from SCHEMAS_DIR/../spec ENOENTs for every npm consumer — five always-on legs reddened
|
|
29
|
-
// MyndHyve's bundle for a reason that had nothing to do with the host. Prose legs are
|
|
30
|
-
// repo-layout only: `null` in the published layout and skipped, never thrown.
|
|
31
|
-
const CHAIN_DOC: string | null = V1_DIR === null ? null : join(V1_DIR, 'workflow-chain-packs.md');
|
|
32
|
-
|
|
33
|
-
function packWith(internal: unknown): Record<string, unknown> {
|
|
34
|
-
return {
|
|
35
|
-
name: 'vendor.acme.factory',
|
|
36
|
-
version: '1.0.0',
|
|
37
|
-
kind: 'workflow-chain',
|
|
38
|
-
engines: { openwop: '^1' },
|
|
39
|
-
chains: [
|
|
40
|
-
{
|
|
41
|
-
chainId: 'acme.child-batch',
|
|
42
|
-
version: '1.0.0',
|
|
43
|
-
label: 'Child Batch (Factory child)',
|
|
44
|
-
description: 'Composition-only child fragment; composed by acme.factory.',
|
|
45
|
-
...(internal !== undefined ? { internal } : {}),
|
|
46
|
-
parameters: {},
|
|
47
|
-
dag: { nodes: [{ id: 'build', typeId: 'core.ai.callPrompt', config: {} }] },
|
|
48
|
-
},
|
|
49
|
-
],
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
describe('workflow-chain-internal-flag §A: corpus (RFC 0135, always-on)', () => {
|
|
54
|
-
it('the manifest §WorkflowChain declares a boolean `internal` property', () => {
|
|
55
|
-
const schema = JSON.parse(readFileSync(MANIFEST, 'utf8')) as {
|
|
56
|
-
$defs?: Record<string, { properties?: Record<string, { type?: string }> }>;
|
|
57
|
-
};
|
|
58
|
-
const internal = schema.$defs?.WorkflowChain?.properties?.internal;
|
|
59
|
-
expect(internal, cite('§WorkflowChain', 'internal property present')).toBeTruthy();
|
|
60
|
-
expect(internal?.type, cite('§WorkflowChain', 'internal is boolean')).toBe('boolean');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('a chain declaring internal: true validates; a non-boolean internal is rejected', () => {
|
|
64
|
-
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
65
|
-
addFormats(ajv);
|
|
66
|
-
const validate = ajv.compile(JSON.parse(readFileSync(MANIFEST, 'utf8')) as Record<string, unknown>);
|
|
67
|
-
expect(validate(packWith(true)), cite('§WorkflowChain', `internal:true validates: ${ajv.errorsText(validate.errors)}`)).toBe(true);
|
|
68
|
-
expect(validate(packWith(undefined)), cite('§WorkflowChain', 'absent internal stays valid (absent ⇒ false)')).toBe(true);
|
|
69
|
-
expect(validate(packWith('yes')), cite('§WorkflowChain', 'non-boolean internal rejected')).toBe(false);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it.skipIf(CHAIN_DOC === null)('the spec documents the MUST-omit-from-default-gallery + not-an-authz-boundary rules', () => {
|
|
73
|
-
const doc = readFileSync(CHAIN_DOC as string, 'utf8');
|
|
74
|
-
expect(doc.includes('Chain visibility (RFC 0135)'), cite('§Chain visibility', 'section present')).toBe(true);
|
|
75
|
-
expect(
|
|
76
|
-
/internal[\s\S]{0,600}MUST omit/i.test(doc),
|
|
77
|
-
cite('§Chain visibility', 'documents MUST omit from default listing'),
|
|
78
|
-
).toBe(true);
|
|
79
|
-
expect(
|
|
80
|
-
/NOT an authorization boundary/i.test(doc),
|
|
81
|
-
cite('§Chain visibility', 'documents internal is not an authz boundary'),
|
|
82
|
-
).toBe(true);
|
|
83
|
-
});
|
|
84
|
-
});
|