@passioncode-ai/fabric-agent-adapter 0.82.0 → 0.83.1
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 +122 -3
- package/README.md +6 -3
- package/SECURITY.md +4 -3
- 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 +7 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +4 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/source-and-instance.md +18 -4
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +16 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +31 -15
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +37 -12
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +21 -148
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check_cli.py +164 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +15 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +3 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/README.md +76 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/config.example.json +28 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/data_agent.py +366 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +32 -17
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/config.example.json +4 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support-agent.mjs +115 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +63 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/a2a.md +98 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +20 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +14 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +5 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +8 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/ops-digest.md +75 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +7 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +25 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +10 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +70 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_a2a.py +158 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a-server.mjs +274 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a.mjs +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-fixture.mjs +14 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +54 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +45 -31
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +16 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-connections.mjs +26 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +18 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs +6 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +21 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +3 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +6 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +20 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +5 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +15 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-mandate.mjs +5 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +24 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ops-digest.mjs +455 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +17 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +18 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +55 -19
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +15 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-socket-proof.mjs +180 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +5 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-tailnet-identity.mjs +468 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a.py +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a_server.py +404 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +6 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py +16 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +97 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +35 -48
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +15 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +27 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +18 -13
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +23 -14
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +3 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +31 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +18 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +9 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +55 -96
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +98 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +15 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +43 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ops_digest.py +647 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +16 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +20 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +93 -93
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +37 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_socket_proof.py +390 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +3 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_tailnet_identity.py +492 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +365 -53
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample-remote-service.mjs +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +16 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/tailnet-groups.schema.json +69 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +5 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agent-templates.md +16 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-that-answer-people.md +14 -9
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +4 -2
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +18 -4
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +2 -1
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/admit.py +162 -0
- package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/agents-database.md +10 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +14 -9
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +7 -4
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +10 -5
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +2 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +16 -5
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +79 -8
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +147 -45
|
@@ -5,7 +5,7 @@ import crypto from 'node:crypto';
|
|
|
5
5
|
import { Backoff, HubError, normalProject } from './fabric-hub.mjs';
|
|
6
6
|
import { PRIMARY_CAPABILITY, acceptedRevisions, feedDataProblems, negotiateRevision } from './fabric-service.mjs';
|
|
7
7
|
import { BoardError, CAPABILITY, LEASE_SECONDS, REQUEST_STATES, SUMMARY_MAX, Stopped, isInt, isObj, outcomeProblems, receiptProblems } from './fabric-board-shapes.mjs';
|
|
8
|
-
import { BoardRequest, BoardSession, Checkpoint, PERSONAL_BOARD, covers, mandateOptions, readMandate } from './fabric-board-link.mjs';
|
|
8
|
+
import { BoardRequest, BoardSession, Checkpoint, PERSONAL_BOARD, covers, mandateOptions, noticeLinks, readBackProblem, readMandate, senderAgent } from './fabric-board-link.mjs';
|
|
9
9
|
import { HOLD_RETRY_MAX_SECONDS, LEASE_MARGIN_SECONDS, SESSION_CODES, SlotKeeper } from './fabric-board-slots.mjs';
|
|
10
10
|
|
|
11
11
|
export { BoardRequest, BoardSession, Checkpoint, PERSONAL_BOARD }; // re-exported: callers import them from here since 0.40.0
|
|
@@ -20,6 +20,8 @@ export const POLL_JITTER_SECONDS = 5;
|
|
|
20
20
|
export const DRAIN_SECONDS = 10;
|
|
21
21
|
/** An idle responder's leave waits this long for the DELETE, so an idle stop ends within LC-01's 5 s (FAA-173). */
|
|
22
22
|
export const IDLE_LEAVE_SECONDS = 3;
|
|
23
|
+
/** `board.down` is said once per outage window: said again only after the board stayed up this long (FAA-187 (22)). */
|
|
24
|
+
export const DOWN_QUIET_SECONDS = 300;
|
|
23
25
|
|
|
24
26
|
export const EVENT_SCHEMA = 'urn:fabric:schema:board-responder-event:1';
|
|
25
27
|
|
|
@@ -35,16 +37,18 @@ export class WorkContext {
|
|
|
35
37
|
#responder;
|
|
36
38
|
#entry;
|
|
37
39
|
#caller;
|
|
38
|
-
|
|
40
|
+
#agent;
|
|
41
|
+
constructor(responder, entry, mandate = null, caller = null, callerAgent = null) {
|
|
39
42
|
this.#responder = responder;
|
|
40
43
|
this.#entry = entry;
|
|
41
44
|
this.#caller = caller;
|
|
45
|
+
this.#agent = callerAgent; // FAA-135: the sender's agent id identifyAgent named, binding the mandate's chain
|
|
42
46
|
this.stopped = false;
|
|
43
47
|
this.mandate = mandate; // the operator's mandate the request carried, when the responder reads one (FAA-135)
|
|
44
48
|
}
|
|
45
49
|
/** Whether the request's mandate covers `call` (`mandateCovers`' answer); null when none travelled. */
|
|
46
50
|
mandateCovers(call) {
|
|
47
|
-
return covers(this.mandate, call, this.#responder.agentId, this.#responder.wall, this.#caller);
|
|
51
|
+
return covers(this.mandate, call, this.#responder.agentId, this.#responder.wall, this.#caller, this.#agent);
|
|
48
52
|
}
|
|
49
53
|
toJSON() {
|
|
50
54
|
return { stopped: this.stopped }; // never the mandate: it holds the operator's words
|
|
@@ -82,33 +86,39 @@ function noticeSubject(value, project, episode) {
|
|
|
82
86
|
*/
|
|
83
87
|
export class Responder extends SlotKeeper {
|
|
84
88
|
constructor({ hub, projectId, capabilities, credentialFile, checkpointFile, handler, admit, observe, emit, service = 'service',
|
|
85
|
-
now = () => performance.now() / 1000, rand = Math.random, sessionFactory, agentId = null, mandateFrom = null, wall = Date.now } = {}) {
|
|
89
|
+
now = () => performance.now() / 1000, rand = Math.random, sessionFactory, agentId = null, mandateFrom = null, wall = Date.now, identifyAgent = null, links = null } = {}) {
|
|
86
90
|
super();
|
|
87
|
-
if (typeof admit !== 'function' || typeof handler !== 'function'
|
|
91
|
+
if (typeof admit !== 'function' || typeof handler !== 'function' || !(identifyAgent == null || typeof identifyAgent === 'function')) {
|
|
92
|
+
throw new BoardError('invalid_arguments', 'A responder needs the service\'s own input gate (admit) and a handler; identifyAgent, when given, is a function of the sender (FAA-135).');
|
|
93
|
+
}
|
|
88
94
|
const caps = [...(capabilities ?? [])];
|
|
89
95
|
if (!caps.length || caps.some((c) => typeof c !== 'string' || !CAPABILITY.test(c)) || new Set(caps).size !== caps.length) throw new BoardError('invalid_arguments', 'Name each capability once, [a-z][a-z0-9._-]{1,127}.');
|
|
90
96
|
// FAA-244: DEC-0140 PR-1, a session's slot, never a service's
|
|
91
97
|
if (caps.includes(PRIMARY_CAPABILITY)) throw new BoardError('invalid_arguments', `${PRIMARY_CAPABILITY} is a Project's primary responder's, a session's: a service answers the capabilities it declares (DEC-0140 PR-1).`);
|
|
92
98
|
Object.assign(this, { hub, capabilities: caps, credentialFile, handler, admit, observe, emitTo: emit, now, rand });
|
|
93
99
|
this.project = normalProject(projectId);
|
|
94
|
-
// FAA-135: read the operator's mandate from a trusted request of his own Project (opt-in; null reads none)
|
|
95
|
-
|
|
100
|
+
// FAA-135: read the operator's mandate from a trusted request of his own Project (opt-in; null reads none), and bind
|
|
101
|
+
// its chain with the agent id identifyAgent names for the sender (asked only when a mandate travelled)
|
|
102
|
+
Object.assign(this, { mandateFrom: mandateOptions(agentId, mandateFrom), agentId: agentId ?? null, wall, identifyAgent: identifyAgent ?? null });
|
|
103
|
+
this.links = noticeLinks(links); // FAA-187 (21): the page that resolves each notify kind
|
|
96
104
|
this.service = String(service).replace(/[^A-Za-z0-9._-]/g, '-').slice(0, 40) || 'service';
|
|
97
105
|
this.checkpoint = new Checkpoint(checkpointFile);
|
|
98
106
|
this.sessionFactory = sessionFactory ?? ((grant) => new BoardSession(grant));
|
|
99
107
|
this.backoff = new Backoff({ rand });
|
|
100
108
|
this.session = null;
|
|
101
109
|
this.epoch = null;
|
|
102
|
-
|
|
110
|
+
// FAA-196 (24): maps keyed by a capability's name have no prototype, so `constructor` is a name like any other
|
|
111
|
+
this.slots = Object.create(null);
|
|
103
112
|
this.current = null;
|
|
104
113
|
this.state = 'idle';
|
|
105
114
|
this.waitUntil = 0;
|
|
106
115
|
this.stopping = false;
|
|
107
116
|
/** Whether shutdown found work to drain, published before it waits; null before shutdown starts (FAA-186). */
|
|
108
117
|
this.draining = null;
|
|
109
|
-
this.claimKeys =
|
|
118
|
+
this.claimKeys = Object.create(null);
|
|
110
119
|
this.probing = new Set(); // moved slots whose move-back claim lost its answer: asked again under the same key
|
|
111
|
-
this.said =
|
|
120
|
+
this.said = Object.create(null);
|
|
121
|
+
this.upSince = null; // when the board was last seen up after a fault (FAA-187 (22))
|
|
112
122
|
this.rr = 0;
|
|
113
123
|
this.reconcileAt = 0;
|
|
114
124
|
/** The first pause under an operator's hold (DEC-0079); it doubles to HOLD_RETRY_MAX_SECONDS. */
|
|
@@ -118,7 +128,7 @@ export class Responder extends SlotKeeper {
|
|
|
118
128
|
this.fenceQueue = Promise.resolve();
|
|
119
129
|
this.toResume = []; // transferred requests waiting until nothing runs (finding 39)
|
|
120
130
|
this.suspect = new Set(); // capabilities a fenced call was refused on: com.status says whether they moved (FAA-155)
|
|
121
|
-
this.confirmed =
|
|
131
|
+
this.confirmed = Object.create(null); // this session's: the generation it held, or com.status showed unmoved
|
|
122
132
|
}
|
|
123
133
|
|
|
124
134
|
/** Runs fn after every fenced call queued before it (finding 36): an await inside a call never lets another in. */
|
|
@@ -148,7 +158,8 @@ export class Responder extends SlotKeeper {
|
|
|
148
158
|
// (events-and-notifications.md, "Say who wants what"; 0.56.0 seam r30).
|
|
149
159
|
const subject = noticeSubject(value, this.project, episode);
|
|
150
160
|
try {
|
|
151
|
-
|
|
161
|
+
const link = notify && this.links && Object.hasOwn(this.links, kind) ? { link: this.links[kind] } : {}; // the page that resolves it (FAA-187 (21))
|
|
162
|
+
this.emitTo(kind, level, text, { notify, subject, ...link, ...(data ? { data } : {}) });
|
|
152
163
|
} catch {
|
|
153
164
|
// an event that cannot be written never stops the responder
|
|
154
165
|
}
|
|
@@ -190,8 +201,8 @@ export class Responder extends SlotKeeper {
|
|
|
190
201
|
|
|
191
202
|
async connect() {
|
|
192
203
|
this.state = 'connecting';
|
|
193
|
-
this.slots =
|
|
194
|
-
this.confirmed =
|
|
204
|
+
this.slots = Object.create(null); // the previous session's slots are not this one's: a refusal now reconnects (FAA-163)
|
|
205
|
+
this.confirmed = Object.create(null); // nor its confirmations: a slot may have moved between the two sessions
|
|
195
206
|
this.probing = new Set(); // nor its lost move-back claims
|
|
196
207
|
const grant = await this.hub.joinBoard(this.project, this.credentialFile);
|
|
197
208
|
const missing = this.capabilities.filter((c) => !grant.capabilities.includes(c));
|
|
@@ -202,10 +213,10 @@ export class Responder extends SlotKeeper {
|
|
|
202
213
|
if (missing.length) {
|
|
203
214
|
this.emit('board.access.needed', 'warning', `The operator has not let this service answer ${missing.join(', ')} on this Project; ask in Fabric (Settings → Agent access).`, { notify: true, episode: missing.join(','), capabilities: missing });
|
|
204
215
|
}
|
|
205
|
-
this.slots =
|
|
216
|
+
this.slots = Object.create(null);
|
|
206
217
|
await this.recover(status, this.capabilities.filter((c) => grant.capabilities.includes(c)));
|
|
207
218
|
this.state = 'ready';
|
|
208
|
-
for (const kind of ['board.
|
|
219
|
+
for (const kind of ['board.blocked', ...(missing.length ? [] : ['board.access.needed'])]) delete this.said[kind]; // board.down waits for its quiet window (step)
|
|
209
220
|
}
|
|
210
221
|
|
|
211
222
|
async enroll(capability) {
|
|
@@ -262,7 +273,7 @@ export class Responder extends SlotKeeper {
|
|
|
262
273
|
}
|
|
263
274
|
|
|
264
275
|
async recover(status, capabilities) {
|
|
265
|
-
const transfer =
|
|
276
|
+
const transfer = Object.create(null);
|
|
266
277
|
const reconcile = [];
|
|
267
278
|
for (const entry of Object.values(this.checkpoint.entries)) {
|
|
268
279
|
if (!capabilities.includes(entry.capability)) continue;
|
|
@@ -281,7 +292,7 @@ export class Responder extends SlotKeeper {
|
|
|
281
292
|
else reconcile.push(entry);
|
|
282
293
|
}
|
|
283
294
|
const responders = (status.responders ?? []).filter(isObj);
|
|
284
|
-
const generations = Object.fromEntries(responders.map((r) => [r.capability, r.generation ?? 0]));
|
|
295
|
+
const generations = Object.assign(Object.create(null), Object.fromEntries(responders.map((r) => [r.capability, r.generation ?? 0])));
|
|
285
296
|
const onHold = new Set(responders.filter((r) => r.held === true).map((r) => r.capability)); // DEC-0079
|
|
286
297
|
let failure = null;
|
|
287
298
|
for (const capability of capabilities) {
|
|
@@ -470,10 +481,9 @@ export class Responder extends SlotKeeper {
|
|
|
470
481
|
if (!this.session) await this.connect();
|
|
471
482
|
await this.tick();
|
|
472
483
|
this.backoff.reset();
|
|
473
|
-
if (this.state === 'down')
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
484
|
+
if (this.state === 'down') this.state = 'ready'; // a lost call was sent again and answered: the session held throughout
|
|
485
|
+
this.upSince ??= now;
|
|
486
|
+
if (now - this.upSince >= DOWN_QUIET_SECONDS) delete this.said['board.down']; // up a whole window: a later outage is said again
|
|
477
487
|
return this.interval();
|
|
478
488
|
} catch (e) {
|
|
479
489
|
if (!(e instanceof BoardError) && !(e instanceof HubError)) throw e;
|
|
@@ -492,6 +502,7 @@ export class Responder extends SlotKeeper {
|
|
|
492
502
|
|
|
493
503
|
async fault(e) {
|
|
494
504
|
const code = e.code ?? 'unreachable';
|
|
505
|
+
this.upSince = null;
|
|
495
506
|
const connecting = this.state === 'connecting' && !Object.values(this.slots).some((s) => s?.state === 'held');
|
|
496
507
|
let wait = this.backoff.next();
|
|
497
508
|
const left = this.leaseLeft() - LEASE_MARGIN_SECONDS;
|
|
@@ -554,9 +565,10 @@ export class Responder extends SlotKeeper {
|
|
|
554
565
|
await this.gone(capability);
|
|
555
566
|
this.suspect.delete(capability);
|
|
556
567
|
}
|
|
568
|
+
const shared = {}; // one com.status read a tick for every moved slot (FAA-194 (5))
|
|
557
569
|
for (const [capability, slot] of Object.entries(this.slots)) {
|
|
558
570
|
if (slot.state === 'moved') { // never enrolled again on its own, even when the new holder lapses (FAA-155)
|
|
559
|
-
if (this.now() - (slot.renewedAt ?? 0) >= RENEW_SECONDS - 0.5) await this.movedBack(capability); // unless com.status shows a later generation it may hold (FAA-165)
|
|
571
|
+
if (this.now() - (slot.renewedAt ?? 0) >= RENEW_SECONDS - 0.5) await this.movedBack(capability, shared); // unless com.status shows a later generation it may hold (FAA-165)
|
|
560
572
|
continue;
|
|
561
573
|
}
|
|
562
574
|
if (slot.state === 'paused') {
|
|
@@ -581,7 +593,7 @@ export class Responder extends SlotKeeper {
|
|
|
581
593
|
}
|
|
582
594
|
|
|
583
595
|
async continueLapsed() {
|
|
584
|
-
const lapsed =
|
|
596
|
+
const lapsed = Object.create(null);
|
|
585
597
|
for (const entry of Object.values(this.checkpoint.entries)) {
|
|
586
598
|
if (entry.step === 'lapsed' && this.slots[entry.capability]?.state === 'held') (lapsed[entry.capability] ??= []).push(JSON.parse(JSON.stringify(entry)));
|
|
587
599
|
}
|
|
@@ -648,13 +660,13 @@ export class Responder extends SlotKeeper {
|
|
|
648
660
|
revision: receipt.request.revision, step: 'claimed', keys: {}, effect: false };
|
|
649
661
|
this.checkpoint.put(entry);
|
|
650
662
|
const message = await this.call('com.get', { message: entry.message });
|
|
651
|
-
const
|
|
652
|
-
if (
|
|
663
|
+
const problem = readBackProblem(message, entry, this.project);
|
|
664
|
+
if (problem === 'digest-mismatch') {
|
|
653
665
|
this.checkpoint.drop(entry.request);
|
|
654
666
|
this.emit('board.request.skipped', 'warning', 'A claimed request was not accepted: the message read back carries another digest than the claim. Nothing was done; the claim lapses.', { request: entry.request, capability, reason: 'digest-mismatch' });
|
|
655
667
|
return;
|
|
656
668
|
}
|
|
657
|
-
if (
|
|
669
|
+
if (problem) {
|
|
658
670
|
this.checkpoint.drop(entry.request);
|
|
659
671
|
this.emit('board.request.skipped', 'warning', 'A claimed request was not accepted: it is not addressed to this Project and capability. Nothing was done; the claim lapses.', { request: entry.request, capability, reason: 'not-addressed' });
|
|
660
672
|
return;
|
|
@@ -701,12 +713,14 @@ export class Responder extends SlotKeeper {
|
|
|
701
713
|
return true;
|
|
702
714
|
}
|
|
703
715
|
|
|
704
|
-
async runWork(entry, request) {
|
|
705
|
-
|
|
716
|
+
async runWork(entry, request, problem = null) {
|
|
717
|
+
let { mandate, unread } = readMandate(request, this.mandateFrom, this.agentId);
|
|
718
|
+
if (problem && (mandate !== null || unread)) [mandate, unread] = [null, problem]; // FAA-196 (1): another message than accepted
|
|
706
719
|
if (unread) { // by its code alone: the block holds the operator's words
|
|
707
720
|
this.emit('board.request.mandate_unread', 'warning', `Request ${entry.request} carries a mandate the kit does not read (${unread}); it is worked without one.`, { request: entry.request, reason: unread });
|
|
708
721
|
}
|
|
709
|
-
const
|
|
722
|
+
const sender = isObj(request.sender) ? request.sender : {};
|
|
723
|
+
const ctx = new WorkContext(this, entry, mandate, sender.project ?? null, senderAgent(this.identifyAgent, sender, mandate));
|
|
710
724
|
const work = { entry, request, ctx, done: false, outcome: null, failure: null, renewedAt: this.now() };
|
|
711
725
|
this.current = work;
|
|
712
726
|
work.promise = Promise.resolve().then(() => this.handler(request, ctx)).then((outcome) => { work.outcome = outcome; }, (e) => { work.failure = e; }).finally(() => { work.done = true; });
|
|
@@ -721,7 +735,7 @@ export class Responder extends SlotKeeper {
|
|
|
721
735
|
return;
|
|
722
736
|
}
|
|
723
737
|
const message = await this.call('com.get', { message: entry.message });
|
|
724
|
-
await this.runWork(entry, new BoardRequest(message, entry.capability));
|
|
738
|
+
await this.runWork(entry, new BoardRequest(message, entry.capability), readBackProblem(message, entry, this.project));
|
|
725
739
|
}
|
|
726
740
|
|
|
727
741
|
/**
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs
CHANGED
|
@@ -131,7 +131,7 @@ export class SlotKeeper {
|
|
|
131
131
|
delete this.confirmed[capability];
|
|
132
132
|
delete this.claimKeys[capability]; // the move returned that claim to the queue: its receipt is dead
|
|
133
133
|
if (!('moved' in held)) { // once, even across restarts
|
|
134
|
-
this.emit('board.slot.moved', 'warning', `The ${capability} slot on this Project moved to another session. This service stopped answering it; accepted work stays
|
|
134
|
+
this.emit('board.slot.moved', 'warning', `The ${capability} slot on this Project moved to another session. This service stopped answering it; accepted work stays accepted (work begun is the new holder's). The operator brings it back by take-back (once the other session lets it go, or at a restart or reconnect with accepted work waiting; neither alone does) or, where Fabric can, a move back here. Else ask its maintainer.`, { notify: true, episode: capability, capability, generation: now, held: held.generation });
|
|
135
135
|
this.note(capability, { ...held, moved: now }); // said again at the next start if it cannot be written
|
|
136
136
|
}
|
|
137
137
|
return true;
|
|
@@ -143,14 +143,21 @@ export class SlotKeeper {
|
|
|
143
143
|
* com.claim asks whether the slot is its own. A claim that lands makes it held again and starts what it handed out; a
|
|
144
144
|
* lease_expired one (the move reached this session but its lease lapsed) enrolls again; a fenced one is another
|
|
145
145
|
* session's, recorded so the same generation is not asked about twice. Never an enroll or a replace on a slot another
|
|
146
|
-
* session holds: a slot moved off stays off unless it is handed back.
|
|
146
|
+
* session holds: a slot moved off stays off unless it is handed back. `shared` holds the tick's one com.status read,
|
|
147
|
+
* shared by every moved slot (FAA-194 (5)).
|
|
147
148
|
*/
|
|
148
|
-
async movedBack(capability) {
|
|
149
|
+
async movedBack(capability, shared = null) {
|
|
149
150
|
if (this.current || this.stopping) return; // a claim now could hand out a second request, or start work while the responder stops
|
|
150
151
|
const held = this.checkpoint.slots[capability] ?? {};
|
|
151
152
|
if (held.project !== this.project || !isInt(held.moved, 1)) return;
|
|
152
153
|
if (this.slots[capability]) this.slots[capability].renewedAt = this.now();
|
|
153
|
-
|
|
154
|
+
let status;
|
|
155
|
+
if (shared && 'status' in shared) status = shared.status;
|
|
156
|
+
else {
|
|
157
|
+
status = await this.status();
|
|
158
|
+
if (shared) shared.status = status;
|
|
159
|
+
}
|
|
160
|
+
const now = (status.responders ?? []).find((r) => isObj(r) && r.capability === capability)?.generation;
|
|
154
161
|
if (!isInt(now, 1) || now <= held.moved) return;
|
|
155
162
|
if (!this.probing.has(capability) || !this.claimKeys[capability]) { // a fresh key: one from before the move may hold a dead receipt
|
|
156
163
|
this.claimKeys[capability] = this.key('claim');
|
|
@@ -201,14 +208,17 @@ export class SlotKeeper {
|
|
|
201
208
|
}
|
|
202
209
|
|
|
203
210
|
/** The slot is this session's again at `generation`: held, its record rewritten without the move (FAA-165). */
|
|
204
|
-
back(capability, held, generation) {
|
|
205
|
-
|
|
211
|
+
async back(capability, held, generation) {
|
|
212
|
+
const back = await this.locked(() => {
|
|
206
213
|
if (!sameRecord(this.checkpoint.slots[capability], held)) return false; // a takeBack forgot it; its own next tick enrolls
|
|
207
214
|
this.slots[capability] = { state: 'held', generation, renewedAt: -Infinity }; // no lease yet
|
|
208
215
|
this.heldAt(capability, generation);
|
|
209
216
|
delete this.said['board.slot.moved']; // a later move is said again
|
|
210
217
|
return true;
|
|
211
218
|
});
|
|
219
|
+
// FAA-194 (8): the feed no longer ends on the move's warning
|
|
220
|
+
if (back) this.emit('board.slot.returned', 'info', `The ${capability} slot on this Project is this service's again: the operator moved it back here, and it answers it from now on.`, { capability, generation });
|
|
221
|
+
return back;
|
|
212
222
|
}
|
|
213
223
|
|
|
214
224
|
/**
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-connections.mjs
CHANGED
|
@@ -88,7 +88,28 @@ function shapeProblems(value) {
|
|
|
88
88
|
// The contract's KEY_SHAPE (src/connections.ts): a provider key where it starts a word, case-sensitive prefixes, the
|
|
89
89
|
// `sk-` rules and a Telegram bot token.
|
|
90
90
|
const KEY_SHAPE = /(?:^|[^A-Za-z0-9])(?:sk-(?=[A-Za-z0-9_-]*\d)(?:(?=[A-Za-z0-9_-]*[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,}|(?:bot)?\d{6,12}:(?=[A-Za-z0-9_-]*[A-Z])(?=[A-Za-z0-9_-]*[a-z])[A-Za-z0-9_-]{34,35}(?![A-Za-z0-9_-]))/;
|
|
91
|
-
const
|
|
91
|
+
const SCHEME_CHAR = /[a-z0-9+.-]/i;
|
|
92
|
+
const SCHEME_LETTER = /[a-z]/i;
|
|
93
|
+
/**
|
|
94
|
+
* The [start, end] of each URL scheme and its `://` in a piece, where `/[a-z][a-z0-9+.-]*:\/\//gi` finds them left to
|
|
95
|
+
* right, read in linear time (FAA-322: the pattern tried every letter of a long run with no `://`). From each `://` the
|
|
96
|
+
* run of scheme characters before it is read back, and the match starts at its first letter at or after where the last
|
|
97
|
+
* match ended. The twin of fabric_connections._scheme_spans.
|
|
98
|
+
*/
|
|
99
|
+
export function schemeSpans(piece) {
|
|
100
|
+
const spans = [];
|
|
101
|
+
let after = 0;
|
|
102
|
+
for (let at = piece.indexOf('://'); at >= 0; at = piece.indexOf('://', at + 1)) {
|
|
103
|
+
let start = at;
|
|
104
|
+
while (start > after && SCHEME_CHAR.test(piece[start - 1])) start -= 1;
|
|
105
|
+
while (start < at && !SCHEME_LETTER.test(piece[start])) start += 1;
|
|
106
|
+
if (start < at) {
|
|
107
|
+
spans.push([start, at + 3]);
|
|
108
|
+
after = at + 3;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return spans;
|
|
112
|
+
}
|
|
92
113
|
const CLOSING = /[)\]}>"'.,;:!?]/;
|
|
93
114
|
const TRIED_BACK = 1;
|
|
94
115
|
/** The URLs a member may hold, from every scheme in each whitespace-delimited piece, without the closing brackets,
|
|
@@ -96,8 +117,8 @@ const TRIED_BACK = 1;
|
|
|
96
117
|
function urlsIn(value) {
|
|
97
118
|
const urls = [];
|
|
98
119
|
for (const piece of value.split(/\s+/)) {
|
|
99
|
-
for (const
|
|
100
|
-
const url = piece.slice(
|
|
120
|
+
for (const [start] of schemeSpans(piece)) {
|
|
121
|
+
const url = piece.slice(start);
|
|
101
122
|
let end = url.length;
|
|
102
123
|
while (end > 0 && CLOSING.test(url[end - 1])) end -= 1;
|
|
103
124
|
for (let back = 0; back <= TRIED_BACK && end + back <= url.length; back += 1) urls.push(url.slice(0, end + back));
|
|
@@ -108,8 +129,8 @@ function urlsIn(value) {
|
|
|
108
129
|
/** A long secret-shaped run in a piece read whole; in a piece that holds a URL, the text before its first scheme's `://`. */
|
|
109
130
|
const longRun = (value) =>
|
|
110
131
|
value.split(/\s+/).some((piece) => {
|
|
111
|
-
const
|
|
112
|
-
const cut =
|
|
132
|
+
const found = schemeSpans(piece);
|
|
133
|
+
const cut = found.length ? found[0][1] - 3 : -1;
|
|
113
134
|
return longSecret(cut < 0 ? piece : piece.slice(0, cut));
|
|
114
135
|
});
|
|
115
136
|
// hasJwt reads a JWT in one pass, where JWT_SHAPE backtracks from every `eyJ` of a long run (FAA-313).
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs
CHANGED
|
@@ -15,10 +15,10 @@
|
|
|
15
15
|
import crypto from 'node:crypto';
|
|
16
16
|
import fs from 'node:fs';
|
|
17
17
|
import http from 'node:http';
|
|
18
|
-
import net from 'node:net';
|
|
19
18
|
import { hubKeyCertified } from './fabric-estate.mjs';
|
|
20
19
|
import { atomicWrite } from './fabric-paths.mjs'; // the kit's private writer (FAA-85)
|
|
21
20
|
import { CheckedAgent, ListenerRefused } from './fabric-peer-owner.mjs'; // a loopback key set from this account's listener only (FAA-255)
|
|
21
|
+
import { loopbackHost } from './fabric-socket-proof.mjs';
|
|
22
22
|
import { effectClosure } from './fabric-mandate.mjs'; // defined once beside the mandate that reads it too (DEC-0125)
|
|
23
23
|
|
|
24
24
|
export const REFUSAL_CODES = Object.freeze([
|
|
@@ -413,7 +413,8 @@ export function readDelegationToken(headers) {
|
|
|
413
413
|
/** The digest an approval binds: sha256: and the hex SHA-256 of the action's RFC 8785 form. */
|
|
414
414
|
export const actionDigest = (action) => `sha256:${crypto.createHash('sha256').update(jcs(action), 'utf8').digest('hex')}`;
|
|
415
415
|
|
|
416
|
-
|
|
416
|
+
// A key set source's loopback host: the kit's one rule, shared with fabric-http's checkUrl (FAA-187 (15)).
|
|
417
|
+
const loopback = loopbackHost;
|
|
417
418
|
|
|
418
419
|
const KEY_SET_MAX = 65536;
|
|
419
420
|
const KEY_SET_AGENT = new CheckedAgent({ keepAlive: false });
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs
CHANGED
|
@@ -12,13 +12,13 @@ import { protocolsFindings } from './fabric-protocols.mjs';
|
|
|
12
12
|
import { CONNECTIONS_EXTENSION_KEY, connectionsView } from './fabric-connections.mjs';
|
|
13
13
|
import { ACTIONS_EXTENSION_KEY, actionsExtension, actionsExtensionRules } from './fabric-actions.mjs';
|
|
14
14
|
import { a2aSurfaceProblems } from './fabric-a2a.mjs';
|
|
15
|
-
import { ROUTES } from './fabric-usage.mjs';
|
|
15
|
+
import { ROUTES, isPlainObject } from './fabric-usage.mjs';
|
|
16
16
|
|
|
17
17
|
// The embedded dashboard declaration (fabric-dashboard/0.1, DEC-0036 ED-9), under the well-known
|
|
18
18
|
// document's `extensions`. The kit is assets/dashboard-kit; its version is the one in kit.json.
|
|
19
19
|
export const DASHBOARD_EXTENSION_KEY = 'https://fabric.passioncode.ai/agent-contract/extensions/dashboard/0.1';
|
|
20
20
|
|
|
21
|
-
export const DASHBOARD_KIT = 'fabric-dashboard-kit@1.1.
|
|
21
|
+
export const DASHBOARD_KIT = 'fabric-dashboard-kit@1.1.8';
|
|
22
22
|
|
|
23
23
|
export const REFERENCE_THEMES = ['graphite', 'fabric', 'paper', 'forest', 'ocean', 'ember', 'contrast'];
|
|
24
24
|
|
|
@@ -47,7 +47,8 @@ export function readDescriptors(dir = servicesDir()) {
|
|
|
47
47
|
if (!fs.existsSync(dir)) return [];
|
|
48
48
|
return fs.readdirSync(dir).filter((n) => n.endsWith('.json')).sort().flatMap((n) => {
|
|
49
49
|
try {
|
|
50
|
-
|
|
50
|
+
const value = JSON.parse(fs.readFileSync(path.join(dir, n), 'utf8'));
|
|
51
|
+
return isPlainObject(value) ? [[path.join(dir, n), value]] : []; // no object, no descriptor (FAA-200 (50))
|
|
51
52
|
} catch {
|
|
52
53
|
return [];
|
|
53
54
|
}
|
|
@@ -66,14 +67,16 @@ export function validateDescriptor(d) {
|
|
|
66
67
|
}
|
|
67
68
|
if (problems.length) return problems;
|
|
68
69
|
if (d.protocol !== PROTOCOL) problems.push(`protocol must be ${PROTOCOL}`);
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
for (const [key, pattern] of [['id', ID], ['instance', INSTANCE]]) { // a string before its pattern (FAA-200 (9), (36))
|
|
71
|
+
if (typeof d[key] !== 'string') problems.push(`${key} must be a string`);
|
|
72
|
+
else if (!pattern.test(d[key])) problems.push(`${key} must match ${pattern}`);
|
|
73
|
+
}
|
|
71
74
|
if (remote) problems.push(...remoteOriginProblems(d.origin));
|
|
72
75
|
else if (d.origin !== undefined && !ORIGIN.test(d.origin)) problems.push('origin must be http://127.0.0.1:<port>');
|
|
73
76
|
if (!d.auth?.tokenFile) problems.push('auth.tokenFile is required');
|
|
74
|
-
else if (!LOCAL_PATH.test(String(d.auth.tokenFile))) problems.push('auth.tokenFile must be
|
|
77
|
+
else if (!LOCAL_PATH.test(String(d.auth.tokenFile))) problems.push('auth.tokenFile must be an absolute local path (/, ~/ or a drive letter), never a relative path, never a network share');
|
|
75
78
|
for (const value of [...(d.paths?.data !== undefined ? [d.paths.data] : []), ...(d.paths?.logs ?? [])]) {
|
|
76
|
-
if (!LOCAL_PATH.test(String(value))) problems.push(`paths must be local paths, never a network share: ${JSON.stringify(value)}`);
|
|
79
|
+
if (!LOCAL_PATH.test(String(value))) problems.push(`paths must be absolute local paths (/, ~/ or a drive letter), never a relative path, never a network share: ${JSON.stringify(value)}`);
|
|
77
80
|
}
|
|
78
81
|
if (!(typeof (d.auth?.header ?? 'Authorization') === 'string' && AUTH_HEADER.test(d.auth?.header ?? 'Authorization'))) problems.push(`auth.header must match ${AUTH_HEADER.source}`);
|
|
79
82
|
if (!['Bearer', 'none'].includes(d.auth?.scheme ?? 'Bearer')) problems.push('auth.scheme is Bearer or none');
|
|
@@ -198,7 +201,7 @@ export function descriptorMemberProblems(d) {
|
|
|
198
201
|
else if (URI_PASSWORD.test(repository)) problems.push('source.repository carries no password: it names the repository, never a credential'); // before the host
|
|
199
202
|
else if (!authorityOk(repository)) problems.push('source.repository names a host, and a port of digits up to 65535 when it has one');
|
|
200
203
|
else if (repository.includes('@') && !HIERARCHICAL.test(repository)) problems.push('source.repository with a user is a URL with an authority (https://user@host/path), never an scp form'); // readings 7
|
|
201
|
-
else if (repositoryCredential(repository)) problems.push(
|
|
204
|
+
else if (repositoryCredential(repository)) problems.push("FAC-SEM-086: /source/repository holds a value shaped like a credential; it names a repository: write the repository's name in lower case if it is one"); // its name is no key (seam 6)
|
|
202
205
|
}
|
|
203
206
|
}
|
|
204
207
|
if ('peer' in d) {
|
|
@@ -428,15 +431,18 @@ export function portListenerRefusal(port, reader = portOwner) {
|
|
|
428
431
|
* account listens on (FAA-225; `portOwner(port) -> [kind, uid] | null` reads it, the kit's own by default).
|
|
429
432
|
* FAA-198: a new descriptor names its source.repository; rewriting one already installed without it only warns
|
|
430
433
|
* (process.emitWarning, code FABRIC_NO_SOURCE_REPOSITORY), so an installed agent's update keeps working. */
|
|
431
|
-
|
|
434
|
+
/** The rewrite warning of a descriptor that names no source.repository (FAA-198). */
|
|
435
|
+
export function warnNoSource(d) {
|
|
436
|
+
process.emitWarning(`Descriptor ${d.id}.${d.instance} ${NO_SOURCE_REPOSITORY}; a new install is refused without it.`, { code: 'FABRIC_NO_SOURCE_REPOSITORY' });
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export function writeDescriptor(d, dir = servicesDir(), { portOwner: reader = portOwner, warned = false } = {}) {
|
|
432
440
|
const problems = validateDescriptor(d);
|
|
433
441
|
if (problems.length) throw new ServiceError(`Descriptor is invalid: ${problems.join('; ')}.`);
|
|
434
442
|
const me = `${d.id}.${d.instance}`;
|
|
435
443
|
const refusal = sourceRefusal(d, dir);
|
|
436
444
|
if (refusal) throw new ServiceError(refusal);
|
|
437
|
-
if (!namesRepository(d))
|
|
438
|
-
process.emitWarning(`Descriptor ${me} ${NO_SOURCE_REPOSITORY}; a new install is refused without it.`, { code: 'FABRIC_NO_SOURCE_REPOSITORY' });
|
|
439
|
-
}
|
|
445
|
+
if (!namesRepository(d) && !warned) warnNoSource(d); // registerRemote warns itself, before its token (FAA-200 (1))
|
|
440
446
|
// DEC-0019: only a local placement claims a port on this computer; DEC-0069: an app's descriptor names none.
|
|
441
447
|
const port = placementOf(d) === 'remote' || d.origin === undefined ? null : portOf(d.origin);
|
|
442
448
|
for (const [file, other] of readDescriptors(dir)) {
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs
CHANGED
|
@@ -18,7 +18,12 @@ const TRACE_ID = /^(?!0{32}$)[0-9a-f]{32}$/;
|
|
|
18
18
|
|
|
19
19
|
const SPAN_ID = /^(?!0{16}$)[0-9a-f]{16}$/;
|
|
20
20
|
|
|
21
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* One activity event. `level` is one of LEVELS; `warn`, the dashboard kit's chip for "a person is needed"
|
|
23
|
+
* (`.fd-s-warn`), is stored as `warning`, the feed's name for the same level, so a reader that draws chips shows a
|
|
24
|
+
* stored `warning` as the `warn` chip (FAA-276, FAA-277 (6)). `data` (DEC-0070) adds machine-readable fields,
|
|
25
|
+
* `{schema, value}`, never a secret: refused here, so no feed carries one.
|
|
26
|
+
*/
|
|
22
27
|
export function makeEvent(id, at, kind, given, text, { subject, link, notify, traceId, spanId, data } = {}) {
|
|
23
28
|
const level = typeof given === 'string' && Object.hasOwn(LEVEL_ALIASES, given) ? LEVEL_ALIASES[given] : given;
|
|
24
29
|
if (!LEVELS.includes(level)) throw new ServiceError(`level must be one of ${LEVELS.join(', ')} (or \`warn\` for \`warning\`).`);
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
// the contract's pause grammar (`quota-exhausted[ until <until>]: waiting <reason> since <since>[: <message>]`; the
|
|
3
3
|
// pinned interop-job schema is closed, so `waiting {reason, since, until, message}` rides statusMessage, never a field
|
|
4
4
|
// of the job), one push per hold, and the operator's scoped lift. The twin of fabric_holds.py, held to the same
|
|
5
|
-
// vectors (test/fixtures/holds/waiting-vectors.json).
|
|
5
|
+
// vectors (test/fixtures/holds/waiting-vectors.json). A record written since 0.83.0 carries `squash: 'ecmascript'`; one
|
|
6
|
+
// without it is matched under both squashes, as 0.81.0's Python twin squashed names with str.split() (FAA-320,
|
|
7
|
+
// test/fixtures/holds/legacy-squash-vectors.json). Part of the Node service kit (layer L3, after fabric-interop).
|
|
6
8
|
// Node 20+, no dependency.
|
|
7
9
|
import crypto from 'node:crypto';
|
|
8
10
|
import fs from 'node:fs';
|
|
@@ -40,6 +42,17 @@ const reasonOk = (reason, what) => {
|
|
|
40
42
|
// (FAA-316): Python's str.split() also takes U+001C to U+001F and U+0085 and not U+FEFF.
|
|
41
43
|
const SPACES = /[\t\n\v\f\r \u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+/g;
|
|
42
44
|
const squash = (text) => String(text).replace(SPACES, ' ').replace(/^ | $/g, '');
|
|
45
|
+
/** What a record written since 0.83.0 carries: its names were squashed with ECMAScript's whitespace (FAA-320). */
|
|
46
|
+
export const SQUASH = 'ecmascript';
|
|
47
|
+
// Python's str.isspace() class, the one 0.81.0's Python twin squashed a name with (`" ".join(text.split())`).
|
|
48
|
+
const PY_SPACES = /[\t\n\v\f\r\u001c-\u001f \u0085\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000]+/g;
|
|
49
|
+
const legacySquash = (text) => String(text).replace(PY_SPACES, ' ').replace(/^ | $/g, '');
|
|
50
|
+
// The stored forms `caller` matches in `record`: the ECMAScript squash, and for a record with no `squash` mark (written
|
|
51
|
+
// before 0.83.0) also the squash 0.81.0's Python twin wrote (FAA-320). null matches only null.
|
|
52
|
+
const names = (caller, record) => {
|
|
53
|
+
if (typeof caller !== 'string') return [caller];
|
|
54
|
+
return record.squash === SQUASH ? [squash(caller)] : [squash(caller), legacySquash(caller)];
|
|
55
|
+
};
|
|
43
56
|
|
|
44
57
|
/** A held job's statusMessage; the message is shortened, never refused, to fit 500 characters. */
|
|
45
58
|
export function waitingMessage(reason, since, until = null, message = null) {
|
|
@@ -149,6 +162,7 @@ export class HoldBook {
|
|
|
149
162
|
|
|
150
163
|
/** Hold `work` for `caller` by `reason`: {hold, statusMessage, push}; push only when this call opened the hold. */
|
|
151
164
|
hold({ caller, work, reason, until = null, message = null, jobId = null, now = null }) {
|
|
165
|
+
const rawCaller = caller;
|
|
152
166
|
caller = text(caller, 'caller');
|
|
153
167
|
work = text(work, 'work');
|
|
154
168
|
reasonOk(reason, 'hold');
|
|
@@ -158,10 +172,10 @@ export class HoldBook {
|
|
|
158
172
|
}
|
|
159
173
|
const data = this.load();
|
|
160
174
|
HoldBook.expire(data, utcTime(at));
|
|
161
|
-
let hold = data.holds.find((h) => h.closed === null && h.reason === reason && h.caller
|
|
175
|
+
let hold = data.holds.find((h) => h.closed === null && h.reason === reason && names(rawCaller, h).includes(h.caller));
|
|
162
176
|
const opened = hold === undefined;
|
|
163
177
|
if (opened) {
|
|
164
|
-
hold = { id: newId(), reason, caller, work, since: at, until, jobs: [], closed: null };
|
|
178
|
+
hold = { id: newId(), reason, caller, work, since: at, until, jobs: [], closed: null, squash: SQUASH };
|
|
165
179
|
data.holds.push(hold);
|
|
166
180
|
}
|
|
167
181
|
if (jobId !== null && !hold.jobs.includes(String(jobId))) hold.jobs.push(String(jobId));
|
|
@@ -176,7 +190,7 @@ export class HoldBook {
|
|
|
176
190
|
release({ reason, caller, now = null }) {
|
|
177
191
|
const at = this.now(now);
|
|
178
192
|
const data = this.load();
|
|
179
|
-
const hold = data.holds.find((h) => h.closed === null && h.reason === reason && h.caller
|
|
193
|
+
const hold = data.holds.find((h) => h.closed === null && h.reason === reason && names(caller, h).includes(h.caller));
|
|
180
194
|
if (!hold) return null;
|
|
181
195
|
hold.closed = 'released';
|
|
182
196
|
hold.closedAt = at;
|
|
@@ -211,12 +225,12 @@ export class HoldBook {
|
|
|
211
225
|
throw new ServiceError('A raised limit needs its window\'s end (until): it falls back when the window ends.');
|
|
212
226
|
}
|
|
213
227
|
const record = { id: newId(), reason, caller: caller === null ? null : text(caller, 'caller'),
|
|
214
|
-
job: jobId === null ? null : String(jobId), from: at, until, [kind]: size, decision: { ...decision }, at };
|
|
228
|
+
job: jobId === null ? null : String(jobId), from: at, until, [kind]: size, decision: { ...decision }, at, squash: SQUASH };
|
|
215
229
|
if (kind !== 'limit') record.left = size;
|
|
216
230
|
const data = this.load();
|
|
217
231
|
HoldBook.expire(data, utcTime(at));
|
|
218
232
|
for (const hold of data.holds) {
|
|
219
|
-
if (hold.closed === null && hold.reason === reason && (
|
|
233
|
+
if (hold.closed === null && hold.reason === reason && (caller === null || names(caller, hold).includes(hold.caller))
|
|
220
234
|
&& (jobId === null || hold.jobs.includes(String(jobId)))) {
|
|
221
235
|
hold.closed = 'lifted';
|
|
222
236
|
hold.closedAt = at;
|
|
@@ -234,7 +248,7 @@ export class HoldBook {
|
|
|
234
248
|
|
|
235
249
|
static matching(data, reason, caller, jobId, at, kind) {
|
|
236
250
|
return data.lifts.filter((l) => l.reason === reason && kind in l && HoldBook.live(l, at)
|
|
237
|
-
&& (l.caller === null || l.caller
|
|
251
|
+
&& (l.caller === null || names(caller, l).includes(l.caller)) && (l.job === null || l.job === jobId));
|
|
238
252
|
}
|
|
239
253
|
|
|
240
254
|
/** Whether a lift lets work past the limit: {admitted: true, lift, left} spends one job or `costUsd`, else {admitted: false}. */
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
import http from 'node:http';
|
|
23
23
|
import net from 'node:net';
|
|
24
24
|
import { CheckedAgent, ListenerRefused, TABLE_RESTRICTED, UNKNOWN, method as tableMethod, owner as ownerOf, refusal, serverEnd } from './fabric-peer-owner.mjs';
|
|
25
|
+
import { loopbackHost } from './fabric-socket-proof.mjs';
|
|
25
26
|
|
|
26
27
|
export const RETRY_STATUSES = Object.freeze([408, 425, 429, 500, 502, 503, 504]);
|
|
27
28
|
export const IDEMPOTENT_METHODS = Object.freeze(['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE']);
|
|
@@ -78,11 +79,8 @@ export function redactUrl(url) {
|
|
|
78
79
|
return `${parts.protocol}//${parts.host}${parts.pathname}`;
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
if (h === 'localhost' || h.endsWith('.localhost') || h === '::1') return true;
|
|
84
|
-
return /^127\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(h);
|
|
85
|
-
}
|
|
82
|
+
// Loopback as new URL leaves the host: the kit's one rule, shared with fabric-delegation (FAA-187 (15)).
|
|
83
|
+
const isLoopback = loopbackHost;
|
|
86
84
|
|
|
87
85
|
// Refuse what this client never sends: another scheme, no host, credentials in the URL, plain http off loopback.
|
|
88
86
|
export function checkUrl(url, { insecure = false } = {}) {
|
|
@@ -79,7 +79,8 @@ export function fabricRoot(platform = process.platform, env = process.env) {
|
|
|
79
79
|
/**
|
|
80
80
|
* true: a process of this OS account runs under pid; false: none does. null (falsy): a process of another account does
|
|
81
81
|
* (EPERM), as when the id hub.json names was reused: not verified, never called running (FAA-304, the session hooks'
|
|
82
|
-
* answer since FAA-254). Never signals it.
|
|
82
|
+
* answer since FAA-254). Never signals it. On Windows libuv opens the process for terminate, query and synchronize, so
|
|
83
|
+
* an elevated process of the same user answers EPERM too; the Python twin asks OpenProcess for the same access (FAA-323).
|
|
83
84
|
*/
|
|
84
85
|
export function pidAlive(pid) {
|
|
85
86
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
@@ -192,7 +193,7 @@ export function readCredential(file) {
|
|
|
192
193
|
* is HubError hub-unavailable (retryable); a listener that is not this OS account's process is HubError foreign-user
|
|
193
194
|
* (retryable) before the bearer is sent (DEC-0154 HA-2): the connection is checked from the socket table first.
|
|
194
195
|
*/
|
|
195
|
-
export function postMcp(origin, body, bearer, { session, timeout = 15000, method = 'POST', protocolVersion, agent = HUB_AGENT } = {}) {
|
|
196
|
+
export function postMcp(origin, body, bearer, { session, timeout = 15000, method = 'POST', protocolVersion, agent = HUB_AGENT, deadline = 0 } = {}) {
|
|
196
197
|
const m = LOOPBACK_ORIGIN.exec(origin);
|
|
197
198
|
if (!m) return Promise.reject(new HubError('not-a-hub', 'Only the hub\'s loopback origin is used.'));
|
|
198
199
|
const payload = method === 'DELETE' ? null : Buffer.from(JSON.stringify(body));
|
|
@@ -224,6 +225,9 @@ export function postMcp(origin, body, bearer, { session, timeout = 15000, method
|
|
|
224
225
|
res.on('error', () => reject(new HubError('hub-unavailable', 'Fabric\'s hub did not answer (the answer broke off).', true)));
|
|
225
226
|
});
|
|
226
227
|
req.on('timeout', () => req.destroy(new Error('timeout')));
|
|
228
|
+
// `deadline` (ms) bounds the whole request, where `timeout` bounds each socket read (FAA-187 (12))
|
|
229
|
+
const timer = deadline > 0 ? setTimeout(() => req.destroy(new Error('deadline')), deadline) : null;
|
|
230
|
+
if (timer) req.on('close', () => clearTimeout(timer));
|
|
227
231
|
req.on('error', (e) => reject(e instanceof ListenerRefused
|
|
228
232
|
? listenerRefusal(e)
|
|
229
233
|
: new HubError('hub-unavailable', `Fabric's hub did not answer (${e.code ?? e.message}).`, true)));
|