@passioncode-ai/fabric-agent-adapter 0.82.0 → 0.84.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 +169 -3
- package/README.md +7 -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_identifiers.py +1 -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 +38 -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 +4 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +3 -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 +28 -12
- 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 +1 -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 +13 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +11 -7
- 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 +275 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a.mjs +2 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-actions.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +5 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-backups.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-fixture.mjs +15 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +55 -11
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +47 -31
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-shapes.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +25 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ceremonies.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-connections.mjs +27 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-context.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-crypto.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +4 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +19 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate-rules.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs +7 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +22 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +4 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +7 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-identifiers.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +21 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +6 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-jsvalue.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-load.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +16 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-logs.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-mandate.mjs +6 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +25 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ops-digest.mjs +456 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package-schema.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +18 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-page-links.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +19 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +56 -19
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-platform.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-profiles.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +16 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-share.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-socket-proof.mjs +181 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +20 -15
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-tailnet-identity.mjs +469 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-tailnet.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-time.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a.py +3 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a_server.py +405 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_actions.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +7 -2
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_backups.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py +17 -3
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +98 -9
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +37 -48
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_shapes.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +25 -8
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ceremonies.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +28 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_crypto.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +19 -13
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +24 -14
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate_rules.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +4 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +32 -7
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +19 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +10 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_identifiers.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +56 -96
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +99 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_jsvalue.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_load.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +16 -6
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_logs.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_mandate.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +44 -1
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ops_digest.py +648 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +17 -4
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package_schema.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_page_links.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +21 -5
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +108 -108
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_platform.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +38 -12
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_share.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_socket_proof.py +391 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +19 -16
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_tailnet.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_tailnet_identity.py +493 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_time.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +1 -0
- package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +632 -104
- 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 +17 -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 +6 -5
- 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 +4 -3
- package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/admit.py +182 -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
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
// The board responder (DEC-0078, FAA-12b): the work context a handler sees and the responder loop. The session, the
|
|
2
3
|
// checkpoint and the request live in fabric-board-link.mjs and are re-exported here.
|
|
3
4
|
// Part of the Node board connector (FAA-65, layer L4); the twin of fabric_board_session.py. Node 20+, no dependency.
|
|
@@ -5,7 +6,7 @@ import crypto from 'node:crypto';
|
|
|
5
6
|
import { Backoff, HubError, normalProject } from './fabric-hub.mjs';
|
|
6
7
|
import { PRIMARY_CAPABILITY, acceptedRevisions, feedDataProblems, negotiateRevision } from './fabric-service.mjs';
|
|
7
8
|
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';
|
|
9
|
+
import { BoardRequest, BoardSession, Checkpoint, PERSONAL_BOARD, covers, mandateOptions, noticeLinks, readBackProblem, readMandate, senderAgent } from './fabric-board-link.mjs';
|
|
9
10
|
import { HOLD_RETRY_MAX_SECONDS, LEASE_MARGIN_SECONDS, SESSION_CODES, SlotKeeper } from './fabric-board-slots.mjs';
|
|
10
11
|
|
|
11
12
|
export { BoardRequest, BoardSession, Checkpoint, PERSONAL_BOARD }; // re-exported: callers import them from here since 0.40.0
|
|
@@ -20,6 +21,8 @@ export const POLL_JITTER_SECONDS = 5;
|
|
|
20
21
|
export const DRAIN_SECONDS = 10;
|
|
21
22
|
/** 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
23
|
export const IDLE_LEAVE_SECONDS = 3;
|
|
24
|
+
/** `board.down` is said once per outage window: said again only after the board stayed up this long (FAA-187 (22)). */
|
|
25
|
+
export const DOWN_QUIET_SECONDS = 300;
|
|
23
26
|
|
|
24
27
|
export const EVENT_SCHEMA = 'urn:fabric:schema:board-responder-event:1';
|
|
25
28
|
|
|
@@ -35,16 +38,18 @@ export class WorkContext {
|
|
|
35
38
|
#responder;
|
|
36
39
|
#entry;
|
|
37
40
|
#caller;
|
|
38
|
-
|
|
41
|
+
#agent;
|
|
42
|
+
constructor(responder, entry, mandate = null, caller = null, callerAgent = null) {
|
|
39
43
|
this.#responder = responder;
|
|
40
44
|
this.#entry = entry;
|
|
41
45
|
this.#caller = caller;
|
|
46
|
+
this.#agent = callerAgent; // FAA-135: the sender's agent id identifyAgent named, binding the mandate's chain
|
|
42
47
|
this.stopped = false;
|
|
43
48
|
this.mandate = mandate; // the operator's mandate the request carried, when the responder reads one (FAA-135)
|
|
44
49
|
}
|
|
45
50
|
/** Whether the request's mandate covers `call` (`mandateCovers`' answer); null when none travelled. */
|
|
46
51
|
mandateCovers(call) {
|
|
47
|
-
return covers(this.mandate, call, this.#responder.agentId, this.#responder.wall, this.#caller);
|
|
52
|
+
return covers(this.mandate, call, this.#responder.agentId, this.#responder.wall, this.#caller, this.#agent);
|
|
48
53
|
}
|
|
49
54
|
toJSON() {
|
|
50
55
|
return { stopped: this.stopped }; // never the mandate: it holds the operator's words
|
|
@@ -82,43 +87,50 @@ function noticeSubject(value, project, episode) {
|
|
|
82
87
|
*/
|
|
83
88
|
export class Responder extends SlotKeeper {
|
|
84
89
|
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 } = {}) {
|
|
90
|
+
now = () => performance.now() / 1000, rand = Math.random, sessionFactory, agentId = null, mandateFrom = null, wall = Date.now, identifyAgent = null, links = null } = {}) {
|
|
86
91
|
super();
|
|
87
|
-
if (typeof admit !== 'function' || typeof handler !== 'function'
|
|
92
|
+
if (typeof admit !== 'function' || typeof handler !== 'function' || !(identifyAgent == null || typeof identifyAgent === 'function')) {
|
|
93
|
+
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).');
|
|
94
|
+
}
|
|
88
95
|
const caps = [...(capabilities ?? [])];
|
|
89
96
|
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
97
|
// FAA-244: DEC-0140 PR-1, a session's slot, never a service's
|
|
91
98
|
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
99
|
Object.assign(this, { hub, capabilities: caps, credentialFile, handler, admit, observe, emitTo: emit, now, rand });
|
|
93
100
|
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
|
-
|
|
101
|
+
// FAA-135: read the operator's mandate from a trusted request of his own Project (opt-in; null reads none), and bind
|
|
102
|
+
// its chain with the agent id identifyAgent names for the sender (asked only when a mandate travelled)
|
|
103
|
+
Object.assign(this, { mandateFrom: mandateOptions(agentId, mandateFrom), agentId: agentId ?? null, wall, identifyAgent: identifyAgent ?? null });
|
|
104
|
+
this.links = noticeLinks(links); // FAA-187 (21): the page that resolves each notify kind
|
|
96
105
|
this.service = String(service).replace(/[^A-Za-z0-9._-]/g, '-').slice(0, 40) || 'service';
|
|
97
106
|
this.checkpoint = new Checkpoint(checkpointFile);
|
|
98
107
|
this.sessionFactory = sessionFactory ?? ((grant) => new BoardSession(grant));
|
|
99
108
|
this.backoff = new Backoff({ rand });
|
|
100
109
|
this.session = null;
|
|
101
110
|
this.epoch = null;
|
|
102
|
-
|
|
111
|
+
// FAA-196 (24): maps keyed by a capability's name have no prototype, so `constructor` is a name like any other
|
|
112
|
+
this.slots = Object.create(null);
|
|
103
113
|
this.current = null;
|
|
104
114
|
this.state = 'idle';
|
|
105
115
|
this.waitUntil = 0;
|
|
106
116
|
this.stopping = false;
|
|
107
117
|
/** Whether shutdown found work to drain, published before it waits; null before shutdown starts (FAA-186). */
|
|
108
118
|
this.draining = null;
|
|
109
|
-
this.claimKeys =
|
|
119
|
+
this.claimKeys = Object.create(null);
|
|
110
120
|
this.probing = new Set(); // moved slots whose move-back claim lost its answer: asked again under the same key
|
|
111
|
-
this.said =
|
|
121
|
+
this.said = Object.create(null);
|
|
122
|
+
this.upSince = null; // when the board was last seen up after a fault (FAA-187 (22))
|
|
112
123
|
this.rr = 0;
|
|
113
124
|
this.reconcileAt = 0;
|
|
114
125
|
/** The first pause under an operator's hold (DEC-0079); it doubles to HOLD_RETRY_MAX_SECONDS. */
|
|
115
126
|
this.holdDelay = POLL_SECONDS;
|
|
116
127
|
this.onHold = new Set();
|
|
128
|
+
this.takenBack = new Set(); // capabilities the operator took back, not yet held again (FAA-325)
|
|
117
129
|
/** One fenced call at a time, and the entry edits beside them (FAA-77, finding 36). */
|
|
118
130
|
this.fenceQueue = Promise.resolve();
|
|
119
131
|
this.toResume = []; // transferred requests waiting until nothing runs (finding 39)
|
|
120
132
|
this.suspect = new Set(); // capabilities a fenced call was refused on: com.status says whether they moved (FAA-155)
|
|
121
|
-
this.confirmed =
|
|
133
|
+
this.confirmed = Object.create(null); // this session's: the generation it held, or com.status showed unmoved
|
|
122
134
|
}
|
|
123
135
|
|
|
124
136
|
/** Runs fn after every fenced call queued before it (finding 36): an await inside a call never lets another in. */
|
|
@@ -148,7 +160,8 @@ export class Responder extends SlotKeeper {
|
|
|
148
160
|
// (events-and-notifications.md, "Say who wants what"; 0.56.0 seam r30).
|
|
149
161
|
const subject = noticeSubject(value, this.project, episode);
|
|
150
162
|
try {
|
|
151
|
-
|
|
163
|
+
const link = notify && this.links && Object.hasOwn(this.links, kind) ? { link: this.links[kind] } : {}; // the page that resolves it (FAA-187 (21))
|
|
164
|
+
this.emitTo(kind, level, text, { notify, subject, ...link, ...(data ? { data } : {}) });
|
|
152
165
|
} catch {
|
|
153
166
|
// an event that cannot be written never stops the responder
|
|
154
167
|
}
|
|
@@ -190,8 +203,8 @@ export class Responder extends SlotKeeper {
|
|
|
190
203
|
|
|
191
204
|
async connect() {
|
|
192
205
|
this.state = 'connecting';
|
|
193
|
-
this.slots =
|
|
194
|
-
this.confirmed =
|
|
206
|
+
this.slots = Object.create(null); // the previous session's slots are not this one's: a refusal now reconnects (FAA-163)
|
|
207
|
+
this.confirmed = Object.create(null); // nor its confirmations: a slot may have moved between the two sessions
|
|
195
208
|
this.probing = new Set(); // nor its lost move-back claims
|
|
196
209
|
const grant = await this.hub.joinBoard(this.project, this.credentialFile);
|
|
197
210
|
const missing = this.capabilities.filter((c) => !grant.capabilities.includes(c));
|
|
@@ -202,10 +215,10 @@ export class Responder extends SlotKeeper {
|
|
|
202
215
|
if (missing.length) {
|
|
203
216
|
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
217
|
}
|
|
205
|
-
this.slots =
|
|
218
|
+
this.slots = Object.create(null);
|
|
206
219
|
await this.recover(status, this.capabilities.filter((c) => grant.capabilities.includes(c)));
|
|
207
220
|
this.state = 'ready';
|
|
208
|
-
for (const kind of ['board.
|
|
221
|
+
for (const kind of ['board.blocked', ...(missing.length ? [] : ['board.access.needed'])]) delete this.said[kind]; // board.down waits for its quiet window (step)
|
|
209
222
|
}
|
|
210
223
|
|
|
211
224
|
async enroll(capability) {
|
|
@@ -262,7 +275,7 @@ export class Responder extends SlotKeeper {
|
|
|
262
275
|
}
|
|
263
276
|
|
|
264
277
|
async recover(status, capabilities) {
|
|
265
|
-
const transfer =
|
|
278
|
+
const transfer = Object.create(null);
|
|
266
279
|
const reconcile = [];
|
|
267
280
|
for (const entry of Object.values(this.checkpoint.entries)) {
|
|
268
281
|
if (!capabilities.includes(entry.capability)) continue;
|
|
@@ -281,7 +294,7 @@ export class Responder extends SlotKeeper {
|
|
|
281
294
|
else reconcile.push(entry);
|
|
282
295
|
}
|
|
283
296
|
const responders = (status.responders ?? []).filter(isObj);
|
|
284
|
-
const generations = Object.fromEntries(responders.map((r) => [r.capability, r.generation ?? 0]));
|
|
297
|
+
const generations = Object.assign(Object.create(null), Object.fromEntries(responders.map((r) => [r.capability, r.generation ?? 0])));
|
|
285
298
|
const onHold = new Set(responders.filter((r) => r.held === true).map((r) => r.capability)); // DEC-0079
|
|
286
299
|
let failure = null;
|
|
287
300
|
for (const capability of capabilities) {
|
|
@@ -470,10 +483,9 @@ export class Responder extends SlotKeeper {
|
|
|
470
483
|
if (!this.session) await this.connect();
|
|
471
484
|
await this.tick();
|
|
472
485
|
this.backoff.reset();
|
|
473
|
-
if (this.state === 'down')
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
}
|
|
486
|
+
if (this.state === 'down') this.state = 'ready'; // a lost call was sent again and answered: the session held throughout
|
|
487
|
+
this.upSince ??= now;
|
|
488
|
+
if (now - this.upSince >= DOWN_QUIET_SECONDS) delete this.said['board.down']; // up a whole window: a later outage is said again
|
|
477
489
|
return this.interval();
|
|
478
490
|
} catch (e) {
|
|
479
491
|
if (!(e instanceof BoardError) && !(e instanceof HubError)) throw e;
|
|
@@ -492,6 +504,7 @@ export class Responder extends SlotKeeper {
|
|
|
492
504
|
|
|
493
505
|
async fault(e) {
|
|
494
506
|
const code = e.code ?? 'unreachable';
|
|
507
|
+
this.upSince = null;
|
|
495
508
|
const connecting = this.state === 'connecting' && !Object.values(this.slots).some((s) => s?.state === 'held');
|
|
496
509
|
let wait = this.backoff.next();
|
|
497
510
|
const left = this.leaseLeft() - LEASE_MARGIN_SECONDS;
|
|
@@ -554,9 +567,10 @@ export class Responder extends SlotKeeper {
|
|
|
554
567
|
await this.gone(capability);
|
|
555
568
|
this.suspect.delete(capability);
|
|
556
569
|
}
|
|
570
|
+
const shared = {}; // one com.status read a tick for every moved slot (FAA-194 (5))
|
|
557
571
|
for (const [capability, slot] of Object.entries(this.slots)) {
|
|
558
572
|
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)
|
|
573
|
+
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
574
|
continue;
|
|
561
575
|
}
|
|
562
576
|
if (slot.state === 'paused') {
|
|
@@ -581,7 +595,7 @@ export class Responder extends SlotKeeper {
|
|
|
581
595
|
}
|
|
582
596
|
|
|
583
597
|
async continueLapsed() {
|
|
584
|
-
const lapsed =
|
|
598
|
+
const lapsed = Object.create(null);
|
|
585
599
|
for (const entry of Object.values(this.checkpoint.entries)) {
|
|
586
600
|
if (entry.step === 'lapsed' && this.slots[entry.capability]?.state === 'held') (lapsed[entry.capability] ??= []).push(JSON.parse(JSON.stringify(entry)));
|
|
587
601
|
}
|
|
@@ -648,13 +662,13 @@ export class Responder extends SlotKeeper {
|
|
|
648
662
|
revision: receipt.request.revision, step: 'claimed', keys: {}, effect: false };
|
|
649
663
|
this.checkpoint.put(entry);
|
|
650
664
|
const message = await this.call('com.get', { message: entry.message });
|
|
651
|
-
const
|
|
652
|
-
if (
|
|
665
|
+
const problem = readBackProblem(message, entry, this.project);
|
|
666
|
+
if (problem === 'digest-mismatch') {
|
|
653
667
|
this.checkpoint.drop(entry.request);
|
|
654
668
|
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
669
|
return;
|
|
656
670
|
}
|
|
657
|
-
if (
|
|
671
|
+
if (problem) {
|
|
658
672
|
this.checkpoint.drop(entry.request);
|
|
659
673
|
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
674
|
return;
|
|
@@ -701,12 +715,14 @@ export class Responder extends SlotKeeper {
|
|
|
701
715
|
return true;
|
|
702
716
|
}
|
|
703
717
|
|
|
704
|
-
async runWork(entry, request) {
|
|
705
|
-
|
|
718
|
+
async runWork(entry, request, problem = null) {
|
|
719
|
+
let { mandate, unread } = readMandate(request, this.mandateFrom, this.agentId);
|
|
720
|
+
if (problem && (mandate !== null || unread)) [mandate, unread] = [null, problem]; // FAA-196 (1): another message than accepted
|
|
706
721
|
if (unread) { // by its code alone: the block holds the operator's words
|
|
707
722
|
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
723
|
}
|
|
709
|
-
const
|
|
724
|
+
const sender = isObj(request.sender) ? request.sender : {};
|
|
725
|
+
const ctx = new WorkContext(this, entry, mandate, sender.project ?? null, senderAgent(this.identifyAgent, sender, mandate));
|
|
710
726
|
const work = { entry, request, ctx, done: false, outcome: null, failure: null, renewedAt: this.now() };
|
|
711
727
|
this.current = work;
|
|
712
728
|
work.promise = Promise.resolve().then(() => this.handler(request, ctx)).then((outcome) => { work.outcome = outcome; }, (e) => { work.failure = e; }).finally(() => { work.done = true; });
|
|
@@ -721,7 +737,7 @@ export class Responder extends SlotKeeper {
|
|
|
721
737
|
return;
|
|
722
738
|
}
|
|
723
739
|
const message = await this.call('com.get', { message: entry.message });
|
|
724
|
-
await this.runWork(entry, new BoardRequest(message, entry.capability));
|
|
740
|
+
await this.runWork(entry, new BoardRequest(message, entry.capability), readBackProblem(message, entry, this.project));
|
|
725
741
|
}
|
|
726
742
|
|
|
727
743
|
/**
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-shapes.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
// The project board's wire (fabric-project-comms/0.1, DEC-0022, DEC-0073, DEC-0077): constants, refusal codes, the request digest, the closed shapes the kit reads back, evidence.
|
|
2
3
|
// Part of the Node board connector (FAA-65, layer L4); the twin of fabric_board_shapes.py. Node 20+, no dependency.
|
|
3
4
|
import crypto from 'node:crypto';
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
// The board responder's slot records (FAA-155, FAA-163): the generation it holds each slot at, a move to another
|
|
2
3
|
// session and its take-back, and a capability whose enroll was refused, retried on its own. Part of the board
|
|
3
4
|
// connector (FAA-65, layer L4); Responder (fabric-board-session.mjs) extends SlotKeeper.
|
|
@@ -27,6 +28,9 @@ export class SlotKeeper {
|
|
|
27
28
|
this.confirmed[capability] = generation;
|
|
28
29
|
delete this.said[`board.slot.refused:${capability}`]; // held again: a later refusal is said again
|
|
29
30
|
delete this.said[`board.slot.contested:${capability}`]; // and a later contest, by enroll or replace
|
|
31
|
+
if (this.takenBack.delete(capability)) { // FAA-325: held again after the operator's take-back, said once
|
|
32
|
+
this.emit('board.slot.returned', 'info', `The ${capability} slot on this Project is this service's again: the operator took it back, and it answers it from now on.`, { capability, generation });
|
|
33
|
+
}
|
|
30
34
|
}
|
|
31
35
|
|
|
32
36
|
/** The slot record, best effort: one that cannot be written now is written at the next receipt. */
|
|
@@ -131,7 +135,7 @@ export class SlotKeeper {
|
|
|
131
135
|
delete this.confirmed[capability];
|
|
132
136
|
delete this.claimKeys[capability]; // the move returned that claim to the queue: its receipt is dead
|
|
133
137
|
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
|
|
138
|
+
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
139
|
this.note(capability, { ...held, moved: now }); // said again at the next start if it cannot be written
|
|
136
140
|
}
|
|
137
141
|
return true;
|
|
@@ -143,14 +147,21 @@ export class SlotKeeper {
|
|
|
143
147
|
* 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
148
|
* lease_expired one (the move reached this session but its lease lapsed) enrolls again; a fenced one is another
|
|
145
149
|
* 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.
|
|
150
|
+
* session holds: a slot moved off stays off unless it is handed back. `shared` holds the tick's one com.status read,
|
|
151
|
+
* shared by every moved slot (FAA-194 (5)).
|
|
147
152
|
*/
|
|
148
|
-
async movedBack(capability) {
|
|
153
|
+
async movedBack(capability, shared = null) {
|
|
149
154
|
if (this.current || this.stopping) return; // a claim now could hand out a second request, or start work while the responder stops
|
|
150
155
|
const held = this.checkpoint.slots[capability] ?? {};
|
|
151
156
|
if (held.project !== this.project || !isInt(held.moved, 1)) return;
|
|
152
157
|
if (this.slots[capability]) this.slots[capability].renewedAt = this.now();
|
|
153
|
-
|
|
158
|
+
let status;
|
|
159
|
+
if (shared && 'status' in shared) status = shared.status;
|
|
160
|
+
else {
|
|
161
|
+
status = await this.status();
|
|
162
|
+
if (shared) shared.status = status;
|
|
163
|
+
}
|
|
164
|
+
const now = (status.responders ?? []).find((r) => isObj(r) && r.capability === capability)?.generation;
|
|
154
165
|
if (!isInt(now, 1) || now <= held.moved) return;
|
|
155
166
|
if (!this.probing.has(capability) || !this.claimKeys[capability]) { // a fresh key: one from before the move may hold a dead receipt
|
|
156
167
|
this.claimKeys[capability] = this.key('claim');
|
|
@@ -201,14 +212,18 @@ export class SlotKeeper {
|
|
|
201
212
|
}
|
|
202
213
|
|
|
203
214
|
/** The slot is this session's again at `generation`: held, its record rewritten without the move (FAA-165). */
|
|
204
|
-
back(capability, held, generation) {
|
|
205
|
-
|
|
215
|
+
async back(capability, held, generation) {
|
|
216
|
+
const back = await this.locked(() => {
|
|
206
217
|
if (!sameRecord(this.checkpoint.slots[capability], held)) return false; // a takeBack forgot it; its own next tick enrolls
|
|
207
218
|
this.slots[capability] = { state: 'held', generation, renewedAt: -Infinity }; // no lease yet
|
|
219
|
+
this.takenBack.delete(capability); // said below as a move back, once
|
|
208
220
|
this.heldAt(capability, generation);
|
|
209
221
|
delete this.said['board.slot.moved']; // a later move is said again
|
|
210
222
|
return true;
|
|
211
223
|
});
|
|
224
|
+
// FAA-194 (8): the feed no longer ends on the move's warning
|
|
225
|
+
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 });
|
|
226
|
+
return back;
|
|
212
227
|
}
|
|
213
228
|
|
|
214
229
|
/**
|
|
@@ -220,8 +235,11 @@ export class SlotKeeper {
|
|
|
220
235
|
this.checkpoint.slot(capability, null);
|
|
221
236
|
delete this.claimKeys[capability]; // a receipt from before the take-back may be dead
|
|
222
237
|
this.probing.delete(capability);
|
|
223
|
-
|
|
238
|
+
const state = this.slots[capability]?.state;
|
|
239
|
+
if (state === 'moved') this.slots[capability] = { state: 'contested', renewedAt: 0 };
|
|
224
240
|
delete this.said['board.slot.moved'];
|
|
241
|
+
// board.slot.returned is said once the slot is held again (FAA-325); a slot still held has nothing to say
|
|
242
|
+
if (state !== 'held') this.takenBack.add(capability);
|
|
225
243
|
}
|
|
226
244
|
|
|
227
245
|
// -- one capability's refusal (FAA-163) --
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
// The project board connector, Node: the facade (fabric-project-comms/0.1; the twin of fabric_board.py).
|
|
2
3
|
// 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
4
|
// fabric-board-fixture. This file re-exports everything it exported at 0.39.1. Node 20+, no dependency.
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ceremonies.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
// The estate ceremonies' headless join (contract DEC-0084), the Node kit; the twin of fabric_ceremonies.py, answer for
|
|
2
3
|
// answer (test/fixtures/contract/ceremony-vectors.json). Node built-ins only.
|
|
3
4
|
// verifyEnrollToken (the hub's and the node's verifier of an enroll-token/1), enrollJoinInput (what the joining machine
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-connections.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
// What an agent uses: fabric-connections/0.1, the extension keyed extensions/connections/0.1 (DEC-0096, FAC-SEM-127).
|
|
2
3
|
// connectionsView reads a well-known document as the contract's connectionsView does (it never throws);
|
|
3
4
|
// connectionsExtension builds the `extensions` entry a service declares, `state` from its own checks and its degraded[].
|
|
@@ -88,7 +89,28 @@ function shapeProblems(value) {
|
|
|
88
89
|
// The contract's KEY_SHAPE (src/connections.ts): a provider key where it starts a word, case-sensitive prefixes, the
|
|
89
90
|
// `sk-` rules and a Telegram bot token.
|
|
90
91
|
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
|
|
92
|
+
const SCHEME_CHAR = /[a-z0-9+.-]/i;
|
|
93
|
+
const SCHEME_LETTER = /[a-z]/i;
|
|
94
|
+
/**
|
|
95
|
+
* The [start, end] of each URL scheme and its `://` in a piece, where `/[a-z][a-z0-9+.-]*:\/\//gi` finds them left to
|
|
96
|
+
* right, read in linear time (FAA-322: the pattern tried every letter of a long run with no `://`). From each `://` the
|
|
97
|
+
* run of scheme characters before it is read back, and the match starts at its first letter at or after where the last
|
|
98
|
+
* match ended. The twin of fabric_connections._scheme_spans.
|
|
99
|
+
*/
|
|
100
|
+
export function schemeSpans(piece) {
|
|
101
|
+
const spans = [];
|
|
102
|
+
let after = 0;
|
|
103
|
+
for (let at = piece.indexOf('://'); at >= 0; at = piece.indexOf('://', at + 1)) {
|
|
104
|
+
let start = at;
|
|
105
|
+
while (start > after && SCHEME_CHAR.test(piece[start - 1])) start -= 1;
|
|
106
|
+
while (start < at && !SCHEME_LETTER.test(piece[start])) start += 1;
|
|
107
|
+
if (start < at) {
|
|
108
|
+
spans.push([start, at + 3]);
|
|
109
|
+
after = at + 3;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return spans;
|
|
113
|
+
}
|
|
92
114
|
const CLOSING = /[)\]}>"'.,;:!?]/;
|
|
93
115
|
const TRIED_BACK = 1;
|
|
94
116
|
/** The URLs a member may hold, from every scheme in each whitespace-delimited piece, without the closing brackets,
|
|
@@ -96,8 +118,8 @@ const TRIED_BACK = 1;
|
|
|
96
118
|
function urlsIn(value) {
|
|
97
119
|
const urls = [];
|
|
98
120
|
for (const piece of value.split(/\s+/)) {
|
|
99
|
-
for (const
|
|
100
|
-
const url = piece.slice(
|
|
121
|
+
for (const [start] of schemeSpans(piece)) {
|
|
122
|
+
const url = piece.slice(start);
|
|
101
123
|
let end = url.length;
|
|
102
124
|
while (end > 0 && CLOSING.test(url[end - 1])) end -= 1;
|
|
103
125
|
for (let back = 0; back <= TRIED_BACK && end + back <= url.length; back += 1) urls.push(url.slice(0, end + back));
|
|
@@ -108,8 +130,8 @@ function urlsIn(value) {
|
|
|
108
130
|
/** 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
131
|
const longRun = (value) =>
|
|
110
132
|
value.split(/\s+/).some((piece) => {
|
|
111
|
-
const
|
|
112
|
-
const cut =
|
|
133
|
+
const found = schemeSpans(piece);
|
|
134
|
+
const cut = found.length ? found[0][1] - 3 : -1;
|
|
113
135
|
return longSecret(cut < 0 ? piece : piece.slice(0, cut));
|
|
114
136
|
});
|
|
115
137
|
// 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-context.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
// One instance per context (FAA-125, organization row N-11; contract DEC-0082, "One instance per context on a machine"):
|
|
2
3
|
// from a context, `personal` or `org:<orgId>`, the service instance, its folders, descriptor and instance lock.
|
|
3
4
|
// Part of the Node service kit (layer L3, after fabric-descriptor); the twin of fabric_context.py, answer for answer
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-crypto.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
// The Node kit's cryptography helpers, once (FAA-65, layer L1); the twin of fabric_crypto.py. Signature verification
|
|
2
3
|
// itself is Node's (`crypto.verify`); what lives here is what the runtime does not refuse on its own: keys of small
|
|
3
4
|
// order (DEC-0045) and P-256 points off the curve, plus sha256. Node 20+, no dependency.
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
// The callee's side of fabric-delegation/0.1 (Fabric Agent Contract DEC-0039, Fabric ADR-0144): check a
|
|
2
3
|
// delegation token or an approval offline against the estate's published keys, and answer `ok` or one refusal
|
|
3
4
|
// code. A port of the contract's `src/delegation.ts` at the pinned revision; the shared vectors in
|
|
@@ -15,10 +16,10 @@
|
|
|
15
16
|
import crypto from 'node:crypto';
|
|
16
17
|
import fs from 'node:fs';
|
|
17
18
|
import http from 'node:http';
|
|
18
|
-
import net from 'node:net';
|
|
19
19
|
import { hubKeyCertified } from './fabric-estate.mjs';
|
|
20
20
|
import { atomicWrite } from './fabric-paths.mjs'; // the kit's private writer (FAA-85)
|
|
21
21
|
import { CheckedAgent, ListenerRefused } from './fabric-peer-owner.mjs'; // a loopback key set from this account's listener only (FAA-255)
|
|
22
|
+
import { loopbackHost } from './fabric-socket-proof.mjs';
|
|
22
23
|
import { effectClosure } from './fabric-mandate.mjs'; // defined once beside the mandate that reads it too (DEC-0125)
|
|
23
24
|
|
|
24
25
|
export const REFUSAL_CODES = Object.freeze([
|
|
@@ -413,7 +414,8 @@ export function readDelegationToken(headers) {
|
|
|
413
414
|
/** The digest an approval binds: sha256: and the hex SHA-256 of the action's RFC 8785 form. */
|
|
414
415
|
export const actionDigest = (action) => `sha256:${crypto.createHash('sha256').update(jcs(action), 'utf8').digest('hex')}`;
|
|
415
416
|
|
|
416
|
-
|
|
417
|
+
// A key set source's loopback host: the kit's one rule, shared with fabric-http's checkUrl (FAA-187 (15)).
|
|
418
|
+
const loopback = loopbackHost;
|
|
417
419
|
|
|
418
420
|
const KEY_SET_MAX = 65536;
|
|
419
421
|
const KEY_SET_AGENT = new CheckedAgent({ keepAlive: false });
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
// How a service is found (fabric-service/0.1): its descriptor, peers and app descriptors (DEC-0061, DEC-0069), build identity and the well-known document.
|
|
2
3
|
// Part of the Node service kit (FAA-65, layer L3); the twin of fabric_descriptor.py. Node 20+, no dependency.
|
|
3
4
|
import { spawnSync } from 'node:child_process';
|
|
@@ -12,13 +13,13 @@ import { protocolsFindings } from './fabric-protocols.mjs';
|
|
|
12
13
|
import { CONNECTIONS_EXTENSION_KEY, connectionsView } from './fabric-connections.mjs';
|
|
13
14
|
import { ACTIONS_EXTENSION_KEY, actionsExtension, actionsExtensionRules } from './fabric-actions.mjs';
|
|
14
15
|
import { a2aSurfaceProblems } from './fabric-a2a.mjs';
|
|
15
|
-
import { ROUTES } from './fabric-usage.mjs';
|
|
16
|
+
import { ROUTES, isPlainObject } from './fabric-usage.mjs';
|
|
16
17
|
|
|
17
18
|
// The embedded dashboard declaration (fabric-dashboard/0.1, DEC-0036 ED-9), under the well-known
|
|
18
19
|
// document's `extensions`. The kit is assets/dashboard-kit; its version is the one in kit.json.
|
|
19
20
|
export const DASHBOARD_EXTENSION_KEY = 'https://fabric.passioncode.ai/agent-contract/extensions/dashboard/0.1';
|
|
20
21
|
|
|
21
|
-
export const DASHBOARD_KIT = 'fabric-dashboard-kit@1.1.
|
|
22
|
+
export const DASHBOARD_KIT = 'fabric-dashboard-kit@1.1.9';
|
|
22
23
|
|
|
23
24
|
export const REFERENCE_THEMES = ['graphite', 'fabric', 'paper', 'forest', 'ocean', 'ember', 'contrast'];
|
|
24
25
|
|
|
@@ -47,7 +48,8 @@ export function readDescriptors(dir = servicesDir()) {
|
|
|
47
48
|
if (!fs.existsSync(dir)) return [];
|
|
48
49
|
return fs.readdirSync(dir).filter((n) => n.endsWith('.json')).sort().flatMap((n) => {
|
|
49
50
|
try {
|
|
50
|
-
|
|
51
|
+
const value = JSON.parse(fs.readFileSync(path.join(dir, n), 'utf8'));
|
|
52
|
+
return isPlainObject(value) ? [[path.join(dir, n), value]] : []; // no object, no descriptor (FAA-200 (50))
|
|
51
53
|
} catch {
|
|
52
54
|
return [];
|
|
53
55
|
}
|
|
@@ -66,14 +68,16 @@ export function validateDescriptor(d) {
|
|
|
66
68
|
}
|
|
67
69
|
if (problems.length) return problems;
|
|
68
70
|
if (d.protocol !== PROTOCOL) problems.push(`protocol must be ${PROTOCOL}`);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
for (const [key, pattern] of [['id', ID], ['instance', INSTANCE]]) { // a string before its pattern (FAA-200 (9), (36))
|
|
72
|
+
if (typeof d[key] !== 'string') problems.push(`${key} must be a string`);
|
|
73
|
+
else if (!pattern.test(d[key])) problems.push(`${key} must match ${pattern}`);
|
|
74
|
+
}
|
|
71
75
|
if (remote) problems.push(...remoteOriginProblems(d.origin));
|
|
72
76
|
else if (d.origin !== undefined && !ORIGIN.test(d.origin)) problems.push('origin must be http://127.0.0.1:<port>');
|
|
73
77
|
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
|
|
78
|
+
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
79
|
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)}`);
|
|
80
|
+
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
81
|
}
|
|
78
82
|
if (!(typeof (d.auth?.header ?? 'Authorization') === 'string' && AUTH_HEADER.test(d.auth?.header ?? 'Authorization'))) problems.push(`auth.header must match ${AUTH_HEADER.source}`);
|
|
79
83
|
if (!['Bearer', 'none'].includes(d.auth?.scheme ?? 'Bearer')) problems.push('auth.scheme is Bearer or none');
|
|
@@ -198,7 +202,7 @@ export function descriptorMemberProblems(d) {
|
|
|
198
202
|
else if (URI_PASSWORD.test(repository)) problems.push('source.repository carries no password: it names the repository, never a credential'); // before the host
|
|
199
203
|
else if (!authorityOk(repository)) problems.push('source.repository names a host, and a port of digits up to 65535 when it has one');
|
|
200
204
|
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(
|
|
205
|
+
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
206
|
}
|
|
203
207
|
}
|
|
204
208
|
if ('peer' in d) {
|
|
@@ -428,15 +432,18 @@ export function portListenerRefusal(port, reader = portOwner) {
|
|
|
428
432
|
* account listens on (FAA-225; `portOwner(port) -> [kind, uid] | null` reads it, the kit's own by default).
|
|
429
433
|
* FAA-198: a new descriptor names its source.repository; rewriting one already installed without it only warns
|
|
430
434
|
* (process.emitWarning, code FABRIC_NO_SOURCE_REPOSITORY), so an installed agent's update keeps working. */
|
|
431
|
-
|
|
435
|
+
/** The rewrite warning of a descriptor that names no source.repository (FAA-198). */
|
|
436
|
+
export function warnNoSource(d) {
|
|
437
|
+
process.emitWarning(`Descriptor ${d.id}.${d.instance} ${NO_SOURCE_REPOSITORY}; a new install is refused without it.`, { code: 'FABRIC_NO_SOURCE_REPOSITORY' });
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
export function writeDescriptor(d, dir = servicesDir(), { portOwner: reader = portOwner, warned = false } = {}) {
|
|
432
441
|
const problems = validateDescriptor(d);
|
|
433
442
|
if (problems.length) throw new ServiceError(`Descriptor is invalid: ${problems.join('; ')}.`);
|
|
434
443
|
const me = `${d.id}.${d.instance}`;
|
|
435
444
|
const refusal = sourceRefusal(d, dir);
|
|
436
445
|
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
|
-
}
|
|
446
|
+
if (!namesRepository(d) && !warned) warnNoSource(d); // registerRemote warns itself, before its token (FAA-200 (1))
|
|
440
447
|
// DEC-0019: only a local placement claims a port on this computer; DEC-0069: an app's descriptor names none.
|
|
441
448
|
const port = placementOf(d) === 'remote' || d.origin === undefined ? null : portOf(d.origin);
|
|
442
449
|
for (const [file, other] of readDescriptors(dir)) {
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate-rules.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
// `estate-record/1` (Fabric Agent Contract DEC-0055): the record's own rules, without a verifier; the twin of
|
|
2
3
|
// fabric_estate_rules.py. The contract's src/estate-record-rules.ts at the pin, held to the contract's own vectors
|
|
3
4
|
// (test/fixtures/contract/estate-vectors.json). Split from fabric-estate.mjs (0.52.0, decision D8) so each module stays
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
// `estate-record/1` (Fabric Agent Contract DEC-0055): verify an estate revision and check its rules, the twin of
|
|
2
3
|
// fabric_estate.py. The contract's src/estate-record-rules.ts, src/webauthn.ts and src/p256.ts at the pin, held to the
|
|
3
4
|
// contract's own vectors (test/fixtures/contract/estate-vectors.json):
|
package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
|
|
1
2
|
// A service's events (DEC-0029): making them, paging them and the JSONL event log.
|
|
2
3
|
// Part of the Node service kit (FAA-65, layer L3); the twin of fabric_events.py. Node 20+, no dependency.
|
|
3
4
|
import { ServiceError } from './fabric-base.mjs';
|
|
@@ -18,7 +19,12 @@ const TRACE_ID = /^(?!0{32}$)[0-9a-f]{32}$/;
|
|
|
18
19
|
|
|
19
20
|
const SPAN_ID = /^(?!0{16}$)[0-9a-f]{16}$/;
|
|
20
21
|
|
|
21
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* One activity event. `level` is one of LEVELS; `warn`, the dashboard kit's chip for "a person is needed"
|
|
24
|
+
* (`.fd-s-warn`), is stored as `warning`, the feed's name for the same level, so a reader that draws chips shows a
|
|
25
|
+
* stored `warning` as the `warn` chip (FAA-276, FAA-277 (6)). `data` (DEC-0070) adds machine-readable fields,
|
|
26
|
+
* `{schema, value}`, never a secret: refused here, so no feed carries one.
|
|
27
|
+
*/
|
|
22
28
|
export function makeEvent(id, at, kind, given, text, { subject, link, notify, traceId, spanId, data } = {}) {
|
|
23
29
|
const level = typeof given === 'string' && Object.hasOwn(LEVEL_ALIASES, given) ? LEVEL_ALIASES[given] : given;
|
|
24
30
|
if (!LEVELS.includes(level)) throw new ServiceError(`level must be one of ${LEVELS.join(', ')} (or \`warn\` for \`warning\`).`);
|