@passioncode-ai/fabric-agent-adapter 0.81.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 +200 -3
- package/README.md +11 -6
- package/SECURITY.md +8 -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 +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/dashboard-links.md +9 -1
- 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 +25 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +19 -1
- 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 +274 -17
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +24 -151
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check_cli.py +164 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_tree.py +3 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +17 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +24 -6
- 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 +14 -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 +23 -10
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +15 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +9 -1
- 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 +6 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +28 -4
- 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 +30 -11
- 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_dashboard_link.py +19 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +4 -2
- 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/check_service_channel_agent.py +7 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +22 -11
- 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 +29 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +5 -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 +27 -10
- 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 +21 -8
- 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 +88 -5
- 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 +133 -30
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +44 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +3 -0
- 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 +35 -159
- 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 +43 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +21 -18
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +39 -17
- 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 +92 -10
- 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 +180 -162
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +274 -17
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +3 -0
- 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 +465 -55
- 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/dashboard-links.md +9 -1
- 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/creating-fabric-agents/scripts/check_dashboard_link.py +19 -1
- package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +2 -2
- 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 +30 -17
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +53 -35
- 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 +34 -21
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +96 -15
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +147 -45
|
@@ -56,8 +56,9 @@ export class HubError extends Error {
|
|
|
56
56
|
/**
|
|
57
57
|
* The HubError for a hub port the peer-owner check refused (DEC-0154 HA-2): code `foreign-user`, the pinned contract's
|
|
58
58
|
* state name, with the owner's reason in the sentence and in `data.reason` (FAA-272): `table-restricted` when this
|
|
59
|
-
* runtime's socket table hides other processes' sockets,
|
|
60
|
-
* owner's kind or the check's reason
|
|
59
|
+
* runtime's socket table hides other processes' sockets, or, on macOS, holds no row for this connection (DEC-0155
|
|
60
|
+
* RT-1, FAA-312), which never clears, so it is not retryable; otherwise the owner's kind or the check's reason
|
|
61
|
+
* (`another-account`, or `no-row`, which only Linux and Windows give), retryable.
|
|
61
62
|
*/
|
|
62
63
|
export function listenerRefusal(e) {
|
|
63
64
|
const kind = e?.owner?.kind ?? null;
|
|
@@ -75,14 +76,19 @@ export function fabricRoot(platform = process.platform, env = process.env) {
|
|
|
75
76
|
return path.join(env.XDG_DATA_HOME || path.join(home, '.local/share'), 'passioncode-fabric');
|
|
76
77
|
}
|
|
77
78
|
|
|
78
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* true: a process of this OS account runs under pid; false: none does. null (falsy): a process of another account does
|
|
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. 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).
|
|
84
|
+
*/
|
|
79
85
|
export function pidAlive(pid) {
|
|
80
86
|
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
81
87
|
try {
|
|
82
88
|
process.kill(pid, 0);
|
|
83
89
|
return true;
|
|
84
|
-
} catch {
|
|
85
|
-
return false;
|
|
90
|
+
} catch (e) {
|
|
91
|
+
return e?.code === 'EPERM' ? null : false;
|
|
86
92
|
}
|
|
87
93
|
}
|
|
88
94
|
|
|
@@ -108,7 +114,8 @@ function readSmall(file) {
|
|
|
108
114
|
|
|
109
115
|
/**
|
|
110
116
|
* hub.json read the way an agent must, every call. Answers {origin, mcpUrl, doorTokenFile, pid, startedAt, features} or
|
|
111
|
-
* throws HubError (not-running, retryable; not-a-hub
|
|
117
|
+
* throws HubError (not-running, retryable; not-a-hub, retryable when hub.json names another OS account's process, not
|
|
118
|
+
* verified, FAA-304). Reads no token. `estate` is {record, applied}: with it, a hubKey
|
|
112
119
|
* the document names is checked by hubKeyCertified (DEC-0060); fabric-hub/0.1 names none.
|
|
113
120
|
*/
|
|
114
121
|
export function locateHub(root = fabricRoot(), { alive = pidAlive, estate, now } = {}) {
|
|
@@ -142,7 +149,10 @@ export function locateHub(root = fabricRoot(), { alive = pidAlive, estate, now }
|
|
|
142
149
|
sameRoot = false;
|
|
143
150
|
}
|
|
144
151
|
if (!sameRoot) throw notHub('names a door token outside the registry root');
|
|
145
|
-
|
|
152
|
+
const live = alive(doc.pid);
|
|
153
|
+
// FAA-304: another account's process under that id, so not this account's Fabric (as FAA-254)
|
|
154
|
+
if (live === null) throw new HubError('not-a-hub', `Fabric's hub.json names process ${doc.pid}, which runs under another OS account: not verified.`, true);
|
|
155
|
+
if (!live) throw new HubError('not-running', `Fabric is not running: hub.json names process ${doc.pid}, which is not running.`, true);
|
|
146
156
|
const features = Array.isArray(doc.features) ? doc.features.filter((f) => typeof f === 'string') : [];
|
|
147
157
|
if (estate && doc.hubKey && typeof doc.hubKey === 'object') {
|
|
148
158
|
const verdict = hubKeyCertified(estate.record, doc.hubKey.kid, doc.hubKey.x, now ?? Date.now() / 1000, estate.applied ?? undefined);
|
|
@@ -183,7 +193,7 @@ export function readCredential(file) {
|
|
|
183
193
|
* is HubError hub-unavailable (retryable); a listener that is not this OS account's process is HubError foreign-user
|
|
184
194
|
* (retryable) before the bearer is sent (DEC-0154 HA-2): the connection is checked from the socket table first.
|
|
185
195
|
*/
|
|
186
|
-
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 } = {}) {
|
|
187
197
|
const m = LOOPBACK_ORIGIN.exec(origin);
|
|
188
198
|
if (!m) return Promise.reject(new HubError('not-a-hub', 'Only the hub\'s loopback origin is used.'));
|
|
189
199
|
const payload = method === 'DELETE' ? null : Buffer.from(JSON.stringify(body));
|
|
@@ -215,6 +225,9 @@ export function postMcp(origin, body, bearer, { session, timeout = 15000, method
|
|
|
215
225
|
res.on('error', () => reject(new HubError('hub-unavailable', 'Fabric\'s hub did not answer (the answer broke off).', true)));
|
|
216
226
|
});
|
|
217
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));
|
|
218
231
|
req.on('error', (e) => reject(e instanceof ListenerRefused
|
|
219
232
|
? listenerRefusal(e)
|
|
220
233
|
: new HubError('hub-unavailable', `Fabric's hub did not answer (${e.code ?? e.message}).`, true)));
|
|
@@ -7,7 +7,7 @@ import crypto from 'node:crypto';
|
|
|
7
7
|
import { ServiceError } from './fabric-base.mjs';
|
|
8
8
|
import { jcs, refusalFromAction, refusalToolResult } from './fabric-delegation.mjs';
|
|
9
9
|
import { actionDecision, decisionShape, jobCapability } from './fabric-actions.mjs'; // DEC-0121
|
|
10
|
-
import { EFFECT_CLASSES, OPERATOR_SOURCE_PROTOCOL, mandateCovers, mandateProblem, withoutSource } from './fabric-mandate.mjs'; // DEC-0125, DEC-0126
|
|
10
|
+
import { EFFECT_CLASSES, OPERATOR_SOURCE_PROTOCOL, isAgentId, mandateCovers, mandateProblem, withoutSource } from './fabric-mandate.mjs'; // DEC-0125, DEC-0126
|
|
11
11
|
import { isString } from './fabric-jsvalue.mjs'; // the core's (FAA-65)
|
|
12
12
|
import { isObject } from './fabric-jsvalue.mjs'; // the core's (FAA-65)
|
|
13
13
|
|
|
@@ -277,6 +277,25 @@ export function jobStep({ mandate, tier, now, self, capability, key, effect, cal
|
|
|
277
277
|
/** The project of a call whose project could not be read: never in a mandate's projects (FAA-139). */
|
|
278
278
|
export const UNREADABLE_PROJECT = '\u0000unreadable';
|
|
279
279
|
|
|
280
|
+
/**
|
|
281
|
+
* The caller's agent id when a service's own hook answered something no chain can hold (an id the registry refuses, not
|
|
282
|
+
* a string, or the hook threw): passed as callerAgent it matches no agent, so every mandate reads
|
|
283
|
+
* mandate-not-for-this-call (fail closed, FAA-135). Never an agent id (isAgentId refuses it).
|
|
284
|
+
*/
|
|
285
|
+
export const UNREADABLE_AGENT = '\u0000unreadable-agent';
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* FAA-135: the caller's agent id as a service's own hook named it from a per-agent credential (contract delegation.md,
|
|
289
|
+
* `surfaces.mcp.auth: own`), read as the mandate chain binds it: pass the answer as actionGate's and jobStep's
|
|
290
|
+
* `callerAgent`. null or undefined (no per-agent credential) is null, and the chain's end alone binds the mandate, as
|
|
291
|
+
* before; an agent id the registry accepts is that id; anything else is UNREADABLE_AGENT. A hook that throws: pass
|
|
292
|
+
* UNREADABLE_AGENT. The Python twin's McpToolServer(identify_agent=) reads it so; never a name the call carries.
|
|
293
|
+
*/
|
|
294
|
+
export function identifiedAgent(value) {
|
|
295
|
+
if (value === null || value === undefined) return null;
|
|
296
|
+
return isAgentId(value) ? value : UNREADABLE_AGENT;
|
|
297
|
+
}
|
|
298
|
+
|
|
280
299
|
/** A spend estimate as a mandate reads it (FAA-139): a finite number stays, anything else is none (a ceiling refuses it). */
|
|
281
300
|
export function mandateSpend(value) {
|
|
282
301
|
return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs
CHANGED
|
@@ -43,6 +43,8 @@ export {
|
|
|
43
43
|
mandateSpend,
|
|
44
44
|
keptStep,
|
|
45
45
|
UNREADABLE_PROJECT,
|
|
46
|
+
UNREADABLE_AGENT,
|
|
47
|
+
identifiedAgent,
|
|
46
48
|
actionRefusalToolResult,
|
|
47
49
|
usageFindings,
|
|
48
50
|
usageToward,
|
|
@@ -271,6 +273,9 @@ export class JobStore {
|
|
|
271
273
|
requestInput(jobId, inputRequests, statusMessage, { steps } = {}) {
|
|
272
274
|
const entries = Object.entries(inputRequests ?? {});
|
|
273
275
|
if (!entries.length || Array.isArray(inputRequests) || typeof inputRequests !== 'object') throw new InteropError('input_required needs a JSON object of at least one input request.'); // a JSON object of requests, first (unit 38, 40)
|
|
276
|
+
if (steps !== null && steps !== undefined && (typeof steps !== 'object' || Array.isArray(steps))) { // FAA-196 (26)
|
|
277
|
+
throw new InteropError('steps maps an input request\'s key to its approval step: a JSON object, or none.');
|
|
278
|
+
}
|
|
274
279
|
const kept = Object.create(null); // the value checked is the value kept (seam 34); a key __proto__ stays a key (35)
|
|
275
280
|
for (const [key, step] of Object.entries(steps ?? {})) {
|
|
276
281
|
kept[key] = Object.hasOwn(inputRequests, key) && step !== null && step !== undefined ? keptStep(step) : {};
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
// One copy of a service, locked before any side effect (LC-01, DEC-0057): the instance lock and the supervisor marker.
|
|
2
2
|
// Part of the Node service kit (FAA-65, layer L3); the twin of fabric_lock.py. Node 20+ (22+ for the Linux lock), no dependency.
|
|
3
|
+
//
|
|
4
|
+
// A lock that moves under its holder (FAA-303). Linux and Windows hold a name keyed by the lock's path (an abstract
|
|
5
|
+
// unix socket, a named pipe), so a folder swapped under a running copy cannot let a second copy in. macOS has no such
|
|
6
|
+
// name: O_EXLOCK (and the Python twin's flock) lock an inode, so after a data import swaps the data folder a second
|
|
7
|
+
// copy locks the new file and starts. There the holder watches its lock instead (InstanceLock#watch, started by
|
|
8
|
+
// holdSingleInstance): every LOCK_WATCH_SECONDS it compares the inode it locked with the file at the path, and once
|
|
9
|
+
// the path names another file it stops with 75 and one sentence naming the path, before writing anything more into a
|
|
10
|
+
// folder that is no longer the one it read. Two copies then overlap for at most one interval. A path-keyed name on
|
|
11
|
+
// macOS was weighed and left: it needs a file in a folder outside the data folder that the system never cleans and
|
|
12
|
+
// every copy can write, which Node alone cannot promise.
|
|
3
13
|
import crypto from 'node:crypto';
|
|
4
14
|
import fs from 'node:fs';
|
|
5
15
|
import net from 'node:net';
|
|
@@ -15,6 +25,12 @@ export const LOCK_BACKOFF_FIRST_SECONDS = 0.5;
|
|
|
15
25
|
|
|
16
26
|
export const LOCK_BACKOFF_MAX_SECONDS = 30;
|
|
17
27
|
|
|
28
|
+
/**
|
|
29
|
+
* How often a holder that has no path-keyed name (macOS) compares the inode it locked with the file at the lock's path
|
|
30
|
+
* (FAA-303): one stat and one fstat; two copies overlap for at most this long after a folder swap.
|
|
31
|
+
*/
|
|
32
|
+
export const LOCK_WATCH_SECONDS = 2;
|
|
33
|
+
|
|
18
34
|
const O_EXLOCK = 0x20; // BSD/macOS: open(2) takes flock(LOCK_EX); absent from fs.constants
|
|
19
35
|
|
|
20
36
|
export class AlreadyRunning extends ServiceError {
|
|
@@ -34,6 +50,7 @@ export class InstanceLock {
|
|
|
34
50
|
this.path = path.join(dataDir, 'service.lock');
|
|
35
51
|
this.fd = null;
|
|
36
52
|
this.server = null;
|
|
53
|
+
this.timer = null;
|
|
37
54
|
}
|
|
38
55
|
|
|
39
56
|
async acquire() {
|
|
@@ -124,11 +141,75 @@ export class InstanceLock {
|
|
|
124
141
|
return true;
|
|
125
142
|
}
|
|
126
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Re-check, every `intervalSeconds` on an unref'd timer, that the file this copy locked is still the file at the path
|
|
146
|
+
* (FAA-303); the first time the path names another file (a swapped folder's lock, or the one a second copy made),
|
|
147
|
+
* call `onLost(lock)` once and stop watching. The default stops the process with 75 and one sentence
|
|
148
|
+
* (stopLostHolder). A path that is gone or cannot be read now is read as nothing there yet and tried again next time:
|
|
149
|
+
* a second copy has to make a file there to start, and the watch then sees that file. `release` ends the watch. Only
|
|
150
|
+
* a lock held as a file is watched (macOS): elsewhere the descriptor is null and the path-keyed name keeps a second
|
|
151
|
+
* copy out, so this is a no-op, as for a lock not held.
|
|
152
|
+
*/
|
|
153
|
+
watch({ onLost, intervalSeconds = LOCK_WATCH_SECONDS } = {}) {
|
|
154
|
+
if (this.fd === null || this.timer !== null) return this;
|
|
155
|
+
const fd = this.fd;
|
|
156
|
+
const react = onLost ?? stopLostHolder;
|
|
157
|
+
this.timer = setInterval(() => {
|
|
158
|
+
let here;
|
|
159
|
+
let held;
|
|
160
|
+
try {
|
|
161
|
+
here = fs.statSync(this.path);
|
|
162
|
+
held = fs.fstatSync(fd);
|
|
163
|
+
} catch {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
if (here.dev === held.dev && here.ino === held.ino) return;
|
|
167
|
+
clearInterval(this.timer);
|
|
168
|
+
this.timer = null;
|
|
169
|
+
react(this);
|
|
170
|
+
}, intervalSeconds * 1000);
|
|
171
|
+
this.timer.unref();
|
|
172
|
+
return this;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Let the lock go. Never throws, so a release in a `finally` never replaces the caller's own outcome (FAA-196 (11)):
|
|
177
|
+
* the descriptor and the name's server are forgotten before they are closed, so a retry never closes a descriptor
|
|
178
|
+
* reused since, and a close that fails still closes the server.
|
|
179
|
+
*/
|
|
127
180
|
release() {
|
|
128
|
-
if (this.
|
|
129
|
-
|
|
181
|
+
if (this.timer !== null) clearInterval(this.timer);
|
|
182
|
+
const { fd, server } = this;
|
|
183
|
+
this.timer = null;
|
|
130
184
|
this.fd = null;
|
|
131
185
|
this.server = null;
|
|
186
|
+
try {
|
|
187
|
+
if (fd !== null) fs.closeSync(fd);
|
|
188
|
+
} catch { // the descriptor is gone either way; the kernel frees the lock with it
|
|
189
|
+
} finally {
|
|
190
|
+
try {
|
|
191
|
+
server?.close();
|
|
192
|
+
} catch { // nothing to add to the caller's own outcome
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** The one sentence a holder whose lock moved under it stops with (FAA-303). */
|
|
199
|
+
export function lostLockMessage(lockPath) {
|
|
200
|
+
return `${lockPath} is no longer the file this copy locked (the data folder moved or was swapped?): stopping, so a copy started on the folder now at that path runs alone.`;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* InstanceLock#watch's default: write lostLockMessage and end the process with 75 at once. Not a graceful stop: a
|
|
205
|
+
* shutdown would write this copy's state into the folder now at the path, which is not the one it read; the kit's
|
|
206
|
+
* journals make an abrupt stop recoverable (LC-03).
|
|
207
|
+
*/
|
|
208
|
+
export function stopLostHolder(lock) {
|
|
209
|
+
try {
|
|
210
|
+
fs.writeSync(2, `${lostLockMessage(lock.path)}\n`);
|
|
211
|
+
} finally {
|
|
212
|
+
process.exit(EXIT_ALREADY_RUNNING);
|
|
132
213
|
}
|
|
133
214
|
}
|
|
134
215
|
|
|
@@ -253,10 +334,12 @@ const sleepSeconds = (s) => new Promise((resolve) => setTimeout(resolve, s * 100
|
|
|
253
334
|
* supervisor that restarts on exit (launchd's KeepAlive) an immediate exit is a respawn every ThrottleInterval,
|
|
254
335
|
* forever; so a supervised copy first backs off in-process (0.5 s doubling to 30 s,
|
|
255
336
|
* LOCK_WAIT_SUPERVISED_SECONDS in all), takes over if the holder leaves, and exits 75 only when
|
|
256
|
-
* the wait runs out. `waitSeconds` overrides the choice (0 = never wait).
|
|
337
|
+
* the wait runs out. `waitSeconds` overrides the choice (0 = never wait). Where the lock is held as a file (macOS)
|
|
338
|
+
* the held lock is watched (InstanceLock#watch, FAA-303): a folder swapped under the running copy stops it with 75;
|
|
339
|
+
* `onLost` replaces that reaction.
|
|
257
340
|
*/
|
|
258
341
|
export async function holdSingleInstance(dataDir, {
|
|
259
|
-
waitSeconds, sleep = sleepSeconds, clock = () => performance.now() / 1000, exit = (code) => process.exit(code), env, argv,
|
|
342
|
+
waitSeconds, sleep = sleepSeconds, clock = () => performance.now() / 1000, exit = (code) => process.exit(code), env, argv, onLost,
|
|
260
343
|
} = {}) {
|
|
261
344
|
// DEC-0057: markers that disagree exit 75 at once, before the lock, naming them.
|
|
262
345
|
const marker = supervisorMarker({ ...(env === undefined ? {} : { env }), ...(argv === undefined ? {} : { argv }) });
|
|
@@ -270,7 +353,7 @@ export async function holdSingleInstance(dataDir, {
|
|
|
270
353
|
let announced = false;
|
|
271
354
|
for (;;) {
|
|
272
355
|
try {
|
|
273
|
-
return await new InstanceLock(dataDir).acquire();
|
|
356
|
+
return (await new InstanceLock(dataDir).acquire()).watch({ onLost });
|
|
274
357
|
} catch (error) {
|
|
275
358
|
if (!(error instanceof AlreadyRunning)) throw error;
|
|
276
359
|
const remaining = deadline - clock();
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-mandate.mjs
CHANGED
|
@@ -193,6 +193,11 @@ const invalid = (detail) => ({ ok: false, code: MANDATE_INVALID, data: { detail
|
|
|
193
193
|
* being where the operator said the words when the mandate carries it (DEC-0126 OA-6). `callerAgent` is
|
|
194
194
|
* passed only when the callee authenticated the caller's agent id; without one the chain's end alone binds it. A missing
|
|
195
195
|
* or non-object `call` is read as a call that names nothing (the reference would throw).
|
|
196
|
+
*
|
|
197
|
+
* A call is a plain object of its own data properties (FAA-196 (19)): its members are read as own members only, where
|
|
198
|
+
* the contract's reference reads plain properties, so an inherited or getter `project`, `action` or `spendUsd` is none
|
|
199
|
+
* here and may be covered where the reference refuses it. Build the call as an object literal of the values the work
|
|
200
|
+
* names, as the kit's own readers do (the board's `covers`, actionGate, jobStep).
|
|
196
201
|
*/
|
|
197
202
|
export function mandateCovers(input) {
|
|
198
203
|
const field = (key) => member(input, key);
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs
CHANGED
|
@@ -86,6 +86,26 @@ function checkAttribution(projectId, taskRunId) {
|
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
// Why a base URL is not sent the same by both twins, read from the URL as sent (its ends trimmed), or null: a backslash
|
|
90
|
+
// anywhere (new URL reads it as a slash), and after a `//` authority a path holding anything but URL path characters
|
|
91
|
+
// and %XX escapes (Python refused a space or a character outside ASCII where new URL percent-encoded it, and new URL
|
|
92
|
+
// removes a tab or a line break) or a dot segment, escaped or not (new URL collapses it). The Python twin's
|
|
93
|
+
// base_url_problem is the same rule (FAA-187 (18), kit-openrouter-cases.json).
|
|
94
|
+
export function baseUrlProblem(base) {
|
|
95
|
+
const text = String(base).replace(/^[\x00-\x20]+|[\x00-\x20]+$/g, '');
|
|
96
|
+
if (text.includes('\\')) return 'the base URL holds a backslash';
|
|
97
|
+
if (/[\t\n\r]/.test(text)) return 'the base URL holds a tab or a line break'; // new URL removes them and sends the rest
|
|
98
|
+
const rest = text.slice(text.indexOf(':') + 1);
|
|
99
|
+
if (text.indexOf(':') < 0 || !rest.startsWith('//')) return null; // no authority as written: checkUrl reads it
|
|
100
|
+
const after = rest.slice(2);
|
|
101
|
+
const path = after.includes('/') ? after.slice(after.indexOf('/')) : '';
|
|
102
|
+
if (!/^(?:\/(?:[A-Za-z0-9._~!$&'()*+,;=:@-]|%[0-9A-Fa-f]{2})*)*$/.test(path)) {
|
|
103
|
+
return 'the base URL\'s path holds a character outside URL path characters and %XX escapes';
|
|
104
|
+
}
|
|
105
|
+
if (path.split('/').slice(1).some((seg) => ['.', '..'].includes(seg.replace(/%2e/gi, '.')))) return 'the base URL\'s path holds a dot segment';
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
|
|
89
109
|
export class Client {
|
|
90
110
|
constructor({ ceilingUsd, ledger, prices, keyEnv = 'OPENROUTER_API_KEY', window = 'day', baseUrl = BASE_URL,
|
|
91
111
|
attribution = null, projectId, unmeasuredCallUsd = null, deadlineS = 120, retries = 1, env = process.env,
|
|
@@ -108,13 +128,14 @@ export class Client {
|
|
|
108
128
|
if (/[?#]/.test(baseUrl)) {
|
|
109
129
|
throw new OpenRouterError('the base URL holds no query or fragment', { kind: 'config' });
|
|
110
130
|
}
|
|
131
|
+
const problem = baseUrlProblem(baseUrl);
|
|
132
|
+
if (problem) throw new OpenRouterError(`${problem} (both kits would not send it alike)`, { kind: 'config' });
|
|
111
133
|
http.checkUrl(baseUrl);
|
|
112
134
|
checkAttribution(projectId, undefined);
|
|
113
135
|
Object.assign(this, { ceilingUsd, ledger, keyEnv, window, attribution, projectId, unmeasuredCallUsd, deadlineS, retries });
|
|
114
136
|
this.prices = Object.fromEntries(Object.entries(prices ?? {}).map(([k, v]) => [k, { ...v }]));
|
|
115
|
-
// The base URL as checkUrl read it:
|
|
116
|
-
//
|
|
117
|
-
// composed them in 0.55.0 (0.56.0 product r20, unit r22, seam r22; the Python twin refuses what urllib cannot send).
|
|
137
|
+
// The base URL as checkUrl read it: its ends trimmed of C0 controls and spaces, so `.../v1/ ` is composed as
|
|
138
|
+
// `.../v1/chat/completions` (0.56.0 product r20); a tab or line break inside is refused above since FAA-187 (18).
|
|
118
139
|
this.baseUrl = baseUrl.replace(/[\t\n\r]/g, '').replace(/^[\x00-\x20]+|[\x00-\x20]+$/g, '').replace(/\/+$/, '');
|
|
119
140
|
this.env = env;
|
|
120
141
|
this.transport = transport;
|