@kybernesis/kyberagent-extension-sdk 0.1.0-alpha

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.
@@ -0,0 +1,967 @@
1
+ import {
2
+ SeamPermissionDenied,
3
+ clearRawContributionIdUsages,
4
+ defineComposerCapability,
5
+ defineContextualAction,
6
+ defineDataEndpoint,
7
+ defineExtension,
8
+ defineSurface,
9
+ emitExtensionContributionSet,
10
+ gatedHostPort,
11
+ makeAgentId,
12
+ makeSeamCallContext,
13
+ parseContributionId,
14
+ rawContributionIdUsages,
15
+ requiresHostCapabilities,
16
+ requiresKernelPermissions,
17
+ validateDefinitionCrossRefs
18
+ } from "../chunk-ZDAZ7Q2A.js";
19
+
20
+ // src/testing/in-memory-host.ts
21
+ var InMemoryBrainProvider = class {
22
+ constructor(seed = {}) {
23
+ this.seed = seed;
24
+ }
25
+ seed;
26
+ async stats() {
27
+ return { entities: this.seed.entities?.length ?? 0, notes: this.seed.notes?.length ?? 0 };
28
+ }
29
+ async query(text, opts) {
30
+ const needle = text.trim().toLowerCase();
31
+ const hits = (this.seed.entities ?? []).filter((entity) => entity.title.toLowerCase().includes(needle)).map((entity, index) => ({ id: entity.id, title: entity.title, score: Number((1 - index * 0.1).toFixed(2)) }));
32
+ return typeof opts?.limit === "number" ? hits.slice(0, opts.limit) : hits;
33
+ }
34
+ };
35
+ var InMemoryHost = class {
36
+ brains = /* @__PURE__ */ new Map();
37
+ constructor(seed = {}) {
38
+ for (const [agentName, agentBrain] of Object.entries(seed)) {
39
+ this.brains.set(agentName, new InMemoryBrainProvider(agentBrain));
40
+ }
41
+ }
42
+ brain(agentName) {
43
+ let provider = this.brains.get(agentName);
44
+ if (!provider) {
45
+ provider = new InMemoryBrainProvider();
46
+ this.brains.set(agentName, provider);
47
+ }
48
+ return provider;
49
+ }
50
+ /**
51
+ * Inject a gated, ctx-bound `HostPort` — the GROWN contract an extension actually
52
+ * consumes (red-team A1/A3; P5-PORTS-1 brought forward as conformance-prep). The
53
+ * runner builds the raw port from this host's in-memory state, binds the active
54
+ * agent from `ctx`, then wraps it in `gatedHostPort(grants)` — the SAME gate
55
+ * production uses (SDK-N3). This is what makes C9 ("green against InMemoryHost")
56
+ * a real conformance check instead of a vacuous one.
57
+ */
58
+ inject(ctx, grants) {
59
+ const raw = {
60
+ ctx,
61
+ brain: this.brain(ctx.activeAgent),
62
+ context: makeInMemoryContextPort(ctx),
63
+ command: IN_MEMORY_COMMAND_PORT
64
+ // events omitted (ext-owned; supplied by the extension, not the host) — docs/63 D6
65
+ };
66
+ return gatedHostPort(raw, grants);
67
+ }
68
+ };
69
+ function makeInMemoryContextPort(ctx) {
70
+ return { snapshot: async () => ({ activeAgent: ctx.activeAgent }) };
71
+ }
72
+ var IN_MEMORY_COMMAND_PORT = {
73
+ describe: async () => [],
74
+ invoke: async (commandId, args) => ({
75
+ ok: true,
76
+ data: { invoked: commandId, args: args ?? {} }
77
+ })
78
+ };
79
+
80
+ // src/testing/conformance.ts
81
+ var ALL_GRANTS = /* @__PURE__ */ new Set([
82
+ "brain.read",
83
+ "host.context.read",
84
+ "host.command.invoke"
85
+ ]);
86
+ var NO_GRANTS = /* @__PURE__ */ new Set();
87
+ var GETTER_PERMISSION = {
88
+ brain: "brain.read",
89
+ context: "host.context.read",
90
+ command: "host.command.invoke"
91
+ };
92
+ function ctxFor(principal2, agent) {
93
+ return makeSeamCallContext({ principal: principal2, activeAgent: makeAgentId(agent), requestedAtMs: 0 });
94
+ }
95
+ function instrument(port) {
96
+ const touched = /* @__PURE__ */ new Set();
97
+ const wrapped = {
98
+ ctx: port.ctx,
99
+ get brain() {
100
+ touched.add(GETTER_PERMISSION.brain);
101
+ return port.brain;
102
+ },
103
+ get context() {
104
+ touched.add(GETTER_PERMISSION.context);
105
+ return port.context;
106
+ },
107
+ get command() {
108
+ touched.add(GETTER_PERMISSION.command);
109
+ return port.command;
110
+ },
111
+ events: port.events
112
+ };
113
+ return { port: wrapped, touched };
114
+ }
115
+ var EXERCISE_QUERY = { filter: { q: "" }, limit: 5 };
116
+ var EXERCISE_DESCRIBE = {};
117
+ var EXERCISE_INVOKE = { commandId: "conformance.noop", args: { q: "" } };
118
+ async function exercise(port, ctx) {
119
+ const out = {};
120
+ const p = port;
121
+ if (typeof p.query === "function") out.rows = await p.query(EXERCISE_QUERY, ctx);
122
+ if (typeof p.describe === "function") await p.describe(EXERCISE_DESCRIBE, ctx);
123
+ if (typeof p.invoke === "function") out.response = await p.invoke(EXERCISE_INVOKE, ctx);
124
+ return out;
125
+ }
126
+ async function collectUsage(subject, host) {
127
+ const ctx = ctxFor(subject.principal, "k-brain");
128
+ const { port, touched } = instrument(host.inject(ctx, ALL_GRANTS));
129
+ const contributions = subject.activate(port);
130
+ for (const ans of contributions.values()) {
131
+ try {
132
+ await exercise(ans, ctx);
133
+ } catch {
134
+ }
135
+ }
136
+ return touched;
137
+ }
138
+ function declaredHostPermissions(definition) {
139
+ const gated = new Set(Object.values(GETTER_PERMISSION));
140
+ return new Set((definition.permissions ?? []).filter((p) => gated.has(p)));
141
+ }
142
+ function firstDataEndpoint(contributions) {
143
+ for (const [id, port] of contributions) {
144
+ if (id.startsWith("data.")) return port;
145
+ }
146
+ return void 0;
147
+ }
148
+ var CHECKS = [
149
+ {
150
+ id: "C1",
151
+ title: "injection only \u2014 host reached only through the injected port",
152
+ disposition: "enforced",
153
+ open: "Inject a host port with ZERO grants and exercise the extension.",
154
+ brokenIf: "host-derived data comes back without going through the gated port (a reach for a global).",
155
+ async run({ subjects, negatives, host }) {
156
+ const notes = [];
157
+ const check = async (s) => {
158
+ const ctx = ctxFor(s.principal, "k-brain");
159
+ const port = host.inject(ctx, NO_GRANTS);
160
+ const contributions = s.activate(port);
161
+ let obtainedUngated = false;
162
+ for (const ans of contributions.values()) {
163
+ try {
164
+ const out = await exercise(ans, ctx);
165
+ const rowsBrainy = (out.rows ?? []).some((r) => "title" in r || "top" in r || "noteCount" in r);
166
+ const respBrainy = out.response?.ok === true && typeof out.response.data === "object" && out.response.data !== null && "activeAgent" in out.response.data;
167
+ if (rowsBrainy || respBrainy) obtainedUngated = true;
168
+ } catch (err) {
169
+ if (!(err instanceof SeamPermissionDenied)) throw err;
170
+ }
171
+ }
172
+ return { obtainedUngated };
173
+ };
174
+ for (const s of subjects) {
175
+ if (declaredHostPermissions(s.definition).size === 0) continue;
176
+ const { obtainedUngated } = await check(s);
177
+ if (obtainedUngated) notes.push(`${s.name}: returned host data with no grants \u2014 reached past the injected port`);
178
+ }
179
+ for (const s of negatives) {
180
+ const { obtainedUngated } = await check(s);
181
+ if (!obtainedUngated) notes.push(`negative ${s.name}: expected a global-reach violation, none detected`);
182
+ }
183
+ return { status: notes.length ? "fail" : "pass", notes };
184
+ }
185
+ },
186
+ {
187
+ id: "C2",
188
+ title: "context honoured \u2014 agent-scoped reads use ctx.activeAgent",
189
+ disposition: "enforced",
190
+ open: "Run the data endpoint under two different active agents (seeded differently).",
191
+ brokenIf: "the result is identical across agents (a re-derived/default agent, not ctx.activeAgent).",
192
+ async run({ subjects, negatives, host }) {
193
+ const notes = [];
194
+ const varies = async (s) => {
195
+ const a = ctxFor(s.principal, "k-brain");
196
+ const b = ctxFor(s.principal, "arcana");
197
+ const portA = firstDataEndpoint(s.activate(host.inject(a, ALL_GRANTS)));
198
+ const portB = firstDataEndpoint(s.activate(host.inject(b, ALL_GRANTS)));
199
+ if (!portA || !portB) return void 0;
200
+ const ra = JSON.stringify((await exercise(portA, a)).rows ?? []);
201
+ const rb = JSON.stringify((await exercise(portB, b)).rows ?? []);
202
+ return ra !== rb;
203
+ };
204
+ for (const s of subjects) {
205
+ const v = await varies(s);
206
+ if (v === false) notes.push(`${s.name}: data endpoint returned identical rows across agents \u2014 ignores ctx.activeAgent`);
207
+ }
208
+ for (const s of negatives) {
209
+ const v = await varies(s);
210
+ if (v !== false) notes.push(`negative ${s.name}: expected agent-blind output, but it varied (or had no data endpoint)`);
211
+ }
212
+ return { status: notes.length ? "fail" : "pass", notes };
213
+ }
214
+ },
215
+ {
216
+ id: "C3",
217
+ title: "declarations match usage \u2014 declared permissions \u21D4 host-port calls",
218
+ disposition: "enforced",
219
+ open: "Diff the declared host permissions against the getters the run actually touches.",
220
+ brokenIf: "a declared permission is never used, or a used getter was never declared.",
221
+ async run({ subjects, negatives, host }) {
222
+ const notes = [];
223
+ const mismatch = async (s) => {
224
+ const declared = declaredHostPermissions(s.definition);
225
+ const used = await collectUsage(s, host);
226
+ const unusedDecl = [...declared].filter((p) => !used.has(p));
227
+ const undeclUsed = [...used].filter((p) => !declared.has(p));
228
+ if (unusedDecl.length || undeclUsed.length) {
229
+ return `declared-not-used=[${unusedDecl.join(",")}] used-not-declared=[${undeclUsed.join(",")}]`;
230
+ }
231
+ return null;
232
+ };
233
+ for (const s of subjects) {
234
+ const m = await mismatch(s);
235
+ if (m) notes.push(`${s.name}: ${m}`);
236
+ }
237
+ for (const s of negatives) {
238
+ const m = await mismatch(s);
239
+ if (!m) notes.push(`negative ${s.name}: expected a declaration/usage mismatch, found none`);
240
+ }
241
+ return { status: notes.length ? "fail" : "pass", notes };
242
+ }
243
+ },
244
+ {
245
+ id: "C4",
246
+ title: "identity from context \u2014 never read extensionId from a payload",
247
+ disposition: "enforced",
248
+ open: "Probe identity with a FORGED extensionId in the payload; compare to ctx.principal.id.",
249
+ brokenIf: "the extension reports the forged payload id instead of ctx.principal.id.",
250
+ run({ subjects, negatives }) {
251
+ const notes = [];
252
+ const FORGED = "plugin.forged-impostor";
253
+ for (const s of subjects) {
254
+ const ctx = ctxFor(s.principal, "k-brain");
255
+ const reported = s.identityProbe(ctx, FORGED);
256
+ if (reported !== ctx.principal.id) notes.push(`${s.name}: reported identity "${reported}" \u2260 ctx.principal.id "${ctx.principal.id}"`);
257
+ }
258
+ for (const s of negatives) {
259
+ const ctx = ctxFor(s.principal, "k-brain");
260
+ const reported = s.identityProbe(ctx, FORGED);
261
+ if (reported === ctx.principal.id) notes.push(`negative ${s.name}: expected it to trust the forged payload id, but it used ctx`);
262
+ }
263
+ return { status: notes.length ? "fail" : "pass", notes };
264
+ }
265
+ },
266
+ {
267
+ id: "C5",
268
+ title: "gated access fails typed \u2014 missing grant \u21D2 seam.permission_denied",
269
+ disposition: "enforced",
270
+ open: "Access each declared host getter with the grant withheld.",
271
+ brokenIf: "it returns/throws anything other than a typed SeamPermissionDenied.",
272
+ run({ subjects, host }) {
273
+ const notes = [];
274
+ for (const s of subjects) {
275
+ const ctx = ctxFor(s.principal, "k-brain");
276
+ for (const perm of declaredHostPermissions(s.definition)) {
277
+ const getter = Object.keys(GETTER_PERMISSION).find(
278
+ (k) => GETTER_PERMISSION[k] === perm
279
+ );
280
+ if (!getter) continue;
281
+ const port = host.inject(ctx, NO_GRANTS);
282
+ try {
283
+ void port[getter];
284
+ notes.push(`${s.name}: accessing .${getter} without a grant did NOT throw`);
285
+ } catch (err) {
286
+ if (!(err instanceof SeamPermissionDenied) || err.permission !== perm) {
287
+ notes.push(`${s.name}: .${getter} threw ${String(err)} instead of SeamPermissionDenied(${perm})`);
288
+ }
289
+ }
290
+ }
291
+ }
292
+ return { status: notes.length ? "fail" : "pass", notes };
293
+ }
294
+ },
295
+ {
296
+ id: "C6",
297
+ title: "typed blocked cause \u2014 a withheld port carries a BlockedReason discriminant",
298
+ disposition: "enforced",
299
+ open: "Model an inactive activation result and read its blocked cause.",
300
+ brokenIf: "the cause is a bare 'blocked' string rather than a typed discriminant.",
301
+ run() {
302
+ const notes = [];
303
+ const withheld = [
304
+ { ok: false, blocked: { cause: "inactive", scope: "agent" } },
305
+ { ok: false, blocked: { cause: "missing-host-capability", missing: ["host.notifications"] } },
306
+ { ok: false, blocked: { cause: "denied-permission", denied: ["brain.read"] } },
307
+ { ok: false, blocked: { cause: "unmet-requirement", requirement: "connector-auth" } }
308
+ ];
309
+ const validCauses = /* @__PURE__ */ new Set(["inactive", "missing-host-capability", "denied-permission", "unmet-requirement"]);
310
+ for (const r of withheld) {
311
+ if (r.ok) {
312
+ notes.push("expected a withheld result");
313
+ continue;
314
+ }
315
+ const cause = r.blocked.cause;
316
+ if (typeof cause !== "string" || cause === "blocked" || !validCauses.has(cause)) {
317
+ notes.push(`blocked cause "${String(cause)}" is not a typed discriminant`);
318
+ }
319
+ }
320
+ return { status: notes.length ? "fail" : "pass", notes };
321
+ }
322
+ },
323
+ {
324
+ id: "C7",
325
+ title: "typed contributionId \u2014 built from { ext, name? }, not a raw string",
326
+ disposition: "enforced",
327
+ open: "Build every contribution through the define* builders and watch the deprecation registry.",
328
+ brokenIf: "a contributionId took the raw-string path (recorded by the deprecation bridge).",
329
+ run({ subjects, negatives }) {
330
+ const notes = [];
331
+ const raw = new Set(rawContributionIdUsages());
332
+ for (const s of subjects) {
333
+ for (const c of emitExtensionContributionSet(s.definition).contributions) {
334
+ if (raw.has(c.contributionId)) notes.push(`${s.name}: "${c.contributionId}" took the deprecated raw-string path`);
335
+ try {
336
+ const key = parseContributionId(c.contributionId);
337
+ if (key.owner !== "ext") notes.push(`${s.name}: "${c.contributionId}" is not an ext-owned id`);
338
+ } catch (err) {
339
+ notes.push(`${s.name}: "${c.contributionId}" is unparseable \u2014 ${String(err)}`);
340
+ }
341
+ }
342
+ }
343
+ for (const s of negatives) {
344
+ const ids = emitExtensionContributionSet(s.definition).contributions.map((c) => c.contributionId);
345
+ if (!ids.some((id) => raw.has(id))) notes.push(`negative ${s.name}: expected a raw-string contributionId, none recorded`);
346
+ }
347
+ return { status: notes.length ? "fail" : "pass", notes };
348
+ }
349
+ },
350
+ {
351
+ id: "C8",
352
+ title: "single-purpose kind \u2014 rendering kinds implement no answering port",
353
+ disposition: "enforced",
354
+ open: "Match each contribution kind against whether it provides an answering port.",
355
+ brokenIf: "a surface/settingsPanel also answers ops, or an answering kind provides no port.",
356
+ run({ subjects, negatives, host }) {
357
+ const notes = [];
358
+ const RENDERING = /* @__PURE__ */ new Set(["surface", "settingsPanel"]);
359
+ const ANSWERING = /* @__PURE__ */ new Set(["dataEndpoint", "composerCapability", "contextualAction"]);
360
+ const judge = (s) => {
361
+ const local = [];
362
+ const ctx = ctxFor(s.principal, "k-brain");
363
+ const ports = s.activate(host.inject(ctx, ALL_GRANTS));
364
+ for (const c of emitExtensionContributionSet(s.definition).contributions) {
365
+ const hasPort = ports.has(c.contributionId);
366
+ if (RENDERING.has(c.kind) && hasPort) local.push(`${c.contributionId} (${c.kind}) provides an answering port`);
367
+ if (ANSWERING.has(c.kind) && !hasPort) local.push(`${c.contributionId} (${c.kind}) provides NO answering port`);
368
+ }
369
+ return local;
370
+ };
371
+ for (const s of subjects) for (const n of judge(s)) notes.push(`${s.name}: ${n}`);
372
+ for (const s of negatives) {
373
+ if (judge(s).length === 0) notes.push(`negative ${s.name}: expected a kind/port mismatch, found none`);
374
+ }
375
+ return { status: notes.length ? "fail" : "pass", notes };
376
+ }
377
+ },
378
+ {
379
+ id: "C9",
380
+ title: "green against InMemoryHost \u2014 the full happy path runs clean",
381
+ disposition: "enforced",
382
+ open: "Activate with correct grants and call every answering port end-to-end.",
383
+ brokenIf: "any conformant subject throws or returns a malformed response on the happy path.",
384
+ async run({ subjects, host }) {
385
+ const notes = [];
386
+ for (const s of subjects) {
387
+ const ctx = ctxFor(s.principal, "k-brain");
388
+ const grants = new Set(s.definition.permissions ?? []);
389
+ try {
390
+ const ports = s.activate(host.inject(ctx, grants));
391
+ for (const [id, ans] of ports) {
392
+ const out = await exercise(ans, ctx);
393
+ if (out.response && out.response.ok !== true && out.response.ok !== false) {
394
+ notes.push(`${s.name}/${id}: response is not a typed SeamResponse`);
395
+ }
396
+ }
397
+ } catch (err) {
398
+ notes.push(`${s.name}: happy path threw ${String(err)}`);
399
+ }
400
+ }
401
+ return { status: notes.length ? "fail" : "pass", notes };
402
+ }
403
+ },
404
+ {
405
+ id: "C10",
406
+ title: "every host route honours activation + gate (incl. the drain side-door)",
407
+ disposition: "phase5",
408
+ open: "Drive each op \u2014 including subscription `drain` \u2014 through the production SeamRunner for an INACTIVE principal.",
409
+ brokenIf: "any route (especially drain) reaches the contribution without a typed denial.",
410
+ async run({ subjects, wiring }) {
411
+ const { runner } = wiring;
412
+ if (!runner) return pending("no production SeamRunner wired (Phase-5 daemon code)");
413
+ const notes = [];
414
+ const ops = ["query", "describe", "invoke", "subscribe", "drain"];
415
+ for (const s of subjects) {
416
+ const ctx = ctxFor(s.principal, "inactive-agent");
417
+ const verdict = runner.activate(s.principal.id, ctx);
418
+ if (verdict.ok) continue;
419
+ for (const op of ops) {
420
+ const res = await runner.route(op, { principalId: s.principal.id, contributionId: "data.x" }, void 0, ctx);
421
+ if (res.ok !== false) notes.push(`${s.name}: route op="${op}" did NOT yield a typed denial for an inactive principal`);
422
+ }
423
+ }
424
+ return { status: notes.length ? "fail" : "pass", notes };
425
+ }
426
+ },
427
+ {
428
+ id: "C11",
429
+ title: "exactly one activation authority",
430
+ disposition: "phase5",
431
+ open: "Ask the SeamRunner how many independent activation sets exist.",
432
+ brokenIf: "the count is not exactly 1 (a parallel set like bridge-sessions inactiveExtensions).",
433
+ run({ wiring }) {
434
+ const { runner } = wiring;
435
+ if (!runner) return pending("no production SeamRunner wired (Phase-5 daemon code)");
436
+ const n = runner.activationAuthorityCount();
437
+ return n === 1 ? { status: "pass", notes: [] } : { status: "fail", notes: [`activationAuthorityCount()=${n}, expected exactly 1`] };
438
+ }
439
+ },
440
+ {
441
+ id: "C12",
442
+ title: "scope is honoured \u2014 per-agent deactivation does not leak to other agents",
443
+ disposition: "phase5",
444
+ open: "Deactivate a principal for one agent and check another agent is still active.",
445
+ brokenIf: "the deactivation blocks other agents, or its scope isn't 'agent'.",
446
+ run({ subjects, wiring }) {
447
+ const { runner } = wiring;
448
+ if (!runner) return pending("no production SeamRunner wired (Phase-5 daemon code)");
449
+ const notes = [];
450
+ for (const s of subjects) {
451
+ const a = runner.activate(s.principal.id, ctxFor(s.principal, "agent-a"));
452
+ const b = runner.activate(s.principal.id, ctxFor(s.principal, "agent-b"));
453
+ if (!a.ok) {
454
+ if (a.blocked.cause === "inactive" && a.blocked.scope !== "agent") {
455
+ notes.push(`${s.name}: agent-a deactivation scope="${a.blocked.scope}", expected 'agent'`);
456
+ }
457
+ if (!b.ok) notes.push(`${s.name}: agent-a deactivation also blocked agent-b (scope leaked)`);
458
+ }
459
+ }
460
+ return { status: notes.length ? "fail" : "pass", notes };
461
+ }
462
+ },
463
+ {
464
+ id: "C13",
465
+ title: "builders validate ids \u2014 unknown permission/capability rejected loudly",
466
+ disposition: "enforced",
467
+ open: "Pass a bogus permission/capability id to the requires* builders.",
468
+ brokenIf: "an unknown id passes through silently instead of throwing.",
469
+ async run() {
470
+ const notes = [];
471
+ const { requiresKernelPermissions: requiresKernelPermissions2, requiresHostCapabilities: requiresHostCapabilities2 } = await import("../index.js");
472
+ const expectThrow = (fn, what) => {
473
+ try {
474
+ fn();
475
+ notes.push(`${what} did not reject an unknown id`);
476
+ } catch {
477
+ }
478
+ };
479
+ expectThrow(() => requiresKernelPermissions2("not.a.permission"), "requiresKernelPermissions");
480
+ expectThrow(() => requiresHostCapabilities2("host.not-real"), "requiresHostCapabilities");
481
+ try {
482
+ requiresKernelPermissions2("brain.read");
483
+ } catch (err) {
484
+ notes.push(`requiresKernelPermissions rejected a KNOWN id: ${String(err)}`);
485
+ }
486
+ return { status: notes.length ? "fail" : "pass", notes };
487
+ }
488
+ },
489
+ {
490
+ id: "C14",
491
+ title: 'one canonical "none" form for host capabilities',
492
+ disposition: "enforced",
493
+ open: "Build the same contribution three ways (omitted / [] / empty requires*) and compare the emitted capabilities.",
494
+ brokenIf: 'the three "no host capability" forms do not normalize to one canonical value.',
495
+ run() {
496
+ const omitted = defineDataEndpoint({ ext: "c14-probe", name: "omitted", displayName: "omitted" });
497
+ const empty = defineDataEndpoint({ ext: "c14-probe", name: "empty", displayName: "empty", requiredHostCapabilities: [] });
498
+ const call = defineDataEndpoint({
499
+ ext: "c14-probe",
500
+ name: "call",
501
+ displayName: "call",
502
+ requiredHostCapabilities: requiresHostCapabilities()
503
+ });
504
+ const forms = [omitted, empty, call].map((c) => JSON.stringify(c.requiredHostCapabilities ?? null));
505
+ const notes = new Set(forms).size === 1 ? [] : [`the three "none" forms normalize differently: ${forms.join(" | ")}`];
506
+ return { status: notes.length ? "fail" : "pass", notes };
507
+ }
508
+ },
509
+ {
510
+ id: "C15",
511
+ title: "contribution \u2286 extension declarations (kernel permissions)",
512
+ disposition: "enforced",
513
+ open: "Validate the references (must pass) and a definition whose contribution requires an undeclared kernel permission (must be flagged). Host capabilities are contribution-scoped (DIV-8), not subset-checked.",
514
+ brokenIf: "a conformant reference is flagged, or the over-reaching contribution is NOT flagged.",
515
+ run({ subjects, negatives, wiring }) {
516
+ const validate = wiring.validateDefinitionCrossRefs ?? validateDefinitionCrossRefs;
517
+ const notes = [];
518
+ for (const s of subjects) {
519
+ const errors = validate(s.definition);
520
+ if (errors.length) notes.push(`${s.name}: conformant extension flagged \u2014 ${errors.join("; ")}`);
521
+ }
522
+ for (const s of negatives) {
523
+ const errors = validate(s.definition);
524
+ if (errors.length === 0) notes.push(`negative ${s.name}: over-reaching contribution was NOT flagged`);
525
+ }
526
+ return { status: notes.length ? "fail" : "pass", notes };
527
+ }
528
+ },
529
+ {
530
+ id: "C16",
531
+ title: "one gate model per permission domain",
532
+ disposition: "enforced",
533
+ open: "Across subjects gating composer.capabilities.invoke, compare WHERE the permission is declared.",
534
+ brokenIf: "the same domain is gated at the extension level by some and the contribution level by others.",
535
+ run({ subjects, negatives }) {
536
+ const DOMAIN = "composer.capabilities.invoke";
537
+ const styleOf = (s) => {
538
+ if ((s.definition.permissions ?? []).includes(DOMAIN)) return "extension";
539
+ const usesAtContribution = emitExtensionContributionSet(s.definition).contributions.some(
540
+ (c) => (c.requiredKernelPermissions ?? []).includes(DOMAIN)
541
+ );
542
+ return usesAtContribution ? "contribution" : "none";
543
+ };
544
+ const styles = new Set(subjects.map(styleOf).filter((x) => x !== "none"));
545
+ const notes = [];
546
+ if (styles.size > 1) notes.push(`conformant subjects gate ${DOMAIN} inconsistently: ${[...styles].join(" + ")}`);
547
+ if (negatives.length) {
548
+ const allStyles = new Set([...subjects, ...negatives].map(styleOf).filter((x) => x !== "none"));
549
+ if (allStyles.size <= 1) notes.push("negative pair: expected inconsistent gate models across the domain, found uniform");
550
+ }
551
+ return { status: notes.length ? "fail" : "pass", notes };
552
+ }
553
+ },
554
+ {
555
+ id: "grant-store",
556
+ title: "grants come from the activation authority, not a hardcoded list",
557
+ disposition: "phase5",
558
+ open: "Read a principal's grants from the SeamRunner grant store.",
559
+ brokenIf: "grants are sourced from a startup constant rather than the per-principal store (DIV-2).",
560
+ run({ subjects, wiring }) {
561
+ const { runner } = wiring;
562
+ if (!runner) return pending("no production SeamRunner / grant store wired (Phase-5 daemon code)");
563
+ const notes = [];
564
+ for (const s of subjects) {
565
+ const grants = runner.grantsFor(s.principal.id, ctxFor(s.principal, "k-brain"));
566
+ if (!(grants instanceof Set) && typeof grants.has !== "function") {
567
+ notes.push(`${s.name}: grantsFor did not return a permission set`);
568
+ }
569
+ }
570
+ return { status: notes.length ? "fail" : "pass", notes };
571
+ }
572
+ },
573
+ {
574
+ id: "agentId-registry",
575
+ title: "agent id minted only for a registered, host-attested name",
576
+ disposition: "phase5",
577
+ open: "Mint an AgentId for a name that is NOT in the agent registry.",
578
+ brokenIf: "minting succeeds for an unregistered name (today makeAgentId rejects only blanks \u2014 HOST-N3).",
579
+ run({ wiring }) {
580
+ const { mintAttestedAgent, agentRegistry } = wiring;
581
+ if (!mintAttestedAgent || !agentRegistry) return pending("no registry-aware mint wired (Phase-5 host code, HOST-N3)");
582
+ const notes = [];
583
+ try {
584
+ mintAttestedAgent("definitely-not-a-registered-agent", agentRegistry);
585
+ notes.push("minted an AgentId for an UNREGISTERED name \u2014 registry validation missing");
586
+ } catch {
587
+ }
588
+ const known = [...agentRegistry][0];
589
+ if (known) {
590
+ try {
591
+ mintAttestedAgent(known, agentRegistry);
592
+ } catch (err) {
593
+ notes.push(`rejected a REGISTERED name "${known}": ${String(err)}`);
594
+ }
595
+ }
596
+ return { status: notes.length ? "fail" : "pass", notes };
597
+ }
598
+ }
599
+ ];
600
+ function pending(reason) {
601
+ return { status: "pending", notes: [reason] };
602
+ }
603
+ var DEFAULT_HOST = () => new InMemoryHost({
604
+ "k-brain": {
605
+ entities: [
606
+ { id: "1", title: "Acme Corp" },
607
+ { id: "2", title: "Acme Holdings" }
608
+ ],
609
+ notes: ["kb-note-1", "kb-note-2", "kb-note-3"]
610
+ },
611
+ arcana: {
612
+ entities: [{ id: "9", title: "Arcana Archive" }],
613
+ notes: ["arcana-only"]
614
+ }
615
+ });
616
+ async function runConformance(input) {
617
+ const host = input.host ?? DEFAULT_HOST();
618
+ const wiring = input.wiring ?? {};
619
+ const verdicts = [];
620
+ for (const check of CHECKS) {
621
+ const negatives = input.negatives.filter((n) => n.violates === check.id).map((n) => n.subject);
622
+ let outcome;
623
+ try {
624
+ outcome = await check.run({ subjects: input.subjects, negatives, wiring, host });
625
+ } catch (err) {
626
+ outcome = { status: "fail", notes: [`check threw: ${String(err)}`] };
627
+ }
628
+ verdicts.push({
629
+ id: check.id,
630
+ title: check.title,
631
+ disposition: check.disposition,
632
+ status: outcome.status,
633
+ notes: outcome.notes
634
+ });
635
+ }
636
+ const enforcedFailures = verdicts.filter((v) => v.disposition === "enforced" && v.status === "fail").length;
637
+ const phase5Pending = verdicts.filter((v) => v.disposition === "phase5" && v.status === "pending").length;
638
+ const phase5Regressions = verdicts.filter((v) => v.disposition === "phase5" && v.status === "fail").length;
639
+ const negativeGaps = 0;
640
+ return {
641
+ verdicts,
642
+ enforcedFailures,
643
+ phase5Pending,
644
+ phase5Regressions,
645
+ negativeGaps,
646
+ ok: enforcedFailures === 0 && phase5Regressions === 0
647
+ };
648
+ }
649
+ var ICON = { pass: "\u{1F7E2}", fail: "\u{1F534}", pending: "\u{1F7E1}" };
650
+ function renderReport(report, opts) {
651
+ const lines = [];
652
+ lines.push("");
653
+ lines.push(" C1\u2013C16 EXTENSION SDK CONFORMANCE (docs/65 \xA75)");
654
+ lines.push(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
655
+ for (const v of report.verdicts) {
656
+ const tag = v.disposition === "phase5" ? " [phase5]" : "";
657
+ lines.push(` ${ICON[v.status]} ${v.id.padEnd(16)} ${v.title}${tag}`);
658
+ for (const n of v.notes) lines.push(` \u21B3 ${n}`);
659
+ }
660
+ lines.push(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500");
661
+ lines.push(
662
+ ` enforced failures: ${report.enforcedFailures} phase5 pending: ${report.phase5Pending} phase5 regressions: ${report.phase5Regressions}`
663
+ );
664
+ const strictFail = (opts?.strict ?? false) && report.phase5Pending > 0;
665
+ if (report.ok && !strictFail) {
666
+ lines.push(" RESULT: GREEN \u2014 every enforced requirement holds (\u{1F7E1} rows are Phase-5 targets).");
667
+ } else {
668
+ lines.push(" RESULT: RED \u2014 see \u{1F534} rows above.");
669
+ }
670
+ lines.push("");
671
+ return lines.join("\n");
672
+ }
673
+
674
+ // src/testing/conformance-subjects.ts
675
+ clearRawContributionIdUsages();
676
+ var principal = (id) => ({ kind: "plugin", id, attestedBy: "session" });
677
+ var strArg = (v) => typeof v === "string" ? v : "";
678
+ var seamProbe = {
679
+ name: "seam-probe",
680
+ principal: principal("plugin.seam-probe"),
681
+ definition: defineExtension({
682
+ pluginId: "plugin.seam-probe",
683
+ displayName: "Seam Probe",
684
+ permissions: requiresKernelPermissions("brain.read", "host.context.read", "host.command.invoke"),
685
+ surfaces: [
686
+ defineSurface({
687
+ ext: "seam-probe",
688
+ displayName: "Seam Probe",
689
+ requiredHostCapabilities: [],
690
+ mount: "web/seam-probe",
691
+ hostSlots: ["workspace.surface"]
692
+ })
693
+ ],
694
+ dataEndpoints: [defineDataEndpoint({ ext: "seam-probe", name: "brain", displayName: "Seam Probe brain" })],
695
+ composerCapabilities: [
696
+ defineComposerCapability({ ext: "seam-probe", displayName: "Seam Probe composer", requiredHostCapabilities: [] })
697
+ ]
698
+ }),
699
+ activate(host) {
700
+ const map = /* @__PURE__ */ new Map();
701
+ map.set("data.seam-probe.brain", {
702
+ async query(payload, ctx) {
703
+ const hits = await host.brain.query(strArg(payload.filter?.q), { limit: payload.limit });
704
+ return hits.map((h) => ({ id: h.id, title: h.title, principalId: ctx.principal.id }));
705
+ }
706
+ });
707
+ map.set("composer.seam-probe", {
708
+ async describe() {
709
+ return { commands: [{ commandId: "seam-probe.ping", displayName: "Ping host" }] };
710
+ },
711
+ async invoke(payload, ctx) {
712
+ const snapshot = await host.context.snapshot();
713
+ const command = await host.command.invoke(payload.commandId, payload.args);
714
+ return { ok: true, data: { activeAgent: snapshot.activeAgent, principalId: ctx.principal.id, command } };
715
+ }
716
+ });
717
+ return map;
718
+ },
719
+ identityProbe: (ctx) => ctx.principal.id
720
+ };
721
+ var notesLens = {
722
+ name: "notes-lens",
723
+ principal: principal("plugin.notes-lens"),
724
+ definition: defineExtension({
725
+ pluginId: "plugin.notes-lens",
726
+ displayName: "Notes Lens",
727
+ permissions: requiresKernelPermissions("brain.read"),
728
+ surfaces: [
729
+ defineSurface({
730
+ ext: "notes-lens",
731
+ displayName: "Notes Lens",
732
+ requiredHostCapabilities: [],
733
+ mount: "web/notes-lens",
734
+ hostSlots: ["workspace.surface"]
735
+ })
736
+ ],
737
+ dataEndpoints: [defineDataEndpoint({ ext: "notes-lens", name: "recent", displayName: "Notes Lens recent" })],
738
+ contextualActions: [
739
+ defineContextualAction({
740
+ ext: "notes-lens",
741
+ name: "summarize",
742
+ displayName: "Summarize notes",
743
+ requiredHostCapabilities: []
744
+ })
745
+ ]
746
+ }),
747
+ activate(host) {
748
+ const map = /* @__PURE__ */ new Map();
749
+ map.set("data.notes-lens.recent", {
750
+ async query(payload, ctx) {
751
+ const hits = await host.brain.query(strArg(payload.filter?.q), { limit: payload.limit });
752
+ const stats = await host.brain.stats();
753
+ return [{ noteCount: stats.notes, top: hits[0]?.title ?? null, principalId: ctx.principal.id }];
754
+ }
755
+ });
756
+ map.set("contextualAction.notes-lens.summarize", {
757
+ async invoke(payload, ctx) {
758
+ const hits = await host.brain.query(strArg(payload.args?.q), {});
759
+ return { ok: true, data: { summary: hits.map((h) => h.title).join(", "), principalId: ctx.principal.id } };
760
+ }
761
+ });
762
+ return map;
763
+ },
764
+ identityProbe: (ctx) => ctx.principal.id
765
+ };
766
+ var REFERENCE_EXTENSIONS = [seamProbe, notesLens];
767
+ var emptyActivate = () => /* @__PURE__ */ new Map();
768
+ var globalReach = {
769
+ name: "global-reach",
770
+ principal: principal("plugin.global-reach"),
771
+ definition: defineExtension({
772
+ pluginId: "plugin.global-reach",
773
+ displayName: "Global Reach",
774
+ permissions: requiresKernelPermissions("brain.read"),
775
+ dataEndpoints: [defineDataEndpoint({ ext: "global-reach", name: "brain", displayName: "GR brain" })]
776
+ }),
777
+ activate() {
778
+ const map = /* @__PURE__ */ new Map();
779
+ map.set("data.global-reach.brain", {
780
+ async query(_payload, ctx) {
781
+ return [{ id: "leak", title: "Leaked Global Row", principalId: ctx.principal.id }];
782
+ }
783
+ });
784
+ return map;
785
+ },
786
+ identityProbe: (ctx) => ctx.principal.id
787
+ };
788
+ var agentBlind = {
789
+ name: "agent-blind",
790
+ principal: principal("plugin.agent-blind"),
791
+ definition: defineExtension({
792
+ pluginId: "plugin.agent-blind",
793
+ displayName: "Agent Blind",
794
+ permissions: requiresKernelPermissions("brain.read"),
795
+ dataEndpoints: [defineDataEndpoint({ ext: "agent-blind", name: "brain", displayName: "AB brain" })]
796
+ }),
797
+ activate() {
798
+ const map = /* @__PURE__ */ new Map();
799
+ map.set("data.agent-blind.brain", {
800
+ async query(_payload, ctx) {
801
+ return [{ id: "const", title: "CONSTANT", principalId: ctx.principal.id }];
802
+ }
803
+ });
804
+ return map;
805
+ },
806
+ identityProbe: (ctx) => ctx.principal.id
807
+ };
808
+ var underDeclared = {
809
+ name: "under-declared",
810
+ principal: principal("plugin.under-declared"),
811
+ definition: defineExtension({
812
+ pluginId: "plugin.under-declared",
813
+ displayName: "Under Declared",
814
+ permissions: [],
815
+ dataEndpoints: [defineDataEndpoint({ ext: "under-declared", name: "brain", displayName: "UD brain" })]
816
+ }),
817
+ activate(host) {
818
+ const map = /* @__PURE__ */ new Map();
819
+ map.set("data.under-declared.brain", {
820
+ async query(_payload, ctx) {
821
+ const hits = await host.brain.query("", {});
822
+ return hits.map((h) => ({ title: h.title, principalId: ctx.principal.id }));
823
+ }
824
+ });
825
+ return map;
826
+ },
827
+ identityProbe: (ctx) => ctx.principal.id
828
+ };
829
+ var overDeclared = {
830
+ name: "over-declared",
831
+ principal: principal("plugin.over-declared"),
832
+ definition: defineExtension({
833
+ pluginId: "plugin.over-declared",
834
+ displayName: "Over Declared",
835
+ permissions: requiresKernelPermissions("brain.read", "host.command.invoke"),
836
+ dataEndpoints: [defineDataEndpoint({ ext: "over-declared", name: "brain", displayName: "OD brain" })]
837
+ }),
838
+ activate(host) {
839
+ const map = /* @__PURE__ */ new Map();
840
+ map.set("data.over-declared.brain", {
841
+ async query(_payload, ctx) {
842
+ const hits = await host.brain.query("", {});
843
+ return hits.map((h) => ({ title: h.title, principalId: ctx.principal.id }));
844
+ }
845
+ });
846
+ return map;
847
+ },
848
+ identityProbe: (ctx) => ctx.principal.id
849
+ };
850
+ var payloadIdentity = {
851
+ name: "payload-identity",
852
+ principal: principal("plugin.payload-identity"),
853
+ definition: defineExtension({
854
+ pluginId: "plugin.payload-identity",
855
+ displayName: "Payload Identity",
856
+ dataEndpoints: [defineDataEndpoint({ ext: "payload-identity", name: "rows", displayName: "PI rows" })]
857
+ }),
858
+ activate: emptyActivate,
859
+ identityProbe: (_ctx, forgedExtensionId) => forgedExtensionId
860
+ };
861
+ var rawId = {
862
+ name: "raw-id",
863
+ principal: principal("plugin.raw-id"),
864
+ definition: defineExtension({
865
+ pluginId: "plugin.raw-id",
866
+ displayName: "Raw Id",
867
+ surfaces: [defineSurface({ contributionId: "surface.raw-id-thing", displayName: "Raw", requiredHostCapabilities: [] })]
868
+ }),
869
+ activate: emptyActivate,
870
+ identityProbe: (ctx) => ctx.principal.id
871
+ };
872
+ var dualKind = {
873
+ name: "dual-kind",
874
+ principal: principal("plugin.dual-kind"),
875
+ definition: defineExtension({
876
+ pluginId: "plugin.dual-kind",
877
+ displayName: "Dual Kind",
878
+ surfaces: [
879
+ defineSurface({ ext: "dual-kind", displayName: "Dual", requiredHostCapabilities: [], mount: "web/dual" })
880
+ ]
881
+ }),
882
+ activate() {
883
+ const map = /* @__PURE__ */ new Map();
884
+ map.set("surface.dual-kind", {
885
+ async query() {
886
+ return [];
887
+ }
888
+ });
889
+ return map;
890
+ },
891
+ identityProbe: (ctx) => ctx.principal.id
892
+ };
893
+ var overReachContribution = {
894
+ name: "over-reach-contribution",
895
+ principal: principal("plugin.over-reach"),
896
+ definition: defineExtension({
897
+ pluginId: "plugin.over-reach",
898
+ displayName: "Over Reach",
899
+ permissions: [],
900
+ // extension-level: declares NO kernel permissions
901
+ dataEndpoints: [
902
+ defineDataEndpoint({
903
+ ext: "over-reach",
904
+ name: "brain",
905
+ displayName: "OR brain",
906
+ requiredKernelPermissions: ["brain.read"]
907
+ // contribution-level: reaches for one ungranted
908
+ })
909
+ ]
910
+ }),
911
+ activate: emptyActivate,
912
+ identityProbe: (ctx) => ctx.principal.id
913
+ };
914
+ var composerExtensionGated = {
915
+ name: "composer-extension-gated",
916
+ principal: principal("plugin.composer-ext-gated"),
917
+ definition: defineExtension({
918
+ pluginId: "plugin.composer-ext-gated",
919
+ displayName: "Composer Extension Gated",
920
+ permissions: requiresKernelPermissions("composer.capabilities.invoke"),
921
+ composerCapabilities: [
922
+ defineComposerCapability({ ext: "ext-gated", displayName: "Ext gated", requiredHostCapabilities: [] })
923
+ ]
924
+ }),
925
+ activate: emptyActivate,
926
+ identityProbe: (ctx) => ctx.principal.id
927
+ };
928
+ var composerContributionGated = {
929
+ name: "composer-contribution-gated",
930
+ principal: principal("plugin.composer-contrib-gated"),
931
+ definition: defineExtension({
932
+ pluginId: "plugin.composer-contrib-gated",
933
+ displayName: "Composer Contribution Gated",
934
+ composerCapabilities: [
935
+ defineComposerCapability({
936
+ ext: "contrib-gated",
937
+ displayName: "Contrib gated",
938
+ requiredHostCapabilities: [],
939
+ requiredKernelPermissions: ["composer.capabilities.invoke"]
940
+ // gated at the WRONG level vs the pair
941
+ })
942
+ ]
943
+ }),
944
+ activate: emptyActivate,
945
+ identityProbe: (ctx) => ctx.principal.id
946
+ };
947
+ var NEGATIVE_FIXTURES = [
948
+ { violates: "C1", subject: globalReach },
949
+ { violates: "C2", subject: agentBlind },
950
+ { violates: "C3", subject: underDeclared },
951
+ { violates: "C3", subject: overDeclared },
952
+ { violates: "C4", subject: payloadIdentity },
953
+ { violates: "C7", subject: rawId },
954
+ { violates: "C8", subject: dualKind },
955
+ { violates: "C15", subject: overReachContribution },
956
+ { violates: "C16", subject: composerExtensionGated },
957
+ { violates: "C16", subject: composerContributionGated }
958
+ ];
959
+ export {
960
+ InMemoryBrainProvider,
961
+ InMemoryHost,
962
+ NEGATIVE_FIXTURES,
963
+ REFERENCE_EXTENSIONS,
964
+ renderReport,
965
+ runConformance
966
+ };
967
+ //# sourceMappingURL=index.js.map