@passioncode-ai/fabric-agent-adapter 0.79.1 → 0.81.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 +213 -0
- package/README.md +13 -2
- package/SECURITY.md +23 -8
- package/bin/fabric-agent-adapter.js +50 -5
- 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 +6 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +12 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/source-and-instance.md +44 -7
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +107 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +26 -6
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +9 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +24 -5
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +57 -76
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_restart.py +56 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_tree.py +912 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/new_agent.py +308 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +8 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/README.md +67 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/dashboard_service.py +252 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/install.mjs +168 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/dashboard-only/install.py +179 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/README.md +57 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/door.mjs +181 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/key-delivery-door/door.py +237 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +12 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +7 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +12 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +54 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +6 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +33 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +10 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +25 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +44 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_layout.mjs +21 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +26 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +24 -33
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent.py +12 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +136 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +3 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-context.mjs +37 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +118 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +266 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +72 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +16 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-load.mjs +344 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +98 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +7 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +10 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +11 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +114 -20
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +8 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +2 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +97 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py +50 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +120 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +316 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +40 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +15 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_load.py +358 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +103 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +28 -13
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +24 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +23 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +110 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +12 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +469 -29
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +4 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +18 -6
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agent-templates.md +44 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/communication-agent-template.md +19 -328
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +12 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +1 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/new-agent.md +42 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +7 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +44 -7
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +28 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/working-rules.md +54 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/access.schema.json +50 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/connections.schema.json +23 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/data-rooms.schema.json +74 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/manifest.schema.json +170 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/template-header.schema.json +25 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +26 -6
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/template_check.py +491 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/CHANGELOG.md +7 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/TEMPLATE.md +347 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/access.json +24 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/assembly.md +15 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/checks/acceptance.md +17 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/connections.json +29 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/data-rooms.json +9 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/manifest.json +32 -0
- package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +21 -21
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/agents-database.md +4 -4
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/cleanup.md +30 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +2 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +3 -2
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/remote-sessions.md +75 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +28 -13
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/task-context.md +58 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/waits.md +36 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +14 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +143 -36
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +136 -15
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +78 -3
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +9 -2
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
// The call queue and the load report of DEC-0104 (contract src/agent-network.ts, FAC-SEM-131 to FAC-SEM-133), as the
|
|
2
|
+
// contract's reference answers them: a node that hosts a job-backed agent queues its jobs per (agentRef, context) with
|
|
3
|
+
// these and reports its load as service-load/1 under extensions/load/0.1. Part of the Node service kit (FAA-289, layer
|
|
4
|
+
// L3); the twin of fabric_load.py. Node 20+, no dependency. Nothing here throws on what it reads, except
|
|
5
|
+
// loadExtension, which refuses a declaration a reader would treat as not declared.
|
|
6
|
+
//
|
|
7
|
+
// - jobCallerKey(accepted, credential): the opaque key of a job's caller (an accepted delegation's principal, its
|
|
8
|
+
// outermost actor and a partner organization ending the chain; else the credential the node knows the caller by).
|
|
9
|
+
// - nextQueuedJob(queue, running, limits): the job the node starts next, or undefined (fairness between callers).
|
|
10
|
+
// - admitJob(queue, running, job, limits, retryAfterSeconds): {admit: true}, or the queue-full refusal with its data.
|
|
11
|
+
// - queueStatusMessage(position, length), readQueuePosition(statusMessage): `queued <position> of <length>`.
|
|
12
|
+
// - jobQueueRules(value): FAC-SEM-131 over {queue, running, limits, started}.
|
|
13
|
+
// - loadReportRules(report): FAC-SEM-132 on a service-load/1 report; loadReportProblems(report): its schema.
|
|
14
|
+
// - loadView(wellKnown), loadExtensionRules(wellKnown): the declaration as a reader reads it, and FAC-SEM-133.
|
|
15
|
+
// - loadExtension({ path, windowSeconds, limits }): the `extensions` entry a service declares.
|
|
16
|
+
//
|
|
17
|
+
// The kit has no JSON Schema validator: the two shapes are written here from the schemas as the contract's validator
|
|
18
|
+
// reads them (lengths in code points, date-time as ajv-formats' full mode). Times in the queue are UTC instants
|
|
19
|
+
// (interop-common#/$defs/utcInstant); times in the report are RFC 3339 read strictly (the contract's strictTime). The
|
|
20
|
+
// wording of a schema problem is the kit's, not ajv's.
|
|
21
|
+
import { ServiceError } from './fabric-base.mjs';
|
|
22
|
+
import { dateTime } from './fabric-connections.mjs'; // ajv-formats' date-time, as the contract's validator reads it
|
|
23
|
+
import { isObject, isString, objects, show } from './fabric-jsvalue.mjs';
|
|
24
|
+
import { parseRfc3339Ms } from './fabric-time.mjs'; // the contract's strictTime
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @typedef {{ id: string, caller: string, at: string }} QueuedJob A job waiting at the hosting node: its id, its
|
|
28
|
+
* caller's key (jobCallerKey) and when it arrived (a UTC instant).
|
|
29
|
+
* @typedef {{ id: string, caller?: string }} RunningJob A job running: its id and caller.
|
|
30
|
+
* @typedef {{ concurrency: number, perCaller: number, queueMax: number }} QueueLimits The queue's limits per
|
|
31
|
+
* (agentRef, context), as the load extension declares them.
|
|
32
|
+
* @typedef {{ admit: true } | { admit: false, error: { code: 'queue-full', message: string,
|
|
33
|
+
* data: { retryable: true, retryAfterSeconds: number } } }} Admission
|
|
34
|
+
* @typedef {{ code: string, instancePath: string, message: string }} Finding
|
|
35
|
+
* @typedef {{ declared: boolean, valid: boolean, load: { path: string, windowSeconds: number, limits: QueueLimits } | null,
|
|
36
|
+
* errors: string[] }} LoadView
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
export const LOAD_EXTENSION_KEY = 'https://fabric.passioncode.ai/agent-contract/extensions/load/0.1';
|
|
40
|
+
export const LOAD_REPORT_TYPE = 'service-load/1';
|
|
41
|
+
// The refusal code, the same as fabric-interop-rules.mjs's QUEUE_FULL (a test holds them equal); not imported because
|
|
42
|
+
// the facade fabric-service.mjs, which re-exports this module, comes before the interop rules in the kit's order.
|
|
43
|
+
const QUEUE_FULL = 'queue-full';
|
|
44
|
+
export const QUEUE_FULL_MESSAGE = "the agent's queue for this context is full: start again later";
|
|
45
|
+
/** The longest queue a status message names (`queued <position> of <length>`, six digits). */
|
|
46
|
+
export const QUEUE_LENGTH_MAX = 999999;
|
|
47
|
+
export const DEFAULT_RETRY_AFTER_SECONDS = 60;
|
|
48
|
+
|
|
49
|
+
const UTC_INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:[0-5]\d(?:\.\d{1,3})?Z$/;
|
|
50
|
+
const QUEUED = /^queued ([1-9]\d{0,5}) of ([1-9]\d{0,5})$/;
|
|
51
|
+
const ORIGIN_PATH = /^\/(?!\/)[^\s]*$/u;
|
|
52
|
+
const CONTEXT = /^(?:personal|org:[A-Za-z0-9][A-Za-z0-9_-]{0,127})$/;
|
|
53
|
+
const KEY_PATH = `/extensions/${LOAD_EXTENSION_KEY.replace(/~/g, '~0').replace(/\//g, '~1')}`;
|
|
54
|
+
const LIMITS = [['concurrency', 1, 1024], ['perCaller', 1, 1024], ['queueMax', 0, 100000]];
|
|
55
|
+
const ROW_COUNTS = ['queued', 'running', 'completed', 'failed'];
|
|
56
|
+
|
|
57
|
+
/** An own member, or undefined. */
|
|
58
|
+
const member = (value, key) => (isObject(value) && Object.hasOwn(value, key) ? value[key] : undefined);
|
|
59
|
+
/** A UTC instant (`T`, `Z`, at most three fraction digits, a day that exists) in milliseconds, else NaN. */
|
|
60
|
+
const utcMs = (value) => (isString(value) && UTC_INSTANT.test(value) ? parseRfc3339Ms(value) : Number.NaN);
|
|
61
|
+
const strictMs = (value) => (isString(value) ? parseRfc3339Ms(value) : Number.NaN);
|
|
62
|
+
const codePoints = (value) => [...value].length;
|
|
63
|
+
|
|
64
|
+
// --- The call queue (fabric-interop, DEC-0104) ---
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The key of a job's caller at a node that queues (DEC-0104), as the contract's jobCallerKey: for a call whose
|
|
68
|
+
* delegation the node accepted (verifyDelegation's answer), its `principal` and `caller` and the partner organization
|
|
69
|
+
* when the chain ends in one (DEC-0090); for a call with no token, the credential the node knows the caller by (a
|
|
70
|
+
* gate's pass with `principal: null` is that case too). A refused verdict has no key, and neither has an unreadable
|
|
71
|
+
* call: undefined. The key is opaque; only equality matters.
|
|
72
|
+
* @param {unknown} accepted
|
|
73
|
+
* @param {unknown} credential
|
|
74
|
+
* @returns {string | undefined}
|
|
75
|
+
*/
|
|
76
|
+
export function jobCallerKey(accepted, credential) {
|
|
77
|
+
if (isObject(accepted) && !(member(accepted, 'ok') === true && member(accepted, 'principal') === null)) {
|
|
78
|
+
const principal = member(accepted, 'principal');
|
|
79
|
+
const caller = member(accepted, 'caller');
|
|
80
|
+
if (member(accepted, 'ok') !== true || !isString(principal) || principal === '' || !isString(caller) || caller === '') return undefined;
|
|
81
|
+
const given = member(accepted, 'chain');
|
|
82
|
+
const chain = Array.isArray(given) ? given : [];
|
|
83
|
+
const last = chain[chain.length - 1];
|
|
84
|
+
const partner = chain.length > 1 && isString(last) && last.startsWith('org:') ? [last] : [];
|
|
85
|
+
return JSON.stringify(['delegated', principal, caller, ...partner]);
|
|
86
|
+
}
|
|
87
|
+
return isString(credential) && credential !== '' ? JSON.stringify(['credential', credential]) : undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const limitsOk = (limits) => isObject(limits)
|
|
91
|
+
&& [member(limits, 'concurrency'), member(limits, 'perCaller')].every((n) => Number.isSafeInteger(n) && n >= 1)
|
|
92
|
+
&& Number.isSafeInteger(member(limits, 'queueMax')) && limits.queueMax >= 0;
|
|
93
|
+
const waitingJob = (job) => isObject(job) && isString(member(job, 'id')) && isString(member(job, 'caller')) && Number.isFinite(utcMs(member(job, 'at')));
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The job the hosting node starts next for one (agentRef, context), or undefined (DEC-0104). Nothing starts while
|
|
97
|
+
* `concurrency` jobs run (every running job holds a slot, its caller readable or not). Of the waiting jobs whose caller
|
|
98
|
+
* runs fewer than `perCaller`, the one whose caller runs the fewest wins; between those callers, the one whose oldest
|
|
99
|
+
* waiting job arrived first; within a caller, its oldest. Arrival ties break by id (UTF-16 order).
|
|
100
|
+
* @param {unknown} queue
|
|
101
|
+
* @param {unknown} running
|
|
102
|
+
* @param {unknown} limits
|
|
103
|
+
* @returns {string | undefined}
|
|
104
|
+
*/
|
|
105
|
+
export function nextQueuedJob(queue, running, limits) {
|
|
106
|
+
if (!limitsOk(limits)) return undefined;
|
|
107
|
+
if ((Array.isArray(running) ? running.length : 0) >= limits.concurrency) return undefined;
|
|
108
|
+
const runningOf = new Map();
|
|
109
|
+
for (const job of objects(running)) if (isString(member(job, 'caller'))) runningOf.set(job.caller, (runningOf.get(job.caller) ?? 0) + 1);
|
|
110
|
+
const waiting = objects(queue)
|
|
111
|
+
.filter((job) => waitingJob(job) && (runningOf.get(job.caller) ?? 0) < limits.perCaller)
|
|
112
|
+
.sort((a, b) => utcMs(a.at) - utcMs(b.at) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
113
|
+
let best;
|
|
114
|
+
for (const job of waiting) if (best === undefined || (runningOf.get(job.caller) ?? 0) < (runningOf.get(best.caller) ?? 0)) best = job;
|
|
115
|
+
return best?.id;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Whether a start is admitted (DEC-0104): it is when the job would start at once (nextQueuedJob over the queue with it
|
|
120
|
+
* added answers its id), or when fewer than `queueMax` jobs wait; otherwise {admit: false, error} with the queue-full
|
|
121
|
+
* refusal and its `retryAfterSeconds` (1 to 3600, else 60). Unreadable limits or job refuse.
|
|
122
|
+
* @param {unknown} queue
|
|
123
|
+
* @param {unknown} running
|
|
124
|
+
* @param {unknown} job
|
|
125
|
+
* @param {unknown} limits
|
|
126
|
+
* @param {unknown} retryAfterSeconds
|
|
127
|
+
* @returns {Admission}
|
|
128
|
+
*/
|
|
129
|
+
export function admitJob(queue, running, job, limits, retryAfterSeconds) {
|
|
130
|
+
const waiting = objects(queue);
|
|
131
|
+
if (limitsOk(limits) && waitingJob(job) && (nextQueuedJob([...waiting, job], running, limits) === job.id || waiting.length < limits.queueMax)) return { admit: true };
|
|
132
|
+
const after = Number.isSafeInteger(retryAfterSeconds) && retryAfterSeconds >= 1 && retryAfterSeconds <= 3600 ? retryAfterSeconds : DEFAULT_RETRY_AFTER_SECONDS;
|
|
133
|
+
return { admit: false, error: { code: QUEUE_FULL, message: QUEUE_FULL_MESSAGE, data: { retryable: true, retryAfterSeconds: after } } };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* The `statusMessage` of a waiting job (DEC-0104): `queued <position> of <length>`, 1 is next, 1 <= position <= length
|
|
138
|
+
* <= 999999; undefined for anything else.
|
|
139
|
+
* @param {unknown} position
|
|
140
|
+
* @param {unknown} length
|
|
141
|
+
* @returns {string | undefined}
|
|
142
|
+
*/
|
|
143
|
+
export const queueStatusMessage = (position, length) => (Number.isSafeInteger(position) && Number.isSafeInteger(length)
|
|
144
|
+
&& position >= 1 && position <= length && length <= QUEUE_LENGTH_MAX ? `queued ${position} of ${length}` : undefined);
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* A waiting job's place, read from its `statusMessage`, or undefined when it is not the queue's grammar.
|
|
148
|
+
* @param {unknown} statusMessage
|
|
149
|
+
* @returns {{ position: number, length: number } | undefined}
|
|
150
|
+
*/
|
|
151
|
+
export function readQueuePosition(statusMessage) {
|
|
152
|
+
const match = isString(statusMessage) ? QUEUED.exec(statusMessage) : null;
|
|
153
|
+
if (!match) return undefined;
|
|
154
|
+
const position = Number(match[1]);
|
|
155
|
+
const length = Number(match[2]);
|
|
156
|
+
return position >= 1 && position <= length ? { position, length } : undefined;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const finding = (code, instancePath, message) => ({ code, instancePath, message });
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* FAC-SEM-131 on {queue, running, limits, started} (`started`: the id the node started, or null): it is nextQueuedJob's.
|
|
163
|
+
* @param {unknown} value
|
|
164
|
+
* @returns {Finding[]}
|
|
165
|
+
*/
|
|
166
|
+
export function jobQueueRules(value) {
|
|
167
|
+
const want = nextQueuedJob(member(value, 'queue'), member(value, 'running'), member(value, 'limits')) ?? null;
|
|
168
|
+
if ((member(value, 'started') ?? null) === want) return [];
|
|
169
|
+
return [finding('FAC-SEM-131', '/started', want === null ? 'nothing may start now' : `the job to start is ${show(want)}`)];
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// --- The load report (DEC-0104) ---
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* FAC-SEM-132 on a service-load/1 report: windows in order one `windowSeconds` apart, none after `generatedAt`, one
|
|
176
|
+
* row per context, p50 at most p95, a run time only where a job ended.
|
|
177
|
+
* @param {unknown} report
|
|
178
|
+
* @returns {Finding[]}
|
|
179
|
+
*/
|
|
180
|
+
export function loadReportRules(report) {
|
|
181
|
+
const findings = [];
|
|
182
|
+
const flag = (instancePath, message) => findings.push(finding('FAC-SEM-132', instancePath, message));
|
|
183
|
+
const seconds = member(report, 'windowSeconds');
|
|
184
|
+
const step = Number.isSafeInteger(seconds) ? seconds * 1000 : Number.NaN;
|
|
185
|
+
const generated = strictMs(member(report, 'generatedAt'));
|
|
186
|
+
let previous;
|
|
187
|
+
objects(member(report, 'windows')).forEach((window, index) => {
|
|
188
|
+
const start = strictMs(member(window, 'start'));
|
|
189
|
+
if (start > generated) flag(`/windows/${index}/start`, 'a window starts after the report was generated');
|
|
190
|
+
if (previous !== undefined && start - previous !== step) flag(`/windows/${index}/start`, 'windows are not in order one windowSeconds apart');
|
|
191
|
+
previous = start;
|
|
192
|
+
const seen = new Set();
|
|
193
|
+
objects(member(window, 'byContext')).forEach((row, at) => {
|
|
194
|
+
const path = `/windows/${index}/byContext/${at}`;
|
|
195
|
+
const context = member(row, 'context');
|
|
196
|
+
if (isString(context) && seen.has(context)) flag(`${path}/context`, `context ${show(context)} appears twice in one window`);
|
|
197
|
+
if (isString(context)) seen.add(context);
|
|
198
|
+
for (const name of ['waitMs', 'runMs']) {
|
|
199
|
+
const pct = member(row, name);
|
|
200
|
+
if (isObject(pct) && typeof member(pct, 'p50') === 'number' && typeof member(pct, 'p95') === 'number' && pct.p50 > pct.p95) flag(`${path}/${name}`, 'p50 is above p95');
|
|
201
|
+
}
|
|
202
|
+
if (isObject(member(row, 'runMs')) && member(row, 'completed') === 0 && member(row, 'failed') === 0) flag(`${path}/runMs`, 'a run time with no job ended in the window');
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
return findings;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** JSON Schema's integer with its bounds, as ajv reads it: a number with no fraction, never a boolean. */
|
|
209
|
+
function integer(value, low, high, at, errors) {
|
|
210
|
+
if (typeof value !== 'number' || !Number.isFinite(value) || value % 1 !== 0) errors.push(`${at} type`);
|
|
211
|
+
else if (value < low) errors.push(`${at} minimum`);
|
|
212
|
+
else if (high !== undefined && value > high) errors.push(`${at} maximum`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function members(value, allowed, required, at, errors) {
|
|
216
|
+
for (const key of Object.keys(value)) if (!allowed.includes(key)) errors.push(`${at}/${key} additionalProperties`);
|
|
217
|
+
for (const key of required) if (!Object.hasOwn(value, key)) errors.push(`${at || '/'} required ${key}`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function timestamp(value, at, errors) {
|
|
221
|
+
if (!isString(value)) errors.push(`${at} type`);
|
|
222
|
+
else if (!dateTime(value)) errors.push(`${at} format`);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function percentiles(value, at, errors) {
|
|
226
|
+
if (!isObject(value)) {
|
|
227
|
+
errors.push(`${at} type`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
members(value, ['p50', 'p95'], ['p50', 'p95'], at, errors);
|
|
231
|
+
for (const name of ['p50', 'p95']) if (Object.hasOwn(value, name)) integer(value[name], 0, undefined, `${at}/${name}`, errors);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function rowProblems(row, at, errors) {
|
|
235
|
+
if (!isObject(row)) {
|
|
236
|
+
errors.push(`${at} type`);
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
members(row, ['context', ...ROW_COUNTS, 'waitMs', 'runMs'], ['context', ...ROW_COUNTS], at, errors);
|
|
240
|
+
if (Object.hasOwn(row, 'context') && !(isString(row.context) && CONTEXT.test(row.context))) errors.push(`${at}/context ${isString(row.context) ? 'pattern' : 'type'}`);
|
|
241
|
+
for (const name of ROW_COUNTS) if (Object.hasOwn(row, name)) integer(row[name], 0, undefined, `${at}/${name}`, errors);
|
|
242
|
+
for (const name of ['waitMs', 'runMs']) if (Object.hasOwn(row, name)) percentiles(row[name], `${at}/${name}`, errors);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* service-load.schema.json: what makes `report` not a service-load/1 report, each as `<path> <keyword>`; empty for a
|
|
247
|
+
* valid one. Counts and durations only: a member the schema does not name (a caller, a job id) is refused.
|
|
248
|
+
* @param {unknown} report
|
|
249
|
+
* @returns {string[]}
|
|
250
|
+
*/
|
|
251
|
+
export function loadReportProblems(report) {
|
|
252
|
+
if (!isObject(report)) return ['/ type'];
|
|
253
|
+
const errors = [];
|
|
254
|
+
const names = ['type', 'generatedAt', 'windowSeconds', 'windows'];
|
|
255
|
+
members(report, names, names, '', errors);
|
|
256
|
+
if (Object.hasOwn(report, 'type') && report.type !== LOAD_REPORT_TYPE) errors.push('/type const');
|
|
257
|
+
if (Object.hasOwn(report, 'generatedAt')) timestamp(report.generatedAt, '/generatedAt', errors);
|
|
258
|
+
if (Object.hasOwn(report, 'windowSeconds')) integer(report.windowSeconds, 60, 86400, '/windowSeconds', errors);
|
|
259
|
+
if (Object.hasOwn(report, 'windows') && !Array.isArray(report.windows)) errors.push('/windows type');
|
|
260
|
+
else if (Object.hasOwn(report, 'windows')) {
|
|
261
|
+
if (report.windows.length > 168) errors.push('/windows maxItems');
|
|
262
|
+
report.windows.forEach((window, index) => {
|
|
263
|
+
const at = `/windows/${index}`;
|
|
264
|
+
if (!isObject(window)) {
|
|
265
|
+
errors.push(`${at} type`);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
members(window, ['start', 'byContext'], ['start', 'byContext'], at, errors);
|
|
269
|
+
if (Object.hasOwn(window, 'start')) timestamp(window.start, `${at}/start`, errors);
|
|
270
|
+
if (Object.hasOwn(window, 'byContext') && !Array.isArray(window.byContext)) errors.push(`${at}/byContext type`);
|
|
271
|
+
else if (Object.hasOwn(window, 'byContext')) {
|
|
272
|
+
if (window.byContext.length > 64) errors.push(`${at}/byContext maxItems`);
|
|
273
|
+
window.byContext.forEach((row, rowIndex) => rowProblems(row, `${at}/byContext/${rowIndex}`, errors));
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
return errors;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/** service-load-extension.schema.json, each problem as `<path> <keyword>`; empty for a valid declaration. */
|
|
281
|
+
function declarationProblems(value) {
|
|
282
|
+
if (!isObject(value)) return ['/ type'];
|
|
283
|
+
const errors = [];
|
|
284
|
+
members(value, ['path', 'windowSeconds', 'limits'], ['path', 'windowSeconds', 'limits'], '', errors);
|
|
285
|
+
if (Object.hasOwn(value, 'path')) {
|
|
286
|
+
if (!isString(value.path)) errors.push('/path type');
|
|
287
|
+
else {
|
|
288
|
+
if (codePoints(value.path) > 256) errors.push('/path maxLength'); // the originPath's own 2048 is wider
|
|
289
|
+
if (!ORIGIN_PATH.test(value.path)) errors.push('/path pattern');
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (Object.hasOwn(value, 'windowSeconds')) integer(value.windowSeconds, 60, 86400, '/windowSeconds', errors);
|
|
293
|
+
if (Object.hasOwn(value, 'limits')) {
|
|
294
|
+
const { limits } = value;
|
|
295
|
+
if (!isObject(limits)) errors.push('/limits type');
|
|
296
|
+
else {
|
|
297
|
+
const names = LIMITS.map(([name]) => name);
|
|
298
|
+
members(limits, names, names, '/limits', errors);
|
|
299
|
+
for (const [name, low, high] of LIMITS) if (Object.hasOwn(limits, name)) integer(limits[name], low, high, `/limits/${name}`, errors);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return errors;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* The load declaration as a reader reads it (DEC-0104): `declared` when the extension key is present, `valid` when its
|
|
307
|
+
* value is the schema's, `load` the declaration when valid (else null) and `errors` its problems. A reader treats an
|
|
308
|
+
* invalid value as not declared and reports it (FAC-SEM-133).
|
|
309
|
+
* @param {unknown} wellKnown
|
|
310
|
+
* @returns {LoadView}
|
|
311
|
+
*/
|
|
312
|
+
export function loadView(wellKnown) {
|
|
313
|
+
const extensions = member(wellKnown, 'extensions');
|
|
314
|
+
if (!isObject(extensions) || !Object.hasOwn(extensions, LOAD_EXTENSION_KEY)) return { declared: false, valid: false, load: null, errors: [] };
|
|
315
|
+
const value = extensions[LOAD_EXTENSION_KEY];
|
|
316
|
+
const errors = declarationProblems(value);
|
|
317
|
+
return { declared: true, valid: errors.length === 0, load: errors.length === 0 ? value : null, errors };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* FAC-SEM-133 on a well-known document: a value under the load extension key is valid against
|
|
322
|
+
* service-load-extension.schema.json; one finding at the key otherwise.
|
|
323
|
+
* @param {unknown} wellKnown
|
|
324
|
+
* @returns {Finding[]}
|
|
325
|
+
*/
|
|
326
|
+
export function loadExtensionRules(wellKnown) {
|
|
327
|
+
const view = loadView(wellKnown);
|
|
328
|
+
if (!view.declared || view.valid) return [];
|
|
329
|
+
return [finding('FAC-SEM-133', KEY_PATH, `the load declaration is invalid (${view.errors.join('; ')}); a reader treats it as not declared and reports it (DEC-0104)`)];
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* The `extensions` entry a service declares (DEC-0104): where it serves its service-load/1 report (an origin path,
|
|
334
|
+
* served under the descriptor token like the usage report), the report's window and the queue's limits per
|
|
335
|
+
* (agentRef, context). Throws ServiceError on a declaration a reader would treat as not declared.
|
|
336
|
+
* @param {{ path: string, windowSeconds: number, limits: QueueLimits }} declaration
|
|
337
|
+
* @returns {Record<string, { path: string, windowSeconds: number, limits: QueueLimits }>}
|
|
338
|
+
*/
|
|
339
|
+
export function loadExtension({ path, windowSeconds, limits } = {}) {
|
|
340
|
+
const value = { path, windowSeconds, limits: isObject(limits) ? { ...limits } : limits };
|
|
341
|
+
const problems = declarationProblems(value);
|
|
342
|
+
if (problems.length) throw new ServiceError(`Load declaration: ${problems.join('; ')}.`);
|
|
343
|
+
return { [LOAD_EXTENSION_KEY]: value };
|
|
344
|
+
}
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs
CHANGED
|
@@ -509,9 +509,71 @@ const FOLDER_MESSAGES = {
|
|
|
509
509
|
'provider-manifest': 'provider-manifest: the provider manifest must be a JSON object, and when it carries costPresets fabric.package.json must carry the same costPresets (copy them; pack does not)',
|
|
510
510
|
};
|
|
511
511
|
|
|
512
|
+
/**
|
|
513
|
+
* The agent's source manifest (contract DEC-0149, `fabric-source/1`): when the folder carries one, `pack` writes
|
|
514
|
+
* `secrets[]` from its secret slots and `state[]` from its store slots' paths, and FAC-SEM-160 holds them equal.
|
|
515
|
+
*/
|
|
516
|
+
export const SOURCE_FILE = 'fabric.source.json';
|
|
517
|
+
|
|
518
|
+
function sourceManifest(files) {
|
|
519
|
+
if (!files.has(SOURCE_FILE)) return undefined;
|
|
520
|
+
const source = parseStrict(files.get(SOURCE_FILE), SOURCE_FILE);
|
|
521
|
+
if (source.format !== 'fabric-source/1') throw new PackageError(`${SOURCE_FILE} is not a fabric-source/1 manifest (its format member).`);
|
|
522
|
+
return source;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/** `secrets[]` from the secret slots (name, purpose, optional) and `state[]` from the store slots (path, purpose). */
|
|
526
|
+
export function sourceMembers(source) {
|
|
527
|
+
const secrets = [];
|
|
528
|
+
const state = [];
|
|
529
|
+
for (const slot of objects(source.slots)) {
|
|
530
|
+
if (slot.kind === 'secret' && isString(slot.name)) secrets.push({ name: slot.name, purpose: slot.purpose, ...(slot.optional === true ? { optional: true } : {}) });
|
|
531
|
+
else if (slot.kind === 'store' && isString(slot.path)) state.push({ path: slot.path, purpose: slot.purpose });
|
|
532
|
+
}
|
|
533
|
+
return { secrets, state };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* FAC-SEM-160 (contract `sourcePackageFindings`): the package's secrets[] are the source's secret slots, by name and
|
|
538
|
+
* optional; its state[] paths are the store slots' paths when state[] holds any entry.
|
|
539
|
+
*/
|
|
540
|
+
export function sourcePackageFindings(source, pkg) {
|
|
541
|
+
if (!isObject(source) || !isObject(pkg)) return [];
|
|
542
|
+
const out = [];
|
|
543
|
+
const flag = (instancePath, message) => out.push({ code: 'FAC-SEM-160', instancePath, message });
|
|
544
|
+
const slots = objects(source.slots);
|
|
545
|
+
const secrets = new Map(slots.filter((s) => s.kind === 'secret' && isString(s.name)).map((s) => [s.name, s.optional === true]));
|
|
546
|
+
const stores = new Set(slots.filter((s) => s.kind === 'store' && isString(s.path)).map((s) => s.path));
|
|
547
|
+
const packaged = (Array.isArray(pkg.secrets) ? pkg.secrets : []).flatMap((e, i) => (isObject(e) ? [[e, i]] : []));
|
|
548
|
+
const names = new Set(packaged.map(([e]) => e.name));
|
|
549
|
+
for (const name of secrets.keys()) if (!names.has(name)) flag('/package/secrets', `the source's secret slot ${name} is not in the package's secrets[]`);
|
|
550
|
+
for (const [entry, index] of packaged) {
|
|
551
|
+
if (!isString(entry.name) || !secrets.has(entry.name)) flag(`/package/secrets/${index}/name`, `secrets[${index}] is no secret slot of the source`);
|
|
552
|
+
else if (secrets.get(entry.name) !== (entry.optional === true)) flag(`/package/secrets/${index}/optional`, `secrets[${index}] is ${secrets.get(entry.name) ? '' : 'not '}optional in the source`);
|
|
553
|
+
}
|
|
554
|
+
if (!Array.isArray(pkg.state) || pkg.state.length === 0) return out;
|
|
555
|
+
const paths = new Set(objects(pkg.state).map((e) => e.path));
|
|
556
|
+
for (const p of stores) if (!paths.has(p)) flag('/package/state', "a store slot's path of the source is not in the package's state[]");
|
|
557
|
+
pkg.state.forEach((entry, index) => {
|
|
558
|
+
if (isObject(entry) && (!isString(entry.path) || !stores.has(entry.path))) flag(`/package/state/${index}/path`, `state[${index}] is no store slot of the source`);
|
|
559
|
+
});
|
|
560
|
+
return out;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function sourceProblems(pkg, files) {
|
|
564
|
+
if (!files.has(SOURCE_FILE)) return [];
|
|
565
|
+
try {
|
|
566
|
+
return sourcePackageFindings(sourceManifest(files), pkg).map((p) => ({ ...p, rule: p.code }));
|
|
567
|
+
} catch (error) {
|
|
568
|
+
if (!(error instanceof PackageError)) throw error;
|
|
569
|
+
return [{ rule: 'source', instancePath: '/files', message: error.message }];
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
512
573
|
function allProblems(pkg, files) {
|
|
513
574
|
return [
|
|
514
575
|
...schemaProblems(pkg).map((p) => ({ ...p, rule: 'schema' })),
|
|
576
|
+
...sourceProblems(pkg, files),
|
|
515
577
|
...pathProblems(pkg).map((p) => ({ ...p, rule: 'path' })),
|
|
516
578
|
...packageFindings(pkg).map((p) => ({ ...p, rule: p.code })),
|
|
517
579
|
...checkPackageFolder(pkg, files).map((p) => ({ rule: 'folder', problem: p.problem, instancePath: p.path, message: FOLDER_MESSAGES[p.problem] ?? p.problem })),
|
|
@@ -525,6 +587,21 @@ function writeAtomic(file, text) {
|
|
|
525
587
|
fs.renameSync(temp, file);
|
|
526
588
|
}
|
|
527
589
|
|
|
590
|
+
/** The names a hand-written secrets[] declares that the source manifest has no secret slot for, in their order. */
|
|
591
|
+
function secretsLacking(draft, secrets) {
|
|
592
|
+
const have = new Set(secrets.map((s) => s.name));
|
|
593
|
+
const lacking = [];
|
|
594
|
+
for (const entry of Array.isArray(draft.secrets) ? draft.secrets : []) {
|
|
595
|
+
const name = isObject(entry) ? entry.name : undefined;
|
|
596
|
+
if (isString(name) && !have.has(name) && !lacking.includes(name)) lacking.push(name);
|
|
597
|
+
}
|
|
598
|
+
return lacking;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function lackingMessage(lacking) {
|
|
602
|
+
return `${SOURCE_FILE} declares no secret slot for ${lacking.join(', ')}, which ${PACKAGE_FILE}'s secrets[] names: pack writes secrets[] from the manifest's slots and never drops a declared secret. Add each as a slot of kind secret to ${SOURCE_FILE} (or take it out of secrets[]), then pack again`;
|
|
603
|
+
}
|
|
604
|
+
|
|
528
605
|
/** Pack `folder`; with `apply` and no problem, fabric.package.json is rewritten. */
|
|
529
606
|
export function pack(folder, { apply = false } = {}) {
|
|
530
607
|
const files = readFolder(folder);
|
|
@@ -532,8 +609,28 @@ export function pack(folder, { apply = false } = {}) {
|
|
|
532
609
|
throw new PackageError(`Write ${PACKAGE_FILE} in the folder first: schema, kind, id, version, publisher, licence and, for an agent, resultSignal and evals with its receipt. pack fills in agentRef, files[] and digest.`);
|
|
533
610
|
}
|
|
534
611
|
const draft = parseStrict(files.get(PACKAGE_FILE), PACKAGE_FILE);
|
|
612
|
+
const source = sourceManifest(files);
|
|
613
|
+
const written = [];
|
|
614
|
+
const refused = [];
|
|
615
|
+
if (source !== undefined) {
|
|
616
|
+
// DEC-0149: secrets[] and state[] come from the source manifest's slots; an empty state[] is a copy without state.
|
|
617
|
+
const members = sourceMembers(source);
|
|
618
|
+
const lacking = secretsLacking(draft, members.secrets);
|
|
619
|
+
if (lacking.length > 0) {
|
|
620
|
+
// a declared secret is never dropped: the manifest gains its slot first
|
|
621
|
+
refused.push({ rule: 'source', instancePath: '/secrets', message: lackingMessage(lacking) });
|
|
622
|
+
} else if (members.secrets.length > 0 || Object.hasOwn(draft, 'secrets')) {
|
|
623
|
+
Object.defineProperty(draft, 'secrets', { value: members.secrets, enumerable: true, writable: true, configurable: true });
|
|
624
|
+
written.push('secrets[]');
|
|
625
|
+
}
|
|
626
|
+
if (members.state.length > 0 && !(Array.isArray(draft.state) && draft.state.length === 0)) {
|
|
627
|
+
Object.defineProperty(draft, 'state', { value: members.state, enumerable: true, writable: true, configurable: true });
|
|
628
|
+
written.push('state[]');
|
|
629
|
+
}
|
|
630
|
+
}
|
|
535
631
|
const { pkg, notes } = compose(draft, files);
|
|
536
|
-
|
|
632
|
+
if (written.length > 0) notes.push(`${written.join(' and ')} written from ${SOURCE_FILE}`);
|
|
633
|
+
const problems = [...refused, ...allProblems(pkg, files)];
|
|
537
634
|
const suite = isObject(pkg.evals) ? pkg.evals.suite : undefined;
|
|
538
635
|
if (isString(suite) && files.has(suite) && problems.some((p) => p.instancePath === '/evals/receipt/suite')) notes.push(`the suite packed is ${sha256Of(files.get(suite))}: run it again and write that receipt`);
|
|
539
636
|
const answer = { ok: problems.length === 0, applied: false, digest: pkg.digest, files: pkg.files.length, problems, notes };
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs
CHANGED
|
@@ -45,7 +45,7 @@ import http from 'node:http';
|
|
|
45
45
|
import net from 'node:net';
|
|
46
46
|
import os from 'node:os';
|
|
47
47
|
|
|
48
|
-
export const PEER_OWNER_VERSION = '1.
|
|
48
|
+
export const PEER_OWNER_VERSION = '1.2.0+kit.5'; // the Python twin's, Observatory's 1.2.0 with the kit's differences
|
|
49
49
|
export const THIS_ACCOUNT = 'this-account';
|
|
50
50
|
export const ANOTHER_ACCOUNT = 'another-account';
|
|
51
51
|
export const SYSTEM = 'system';
|
|
@@ -154,6 +154,10 @@ export function parseNetstat(text) {
|
|
|
154
154
|
const at = names.indexOf('process:pid');
|
|
155
155
|
const leading = at - names.slice(0, at).filter((w) => w === 'Address').length;
|
|
156
156
|
const trailing = names.length - at - 1;
|
|
157
|
+
// The caller's own connection is in any table it can see: a header and no tcp row is the restricted table (FAA-273).
|
|
158
|
+
if (!lines.slice(header + 1).some((line) => (line.split(/\s+/).filter(Boolean)[0] ?? '').startsWith('tcp'))) {
|
|
159
|
+
throw new TableUnreadable(`${TABLE_RESTRICTED}: netstat printed only its header to this process`);
|
|
160
|
+
}
|
|
157
161
|
const rows = [];
|
|
158
162
|
for (const line of lines.slice(header + 1)) {
|
|
159
163
|
const tok = line.split(/\s+/).filter(Boolean);
|
|
@@ -416,7 +420,7 @@ async function ownerOf(local, remote, opts = {}, server = false) {
|
|
|
416
420
|
// Linux: a connection not yet accept()ed is printed with uid 0 and inode 0; it belongs to the listener on its local
|
|
417
421
|
// address and dies with it, so that listener answers. An accepted connection's own row carries the uid of the
|
|
418
422
|
// process that accepted it: it answers itself, so a listener swapped after another account accepted the client
|
|
419
|
-
// cannot pass (Project Observatory's 1.2.0 reading, board thread ced00f6e; the contract's
|
|
423
|
+
// cannot pass (Project Observatory's 1.2.0 reading, board thread ced00f6e; the contract's CT-254, released).
|
|
420
424
|
// A server row its owner already closed (an orphan) also has inode 0 and reads so: no process holds it, so nothing
|
|
421
425
|
// sent to it reaches anyone (0.74.0 unit reading 6).
|
|
422
426
|
return oneOwner(listenersOf(table.rows, found[0].local), table.how, opts, 'not-accepted');
|
|
@@ -460,7 +464,7 @@ export function peer(socket, opts) {
|
|
|
460
464
|
// For a client's connected socket: who accepted it. The row whose local address is the server's and whose remote
|
|
461
465
|
// address is this socket's own, the server process's accepted socket, so a listener swapped after listen cannot pass
|
|
462
466
|
// for the one that answers. On Linux a connection not yet accept()ed has no owner of its own (inode 0): its listener
|
|
463
|
-
// answers (the contract's
|
|
467
|
+
// answers (the contract's CT-254, HA-2's Linux source).
|
|
464
468
|
export function serverEnd(socket, opts) {
|
|
465
469
|
if (!socket.remoteAddress || !socket.localAddress) return Promise.resolve(owner(UNKNOWN, { reason: 'socket: not connected' }));
|
|
466
470
|
return ownerOf(address(socket.remoteAddress, socket.remotePort), address(socket.localAddress, socket.localPort), opts, true);
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs
CHANGED
|
@@ -12,7 +12,7 @@ export const PROTOCOL_REVISIONS = new Map(Object.entries({
|
|
|
12
12
|
'fabric-interop': ['0.1', '0.2'], 'fabric-local-channel': ['0.1'], 'fabric-local-runner': ['0.1'], 'fabric-machine-report': ['1'], 'fabric-ops-digest': ['0.1'], 'fabric-operator-channel': ['0.1'],
|
|
13
13
|
'fabric-package': ['1'],
|
|
14
14
|
'fabric-peer-share': ['1'], 'fabric-peer-share-envelope': ['1'], 'fabric-peer-share-report': ['1'], 'fabric-project-comms': ['0.1'], 'fabric-proposal': ['1'], 'fabric-provider': ['0.1'], 'fabric-relay': ['0.1'],
|
|
15
|
-
'fabric-secret-envelope': ['1'], 'fabric-service': ['0.1'], 'fabric-settings-backup': ['1'], heartbeat: ['1'], 'learning-note': ['1'], 'machine-report': ['1'], mandate: ['1'],
|
|
15
|
+
'fabric-secret-envelope': ['1'], 'fabric-service': ['0.1'], 'fabric-settings-backup': ['1'], 'fabric-source': ['1'], heartbeat: ['1'], 'learning-note': ['1'], 'machine-report': ['1'], mandate: ['1'],
|
|
16
16
|
memory: ['0.1'], 'node-summary': ['1'], 'operator-source': ['1'], 'org-adaptation': ['1'], 'org-adaptation-receipt': ['1'], 'org-authority': ['1'], 'org-authority-draft': ['1'],
|
|
17
17
|
'org-key-set': ['1'], 'org-key-set-draft': ['1'], 'org-membership': ['1'], 'org-root-registration': ['1'], 'pairing-message': ['1'],
|
|
18
18
|
'peer-share-deletion': ['1'], 'peer-share-frame': ['1'], 'peer-share-revocation': ['1'], pipeline: ['0.1'], 'repair-request': ['1'], 'repair-result': ['1'],
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs
CHANGED
|
@@ -38,8 +38,12 @@ export function credentialName(name) {
|
|
|
38
38
|
|
|
39
39
|
/** A JWT or JWS anywhere in a value. */
|
|
40
40
|
export const JWT_SHAPE = /eyJ[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]{2,}\.[A-Za-z0-9_-]*/;
|
|
41
|
-
/** A value that says it is a credential, or a provider key's shape
|
|
42
|
-
|
|
41
|
+
/** A value that says it is a credential (`Bearer ` at its start, in any case), or a provider key's shape with its prefix
|
|
42
|
+
* in its canonical case only, at the start of a value, after a character that is no ASCII letter or digit, or after an
|
|
43
|
+
* escape; an `sk-` key holds a digit and an upper-case letter with 16 or more characters, or a digit with 32 or more,
|
|
44
|
+
* within 256 characters (the contract's SECRET_VALUE at the pin, DEC-0151). A match includes its boundary character:
|
|
45
|
+
* callers test, never read the matched text or its index. */
|
|
46
|
+
export const SECRET_VALUE = /(^[Bb][Ee][Aa][Rr][Ee][Rr]\s.+|(?:^|[^A-Za-z0-9]|%[0-9A-Fa-f]{2}|\\[nrt"']|\\u[0-9A-Fa-f]{4}|\\x[0-9A-Fa-f]{2}|\\[bfv0ae]|\\[0-7]{1,3}|%25[0-9A-Fa-f]{2}|\x1b\[[0-9;:]*m|(?:\\u001[bB]|\\x1[bB]|\\e|\\033|\\33)\[[0-9;:]*m)(?:sk-(?=[A-Za-z0-9_-]{0,255}?\d)(?:(?=[A-Za-z0-9_-]{0,255}?[A-Z])[A-Za-z0-9_-]{16,}|[A-Za-z0-9_-]{32,})|gh[pousr]_[A-Za-z0-9]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|glpat-[A-Za-z0-9_-]{20,}))/;
|
|
43
47
|
const LONG_RUN = /[A-Za-z0-9+/=_-]{40,}/g;
|
|
44
48
|
export const longSecret = (value) => (value.match(LONG_RUN) ?? []).some((run) => /[a-z]/.test(run) && /[A-Z]/.test(run) && /[0-9]/.test(run));
|
|
45
49
|
|
|
@@ -91,7 +95,10 @@ export function secretUrl(value) {
|
|
|
91
95
|
const REPOSITORY_URI = /^([A-Za-z][A-Za-z0-9+.-]*:(?:\/\/[^/?#]*)?)([^?#]*)([\s\S]*)$/;
|
|
92
96
|
const REPOSITORY_NAME = /^(?:[a-z0-9._-]+|[A-Za-z._-]+)$/; // lower case, or letters with no digit (readings 8)
|
|
93
97
|
const REPOSITORY_AUTHORITY = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//;
|
|
94
|
-
|
|
98
|
+
// A repository name's own key shapes, boundary-free and case-sensitive (DEC-0151): SECRET_VALUE takes a consuming
|
|
99
|
+
// boundary, so composed after a separator look-behind it would miss a key after `-`, `.` or `_`.
|
|
100
|
+
const REPOSITORY_KEYS = 'sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|xox[abposr]-[A-Za-z0-9-]{10,}|AKIA[0-9A-Z]{16}|glpat-[A-Za-z0-9_-]{20,}';
|
|
101
|
+
const KEY_AT_A_WORD = new RegExp(`(?:^|(?<=[._-]))(?:${REPOSITORY_KEYS})`);
|
|
95
102
|
const JWT_AT_A_WORD = new RegExp(`(?:^|(?<=[._-]))${JWT_SHAPE.source}`);
|
|
96
103
|
const SPECIAL_QUERY = /^(?:https?|wss?|ftp|file):/i; // whose query also encodes `'`, as the URL standard does
|
|
97
104
|
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs
CHANGED
|
@@ -43,6 +43,10 @@ export {
|
|
|
43
43
|
} from './fabric-protocols.mjs';
|
|
44
44
|
export { ACTIONS_EXTENSION_KEY, actionDecision, actionRules, actionsExtension, actionsExtensionRules, actionsView, decisionShape } from './fabric-actions.mjs';
|
|
45
45
|
export { CONNECTIONS_EXTENSION_KEY, connectionsExtension, connectionsRules, connectionsView } from './fabric-connections.mjs';
|
|
46
|
+
export { // the call queue and the load report (DEC-0104, FAA-289)
|
|
47
|
+
DEFAULT_RETRY_AFTER_SECONDS, LOAD_EXTENSION_KEY, LOAD_REPORT_TYPE, QUEUE_FULL_MESSAGE, QUEUE_LENGTH_MAX, admitJob, jobCallerKey, jobQueueRules,
|
|
48
|
+
loadExtension, loadExtensionRules, loadReportProblems, loadReportRules, loadView, nextQueuedJob, queueStatusMessage, readQueuePosition,
|
|
49
|
+
} from './fabric-load.mjs';
|
|
46
50
|
export {
|
|
47
51
|
EXIT_ALREADY_RUNNING,
|
|
48
52
|
LOCK_WAIT_SUPERVISED_SECONDS,
|
|
@@ -103,6 +107,10 @@ export {
|
|
|
103
107
|
STATUSES,
|
|
104
108
|
PLACEMENTS,
|
|
105
109
|
MANAGERS,
|
|
110
|
+
PEER_CHECKS,
|
|
111
|
+
connectionRefusal,
|
|
112
|
+
connectionOwner,
|
|
113
|
+
loopbackServer,
|
|
106
114
|
descriptorPath,
|
|
107
115
|
readDescriptors,
|
|
108
116
|
validateDescriptor,
|
|
@@ -118,7 +126,9 @@ export {
|
|
|
118
126
|
fabricButtonPresence,
|
|
119
127
|
placementOf,
|
|
120
128
|
remoteOriginProblems,
|
|
129
|
+
portListenerRefusal,
|
|
121
130
|
portOf,
|
|
131
|
+
portOwner,
|
|
122
132
|
NO_SOURCE_REPOSITORY,
|
|
123
133
|
namesRepository,
|
|
124
134
|
sourceRefusal,
|
|
@@ -179,6 +189,7 @@ export {
|
|
|
179
189
|
taskUninstall,
|
|
180
190
|
launchdPlist,
|
|
181
191
|
SYSTEM_DAEMONS,
|
|
192
|
+
USER_NAME_RULE,
|
|
182
193
|
launchdDomain,
|
|
183
194
|
wellKnownOf,
|
|
184
195
|
jobWellKnownOf,
|