@passioncode-ai/fabric-agent-adapter 0.32.1 → 0.34.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 +90 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +3 -3
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/profile-selection.md +4 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +4 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +35 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/profiles.md +26 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +129 -43
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-profiles.mjs +77 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +192 -50
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +126 -25
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +8 -4
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +1 -1
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs
CHANGED
|
@@ -26,6 +26,41 @@ export const FLAGS = Object.freeze({ UP: 0x01, UV: 0x04, BE: 0x08, BS: 0x10, AT:
|
|
|
26
26
|
const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
27
27
|
const isString = (value) => typeof value === 'string';
|
|
28
28
|
const list = (value) => (Array.isArray(value) ? value.filter(isObject) : []);
|
|
29
|
+
/** The object items of an array with their index as written, so an instancePath names the item even after a non-object. */
|
|
30
|
+
const indexed = (value) => (Array.isArray(value) ? value.flatMap((item, index) => (isObject(item) ? [[item, index]] : [])) : []);
|
|
31
|
+
/** The contract's `show`: String(value), or its JSON where String() throws (an object whose own `toString` is data). */
|
|
32
|
+
const show = (value) => {
|
|
33
|
+
if (typeof value === 'string') return value;
|
|
34
|
+
try {
|
|
35
|
+
return String(value);
|
|
36
|
+
} catch {
|
|
37
|
+
return JSON.stringify(value) ?? typeof value;
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
/** The contract's `toNumber`: Number(value), or NaN where Number() throws. */
|
|
41
|
+
const toNumber = (value) => {
|
|
42
|
+
try {
|
|
43
|
+
return Number(value);
|
|
44
|
+
} catch {
|
|
45
|
+
return Number.NaN;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
/** DEC-0082: the personal context, and an organization's context `org:<orgId>` (the contract's `context-route.ts`). */
|
|
49
|
+
export const PERSONAL_CONTEXT = 'personal';
|
|
50
|
+
export const ORG_CONTEXT_PREFIX = 'org:';
|
|
51
|
+
const ORG_ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
52
|
+
/** The context `value` names, or undefined: `personal` exactly, or `org:` and an organization id; compared as written. */
|
|
53
|
+
export function parseContext(value) {
|
|
54
|
+
if (typeof value !== 'string') return undefined;
|
|
55
|
+
if (value === PERSONAL_CONTEXT) return { kind: 'personal' };
|
|
56
|
+
if (!value.startsWith(ORG_CONTEXT_PREFIX)) return undefined;
|
|
57
|
+
const org = value.slice(ORG_CONTEXT_PREFIX.length);
|
|
58
|
+
return ORG_ID.test(org) ? { kind: 'org', org } : undefined;
|
|
59
|
+
}
|
|
60
|
+
/** A placement's or a secret placement's context: its `context`, `personal` when absent (DEC-0082). */
|
|
61
|
+
const contextOf = (entry) => (entry.context === undefined ? PERSONAL_CONTEXT : entry.context);
|
|
62
|
+
/** A collision-free key over JSON values. */
|
|
63
|
+
const keyOf = (...parts) => JSON.stringify(parts);
|
|
29
64
|
const sha256 = (bytes) => crypto.createHash('sha256').update(bytes).digest();
|
|
30
65
|
const B64URL = /^[A-Za-z0-9_-]+$/;
|
|
31
66
|
const bytes = (text) => Buffer.from(text, 'base64url');
|
|
@@ -255,10 +290,13 @@ export function verifyEstateRecord(record, trust) {
|
|
|
255
290
|
return { ok: true, digest, revision: record.revision, signedBy: credential.credentialId, signCount: assertion.signCount };
|
|
256
291
|
}
|
|
257
292
|
|
|
258
|
-
/**
|
|
259
|
-
|
|
293
|
+
/**
|
|
294
|
+
* What a package verifier trusts for a package placed on a machine in a context (DEC-0082; `personal` when not given);
|
|
295
|
+
* undefined when the record places no such agent there in that context.
|
|
296
|
+
*/
|
|
297
|
+
export function placementTrust(record, machineId, agentRef, context = PERSONAL_CONTEXT) {
|
|
260
298
|
const machine = list(record.machines).find((entry) => entry.id === machineId);
|
|
261
|
-
const placement = list(machine?.packages).find((entry) => entry.agentRef === agentRef);
|
|
299
|
+
const placement = list(machine?.packages).find((entry) => entry.agentRef === agentRef && contextOf(entry) === context);
|
|
262
300
|
if (!placement) return undefined;
|
|
263
301
|
return { publisher: list(record.publisherKeys).filter((key) => key.kid === placement.publisherKid) };
|
|
264
302
|
}
|
|
@@ -335,7 +373,7 @@ function revisionChain(value) {
|
|
|
335
373
|
const parent = isObject(value.parent) ? value.parent : {};
|
|
336
374
|
const next = isObject(value.next) ? value.next : {};
|
|
337
375
|
if (next.estate !== parent.estate) flag('/next/estate', "a revision continues its parent's estate");
|
|
338
|
-
if (next.revision !==
|
|
376
|
+
if (next.revision !== toNumber(parent.revision) + 1) flag('/next/revision', `the revision after ${show(parent.revision)} is ${toNumber(parent.revision) + 1}: a revision never skips or repeats one`);
|
|
339
377
|
let parentDigest;
|
|
340
378
|
try {
|
|
341
379
|
parentDigest = estateRecordDigest(parent);
|
|
@@ -345,14 +383,14 @@ function revisionChain(value) {
|
|
|
345
383
|
if (next.parent !== parentDigest) flag('/next/parent', 'parent names the digest of the revision before it');
|
|
346
384
|
if (estateTime(next.issuedAt) < estateTime(parent.issuedAt)) flag('/next/issuedAt', 'a revision is never issued before its parent');
|
|
347
385
|
const kept = new Set(list(next.revocations).map(revocationKey));
|
|
348
|
-
|
|
349
|
-
if (!kept.has(revocationKey(entry))) flag(`/parent/revocations/${index}`, `the ${
|
|
386
|
+
indexed(parent.revocations).forEach(([entry, index]) => {
|
|
387
|
+
if (!kept.has(revocationKey(entry))) flag(`/parent/revocations/${index}`, `the ${show(entry.kind)} revocation of ${show(entry.ref)} is dropped: a revision keeps every revocation before it`);
|
|
350
388
|
});
|
|
351
389
|
return findings;
|
|
352
390
|
}
|
|
353
391
|
|
|
354
392
|
const validDeviceKey = (key) => {
|
|
355
|
-
const raw = Buffer.from(
|
|
393
|
+
const raw = Buffer.from(show(key.publicKey ?? ''), 'base64');
|
|
356
394
|
return key.alg === 'ed25519' ? raw.length === 32 && !smallOrder(raw) : isValidP256Key(raw);
|
|
357
395
|
};
|
|
358
396
|
|
|
@@ -360,69 +398,73 @@ function keys(value, flag) {
|
|
|
360
398
|
const ids = new Set();
|
|
361
399
|
const hubKids = new Set();
|
|
362
400
|
const hubKeys = new Set();
|
|
363
|
-
|
|
401
|
+
indexed(value.machines).forEach(([machine, m]) => {
|
|
364
402
|
const at = `/machines/${m}`;
|
|
365
|
-
if (ids.has(machine.id)) flag(`${at}/id`, `machine ${
|
|
403
|
+
if (ids.has(machine.id)) flag(`${at}/id`, `machine ${show(machine.id)} is listed twice`);
|
|
366
404
|
ids.add(machine.id);
|
|
367
405
|
const deviceKeys = list(machine.deviceKeys);
|
|
368
406
|
if (deviceKeys.filter((key) => key.use === 'sig').length !== 1) flag(`${at}/deviceKeys`, 'a machine has exactly one signing device key, <id>#sig');
|
|
369
407
|
if (deviceKeys.filter((key) => key.use === 'kem').length > 1) flag(`${at}/deviceKeys`, 'a machine has at most one key-agreement device key, <id>#kem');
|
|
370
408
|
const seen = new Set();
|
|
371
409
|
deviceKeys.forEach((key, k) => {
|
|
372
|
-
if (key.kid !== `${
|
|
410
|
+
if (key.kid !== `${show(machine.id)}#${show(key.use)}`) flag(`${at}/deviceKeys/${k}/kid`, 'a device key is listed as <machine id>#sig or <machine id>#kem, by its role');
|
|
373
411
|
if (!validDeviceKey(key)) flag(`${at}/deviceKeys/${k}/publicKey`, 'an Ed25519 key is never of small order (DEC-0045) and a P-256 key is a valid point (SEC 1 §3.2.2)');
|
|
374
412
|
if (seen.has(key.publicKey)) flag(`${at}/deviceKeys/${k}/publicKey`, 'one key is never used for the other role');
|
|
375
413
|
seen.add(key.publicKey);
|
|
376
414
|
});
|
|
377
415
|
if (isObject(machine.hubKey)) {
|
|
378
416
|
const hub = machine.hubKey;
|
|
379
|
-
if (hubKids.has(hub.kid)) flag(`${at}/hubKey/kid`, `hub key ${
|
|
417
|
+
if (hubKids.has(hub.kid)) flag(`${at}/hubKey/kid`, `hub key ${show(hub.kid)} is certified for two machines: one kid per machine`);
|
|
380
418
|
hubKids.add(hub.kid);
|
|
381
419
|
if (hubKeys.has(hub.x)) flag(`${at}/hubKey/x`, 'one hub public key is certified for two machines under two kids: one key per machine');
|
|
382
420
|
hubKeys.add(hub.x);
|
|
383
|
-
if (smallOrder(Buffer.from(
|
|
421
|
+
if (smallOrder(Buffer.from(show(hub.x ?? ''), 'base64url'))) flag(`${at}/hubKey/x`, 'a hub key of small order verifies forged tokens and is never certified (DEC-0045)');
|
|
384
422
|
}
|
|
385
423
|
});
|
|
386
424
|
const credentials = new Set();
|
|
387
|
-
|
|
425
|
+
indexed(value.authorities).forEach(([authority, index]) => {
|
|
388
426
|
if (credentials.has(authority.credentialId)) flag(`/authorities/${index}/credentialId`, 'a passkey is listed once');
|
|
389
427
|
credentials.add(authority.credentialId);
|
|
390
|
-
if ('problem' in credentialKey(authority.publicKey,
|
|
428
|
+
if ('problem' in credentialKey(authority.publicKey, show(authority.alg))) flag(`/authorities/${index}/publicKey`, "a passkey's public key is a valid P-256 point for ES256 or an Ed25519 key of full order for EdDSA");
|
|
391
429
|
});
|
|
392
430
|
}
|
|
393
431
|
|
|
394
432
|
function placements(value, flag) {
|
|
395
433
|
const kids = new Set();
|
|
396
|
-
|
|
397
|
-
if (kids.has(key.kid)) flag(`/publisherKeys/${index}/kid`, `publisher key ${
|
|
434
|
+
indexed(value.publisherKeys).forEach(([key, index]) => {
|
|
435
|
+
if (kids.has(key.kid)) flag(`/publisherKeys/${index}/kid`, `publisher key ${show(key.kid)} is pinned twice`);
|
|
398
436
|
kids.add(key.kid);
|
|
399
437
|
});
|
|
400
|
-
|
|
438
|
+
indexed(value.machines).forEach(([machine, m]) => {
|
|
401
439
|
const agents = new Set();
|
|
402
|
-
|
|
440
|
+
indexed(machine.packages).forEach(([placement, p]) => {
|
|
403
441
|
const at = `/machines/${m}/packages/${p}`;
|
|
404
|
-
|
|
405
|
-
agents.
|
|
406
|
-
|
|
442
|
+
const key = keyOf(placement.agentRef, contextOf(placement));
|
|
443
|
+
if (agents.has(key)) flag(`${at}/agentRef`, `${show(placement.agentRef)} is placed twice on one machine in the context ${show(contextOf(placement))}`);
|
|
444
|
+
agents.add(key);
|
|
445
|
+
if (!kids.has(placement.publisherKid)) flag(`${at}/publisherKid`, `publisher key ${show(placement.publisherKid)} is not in publisherKeys, so the machine pins no key to verify the package under`);
|
|
407
446
|
});
|
|
408
447
|
});
|
|
409
448
|
}
|
|
410
449
|
|
|
411
450
|
function secretReferences(value, flag) {
|
|
412
451
|
const machineIds = new Set(list(value.machines).map((machine) => machine.id));
|
|
413
|
-
|
|
414
|
-
const placed = new Set(list(machine.packages).map((placement) => placement.agentRef));
|
|
452
|
+
indexed(value.machines).forEach(([machine, m]) => {
|
|
453
|
+
const placed = new Set(list(machine.packages).map((placement) => keyOf(placement.agentRef, contextOf(placement))));
|
|
415
454
|
const slots = new Set();
|
|
416
|
-
|
|
455
|
+
indexed(machine.secrets).forEach(([secret, s]) => {
|
|
417
456
|
const at = `/machines/${m}/secrets/${s}`;
|
|
418
|
-
|
|
457
|
+
// The message never repeats the value, and no message of this rule repeats a secret placement's context.
|
|
458
|
+
for (const field of ['slot', 'from', 'agentRef', 'context']) {
|
|
419
459
|
const text = secret[field];
|
|
420
460
|
if (isString(text) && secretValue(text)) flag(`${at}/${field}`, 'a secret placement names a slot, never a value: this looks like a credential');
|
|
421
461
|
}
|
|
422
|
-
if (!placed.has(secret.agentRef)) flag(`${at}/agentRef`,
|
|
462
|
+
if (!placed.has(keyOf(secret.agentRef, contextOf(secret)))) flag(`${at}/agentRef`, "a secret goes only where a placed package declares it (v4 Path 2): this agent is not placed on this machine in the secret's context");
|
|
423
463
|
if (!machineIds.has(secret.from)) flag(`${at}/from`, 'the slot is filled from a machine of this estate');
|
|
424
|
-
|
|
425
|
-
if (
|
|
464
|
+
// An envelope names no context (DEC-0082), so an organization's instance fills its slot on its own machine.
|
|
465
|
+
else if (contextOf(secret) !== PERSONAL_CONTEXT && secret.from !== machine.id) flag(`${at}/from`, "an organization's instance fills its slot from its own machine's vault, never by an envelope from another machine");
|
|
466
|
+
const key = keyOf(secret.agentRef, secret.slot, contextOf(secret));
|
|
467
|
+
if (slots.has(key)) flag(`${at}/slot`, `slot ${show(secret.slot)} is placed twice for one agent in one context on one machine`);
|
|
426
468
|
slots.add(key);
|
|
427
469
|
});
|
|
428
470
|
});
|
|
@@ -432,10 +474,10 @@ function grantsAndRevocations(value, flag) {
|
|
|
432
474
|
// A grant's id is a URI, so grants and their revocations are keyed by its normal form (DEC-0076).
|
|
433
475
|
const grantKey = (id) => (typeof id === 'string' ? normalIdentifier(id) : id);
|
|
434
476
|
const grantIds = new Set();
|
|
435
|
-
|
|
477
|
+
indexed(value.shareGrants).forEach(([grant, index]) => {
|
|
436
478
|
if (grant.principalKind !== 'external') flag(`/shareGrants/${index}/principalKind`, 'a share grant is to a principal outside the estate: principalKind external');
|
|
437
479
|
const id = isObject(grant.meta) ? grant.meta.id : undefined;
|
|
438
|
-
if (grantIds.has(grantKey(id))) flag(`/shareGrants/${index}/meta/id`, `grant ${
|
|
480
|
+
if (grantIds.has(grantKey(id))) flag(`/shareGrants/${index}/meta/id`, `grant ${show(id)} is listed twice`);
|
|
439
481
|
grantIds.add(grantKey(id));
|
|
440
482
|
});
|
|
441
483
|
const machines = list(value.machines);
|
|
@@ -450,12 +492,50 @@ function grantsAndRevocations(value, flag) {
|
|
|
450
492
|
grant: grantIds,
|
|
451
493
|
}));
|
|
452
494
|
const seen = new Set();
|
|
453
|
-
|
|
495
|
+
indexed(value.revocations).forEach(([revocation, index]) => {
|
|
454
496
|
const ref = revocation.kind === 'grant' ? grantKey(revocation.ref) : revocation.ref;
|
|
455
|
-
const key = `${
|
|
456
|
-
if (seen.has(key)) flag(`/revocations/${index}`, `the ${
|
|
497
|
+
const key = `${show(revocation.kind)} ${show(ref)}`;
|
|
498
|
+
if (seen.has(key)) flag(`/revocations/${index}`, `the ${show(revocation.kind)} ${show(revocation.ref)} is revoked twice`);
|
|
457
499
|
seen.add(key);
|
|
458
|
-
if (listed.get(
|
|
500
|
+
if (listed.get(show(revocation.kind))?.has(ref)) flag(`/revocations/${index}/ref`, `the record still lists the ${show(revocation.kind)} it revokes: revocation outranks addition`);
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* FAC-SEM-110 (DEC-0082): a grant to a principal outside the estate (`principalKind: external`) is used in that
|
|
506
|
+
* principal's organization, so its `context` is never `personal`; absent means not stated.
|
|
507
|
+
*/
|
|
508
|
+
export function grantContextFindings(grant, at) {
|
|
509
|
+
if (!isObject(grant) || grant.principalKind !== 'external' || grant.context !== PERSONAL_CONTEXT) return [];
|
|
510
|
+
return [{ code: 'FAC-SEM-110', instancePath: `${at}/context`, message: "a grant to a principal outside the estate is used in that principal's organization, never in the personal context: name the organization's org:<orgId>, or leave context out" }];
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* FAC-SEM-111 (DEC-0082) for one placement: an instance in an organization's context names, in `grant`, a share grant
|
|
515
|
+
* of the same record (by the normal form of its id, DEC-0076) whose `context` is the placement's, as written, and that
|
|
516
|
+
* the record does not revoke; a personal instance names none; a malformed context fails closed.
|
|
517
|
+
*/
|
|
518
|
+
function placementGrantProblems(record, placement) {
|
|
519
|
+
const context = contextOf(placement);
|
|
520
|
+
if (context === PERSONAL_CONTEXT) {
|
|
521
|
+
return placement.grant === undefined ? [] : [['/grant', "a personal instance runs under no share grant: only a placement in an organization's context names one"]];
|
|
522
|
+
}
|
|
523
|
+
if (typeof placement.grant !== 'string') return [['/grant', `an instance placed in the context ${show(context)} names the share grant it runs under, by its id`]];
|
|
524
|
+
const id = normalIdentifier(placement.grant);
|
|
525
|
+
const grants = list(record.shareGrants).filter((grant) => isObject(grant.meta) && typeof grant.meta.id === 'string' && normalIdentifier(grant.meta.id) === id);
|
|
526
|
+
const problems = [];
|
|
527
|
+
if (grants.length === 0) problems.push(['/grant', `the record has no share grant ${placement.grant}`]);
|
|
528
|
+
else if (parseContext(context)?.kind !== 'org') problems.push(['/context', `${show(context)} is not an organization's context id (org:<orgId>), so no share grant matches it`]);
|
|
529
|
+
else if (!grants.some((grant) => grant.context === context)) problems.push(['/grant', `the share grant ${placement.grant} is not for the context ${show(context)}: its context must be the placement's, as written`]);
|
|
530
|
+
if (list(record.revocations).some((entry) => entry.kind === 'grant' && typeof entry.ref === 'string' && normalIdentifier(entry.ref) === id)) problems.push(['/grant', `the record revokes the share grant ${placement.grant}, so no instance runs under it`]);
|
|
531
|
+
return problems;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function placementGrants(value, flag) {
|
|
535
|
+
indexed(value.machines).forEach(([machine, m]) => {
|
|
536
|
+
indexed(machine.packages).forEach(([placement, p]) => {
|
|
537
|
+
for (const [suffix, message] of placementGrantProblems(value, placement)) flag(`/machines/${m}/packages/${p}${suffix}`, message);
|
|
538
|
+
});
|
|
459
539
|
});
|
|
460
540
|
}
|
|
461
541
|
|
|
@@ -466,8 +546,10 @@ function recordFindings(value) {
|
|
|
466
546
|
placements(value, flagger(findings, 'FAC-SEM-074'));
|
|
467
547
|
secretReferences(value, flagger(findings, 'FAC-SEM-075'));
|
|
468
548
|
grantsAndRevocations(value, flagger(findings, 'FAC-SEM-076'));
|
|
549
|
+
indexed(value.shareGrants).forEach(([grant, index]) => findings.push(...grantContextFindings(grant, `/shareGrants/${index}`)));
|
|
550
|
+
placementGrants(value, flagger(findings, 'FAC-SEM-111'));
|
|
469
551
|
// FAC-SEM-092 (DEC-0063) on each passkey the record lists.
|
|
470
|
-
|
|
552
|
+
indexed(value.authorities).forEach(([authority, index]) => signingCredential(authority, `/authorities/${index}`, flagger(findings, 'FAC-SEM-092')));
|
|
471
553
|
return findings;
|
|
472
554
|
}
|
|
473
555
|
|
|
@@ -480,8 +562,7 @@ function installation(value) {
|
|
|
480
562
|
const findings = [];
|
|
481
563
|
const record = isObject(value.record) ? value.record : {};
|
|
482
564
|
const pkg = isObject(value.package) ? value.package : {};
|
|
483
|
-
const machineIndex =
|
|
484
|
-
const machine = list(record.machines)[machineIndex];
|
|
565
|
+
const [machine, machineIndex] = indexed(record.machines).find(([entry]) => entry.id === value.machine) ?? [undefined, -1];
|
|
485
566
|
let digest;
|
|
486
567
|
try {
|
|
487
568
|
digest = packageDigest(pkg);
|
|
@@ -490,18 +571,23 @@ function installation(value) {
|
|
|
490
571
|
}
|
|
491
572
|
const flag074 = flagger(findings, 'FAC-SEM-074');
|
|
492
573
|
if (!machine) {
|
|
493
|
-
flag074('/machine', `machine ${
|
|
574
|
+
flag074('/machine', `machine ${show(value.machine)} is not in the estate record`);
|
|
494
575
|
return findings;
|
|
495
576
|
}
|
|
496
577
|
if (digest === undefined || digest !== pkg.digest) flag074('/package/digest', 'the package is not the one its digest names: it changed after it was packed');
|
|
497
|
-
const
|
|
498
|
-
|
|
499
|
-
|
|
578
|
+
const context = contextOf(value);
|
|
579
|
+
const [placement, placementIndex] = indexed(machine.packages).find(([entry]) => entry.agentRef === pkg.agentRef && contextOf(entry) === context) ?? [undefined, -1];
|
|
580
|
+
if (!placement) flag074('/package/agentRef', `the record places no ${show(pkg.agentRef)} on machine ${show(value.machine)} in the context ${show(context)}`);
|
|
581
|
+
else if (placement.digest !== digest) flag074('/package', `the record places ${show(placement.digest)} of this agent on this machine in this context, not this package`);
|
|
500
582
|
const declared = new Set(list(pkg.secrets).map((slot) => slot.name));
|
|
501
583
|
const flag075 = flagger(findings, 'FAC-SEM-075');
|
|
502
|
-
|
|
503
|
-
if (secret.agentRef === pkg.agentRef && !declared.has(secret.slot)) flag075(`/record/machines/${machineIndex}/secrets/${index}/slot`, `the package declares no slot ${
|
|
584
|
+
indexed(machine.secrets).forEach(([secret, index]) => {
|
|
585
|
+
if (secret.agentRef === pkg.agentRef && contextOf(secret) === context && !declared.has(secret.slot)) flag075(`/record/machines/${machineIndex}/secrets/${index}/slot`, `the package declares no slot ${show(secret.slot)}: a secret goes only where a placed package declares it`);
|
|
504
586
|
});
|
|
587
|
+
if (placement) {
|
|
588
|
+
const flag111 = flagger(findings, 'FAC-SEM-111');
|
|
589
|
+
for (const [suffix, message] of placementGrantProblems(record, placement)) flag111(`/record/machines/${machineIndex}/packages/${placementIndex}${suffix}`, message);
|
|
590
|
+
}
|
|
505
591
|
return findings;
|
|
506
592
|
}
|
|
507
593
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-profiles.mjs
CHANGED
|
@@ -21,6 +21,21 @@ export class ProfileError extends Error {}
|
|
|
21
21
|
const isObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
22
22
|
const list = (value) => (Array.isArray(value) ? value.filter(isObject) : []);
|
|
23
23
|
const strings = (value) => (Array.isArray(value) ? value.filter((item) => typeof item === 'string') : []);
|
|
24
|
+
/** The object items of an array with their index as written, so an instancePath names the item even after a non-object. */
|
|
25
|
+
const indexed = (value) => (Array.isArray(value) ? value.flatMap((item, index) => (isObject(item) ? [[item, index]] : [])) : []);
|
|
26
|
+
/** DEC-0083: the model class of a model whose vendor publishes its weights openly — descriptive: a policy admits and
|
|
27
|
+
* excludes nothing by it — and what a lane reads when it reads what a member, a client or the public sends. */
|
|
28
|
+
export const OPEN_VENDOR = 'open-vendor';
|
|
29
|
+
export const USER_CONTENT = 'user-content';
|
|
30
|
+
/** The contract's `show`: String(value), or its JSON where String() throws (an object whose own `toString` is data). */
|
|
31
|
+
const show = (value) => {
|
|
32
|
+
if (typeof value === 'string') return value;
|
|
33
|
+
try {
|
|
34
|
+
return String(value);
|
|
35
|
+
} catch {
|
|
36
|
+
return JSON.stringify(value) ?? typeof value;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
24
39
|
const jsonEqual = (a, b) => JSON.stringify(sortKeys(a)) === JSON.stringify(sortKeys(b));
|
|
25
40
|
function sortKeys(value) {
|
|
26
41
|
if (Array.isArray(value)) return value.map(sortKeys);
|
|
@@ -39,7 +54,12 @@ export function resolveModelProfile({ catalogue, profiles, choices = {}, policy
|
|
|
39
54
|
if (!isObject(policy) || !Array.isArray(policy.modelClasses)) return { from: level, profile: id, models };
|
|
40
55
|
const allowed = new Set(strings(policy.modelClasses));
|
|
41
56
|
const rows = new Map(list(isObject(catalogue) ? catalogue.models : undefined).map((row) => [row.id, row]));
|
|
42
|
-
|
|
57
|
+
// The descriptive class open-vendor is not checked (DEC-0083), and a model with no other class does not stay.
|
|
58
|
+
const within = models.filter((model) => {
|
|
59
|
+
const row = rows.get(model);
|
|
60
|
+
const admission = row === undefined ? [] : strings(row.classes).filter((name) => name !== OPEN_VENDOR);
|
|
61
|
+
return admission.length > 0 && admission.every((name) => allowed.has(name));
|
|
62
|
+
});
|
|
43
63
|
if (within.length) return { from: level, profile: id, models: within };
|
|
44
64
|
const meta = isObject(policy.meta) ? policy.meta : {};
|
|
45
65
|
return { refused: 'model-classes-empty', profile: id, policy: { id: String(meta.id), revision: Number(meta.revision) } };
|
|
@@ -50,13 +70,13 @@ const finding = (code, instancePath, message) => ({ code, instancePath, message
|
|
|
50
70
|
function modelCatalogue(value) {
|
|
51
71
|
const findings = [];
|
|
52
72
|
const seen = new Set();
|
|
53
|
-
|
|
54
|
-
if (seen.has(row.id)) findings.push(finding('FAC-SEM-058', `/models/${index}/id`, `model ${
|
|
73
|
+
indexed(value.models).forEach(([row, index]) => {
|
|
74
|
+
if (seen.has(row.id)) findings.push(finding('FAC-SEM-058', `/models/${index}/id`, `model ${show(row.id)} is catalogued more than once`));
|
|
55
75
|
seen.add(row.id);
|
|
56
76
|
const routing = isObject(row.provider) ? row.provider : {};
|
|
57
77
|
const ignored = new Set(strings(routing.ignore));
|
|
58
78
|
for (const slug of strings(routing.order)) {
|
|
59
|
-
if (ignored.has(slug) || ignored.has(slug.split('/')[0])) findings.push(finding('FAC-SEM-058', `/models/${index}/provider`, `model ${
|
|
79
|
+
if (ignored.has(slug) || ignored.has(slug.split('/')[0])) findings.push(finding('FAC-SEM-058', `/models/${index}/provider`, `model ${show(row.id)} both orders and ignores provider ${slug}`));
|
|
60
80
|
}
|
|
61
81
|
});
|
|
62
82
|
return findings;
|
|
@@ -65,8 +85,8 @@ function modelCatalogue(value) {
|
|
|
65
85
|
function modelProfiles(value) {
|
|
66
86
|
const findings = [];
|
|
67
87
|
const seen = new Set();
|
|
68
|
-
|
|
69
|
-
if (seen.has(profile.id)) findings.push(finding('FAC-SEM-058', `/profiles/${index}/id`, `model profile ${
|
|
88
|
+
indexed(value.profiles).forEach(([profile, index]) => {
|
|
89
|
+
if (seen.has(profile.id)) findings.push(finding('FAC-SEM-058', `/profiles/${index}/id`, `model profile ${show(profile.id)} is declared more than once`));
|
|
70
90
|
seen.add(profile.id);
|
|
71
91
|
});
|
|
72
92
|
return findings;
|
|
@@ -90,11 +110,11 @@ function profileResolution(value) {
|
|
|
90
110
|
const findings = [];
|
|
91
111
|
const catalogued = new Set(list(isObject(value.catalogue) ? value.catalogue.models : undefined).map((row) => row.id));
|
|
92
112
|
const ids = new Set();
|
|
93
|
-
|
|
94
|
-
if (ids.has(profile.id)) findings.push(finding('FAC-SEM-060', `/profiles/${i}/id`, `profile id ${
|
|
113
|
+
indexed(value.profiles).forEach(([profile, i]) => {
|
|
114
|
+
if (ids.has(profile.id)) findings.push(finding('FAC-SEM-060', `/profiles/${i}/id`, `profile id ${show(profile.id)} is offered more than once to one resolution`));
|
|
95
115
|
ids.add(profile.id);
|
|
96
|
-
|
|
97
|
-
if (!catalogued.has(model)) findings.push(finding('FAC-SEM-060', `/profiles/${i}/models/${j}`, `profile ${
|
|
116
|
+
(Array.isArray(profile.models) ? profile.models : []).forEach((model, j) => {
|
|
117
|
+
if (typeof model === 'string' && !catalogued.has(model)) findings.push(finding('FAC-SEM-060', `/profiles/${i}/models/${j}`, `profile ${show(profile.id)} names ${model}, which the catalogue does not hold`));
|
|
98
118
|
});
|
|
99
119
|
});
|
|
100
120
|
const choices = isObject(value.choices) ? value.choices : {};
|
|
@@ -103,9 +123,51 @@ function profileResolution(value) {
|
|
|
103
123
|
return findings;
|
|
104
124
|
}
|
|
105
125
|
|
|
126
|
+
/**
|
|
127
|
+
* FAC-SEM-112 (DEC-0083) for one profile: the models that run when the lane reads user content and none of them is an
|
|
128
|
+
* open-vendor model, or undefined when the profile does not read user content or its route has one. Under
|
|
129
|
+
* `between-turns` every model may run; under `none`, or any other value (failing closed), only the first. A model the
|
|
130
|
+
* catalogue does not hold, or whose row is not `open-vendor`, counts as closed. Never throws.
|
|
131
|
+
*/
|
|
132
|
+
export function closedUserContentRoute(profile, catalogue) {
|
|
133
|
+
if (!isObject(profile) || !strings(profile.reads).includes(USER_CONTENT)) return undefined;
|
|
134
|
+
const models = Array.isArray(profile.models) ? profile.models : [];
|
|
135
|
+
const route = profile.fallback === 'between-turns' ? models : models.slice(0, 1);
|
|
136
|
+
const open = new Set();
|
|
137
|
+
for (const row of list(isObject(catalogue) ? catalogue.models : undefined)) {
|
|
138
|
+
if (typeof row.id === 'string' && strings(row.classes).includes(OPEN_VENDOR)) open.add(row.id);
|
|
139
|
+
}
|
|
140
|
+
if (route.some((model) => typeof model === 'string' && open.has(model))) return undefined;
|
|
141
|
+
return route.map((model) => show(model));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** FAC-SEM-112: a profile that reads user content has an open-vendor route, so the lane never runs on closed vendors alone. */
|
|
145
|
+
const closedRoute = (fallback, closed) => (fallback === 'between-turns' ? 'none of its models is' : `its first model, ${closed[0] ?? 'none'}, is not`);
|
|
146
|
+
|
|
147
|
+
/** FAC-SEM-112 on the profiles and on the resolution's outcome: the models the policy leaves keep an open-vendor route. */
|
|
148
|
+
function openVendorRoutes(value) {
|
|
149
|
+
const findings = [];
|
|
150
|
+
const profiles = list(value.profiles);
|
|
151
|
+
indexed(value.profiles).forEach(([profile, i]) => {
|
|
152
|
+
const closed = closedUserContentRoute(profile, value.catalogue);
|
|
153
|
+
if (closed === undefined) return;
|
|
154
|
+
findings.push(finding('FAC-SEM-112', `/profiles/${i}/models`, `profile ${show(profile.id)} reads user content, but ${closedRoute(profile.fallback, closed)} open-vendor in the catalogue: the lane would run on closed vendors alone`));
|
|
155
|
+
});
|
|
156
|
+
const choices = isObject(value.choices) ? value.choices : {};
|
|
157
|
+
const outcome = resolveModelProfile({ catalogue: value.catalogue, profiles: value.profiles, choices, policy: value.policy });
|
|
158
|
+
if (!('models' in outcome)) return findings;
|
|
159
|
+
const profile = profiles.find((item) => item.id === outcome.profile);
|
|
160
|
+
if (!profile || closedUserContentRoute(profile, value.catalogue) !== undefined) return findings;
|
|
161
|
+
const closed = closedUserContentRoute({ ...profile, models: outcome.models }, value.catalogue);
|
|
162
|
+
if (closed === undefined) return findings;
|
|
163
|
+
const meta = isObject(value.policy) && isObject(value.policy.meta) ? value.policy.meta : {};
|
|
164
|
+
findings.push(finding('FAC-SEM-112', '/resolved/models', `the policy ${show(meta.id)} leaves profile ${show(profile.id)}'s lane on closed vendors alone: of the models it admits, ${closedRoute(profile.fallback, closed)} open-vendor in the catalogue`));
|
|
165
|
+
return findings;
|
|
166
|
+
}
|
|
167
|
+
|
|
106
168
|
/** The contract's profileRules: findings for a model-catalogue, model-profiles, runner-profile or profile-resolution; else undefined. */
|
|
107
169
|
export function profileProblems(kind, value) {
|
|
108
|
-
return { 'model-catalogue': modelCatalogue, 'model-profiles': modelProfiles, 'runner-profile': runnerProfile, 'profile-resolution': profileResolution }[kind]?.(value);
|
|
170
|
+
return { 'model-catalogue': modelCatalogue, 'model-profiles': modelProfiles, 'runner-profile': runnerProfile, 'profile-resolution': (v) => [...profileResolution(v), ...openVendorRoutes(v)] }[kind]?.(value);
|
|
109
171
|
}
|
|
110
172
|
|
|
111
173
|
// ---- the doctor's checks ----
|
|
@@ -173,7 +235,10 @@ export function modelUsable(model, catalogue, policy) {
|
|
|
173
235
|
if (!rows.has(model)) return [`model ${model} is not in the model catalogue`];
|
|
174
236
|
if (isObject(policy) && Array.isArray(policy.modelClasses)) {
|
|
175
237
|
const allowed = new Set(strings(policy.modelClasses));
|
|
176
|
-
const
|
|
238
|
+
const admission = strings(rows.get(model).classes).filter((c) => c !== OPEN_VENDOR);
|
|
239
|
+
const policyId = String((isObject(policy.meta) ? policy.meta : {}).id);
|
|
240
|
+
if (!admission.length) return [`model ${model} has no class but ${OPEN_VENDOR}, which admits nothing, so policy ${policyId} does not allow it`];
|
|
241
|
+
const outside = admission.filter((c) => !allowed.has(c));
|
|
177
242
|
if (outside.length) return [`model ${model} has class ${outside.join(', ')}, which policy ${String((isObject(policy.meta) ? policy.meta : {}).id)} does not allow`];
|
|
178
243
|
}
|
|
179
244
|
return [];
|