@passioncode-ai/fabric-agent-adapter 0.40.0 → 0.42.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 +160 -0
- package/README.md +9 -3
- package/SECURITY.md +27 -6
- package/bin/fabric-agent-adapter.js +71 -6
- package/package.json +2 -2
- package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +42 -66
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +4 -2
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/operator-channel-and-repository.md +55 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +9 -3
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/verification.md +15 -0
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +181 -26
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +66 -5
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +81 -23
- package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +28 -30
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +21 -9
- 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/support-agent/README.md +47 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +230 -29
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +21 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +5 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +3 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +3 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +5 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_layout.mjs +68 -24
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +66 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/dashboard_kit.py +53 -19
- 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-link.mjs +145 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +196 -188
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-shapes.mjs +5 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +2 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ceremonies.mjs +6 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +3 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +48 -14
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +34 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +157 -22
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +7 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +12 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_backups.py +6 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +17 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +3 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +162 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +207 -227
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_shapes.py +4 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ceremonies.py +0 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_crypto.py +52 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +101 -45
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +12 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +5 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +13 -10
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +6 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +118 -71
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +272 -74
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +1 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +36 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +3 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +270 -35
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +25 -7
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +29 -60
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +4 -2
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/operator-channel-and-repository.md +55 -0
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +66 -5
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +3 -3
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/native-host-hooks.md +2 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +36 -16
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +11 -0
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +44 -22
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +54 -6
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_hosts.py +72 -13
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mail.py +28 -7
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +277 -49
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/host_hooks.py +10 -1
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/hosts/context.mjs +25 -2
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/hosts/openclaw/index.mjs +4 -2
- package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/hosts/opencode/fabric.mjs +2 -1
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
// The board responder (DEC-0078, FAA-12b):
|
|
1
|
+
// The board responder (DEC-0078, FAA-12b): the work context a handler sees and the responder loop. The session, the
|
|
2
|
+
// checkpoint and the request live in fabric-board-link.mjs and are re-exported here.
|
|
2
3
|
// Part of the Node board connector (FAA-65, layer L4); the twin of fabric_board_session.py. Node 20+, no dependency.
|
|
3
4
|
import crypto from 'node:crypto';
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
|
|
5
|
+
import { Backoff, HubError, normalProject } from './fabric-hub.mjs';
|
|
6
|
+
import { acceptedRevisions, feedDataProblems, negotiateRevision } from './fabric-service.mjs';
|
|
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 } from './fabric-board-link.mjs';
|
|
9
|
+
|
|
10
|
+
export { BoardRequest, BoardSession, Checkpoint }; // re-exported: callers import them from here since 0.40.0
|
|
9
11
|
|
|
10
12
|
export const RENEW_SECONDS = 20;
|
|
11
13
|
|
|
@@ -21,146 +23,12 @@ export const STOP_CODES = Object.freeze(new Set(['fenced', 'lease_expired', 'aut
|
|
|
21
23
|
|
|
22
24
|
const TERMINAL = new Set(['completed', 'failed_known', 'cancelled', 'expired']);
|
|
23
25
|
|
|
26
|
+
/** A lost call is sent again this long before the lease it keeps would lapse (FAA-77, finding 38). */
|
|
27
|
+
export const LEASE_MARGIN_SECONDS = 10;
|
|
28
|
+
|
|
24
29
|
/** How long a held slot waits before it asks again: doubling from the poll interval, never past this. */
|
|
25
30
|
export const HOLD_RETRY_MAX_SECONDS = 900;
|
|
26
31
|
|
|
27
|
-
/** MCP over HTTP with a board session's one-session bearer; calls are serialised; close() sends the session's DELETE. */
|
|
28
|
-
export class BoardSession {
|
|
29
|
-
constructor(grant, { timeout = 15000, post = postMcp } = {}) {
|
|
30
|
-
Object.assign(this, { grant, timeout, post });
|
|
31
|
-
this.mcpSession = null;
|
|
32
|
-
this.protocolVersion = MCP_PROTOCOL_VERSION;
|
|
33
|
-
this.rpc = 0;
|
|
34
|
-
this.queue = Promise.resolve();
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
#serial(fn) {
|
|
38
|
-
const run = this.queue.then(fn, fn);
|
|
39
|
-
this.queue = run.catch(() => {});
|
|
40
|
-
return run;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async #send(body, method = 'POST') {
|
|
44
|
-
try {
|
|
45
|
-
return await this.post(this.grant.origin, body, this.grant.bearer(), { session: this.mcpSession, timeout: this.timeout, method, protocolVersion: this.mcpSession ? this.protocolVersion : undefined });
|
|
46
|
-
} catch (e) {
|
|
47
|
-
if (e instanceof HubError) throw new BoardError('unreachable', 'Fabric\'s hub did not answer; a write may have been stored, so send it again with the same idempotency key.', true);
|
|
48
|
-
throw e;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
open() {
|
|
53
|
-
return this.#serial(async () => {
|
|
54
|
-
this.rpc += 1;
|
|
55
|
-
const answer = await this.#send({ jsonrpc: '2.0', id: this.rpc, method: 'initialize', params: { protocolVersion: MCP_PROTOCOL_VERSION, capabilities: {}, clientInfo: { name: 'fabric-agent-adapter-board', version: '1' } } });
|
|
56
|
-
if (answer.status === 401) throw new BoardError('session-ended', 'Fabric refused the board session\'s bearer; join again.', true);
|
|
57
|
-
const sid = answer.headers['mcp-session-id'];
|
|
58
|
-
if (answer.status !== 200 || !isObj(answer.body) || !isObj(answer.body.result) || !sid) throw new BoardError('unreachable', `Fabric did not open the board session (HTTP ${answer.status}).`, true);
|
|
59
|
-
this.mcpSession = String(sid);
|
|
60
|
-
if (typeof answer.body.result.protocolVersion === 'string') this.protocolVersion = answer.body.result.protocolVersion;
|
|
61
|
-
await this.#send({ jsonrpc: '2.0', method: 'notifications/initialized' });
|
|
62
|
-
});
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
call(tool, args) {
|
|
66
|
-
if (!RESPOND_TOOLS.has(tool)) return Promise.reject(new BoardError('not-in-scope', `${tool} is not served to a board session; use a Project session for it.`, false, tool));
|
|
67
|
-
return this.#serial(async () => {
|
|
68
|
-
if (!this.mcpSession) throw new BoardError('session-ended', 'The board session is not open.', true, tool);
|
|
69
|
-
this.rpc += 1;
|
|
70
|
-
const answer = await this.#send({ jsonrpc: '2.0', id: this.rpc, method: 'tools/call', params: { name: tool, arguments: args } });
|
|
71
|
-
if (answer.status === 401) throw new BoardError('session-ended', 'Fabric ended the board session (it left, was revoked or expired); join again.', true, tool);
|
|
72
|
-
let value;
|
|
73
|
-
try {
|
|
74
|
-
value = toolAnswer(answer, tool);
|
|
75
|
-
} catch (e) {
|
|
76
|
-
throw new BoardError(e.retryable ? 'unreachable' : 'receipt-shape', e.message, e.retryable, tool);
|
|
77
|
-
}
|
|
78
|
-
if ('error' in value) {
|
|
79
|
-
if (refusalProblems(value).length) throw new BoardError('receipt-shape', `Fabric refused ${tool} in a form the contract does not name.`, false, tool);
|
|
80
|
-
throw new BoardError(value.error.code, value.error.message, value.error.retryable, tool);
|
|
81
|
-
}
|
|
82
|
-
return value;
|
|
83
|
-
});
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
close() {
|
|
87
|
-
return this.#serial(async () => {
|
|
88
|
-
if (!this.mcpSession) return;
|
|
89
|
-
try {
|
|
90
|
-
await this.#send({}, 'DELETE');
|
|
91
|
-
} catch {
|
|
92
|
-
// best effort: the lease lapses in 60 s anyway
|
|
93
|
-
}
|
|
94
|
-
this.mcpSession = null;
|
|
95
|
-
});
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/** The responder's journal (0600 JSON, the same file fabric_board.py writes), fsynced before the step it announces. */
|
|
100
|
-
export class Checkpoint {
|
|
101
|
-
constructor(file) {
|
|
102
|
-
this.file = file;
|
|
103
|
-
this.entries = this.#load();
|
|
104
|
-
}
|
|
105
|
-
#load() {
|
|
106
|
-
let text;
|
|
107
|
-
try {
|
|
108
|
-
text = fs.readFileSync(this.file, 'utf8');
|
|
109
|
-
} catch (e) {
|
|
110
|
-
if (e.code === 'ENOENT') return {};
|
|
111
|
-
throw new BoardError('checkpoint', `The board checkpoint ${this.file} cannot be read (${e.code}); fix or move it before answering requests.`);
|
|
112
|
-
}
|
|
113
|
-
let doc;
|
|
114
|
-
try {
|
|
115
|
-
doc = JSON.parse(text);
|
|
116
|
-
} catch {
|
|
117
|
-
doc = null;
|
|
118
|
-
}
|
|
119
|
-
if (!isObj(doc) || doc.version !== 1 || !isObj(doc.requests)) throw new BoardError('checkpoint', `The board checkpoint ${this.file} is not one this kit writes.`);
|
|
120
|
-
return doc.requests;
|
|
121
|
-
}
|
|
122
|
-
#write(entries) {
|
|
123
|
-
try {
|
|
124
|
-
ensurePrivateDir(path.dirname(this.file));
|
|
125
|
-
const sorted = Object.fromEntries(Object.keys(entries).sort().map((k) => [k, entries[k]]));
|
|
126
|
-
atomicWrite(this.file, `${JSON.stringify({ requests: sorted, version: 1 }, null, 1)}\n`, 0o600);
|
|
127
|
-
} catch (e) {
|
|
128
|
-
throw new BoardError('checkpoint', `The board checkpoint could not be written (${e.code ?? e.message}); nothing was sent.`);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
put(entry) {
|
|
132
|
-
const entries = { ...this.entries, [entry.request]: JSON.parse(JSON.stringify(entry)) };
|
|
133
|
-
this.#write(entries);
|
|
134
|
-
this.entries = entries;
|
|
135
|
-
}
|
|
136
|
-
drop(requestId) {
|
|
137
|
-
if (!(requestId in this.entries)) return;
|
|
138
|
-
const { [requestId]: _gone, ...entries } = this.entries;
|
|
139
|
-
this.#write(entries);
|
|
140
|
-
this.entries = entries;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
/** A claimed request as data: never an instruction. */
|
|
145
|
-
export class BoardRequest {
|
|
146
|
-
constructor(message, capability) {
|
|
147
|
-
const req = isObj(message.request) ? message.request : {};
|
|
148
|
-
const body = isObj(message.body) ? message.body : {};
|
|
149
|
-
this.requestId = req.id;
|
|
150
|
-
this.messageId = message.id;
|
|
151
|
-
this.capability = capability;
|
|
152
|
-
this.body = typeof body.text === 'string' ? body.text : null;
|
|
153
|
-
this.bodyFormat = body.format ?? 'text/plain';
|
|
154
|
-
this.artifacts = Array.isArray(message.artifacts) ? [...message.artifacts] : [];
|
|
155
|
-
this.sender = isObj(message.sender) ? message.sender : {};
|
|
156
|
-
this.deadline = req.deadline;
|
|
157
|
-
this.thread = message.thread;
|
|
158
|
-
}
|
|
159
|
-
toJSON() {
|
|
160
|
-
return { requestId: this.requestId, capability: this.capability }; // never the body
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
|
|
164
32
|
/** What a handler may do while it works: progress(note), beginEffect(summary) before it spends or acts, and `stopped`. */
|
|
165
33
|
export class WorkContext {
|
|
166
34
|
#responder;
|
|
@@ -213,6 +81,16 @@ export class Responder {
|
|
|
213
81
|
/** The first pause under an operator's hold (DEC-0079); it doubles to HOLD_RETRY_MAX_SECONDS. */
|
|
214
82
|
this.holdDelay = POLL_SECONDS;
|
|
215
83
|
this.onHold = new Set();
|
|
84
|
+
/** One fenced call at a time, and the entry edits beside them (FAA-77, finding 36). */
|
|
85
|
+
this.fenceQueue = Promise.resolve();
|
|
86
|
+
this.toResume = []; // transferred requests waiting until nothing runs (finding 39)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Runs fn after every fenced call queued before it (finding 36): an await inside a call never lets another in. */
|
|
90
|
+
locked(fn) {
|
|
91
|
+
const run = this.fenceQueue.then(fn, fn);
|
|
92
|
+
this.fenceQueue = run.catch(() => {});
|
|
93
|
+
return run;
|
|
216
94
|
}
|
|
217
95
|
|
|
218
96
|
// -- events --
|
|
@@ -365,12 +243,18 @@ export class Responder {
|
|
|
365
243
|
} else if (transfer[capability]?.length) await this.replace(capability, transfer[capability], generations[capability] ?? 0);
|
|
366
244
|
else await this.enroll(capability);
|
|
367
245
|
}
|
|
368
|
-
for (const entry of reconcile)
|
|
246
|
+
for (const entry of reconcile) {
|
|
247
|
+
if (this.slots[entry.capability]?.state === 'held') await this.reconcile(entry);
|
|
248
|
+
else if (entry.step !== 'unknown') {
|
|
249
|
+
entry.step = 'unknown'; // the slot is not this session's yet: a later tick reconciles it (finding 38)
|
|
250
|
+
this.checkpoint.put(entry);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
369
253
|
}
|
|
370
254
|
|
|
371
255
|
async replace(capability, entries, generation) {
|
|
372
|
-
const args = { idempotency: this.idem(this.key('replace')), capability, expectedGeneration: isInt(generation, 0) ? generation : 0
|
|
373
|
-
|
|
256
|
+
const args = { idempotency: this.idem(this.key('replace')), capability, expectedGeneration: isInt(generation, 0) ? generation : 0 };
|
|
257
|
+
if (entries.length) args.transfer = { requests: entries.map((e) => e.request).slice(0, 64), checkpoint: { summary: 'Resumed by the same service after a restart; no effect had begun.' } };
|
|
374
258
|
let receipt;
|
|
375
259
|
try {
|
|
376
260
|
receipt = this.receipt('com.responder_replace', await this.call('com.responder_replace', args));
|
|
@@ -395,28 +279,84 @@ export class Responder {
|
|
|
395
279
|
this.emit('board.request.held', 'warn', 'An accepted request could not be resumed: this Fabric does not hand back a transferred attempt (DEC-0078). It stays held; nothing was done.', { notify: true, episode: entry.request, request: entry.request, capability });
|
|
396
280
|
continue;
|
|
397
281
|
}
|
|
282
|
+
if (entry.step === 'replying' && !entry.replied && entry.keys?.reply && !entry.probe) {
|
|
283
|
+
// Finding 35: whether the old reply was stored is asked under the old fence, but only from the resume queue, after
|
|
284
|
+
// the new attempt is recorded: a second lost answer then leaves the request queued, never stranded.
|
|
285
|
+
entry.probe = { generation: entry.generation, attempt: entry.attempt, digest: entry.digest, revision: entry.revision, key: entry.keys.reply };
|
|
286
|
+
}
|
|
398
287
|
Object.assign(entry, { generation: item.attempt.generation, attempt: item.attempt.id, digest: item.attempt.digest, revision: item.revision,
|
|
399
288
|
step: ['accepting', 'held', 'lapsed'].includes(entry.step) ? 'accepted' : entry.step });
|
|
289
|
+
// Finding 35: a fenced key's digest named the old attempt; under the new one it is idempotency_conflict.
|
|
290
|
+
entry.keys = entry.keys?.reconcile ? { reconcile: entry.keys.reconcile } : {};
|
|
400
291
|
this.checkpoint.put(entry);
|
|
401
|
-
if (!this.
|
|
292
|
+
if (!this.toResume.includes(entry.request)) this.toResume.push(entry.request);
|
|
402
293
|
}
|
|
294
|
+
await this.resumeNext();
|
|
403
295
|
}
|
|
404
296
|
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
297
|
+
/**
|
|
298
|
+
* A reply whose answer was lost under the old attempt (finding 35): a stored key answers before the fence is checked
|
|
299
|
+
* (project-comms.md, Idempotency), so the old call again, under the old fence kept in `probe`, says whether it was
|
|
300
|
+
* stored; a refusal says it was not. A lost answer, or a board that cannot be read now, throws and leaves `probe` for
|
|
301
|
+
* the next try: neither says whether the reply was stored.
|
|
302
|
+
*/
|
|
303
|
+
async repliedBefore(entry) {
|
|
304
|
+
const { key, generation, attempt, digest, revision } = entry.probe;
|
|
305
|
+
let replied = true;
|
|
306
|
+
try {
|
|
307
|
+
this.receipt('com.reply', await this.call('com.reply', { ...this.replyExtra(entry), fence: this.fence({ ...entry, generation, attempt, digest, revision }, key) }));
|
|
308
|
+
} catch (e) {
|
|
309
|
+
if (['unreachable', 'session-ended', 'authority_changed', 'not_authorized', 'not_available', 'capacity_exceeded'].includes(e.code) || !(e instanceof BoardError)) throw e;
|
|
310
|
+
replied = false;
|
|
410
311
|
}
|
|
312
|
+
if (replied) entry.replied = true;
|
|
313
|
+
delete entry.probe;
|
|
314
|
+
this.checkpoint.put(entry);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Resume a transferred request while nothing runs; each idle tick takes the next (finding 39). A request leaves the
|
|
319
|
+
* queue only once its probe has answered: a lost answer keeps it first in line for the next tick (finding 35).
|
|
320
|
+
*/
|
|
321
|
+
async resumeNext() {
|
|
322
|
+
while (!this.current && this.toResume.length && !this.stopping) {
|
|
323
|
+
const stored = this.checkpoint.entries[this.toResume[0]];
|
|
324
|
+
if (!stored || this.slots[stored.capability]?.state !== 'held') {
|
|
325
|
+
this.toResume.shift();
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const entry = JSON.parse(JSON.stringify(stored));
|
|
329
|
+
if (entry.probe) await this.repliedBefore(entry);
|
|
330
|
+
this.toResume.shift();
|
|
331
|
+
await this.resume(entry);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** An observed outcome: the one already known, else what observe(entry) reads; null when nothing was observed. */
|
|
336
|
+
async observed(entry) {
|
|
411
337
|
let outcome = isObj(entry.outcome) && entry.outcome.observed === true ? entry.outcome : null;
|
|
412
338
|
if (!outcome && this.observe) {
|
|
413
339
|
try {
|
|
414
340
|
outcome = await this.observe({ ...entry });
|
|
415
341
|
} catch {
|
|
416
|
-
outcome = null;
|
|
342
|
+
outcome = null; // an observation that failed is no observation
|
|
417
343
|
}
|
|
418
344
|
}
|
|
419
|
-
|
|
345
|
+
return isObj(outcome) && outcome.observed === true && !outcomeProblems({ ...outcome, observed: true }, true).length ? outcome : null;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async reconcile(entry) {
|
|
349
|
+
const state = await this.readState(entry);
|
|
350
|
+
if (state !== 'outcome_unknown') {
|
|
351
|
+
if (state !== null && (TERMINAL.has(state) || state === 'queued' || state === 'claimed')) this.checkpoint.drop(entry.request);
|
|
352
|
+
else if (state === 'in_progress' && entry.step !== 'unknown') {
|
|
353
|
+
entry.step = 'unknown'; // no next holder took the slot yet: a later tick tries again (finding 38)
|
|
354
|
+
this.checkpoint.put(entry);
|
|
355
|
+
}
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const outcome = await this.observed(entry);
|
|
359
|
+
if (!outcome) {
|
|
420
360
|
entry.step = 'unknown';
|
|
421
361
|
this.checkpoint.put(entry);
|
|
422
362
|
this.emit('board.reconcile.needed', 'warn', 'A request\'s outcome is unknown: the work may have happened. It stays unknown until this service observes what happened.', { notify: true, episode: entry.request, request: entry.request, capability: entry.capability });
|
|
@@ -447,6 +387,10 @@ export class Responder {
|
|
|
447
387
|
if (!this.session) await this.connect();
|
|
448
388
|
await this.tick();
|
|
449
389
|
this.backoff.reset();
|
|
390
|
+
if (this.state === 'down') { // a lost call was sent again and answered: the session held throughout
|
|
391
|
+
this.state = 'ready';
|
|
392
|
+
delete this.said['board.down'];
|
|
393
|
+
}
|
|
450
394
|
return this.interval();
|
|
451
395
|
} catch (e) {
|
|
452
396
|
if (!(e instanceof BoardError) && !(e instanceof HubError)) throw e;
|
|
@@ -454,9 +398,18 @@ export class Responder {
|
|
|
454
398
|
}
|
|
455
399
|
}
|
|
456
400
|
|
|
401
|
+
/** Seconds this session's leases still run, by its own clock: the running attempt's, else the oldest held slot's. */
|
|
402
|
+
leaseLeft() {
|
|
403
|
+
const renewed = this.current ? [this.current.renewedAt ?? 0] : Object.values(this.slots).filter((s) => s.state === 'held').map((s) => s.renewedAt ?? 0);
|
|
404
|
+
return renewed.length && this.session ? Math.min(...renewed) + LEASE_SECONDS - this.now() : 0;
|
|
405
|
+
}
|
|
406
|
+
|
|
457
407
|
async fault(e) {
|
|
458
408
|
const code = e.code ?? 'unreachable';
|
|
459
|
-
|
|
409
|
+
let wait = this.backoff.next();
|
|
410
|
+
const left = this.leaseLeft() - LEASE_MARGIN_SECONDS;
|
|
411
|
+
const keep = code === 'unreachable' && left > 0; // finding 38: the lease holds, so the session and the work stay
|
|
412
|
+
if (keep) wait = Math.max(1, Math.min(wait, left)); // and the call goes again before the lease lapses
|
|
460
413
|
this.waitUntil = this.now() + wait;
|
|
461
414
|
if (['not_authorized', 'authority_changed', 'credential'].includes(code)) {
|
|
462
415
|
this.state = 'not-authorized';
|
|
@@ -465,13 +418,14 @@ export class Responder {
|
|
|
465
418
|
} else if (code === 'held') {
|
|
466
419
|
this.state = 'paused';
|
|
467
420
|
this.emit('board.held', 'info', 'The operator holds new work on this Project; the responder waits and asks again with a growing pause.', { episode: 'held', reason: code });
|
|
468
|
-
} else if (['no-epoch', 'schema-unsupported', 'checkpoint', 'receipt-shape'].includes(code)) {
|
|
421
|
+
} else if (['no-epoch', 'schema-unsupported', 'checkpoint', 'receipt-shape', 'idempotency_conflict'].includes(code)) {
|
|
422
|
+
// idempotency_conflict is a key sent with other content, a defect and not an outage (finding 35)
|
|
469
423
|
this.state = 'down';
|
|
470
424
|
await this.dropSession();
|
|
471
425
|
this.emit('board.blocked', 'error', `The responder stopped answering requests: ${e.message}`, { notify: true, episode: code, reason: code });
|
|
472
426
|
} else {
|
|
473
427
|
this.state = 'down';
|
|
474
|
-
if (['session-ended', 'unreachable', 'not-running', 'not-a-hub', 'hub-unavailable', 'protocol'].includes(code)) await this.dropSession();
|
|
428
|
+
if (['session-ended', 'unreachable', 'not-running', 'not-a-hub', 'hub-unavailable', 'protocol'].includes(code) && !keep) await this.dropSession();
|
|
475
429
|
this.emit('board.down', 'warn', 'Fabric\'s board cannot be reached; the responder tries again with a growing pause.', { episode: 'down', reason: code });
|
|
476
430
|
}
|
|
477
431
|
return wait;
|
|
@@ -480,6 +434,7 @@ export class Responder {
|
|
|
480
434
|
async dropSession() {
|
|
481
435
|
const work = this.current;
|
|
482
436
|
this.current = null;
|
|
437
|
+
this.toResume = []; // the next session transfers them again
|
|
483
438
|
if (work) {
|
|
484
439
|
const { entry } = work;
|
|
485
440
|
if (work.done && !work.failure && !outcomeProblems(work.outcome, !!entry.effect).length) {
|
|
@@ -511,7 +466,10 @@ export class Responder {
|
|
|
511
466
|
}
|
|
512
467
|
if (capability !== heldByWork && this.now() - (slot.renewedAt ?? 0) >= RENEW_SECONDS - 0.5) await this.enroll(capability);
|
|
513
468
|
}
|
|
514
|
-
if (!this.current && !this.stopping)
|
|
469
|
+
if (!this.current && !this.stopping) {
|
|
470
|
+
await this.continueLapsed();
|
|
471
|
+
await this.resumeNext();
|
|
472
|
+
}
|
|
515
473
|
if (!this.current && !this.stopping) {
|
|
516
474
|
await this.reconcilePending();
|
|
517
475
|
await this.claimNext();
|
|
@@ -622,7 +580,7 @@ export class Responder {
|
|
|
622
580
|
this.checkpoint.put(entry);
|
|
623
581
|
let receipt;
|
|
624
582
|
try {
|
|
625
|
-
receipt = await this.fenced('com.accept', entry,
|
|
583
|
+
receipt = await this.fenced('com.accept', entry, entry.keys.accept);
|
|
626
584
|
} catch (e) {
|
|
627
585
|
if (e.code !== 'held') throw e;
|
|
628
586
|
// A hold came between the claim and the accept (DEC-0079): nothing was done, the claim lapses and the request waits.
|
|
@@ -657,16 +615,33 @@ export class Responder {
|
|
|
657
615
|
await this.runWork(entry, new BoardRequest(message, entry.capability));
|
|
658
616
|
}
|
|
659
617
|
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
618
|
+
/**
|
|
619
|
+
* A fenced call, one at a time (finding 36): fence built, sent and receipt applied under the lock. A revision_conflict
|
|
620
|
+
* on com.renew or com.progress (they store nothing) reads the revision and sends once more. On a stop code the attempt
|
|
621
|
+
* is lost: the work stops, and it answers null (authority_changed throws after, so the responder stops claiming).
|
|
622
|
+
*/
|
|
623
|
+
async fenced(op, entry, key, extra = {}, apply = null) {
|
|
624
|
+
let again = op === 'com.renew' || op === 'com.progress';
|
|
625
|
+
for (;;) {
|
|
626
|
+
try {
|
|
627
|
+
return await this.locked(async () => {
|
|
628
|
+
const receipt = this.receipt(op, await this.call(op, { ...extra, fence: this.fence(entry, key) }));
|
|
629
|
+
apply?.(receipt);
|
|
630
|
+
return receipt;
|
|
631
|
+
});
|
|
632
|
+
} catch (e) {
|
|
633
|
+
if (e.code === 'revision_conflict' && again) {
|
|
634
|
+
again = false;
|
|
635
|
+
await this.locked(() => this.readState(entry));
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
if (STOP_CODES.has(e.code) || ['revision_conflict', 'invalid_transition', 'digest_conflict'].includes(e.code)) {
|
|
639
|
+
this.lose(entry, e.code);
|
|
640
|
+
if (e.code === 'authority_changed') throw e;
|
|
641
|
+
return null;
|
|
642
|
+
}
|
|
643
|
+
throw e;
|
|
668
644
|
}
|
|
669
|
-
throw e;
|
|
670
645
|
}
|
|
671
646
|
}
|
|
672
647
|
|
|
@@ -688,8 +663,7 @@ export class Responder {
|
|
|
688
663
|
|
|
689
664
|
async renew(work) {
|
|
690
665
|
if (this.now() - (work.renewedAt ?? 0) < RENEW_SECONDS - 0.5) return;
|
|
691
|
-
|
|
692
|
-
if (!receipt) {
|
|
666
|
+
if (!(await this.fenced('com.renew', work.entry, this.key('renew')))) {
|
|
693
667
|
this.current = null;
|
|
694
668
|
return;
|
|
695
669
|
}
|
|
@@ -698,9 +672,8 @@ export class Responder {
|
|
|
698
672
|
}
|
|
699
673
|
|
|
700
674
|
async progress(entry, note) {
|
|
701
|
-
const
|
|
702
|
-
if (
|
|
703
|
-
if (!(await this.fenced('com.progress', entry, args))) throw new Stopped();
|
|
675
|
+
const extra = note !== undefined && note !== null ? { note: { text: clean(note) || 'Working.' } } : {};
|
|
676
|
+
if (!(await this.fenced('com.progress', entry, this.key('progress'), extra))) throw new Stopped();
|
|
704
677
|
}
|
|
705
678
|
|
|
706
679
|
async effectBegin(entry, summary, ctx) {
|
|
@@ -708,13 +681,18 @@ export class Responder {
|
|
|
708
681
|
entry.step = 'effect-beginning';
|
|
709
682
|
entry.effect = true; // from here a lost attempt is an unknown outcome, never "nothing happened"
|
|
710
683
|
this.checkpoint.put(entry);
|
|
711
|
-
const
|
|
684
|
+
const extra = summary ? { summary: clean(summary) } : {};
|
|
685
|
+
const begun = (receipt) => {
|
|
686
|
+
entry.revision = receipt.request.revision;
|
|
687
|
+
entry.step = 'in-progress';
|
|
688
|
+
this.checkpoint.put(entry);
|
|
689
|
+
};
|
|
712
690
|
let receipt = null;
|
|
713
691
|
let lost = 0;
|
|
714
692
|
let delay = this.holdDelay;
|
|
715
693
|
for (;;) { // a lost answer: the same key again tells whether it was stored
|
|
716
694
|
try {
|
|
717
|
-
receipt = await this.fenced('com.effect_begin', entry,
|
|
695
|
+
receipt = await this.fenced('com.effect_begin', entry, entry.keys.effect_begin, extra, begun);
|
|
718
696
|
break;
|
|
719
697
|
} catch (e) {
|
|
720
698
|
if (e.code === 'held') {
|
|
@@ -742,9 +720,6 @@ export class Responder {
|
|
|
742
720
|
}
|
|
743
721
|
}
|
|
744
722
|
if (!receipt) throw new Stopped();
|
|
745
|
-
entry.revision = receipt.request.revision;
|
|
746
|
-
entry.step = 'in-progress';
|
|
747
|
-
this.checkpoint.put(entry);
|
|
748
723
|
}
|
|
749
724
|
|
|
750
725
|
async finish(work) {
|
|
@@ -755,37 +730,70 @@ export class Responder {
|
|
|
755
730
|
}
|
|
756
731
|
let { outcome } = work;
|
|
757
732
|
if (work.failure) {
|
|
758
|
-
if (entry.effect
|
|
733
|
+
if (entry.effect) {
|
|
734
|
+
await this.settleLive(entry, 'handler-failed');
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
737
|
+
if (work.failure instanceof Stopped) {
|
|
759
738
|
this.current = null;
|
|
760
|
-
if (entry.effect) this.lose(entry, 'handler-failed');
|
|
761
739
|
return;
|
|
762
740
|
}
|
|
763
741
|
outcome = { state: 'failed_known', observed: true, summary: `The service could not do this (${work.failure?.name ?? 'Error'}).` };
|
|
764
742
|
}
|
|
765
743
|
const problems = outcomeProblems(outcome, !!entry.effect);
|
|
766
744
|
if (problems.length) {
|
|
767
|
-
this.
|
|
768
|
-
if (entry.effect) this.
|
|
745
|
+
this.emit('board.outcome.invalid', 'warn', `The handler's outcome was not sent: ${problems[0]}.`, { request: entry.request, capability: entry.capability });
|
|
746
|
+
if (entry.effect) await this.settleLive(entry, problems.some((p) => p.includes('observation_required')) ? 'observation_required' : 'outcome');
|
|
769
747
|
else {
|
|
770
748
|
entry.outcome = { state: 'failed_known', observed: true, summary: 'The service answered in a form it cannot send.' };
|
|
771
749
|
await this.complete(entry);
|
|
772
750
|
}
|
|
773
|
-
this.emit('board.outcome.invalid', 'warn', `The handler's outcome was not sent: ${problems[0]}.`, { request: entry.request, capability: entry.capability });
|
|
774
751
|
return;
|
|
775
752
|
}
|
|
776
753
|
entry.outcome = { ...outcome, observed: outcome.observed ?? !entry.effect };
|
|
777
754
|
await this.complete(entry);
|
|
778
755
|
}
|
|
779
756
|
|
|
757
|
+
/**
|
|
758
|
+
* Finding 37: the handler threw after beginEffect, or answered unobserved, while the attempt is live. Only an observed
|
|
759
|
+
* result settles a started effect (project-comms.md, Request lifecycle), so observe(entry) is completed under the live
|
|
760
|
+
* fence. With nothing observed, com.responder_replace on this responder's own slot is the contract's lost-result
|
|
761
|
+
* operation ("an in_progress one becomes outcome_unknown ... when the next holder takes the slot", Slots end with the
|
|
762
|
+
* session); com.reconcile settles it once something is observed.
|
|
763
|
+
*/
|
|
764
|
+
async settleLive(entry, code) {
|
|
765
|
+
const lost = !!this.current?.ctx.stopped; // the attempt is not live: nothing to settle
|
|
766
|
+
const state = lost ? null : await this.readState(entry);
|
|
767
|
+
if (state === 'accepted') { // effect_begin was never stored, so nothing began: a known failure
|
|
768
|
+
entry.effect = false;
|
|
769
|
+
entry.outcome = { state: 'failed_known', observed: true, summary: `The service could not do this (${code}).` };
|
|
770
|
+
await this.complete(entry);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
const outcome = state === 'in_progress' ? await this.observed(entry) : null;
|
|
774
|
+
if (outcome) {
|
|
775
|
+
entry.outcome = { ...Object.fromEntries(['state', 'summary', 'evidence'].filter((k) => k in outcome).map((k) => [k, outcome[k]])), observed: true };
|
|
776
|
+
await this.complete(entry);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
this.current = null;
|
|
780
|
+
this.lose(entry, code);
|
|
781
|
+
const slot = this.slots[entry.capability] ?? {};
|
|
782
|
+
if (state === 'in_progress' && slot.state === 'held') await this.replace(entry.capability, [], slot.generation ?? 0);
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
replyExtra(entry) {
|
|
786
|
+
const { reply } = entry.outcome;
|
|
787
|
+
return { body: { text: reply.text, ...(reply.format ? { format: reply.format } : {}) }, ...(reply.artifacts?.length ? { artifacts: reply.artifacts } : {}) };
|
|
788
|
+
}
|
|
789
|
+
|
|
780
790
|
async complete(entry) {
|
|
781
791
|
const { outcome } = entry;
|
|
782
|
-
|
|
783
|
-
if (reply && !entry.replied) {
|
|
792
|
+
if (outcome.reply && !entry.replied) {
|
|
784
793
|
entry.keys.reply ??= this.key('reply');
|
|
785
794
|
entry.step = 'replying';
|
|
786
795
|
this.checkpoint.put(entry);
|
|
787
|
-
|
|
788
|
-
if (!(await this.fenced('com.reply', entry, args))) {
|
|
796
|
+
if (!(await this.fenced('com.reply', entry, entry.keys.reply, this.replyExtra(entry)))) {
|
|
789
797
|
this.current = null;
|
|
790
798
|
return;
|
|
791
799
|
}
|
|
@@ -796,7 +804,7 @@ export class Responder {
|
|
|
796
804
|
entry.step = 'completing';
|
|
797
805
|
this.checkpoint.put(entry);
|
|
798
806
|
const sent = Object.fromEntries(['state', 'observed', 'summary', 'evidence'].filter((k) => k in outcome).map((k) => [k, outcome[k]]));
|
|
799
|
-
const receipt = await this.fenced('com.complete', entry,
|
|
807
|
+
const receipt = await this.fenced('com.complete', entry, entry.keys.complete, { outcome: sent });
|
|
800
808
|
this.current = null;
|
|
801
809
|
if (!receipt) return;
|
|
802
810
|
this.checkpoint.drop(entry.request);
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-shapes.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import fs from 'node:fs';
|
|
|
5
5
|
import { sameIdentifier } from './fabric-identifiers.mjs';
|
|
6
6
|
|
|
7
7
|
// A service agent answers a Project's requests on Fabric's project board (fabric-project-comms/0.1; contract DEC-0022,
|
|
8
|
-
// DEC-0073, DEC-0077, DEC-0078; Fabric ADR-0149; FAA-12b, FAA-40). The twin of fabric_board.py
|
|
8
|
+
// DEC-0073, DEC-0077, DEC-0078; Fabric ADR-0149; FAA-12b, FAA-40). The twin of fabric_board.py: the same steps, the
|
|
9
9
|
// same checkpoint file, the same answers; Node built-ins only. fabric-hub.mjs finds the hub.
|
|
10
10
|
//
|
|
11
11
|
// enroll (capability) → claim → read the message → accept (the exact digest) → [effectBegin] → [progress] →
|
|
@@ -14,12 +14,12 @@ import { sameIdentifier } from './fabric-identifiers.mjs';
|
|
|
14
14
|
// Responder runs that loop inside the service process: one tick every 15 s ± 5 s, claiming when idle and renewing the
|
|
15
15
|
// 60 s lease (at about 20 s) while work runs. Before it advances a step it writes and fsyncs a checkpoint; a restart
|
|
16
16
|
// reconciles work whose outcome is unknown (com.reconcile) and takes accepted work back by com.responder_replace with a
|
|
17
|
-
// transfer (DEC-0078's receipt member transferred; a Fabric that does not answer it leaves the work held
|
|
17
|
+
// transfer (DEC-0078's receipt member transferred; a Fabric that does not answer it leaves the work held, never claimed
|
|
18
18
|
// back). It stops acting on fenced, lease_expired and authority_changed.
|
|
19
19
|
//
|
|
20
20
|
// THE REQUEST BODY IS DATA. admit(request) is the service's own input gate, required: null admits, a sentence refuses
|
|
21
|
-
// (the request is accepted and completed failed_known with it). Whatever the handler does with an admitted body
|
|
22
|
-
// the checks it needs before a model, a tool or a spend sees it
|
|
21
|
+
// (the request is accepted and completed failed_known with it). Whatever the handler does with an admitted body (and
|
|
22
|
+
// the checks it needs before a model, a tool or a spend sees it) is the service's own responsibility.
|
|
23
23
|
//
|
|
24
24
|
// A board session is served the respond scope only (RESPOND_TOOLS). com.submit, com.cancel, com.read_ack and
|
|
25
25
|
// com.directory are never sent through one; findProject checks a directory page a Project session read (FAC-SEM-107).
|
|
@@ -63,7 +63,7 @@ const TIMESTAMP = /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(\.\d+)?([Zz]|[+-]\d{2
|
|
|
63
63
|
|
|
64
64
|
const URI = /^[A-Za-z][A-Za-z0-9+.-]*:\S+$/;
|
|
65
65
|
|
|
66
|
-
/** A board refusal (a contract refusal code) or a kit fault
|
|
66
|
+
/** A board refusal (a contract refusal code) or a kit fault; see fabric_board.py's BoardError for the kit's codes. */
|
|
67
67
|
export class BoardError extends Error {
|
|
68
68
|
constructor(code, message, retryable = false, op = undefined, data = {}) {
|
|
69
69
|
super(message);
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The project board connector, Node: the facade (fabric-project-comms/0.1; the twin of fabric_board.py).
|
|
2
|
-
// Since 0.40.0 (FAA-65) it is
|
|
3
|
-
// re-exports everything it exported at 0.39.1. Node 20+, no dependency.
|
|
2
|
+
// Since 0.40.0 (FAA-65) it is layered modules: fabric-board-shapes, fabric-board-link (since 0.41.0), fabric-board-session and
|
|
3
|
+
// fabric-board-fixture. This file re-exports everything it exported at 0.39.1. Node 20+, no dependency.
|
|
4
4
|
export {
|
|
5
5
|
COMMS_PROTOCOL,
|
|
6
6
|
LEASE_SECONDS,
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ceremonies.mjs
CHANGED
|
@@ -22,6 +22,7 @@ const NONCE32 = /^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/;
|
|
|
22
22
|
const ASSERTION_MEMBERS = 'alg,authenticatorData,clientDataJSON,credentialId,format,sig';
|
|
23
23
|
|
|
24
24
|
import { isObject, isString, objects as list, show, jsonEqual } from './fabric-jsvalue.mjs'; // the core's (FAA-65)
|
|
25
|
+
import { clockMs as clockOf } from './fabric-time.mjs'; // the core's (FAA-65)
|
|
25
26
|
const matches = (pattern, value) => isString(value) && pattern.test(value);
|
|
26
27
|
const REGISTER = new Map([
|
|
27
28
|
[ENROLL_TOKEN_TYPE, { fields: ENROLL_TOKEN_FIELDS, check: (f) => matches(ESTATE_SCOPE, f.scope) && matches(SHA256, f.genesis) && f.purpose === ENROLL_PURPOSE
|
|
@@ -46,9 +47,12 @@ function statementFields(token) {
|
|
|
46
47
|
return signingStatement(ENROLL_TOKEN_TYPE, fields) ? fields : undefined;
|
|
47
48
|
}
|
|
48
49
|
|
|
49
|
-
/**
|
|
50
|
+
/**
|
|
51
|
+
* The reference verifier of an enroll-token/1 under trust {record, genesis, at}; `at` is a Date or epoch seconds, as
|
|
52
|
+
* verifyEstateRecord and the Python twin take it. The first failure answers.
|
|
53
|
+
*/
|
|
50
54
|
export function verifyEnrollToken(token, trust) {
|
|
51
|
-
const at = isObject(trust)
|
|
55
|
+
const at = isObject(trust) ? clockOf(trust.at) : Number.NaN;
|
|
52
56
|
if (!Number.isFinite(at)) return { ok: false, problem: 'no-clock' };
|
|
53
57
|
const fields = statementFields(token);
|
|
54
58
|
const record = trust.record;
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs
CHANGED
|
@@ -138,7 +138,8 @@ function keyFor(jwks, kid) {
|
|
|
138
138
|
if ((jwk.use !== undefined && jwk.use !== 'sig') || (jwk.alg !== undefined && jwk.alg !== 'EdDSA')) return { problem: 'key-not-for-signing' };
|
|
139
139
|
if (jwk.kty !== 'OKP' || jwk.crv !== 'Ed25519' || !isNonEmptyString(jwk.x) || jwk.d !== undefined) return { problem: 'unusable-key' };
|
|
140
140
|
const raw = Buffer.from(jwk.x, 'base64url');
|
|
141
|
-
|
|
141
|
+
// D4: exactly the 43 characters that write 32 bytes (Buffer reads padding, `+` and stray characters leniently).
|
|
142
|
+
if (raw.length !== 32 || raw.toString('base64url') !== jwk.x) return { problem: 'unusable-key' };
|
|
142
143
|
if (isSmallOrderKey(raw)) return { problem: 'small-order-key' }; // before any verification (DEC-0045, CT-29)
|
|
143
144
|
try {
|
|
144
145
|
return { key: crypto.createPublicKey({ key: { kty: 'OKP', crv: 'Ed25519', x: jwk.x }, format: 'jwk' }), raw, x: jwk.x };
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs
CHANGED
|
@@ -12,7 +12,7 @@ import { ROUTES } from './fabric-usage.mjs';
|
|
|
12
12
|
// document's `extensions`. The kit is assets/dashboard-kit; its version is the one in kit.json.
|
|
13
13
|
export const DASHBOARD_EXTENSION_KEY = 'https://fabric.passioncode.ai/agent-contract/extensions/dashboard/0.1';
|
|
14
14
|
|
|
15
|
-
export const DASHBOARD_KIT = 'fabric-dashboard-kit@1.1.
|
|
15
|
+
export const DASHBOARD_KIT = 'fabric-dashboard-kit@1.1.2';
|
|
16
16
|
|
|
17
17
|
export const REFERENCE_THEMES = ['graphite', 'fabric', 'paper', 'forest', 'ocean', 'ember', 'contrast'];
|
|
18
18
|
|