@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.
Files changed (165) hide show
  1. package/CHANGELOG.md +169 -3
  2. package/README.md +7 -3
  3. package/SECURITY.md +4 -3
  4. package/package.json +1 -1
  5. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  6. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +7 -2
  7. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/agents-database.md +10 -0
  8. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +4 -2
  9. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/source-and-instance.md +18 -4
  10. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +16 -0
  11. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_identifiers.py +1 -0
  12. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +31 -15
  13. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +38 -12
  14. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +21 -148
  15. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check_cli.py +164 -0
  16. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +15 -8
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +4 -3
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +2 -1
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +3 -2
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +14 -4
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/README.md +76 -0
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/config.example.json +28 -0
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/data-agent/data_agent.py +366 -0
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +32 -17
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/config.example.json +4 -0
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support-agent.mjs +115 -0
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +63 -11
  28. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/a2a.md +98 -0
  29. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/agents-database.md +10 -0
  30. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +28 -12
  31. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +2 -2
  32. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +1 -1
  33. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +14 -2
  34. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +13 -1
  35. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +11 -7
  36. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/ops-digest.md +75 -0
  37. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +7 -5
  38. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +25 -12
  39. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +10 -1
  40. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +70 -0
  41. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_a2a.py +158 -0
  42. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a-server.mjs +275 -0
  43. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-a2a.mjs +2 -1
  44. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-actions.mjs +1 -0
  45. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +5 -2
  46. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-backups.mjs +1 -0
  47. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-base.mjs +1 -0
  48. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-fixture.mjs +15 -2
  49. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +55 -11
  50. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +47 -31
  51. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-shapes.mjs +1 -0
  52. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +25 -7
  53. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +1 -0
  54. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ceremonies.mjs +1 -0
  55. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-connections.mjs +27 -5
  56. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-context.mjs +1 -0
  57. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-crypto.mjs +1 -0
  58. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +4 -2
  59. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +19 -12
  60. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate-rules.mjs +1 -0
  61. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +1 -0
  62. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-events.mjs +7 -1
  63. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +22 -7
  64. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +4 -5
  65. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +7 -2
  66. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-identifiers.mjs +1 -0
  67. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +21 -1
  68. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +6 -0
  69. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-jsvalue.mjs +1 -0
  70. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-load.mjs +1 -0
  71. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +16 -2
  72. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-logs.mjs +1 -0
  73. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-mandate.mjs +6 -0
  74. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +25 -3
  75. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-ops-digest.mjs +456 -0
  76. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package-schema.mjs +1 -0
  77. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +18 -2
  78. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-page-links.mjs +1 -0
  79. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-paths.mjs +19 -5
  80. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +56 -19
  81. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-platform.mjs +1 -0
  82. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-profiles.mjs +1 -0
  83. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +1 -0
  84. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +16 -2
  85. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +1 -0
  86. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-share.mjs +1 -0
  87. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-socket-proof.mjs +181 -0
  88. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +20 -15
  89. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-tailnet-identity.mjs +469 -0
  90. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-tailnet.mjs +1 -0
  91. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-time.mjs +1 -0
  92. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-usage.mjs +1 -0
  93. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a.py +3 -1
  94. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_a2a_server.py +405 -0
  95. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_actions.py +1 -0
  96. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +7 -2
  97. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_backups.py +1 -0
  98. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_base.py +1 -0
  99. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +1 -0
  100. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_fixture.py +17 -3
  101. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +98 -9
  102. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +37 -48
  103. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_shapes.py +1 -0
  104. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +25 -8
  105. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ceremonies.py +1 -0
  106. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +28 -6
  107. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py +1 -0
  108. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_crypto.py +1 -0
  109. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +19 -13
  110. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +24 -14
  111. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +1 -0
  112. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate_rules.py +1 -0
  113. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +4 -1
  114. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +32 -7
  115. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +19 -16
  116. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +10 -5
  117. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_identifiers.py +1 -0
  118. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +56 -96
  119. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +99 -1
  120. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_jsvalue.py +1 -0
  121. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_load.py +1 -0
  122. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +16 -6
  123. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_logs.py +1 -0
  124. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_mandate.py +1 -0
  125. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +44 -1
  126. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_ops_digest.py +648 -0
  127. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +17 -4
  128. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package_schema.py +1 -0
  129. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_page_links.py +1 -0
  130. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_paths.py +21 -5
  131. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +108 -108
  132. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_platform.py +1 -0
  133. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_profiles.py +1 -0
  134. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +1 -0
  135. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +38 -12
  136. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +1 -0
  137. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_share.py +1 -0
  138. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_socket_proof.py +391 -0
  139. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +19 -16
  140. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_tailnet.py +1 -0
  141. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_tailnet_identity.py +493 -0
  142. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_time.py +1 -0
  143. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +1 -0
  144. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +632 -104
  145. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample-remote-service.mjs +2 -1
  146. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +17 -7
  147. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/tailnet-groups.schema.json +69 -0
  148. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +6 -5
  149. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agent-templates.md +16 -0
  150. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-database.md +10 -0
  151. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-that-answer-people.md +14 -9
  152. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +4 -2
  153. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +18 -4
  154. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/where-agents-run.md +4 -3
  155. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/admit.py +182 -0
  156. package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
  157. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
  158. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/agents-database.md +10 -0
  159. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +14 -9
  160. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +7 -4
  161. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +10 -5
  162. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +2 -0
  163. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +16 -5
  164. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +79 -8
  165. 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
- constructor(responder, entry, mandate = null, caller = null) {
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') throw new BoardError('invalid_arguments', 'A responder needs the service\'s own input gate (admit) and a handler.');
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
- Object.assign(this, { mandateFrom: mandateOptions(agentId, mandateFrom), agentId: agentId ?? null, wall });
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
- this.slots = {};
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 = {}; // this session's: the generation it held, or com.status showed unmoved
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
- this.emitTo(kind, level, text, { notify, subject, ...(data ? { data } : {}) });
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 = {}; // the previous session's slots are not this one's: a refusal now reconnects (FAA-163)
194
- this.confirmed = {}; // nor its confirmations: a slot may have moved between the two sessions
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.down', 'board.blocked', ...(missing.length ? [] : ['board.access.needed'])]) delete this.said[kind];
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') { // a lost call was sent again and answered: the session held throughout
474
- this.state = 'ready';
475
- delete this.said['board.down'];
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 req = isObj(message) ? message.request : null;
652
- if (message.digest !== entry.digest) {
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 (!isObj(req) || req.id !== entry.request || req.capability !== capability || req.target !== this.project) {
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
- const { mandate, unread } = readMandate(request, this.mandateFrom, this.agentId);
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 ctx = new WorkContext(this, entry, mandate, isObj(request.sender) ? request.sender.project ?? null : null);
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
  /**
@@ -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';
@@ -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 \`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 with accepted work waiting; a restart alone does not) or, where Fabric offers it, a move back here. Else ask its maintainer.`, { notify: true, episode: capability, capability, generation: now, held: held.generation });
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
- const now = ((await this.status()).responders ?? []).find((r) => isObj(r) && r.capability === capability)?.generation;
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
- return this.locked(() => {
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
- if (this.slots[capability]?.state === 'moved') this.slots[capability] = { state: 'contested', renewedAt: 0 };
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) --
@@ -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.
@@ -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
@@ -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 SCHEME = /[a-z][a-z0-9+.-]*:\/\//gi;
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 match of piece.matchAll(SCHEME)) {
100
- const url = piece.slice(match.index);
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 at = piece.search(SCHEME);
112
- const cut = at < 0 ? -1 : piece.indexOf('://', at);
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).
@@ -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
@@ -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.
@@ -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
- const loopback = (host) => host === 'localhost' || (net.isIP(host.replace(/^\[|\]$/g, '')) !== 0 && (/^127\./.test(host) || host.replace(/^\[|\]$/g, '') === '::1'));
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 });
@@ -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.7';
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
- return [[path.join(dir, n), JSON.parse(fs.readFileSync(path.join(dir, n), 'utf8'))]];
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
- if (!ID.test(d.id)) problems.push(`id must match ${ID}`);
70
- if (!INSTANCE.test(d.instance)) problems.push(`instance must match ${INSTANCE}`);
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 a local path, never a network share');
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('FAC-SEM-086: /source/repository holds a value shaped like a credential; it names a repository'); // its name is no key (seam 6)
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
- export function writeDescriptor(d, dir = servicesDir(), { portOwner: reader = portOwner } = {}) {
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)) {
@@ -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
@@ -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):
@@ -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
- /** One activity event. `level` is one of LEVELS; `warn`, the dashboard chip's name, is stored as `warning`. `data` (DEC-0070) adds machine-readable fields, `{schema, value}`, never a secret: refused here, so no feed carries one. */
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\`).`);