@passioncode-ai/fabric-agent-adapter 0.80.2 → 0.82.0

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 (92) hide show
  1. package/CHANGELOG.md +157 -0
  2. package/README.md +18 -5
  3. package/SECURITY.md +25 -8
  4. package/bin/fabric-agent-adapter.js +24 -1
  5. package/package.json +1 -1
  6. package/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  7. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +6 -4
  8. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +16 -3
  9. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/source-and-instance.md +44 -7
  10. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +116 -4
  11. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/check_dashboard_link.py +33 -3
  12. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +257 -10
  13. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_check.py +57 -77
  14. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_restart.py +56 -0
  15. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_source_tree.py +899 -0
  16. package/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/new_agent.py +308 -0
  17. package/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +7 -4
  18. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +23 -5
  19. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
  20. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
  21. package/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +9 -4
  22. package/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +1 -1
  23. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +4 -2
  24. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +15 -2
  25. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +16 -3
  26. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +5 -2
  27. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +28 -0
  28. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +4 -3
  29. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +47 -10
  30. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/migrating-a-service.md +1 -0
  31. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/package.md +10 -0
  32. package/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +31 -8
  33. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_layout.mjs +21 -0
  34. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_dashboard_link.py +33 -3
  35. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +19 -28
  36. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_channel_agent.py +9 -6
  37. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +146 -3
  38. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-connections.mjs +3 -2
  39. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +2 -0
  40. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +79 -2
  41. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-holds.mjs +269 -0
  42. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +64 -5
  43. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-hub.mjs +16 -7
  44. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-lock.mjs +73 -3
  45. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-package.mjs +98 -1
  46. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-peer-owner.mjs +89 -22
  47. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +1 -1
  48. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-secrets.mjs +39 -6
  49. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +8 -0
  50. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-supervisors.mjs +76 -11
  51. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +8 -153
  52. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +86 -2
  53. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_holds.py +324 -0
  54. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +34 -3
  55. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_hub.py +35 -17
  56. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_lock.py +77 -4
  57. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_package.py +103 -1
  58. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_peer_owner.py +156 -133
  59. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +1 -1
  60. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +257 -10
  61. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +6 -0
  62. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_supervisors.py +64 -3
  63. package/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +246 -24
  64. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +16 -6
  65. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agent-templates.md +44 -0
  66. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/communication-agent-template.md +19 -328
  67. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +16 -3
  68. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/models-and-providers.md +1 -1
  69. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/new-agent.md +42 -0
  70. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/source-and-instance.md +44 -7
  71. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/access.schema.json +50 -0
  72. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/connections.schema.json +23 -0
  73. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/data-rooms.schema.json +74 -0
  74. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/manifest.schema.json +170 -0
  75. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/schemas/template/template-header.schema.json +25 -0
  76. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/check_dashboard_link.py +33 -3
  77. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/scripts/template_check.py +491 -0
  78. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/CHANGELOG.md +7 -0
  79. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/TEMPLATE.md +347 -0
  80. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/access.json +24 -0
  81. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/assembly.md +15 -0
  82. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/checks/acceptance.md +17 -0
  83. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/connections.json +29 -0
  84. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/data-rooms.json +9 -0
  85. package/plugins/fabric-agent-adapter/skills/creating-fabric-agents/templates/communication-agent/manifest.json +32 -0
  86. package/plugins/fabric-agent-adapter/skills/observing-products/SKILL.md +1 -1
  87. package/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +2 -2
  88. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/on-demand-session.md +16 -8
  89. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +46 -31
  90. package/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +1 -1
  91. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +18 -16
  92. package/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +23 -13
@@ -20,7 +20,8 @@
20
20
  // Example URLs write their scheme in angle brackets: read `<https>` as `https` (FAA-212).
21
21
 
22
22
  import http from 'node:http';
23
- import { CheckedAgent, ListenerRefused, TABLE_RESTRICTED } from './fabric-peer-owner.mjs';
23
+ import net from 'node:net';
24
+ import { CheckedAgent, ListenerRefused, TABLE_RESTRICTED, UNKNOWN, method as tableMethod, owner as ownerOf, refusal, serverEnd } from './fabric-peer-owner.mjs';
24
25
 
25
26
  export const RETRY_STATUSES = Object.freeze([408, 425, 429, 500, 502, 503, 504]);
26
27
  export const IDEMPOTENT_METHODS = Object.freeze(['GET', 'HEAD', 'OPTIONS', 'PUT', 'DELETE']);
@@ -158,6 +159,61 @@ function checkHeaders(headers) {
158
159
 
159
160
  const LOOPBACK_AGENT = new CheckedAgent();
160
161
 
162
+ // Whether a connected socket reached this computer's loopback, by the address it connected to, not by the URL's
163
+ // spelling: a name the hosts file maps to 127.0.0.1, `0.0.0.0` and `[::ffff:127.0.0.1]` arrive here (FAA-260).
164
+ export function peerIsLoopback(address) {
165
+ const a = String(address ?? '').split('%', 1)[0].toLowerCase();
166
+ if (a === '::1' || a === '0.0.0.0' || a === '::') return true;
167
+ const v4 = /^(?:::ffff:)?(\d{1,3})\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.exec(a);
168
+ if (v4) return v4[1] === '127';
169
+ const hex = /^::ffff:([0-9a-f]{1,4}):[0-9a-f]{1,4}$/.exec(a);
170
+ return hex !== null && (parseInt(hex[1], 16) >> 8) === 127;
171
+ }
172
+
173
+ // An http.Agent for plain http to a host not spelled as loopback (`insecure`): when the connected socket's peer is a
174
+ // loopback address, its server end is told as CheckedAgent tells it and refused before a byte is written unless it is
175
+ // this OS account's process (DEC-0154 HA-2; 0.74.0 unit readings 7 and 8, FAA-260). Another peer is used as it is.
176
+ export class PeerCheckedAgent extends http.Agent {
177
+ constructor({ ask = {}, ...options } = {}) {
178
+ super(options);
179
+ this.ask = ask;
180
+ }
181
+
182
+ createConnection(options, callback) {
183
+ let done = false;
184
+ const finish = (err, socket) => { if (!done) { done = true; callback(err, socket); } };
185
+ const port = Number(options.port);
186
+ const socket = net.connect({ host: options.host, port, noDelay: true, autoSelectFamily: true,
187
+ ...(options.lookup ? { lookup: options.lookup } : {}) });
188
+ socket.once('error', (err) => finish(err));
189
+ socket.once('connect', async () => {
190
+ if (!peerIsLoopback(socket.remoteAddress)) {
191
+ socket.removeAllListeners('error');
192
+ finish(null, socket);
193
+ return;
194
+ }
195
+ socket.pause();
196
+ let who;
197
+ try {
198
+ who = await serverEnd(socket, this.ask);
199
+ } catch (err) {
200
+ who = ownerOf(UNKNOWN, { method: tableMethod(), reason: `check: ${err?.name ?? 'Error'}` });
201
+ }
202
+ const refused = refusal(who, port, null);
203
+ if (refused) {
204
+ socket.destroy();
205
+ finish(new ListenerRefused(refused[0], `${refused[1]}; nothing was sent`, who));
206
+ return;
207
+ }
208
+ socket.removeAllListeners('error');
209
+ socket.resume();
210
+ finish(null, socket);
211
+ });
212
+ }
213
+ }
214
+
215
+ const PLAIN_AGENT = new PeerCheckedAgent();
216
+
161
217
  // Plain http to a loopback host (DEC-0154 HA-2): node:http over a connection checked before a byte is written, no
162
218
  // redirect followed, the body read to one byte past maxBytes. fetch cannot hand its socket to the check.
163
219
  export function checkedLoopbackTransport({ method, url, headers, body, timeoutS, maxBytes, agent = LOOPBACK_AGENT }) {
@@ -198,12 +254,15 @@ export function checkedLoopbackTransport({ method, url, headers, body, timeoutS,
198
254
  });
199
255
  }
200
256
 
201
- // The default transport: fetch, no redirects followed, the body read to one byte past maxBytes; plain http to a
202
- // loopback host goes through checkedLoopbackTransport.
257
+ // The default transport: fetch, no redirects followed, the body read to one byte past maxBytes; plain http goes
258
+ // through checkedLoopbackTransport, its connection checked when it reaches loopback (FAA-260).
203
259
  export async function fetchTransport({ method, url, headers, body, timeoutS, maxBytes }) {
204
260
  const target = new URL(url);
205
- if (target.protocol === 'http:' && isLoopback(target.hostname)) {
206
- return checkedLoopbackTransport({ method, url, headers, body, timeoutS, maxBytes });
261
+ if (target.protocol === 'http:') {
262
+ // plain http never goes through fetch: a URL spelled as loopback is checked on every connection, any other is
263
+ // checked when its connected peer is a loopback address (FAA-260)
264
+ return checkedLoopbackTransport({ method, url, headers, body, timeoutS, maxBytes,
265
+ ...(isLoopback(target.hostname) ? {} : { agent: PLAIN_AGENT }) });
207
266
  }
208
267
  const res = await fetch(url, { method, headers, body: body ?? undefined, redirect: 'manual',
209
268
  signal: AbortSignal.timeout(Math.max(1, Math.ceil(timeoutS * 1000))) });
@@ -56,8 +56,9 @@ export class HubError extends Error {
56
56
  /**
57
57
  * The HubError for a hub port the peer-owner check refused (DEC-0154 HA-2): code `foreign-user`, the pinned contract's
58
58
  * state name, with the owner's reason in the sentence and in `data.reason` (FAA-272): `table-restricted` when this
59
- * runtime's socket table hides other processes' sockets, which never clears, so it is not retryable; otherwise the
60
- * owner's kind or the check's reason (`another-account`, `no-row`), retryable as before.
59
+ * runtime's socket table hides other processes' sockets, or, on macOS, holds no row for this connection (DEC-0155
60
+ * RT-1, FAA-312), which never clears, so it is not retryable; otherwise the owner's kind or the check's reason
61
+ * (`another-account`, or `no-row`, which only Linux and Windows give), retryable.
61
62
  */
62
63
  export function listenerRefusal(e) {
63
64
  const kind = e?.owner?.kind ?? null;
@@ -75,14 +76,18 @@ export function fabricRoot(platform = process.platform, env = process.env) {
75
76
  return path.join(env.XDG_DATA_HOME || path.join(home, '.local/share'), 'passioncode-fabric');
76
77
  }
77
78
 
78
- /** Whether a process with this id runs as this user (Fabric hub.ts#pidAlive): EPERM is another user's process. */
79
+ /**
80
+ * true: a process of this OS account runs under pid; false: none does. null (falsy): a process of another account does
81
+ * (EPERM), as when the id hub.json names was reused: not verified, never called running (FAA-304, the session hooks'
82
+ * answer since FAA-254). Never signals it.
83
+ */
79
84
  export function pidAlive(pid) {
80
85
  if (!Number.isInteger(pid) || pid <= 0) return false;
81
86
  try {
82
87
  process.kill(pid, 0);
83
88
  return true;
84
- } catch {
85
- return false;
89
+ } catch (e) {
90
+ return e?.code === 'EPERM' ? null : false;
86
91
  }
87
92
  }
88
93
 
@@ -108,7 +113,8 @@ function readSmall(file) {
108
113
 
109
114
  /**
110
115
  * hub.json read the way an agent must, every call. Answers {origin, mcpUrl, doorTokenFile, pid, startedAt, features} or
111
- * throws HubError (not-running, retryable; not-a-hub). Reads no token. `estate` is {record, applied}: with it, a hubKey
116
+ * throws HubError (not-running, retryable; not-a-hub, retryable when hub.json names another OS account's process, not
117
+ * verified, FAA-304). Reads no token. `estate` is {record, applied}: with it, a hubKey
112
118
  * the document names is checked by hubKeyCertified (DEC-0060); fabric-hub/0.1 names none.
113
119
  */
114
120
  export function locateHub(root = fabricRoot(), { alive = pidAlive, estate, now } = {}) {
@@ -142,7 +148,10 @@ export function locateHub(root = fabricRoot(), { alive = pidAlive, estate, now }
142
148
  sameRoot = false;
143
149
  }
144
150
  if (!sameRoot) throw notHub('names a door token outside the registry root');
145
- if (!alive(doc.pid)) throw new HubError('not-running', `Fabric is not running: hub.json names process ${doc.pid}, which is not running.`, true);
151
+ const live = alive(doc.pid);
152
+ // FAA-304: another account's process under that id, so not this account's Fabric (as FAA-254)
153
+ if (live === null) throw new HubError('not-a-hub', `Fabric's hub.json names process ${doc.pid}, which runs under another OS account: not verified.`, true);
154
+ if (!live) throw new HubError('not-running', `Fabric is not running: hub.json names process ${doc.pid}, which is not running.`, true);
146
155
  const features = Array.isArray(doc.features) ? doc.features.filter((f) => typeof f === 'string') : [];
147
156
  if (estate && doc.hubKey && typeof doc.hubKey === 'object') {
148
157
  const verdict = hubKeyCertified(estate.record, doc.hubKey.kid, doc.hubKey.x, now ?? Date.now() / 1000, estate.applied ?? undefined);
@@ -1,5 +1,15 @@
1
1
  // One copy of a service, locked before any side effect (LC-01, DEC-0057): the instance lock and the supervisor marker.
2
2
  // Part of the Node service kit (FAA-65, layer L3); the twin of fabric_lock.py. Node 20+ (22+ for the Linux lock), no dependency.
3
+ //
4
+ // A lock that moves under its holder (FAA-303). Linux and Windows hold a name keyed by the lock's path (an abstract
5
+ // unix socket, a named pipe), so a folder swapped under a running copy cannot let a second copy in. macOS has no such
6
+ // name: O_EXLOCK (and the Python twin's flock) lock an inode, so after a data import swaps the data folder a second
7
+ // copy locks the new file and starts. There the holder watches its lock instead (InstanceLock#watch, started by
8
+ // holdSingleInstance): every LOCK_WATCH_SECONDS it compares the inode it locked with the file at the path, and once
9
+ // the path names another file it stops with 75 and one sentence naming the path, before writing anything more into a
10
+ // folder that is no longer the one it read. Two copies then overlap for at most one interval. A path-keyed name on
11
+ // macOS was weighed and left: it needs a file in a folder outside the data folder that the system never cleans and
12
+ // every copy can write, which Node alone cannot promise.
3
13
  import crypto from 'node:crypto';
4
14
  import fs from 'node:fs';
5
15
  import net from 'node:net';
@@ -15,6 +25,12 @@ export const LOCK_BACKOFF_FIRST_SECONDS = 0.5;
15
25
 
16
26
  export const LOCK_BACKOFF_MAX_SECONDS = 30;
17
27
 
28
+ /**
29
+ * How often a holder that has no path-keyed name (macOS) compares the inode it locked with the file at the lock's path
30
+ * (FAA-303): one stat and one fstat; two copies overlap for at most this long after a folder swap.
31
+ */
32
+ export const LOCK_WATCH_SECONDS = 2;
33
+
18
34
  const O_EXLOCK = 0x20; // BSD/macOS: open(2) takes flock(LOCK_EX); absent from fs.constants
19
35
 
20
36
  export class AlreadyRunning extends ServiceError {
@@ -34,6 +50,7 @@ export class InstanceLock {
34
50
  this.path = path.join(dataDir, 'service.lock');
35
51
  this.fd = null;
36
52
  this.server = null;
53
+ this.timer = null;
37
54
  }
38
55
 
39
56
  async acquire() {
@@ -124,14 +141,65 @@ export class InstanceLock {
124
141
  return true;
125
142
  }
126
143
 
144
+ /**
145
+ * Re-check, every `intervalSeconds` on an unref'd timer, that the file this copy locked is still the file at the path
146
+ * (FAA-303); the first time the path names another file (a swapped folder's lock, or the one a second copy made),
147
+ * call `onLost(lock)` once and stop watching. The default stops the process with 75 and one sentence
148
+ * (stopLostHolder). A path that is gone or cannot be read now is read as nothing there yet and tried again next time:
149
+ * a second copy has to make a file there to start, and the watch then sees that file. `release` ends the watch. Only
150
+ * a lock held as a file is watched (macOS): elsewhere the descriptor is null and the path-keyed name keeps a second
151
+ * copy out, so this is a no-op, as for a lock not held.
152
+ */
153
+ watch({ onLost, intervalSeconds = LOCK_WATCH_SECONDS } = {}) {
154
+ if (this.fd === null || this.timer !== null) return this;
155
+ const fd = this.fd;
156
+ const react = onLost ?? stopLostHolder;
157
+ this.timer = setInterval(() => {
158
+ let here;
159
+ let held;
160
+ try {
161
+ here = fs.statSync(this.path);
162
+ held = fs.fstatSync(fd);
163
+ } catch {
164
+ return;
165
+ }
166
+ if (here.dev === held.dev && here.ino === held.ino) return;
167
+ clearInterval(this.timer);
168
+ this.timer = null;
169
+ react(this);
170
+ }, intervalSeconds * 1000);
171
+ this.timer.unref();
172
+ return this;
173
+ }
174
+
127
175
  release() {
176
+ if (this.timer !== null) clearInterval(this.timer);
128
177
  if (this.fd !== null) fs.closeSync(this.fd);
129
178
  if (this.server) this.server.close();
179
+ this.timer = null;
130
180
  this.fd = null;
131
181
  this.server = null;
132
182
  }
133
183
  }
134
184
 
185
+ /** The one sentence a holder whose lock moved under it stops with (FAA-303). */
186
+ export function lostLockMessage(lockPath) {
187
+ return `${lockPath} is no longer the file this copy locked (the data folder moved or was swapped?): stopping, so a copy started on the folder now at that path runs alone.`;
188
+ }
189
+
190
+ /**
191
+ * InstanceLock#watch's default: write lostLockMessage and end the process with 75 at once. Not a graceful stop: a
192
+ * shutdown would write this copy's state into the folder now at the path, which is not the one it read; the kit's
193
+ * journals make an abrupt stop recoverable (LC-03).
194
+ */
195
+ export function stopLostHolder(lock) {
196
+ try {
197
+ fs.writeSync(2, `${lostLockMessage(lock.path)}\n`);
198
+ } finally {
199
+ process.exit(EXIT_ALREADY_RUNNING);
200
+ }
201
+ }
202
+
135
203
  let abstractNamesSeen = null;
136
204
 
137
205
  /**
@@ -253,10 +321,12 @@ const sleepSeconds = (s) => new Promise((resolve) => setTimeout(resolve, s * 100
253
321
  * supervisor that restarts on exit (launchd's KeepAlive) an immediate exit is a respawn every ThrottleInterval,
254
322
  * forever; so a supervised copy first backs off in-process (0.5 s doubling to 30 s,
255
323
  * LOCK_WAIT_SUPERVISED_SECONDS in all), takes over if the holder leaves, and exits 75 only when
256
- * the wait runs out. `waitSeconds` overrides the choice (0 = never wait).
324
+ * the wait runs out. `waitSeconds` overrides the choice (0 = never wait). Where the lock is held as a file (macOS)
325
+ * the held lock is watched (InstanceLock#watch, FAA-303): a folder swapped under the running copy stops it with 75;
326
+ * `onLost` replaces that reaction.
257
327
  */
258
328
  export async function holdSingleInstance(dataDir, {
259
- waitSeconds, sleep = sleepSeconds, clock = () => performance.now() / 1000, exit = (code) => process.exit(code), env, argv,
329
+ waitSeconds, sleep = sleepSeconds, clock = () => performance.now() / 1000, exit = (code) => process.exit(code), env, argv, onLost,
260
330
  } = {}) {
261
331
  // DEC-0057: markers that disagree exit 75 at once, before the lock, naming them.
262
332
  const marker = supervisorMarker({ ...(env === undefined ? {} : { env }), ...(argv === undefined ? {} : { argv }) });
@@ -270,7 +340,7 @@ export async function holdSingleInstance(dataDir, {
270
340
  let announced = false;
271
341
  for (;;) {
272
342
  try {
273
- return await new InstanceLock(dataDir).acquire();
343
+ return (await new InstanceLock(dataDir).acquire()).watch({ onLost });
274
344
  } catch (error) {
275
345
  if (!(error instanceof AlreadyRunning)) throw error;
276
346
  const remaining = deadline - clock();
@@ -509,9 +509,71 @@ const FOLDER_MESSAGES = {
509
509
  'provider-manifest': 'provider-manifest: the provider manifest must be a JSON object, and when it carries costPresets fabric.package.json must carry the same costPresets (copy them; pack does not)',
510
510
  };
511
511
 
512
+ /**
513
+ * The agent's source manifest (contract DEC-0149, `fabric-source/1`): when the folder carries one, `pack` writes
514
+ * `secrets[]` from its secret slots and `state[]` from its store slots' paths, and FAC-SEM-160 holds them equal.
515
+ */
516
+ export const SOURCE_FILE = 'fabric.source.json';
517
+
518
+ function sourceManifest(files) {
519
+ if (!files.has(SOURCE_FILE)) return undefined;
520
+ const source = parseStrict(files.get(SOURCE_FILE), SOURCE_FILE);
521
+ if (source.format !== 'fabric-source/1') throw new PackageError(`${SOURCE_FILE} is not a fabric-source/1 manifest (its format member).`);
522
+ return source;
523
+ }
524
+
525
+ /** `secrets[]` from the secret slots (name, purpose, optional) and `state[]` from the store slots (path, purpose). */
526
+ export function sourceMembers(source) {
527
+ const secrets = [];
528
+ const state = [];
529
+ for (const slot of objects(source.slots)) {
530
+ if (slot.kind === 'secret' && isString(slot.name)) secrets.push({ name: slot.name, purpose: slot.purpose, ...(slot.optional === true ? { optional: true } : {}) });
531
+ else if (slot.kind === 'store' && isString(slot.path)) state.push({ path: slot.path, purpose: slot.purpose });
532
+ }
533
+ return { secrets, state };
534
+ }
535
+
536
+ /**
537
+ * FAC-SEM-160 (contract `sourcePackageFindings`): the package's secrets[] are the source's secret slots, by name and
538
+ * optional; its state[] paths are the store slots' paths when state[] holds any entry.
539
+ */
540
+ export function sourcePackageFindings(source, pkg) {
541
+ if (!isObject(source) || !isObject(pkg)) return [];
542
+ const out = [];
543
+ const flag = (instancePath, message) => out.push({ code: 'FAC-SEM-160', instancePath, message });
544
+ const slots = objects(source.slots);
545
+ const secrets = new Map(slots.filter((s) => s.kind === 'secret' && isString(s.name)).map((s) => [s.name, s.optional === true]));
546
+ const stores = new Set(slots.filter((s) => s.kind === 'store' && isString(s.path)).map((s) => s.path));
547
+ const packaged = (Array.isArray(pkg.secrets) ? pkg.secrets : []).flatMap((e, i) => (isObject(e) ? [[e, i]] : []));
548
+ const names = new Set(packaged.map(([e]) => e.name));
549
+ for (const name of secrets.keys()) if (!names.has(name)) flag('/package/secrets', `the source's secret slot ${name} is not in the package's secrets[]`);
550
+ for (const [entry, index] of packaged) {
551
+ if (!isString(entry.name) || !secrets.has(entry.name)) flag(`/package/secrets/${index}/name`, `secrets[${index}] is no secret slot of the source`);
552
+ else if (secrets.get(entry.name) !== (entry.optional === true)) flag(`/package/secrets/${index}/optional`, `secrets[${index}] is ${secrets.get(entry.name) ? '' : 'not '}optional in the source`);
553
+ }
554
+ if (!Array.isArray(pkg.state) || pkg.state.length === 0) return out;
555
+ const paths = new Set(objects(pkg.state).map((e) => e.path));
556
+ for (const p of stores) if (!paths.has(p)) flag('/package/state', "a store slot's path of the source is not in the package's state[]");
557
+ pkg.state.forEach((entry, index) => {
558
+ if (isObject(entry) && (!isString(entry.path) || !stores.has(entry.path))) flag(`/package/state/${index}/path`, `state[${index}] is no store slot of the source`);
559
+ });
560
+ return out;
561
+ }
562
+
563
+ function sourceProblems(pkg, files) {
564
+ if (!files.has(SOURCE_FILE)) return [];
565
+ try {
566
+ return sourcePackageFindings(sourceManifest(files), pkg).map((p) => ({ ...p, rule: p.code }));
567
+ } catch (error) {
568
+ if (!(error instanceof PackageError)) throw error;
569
+ return [{ rule: 'source', instancePath: '/files', message: error.message }];
570
+ }
571
+ }
572
+
512
573
  function allProblems(pkg, files) {
513
574
  return [
514
575
  ...schemaProblems(pkg).map((p) => ({ ...p, rule: 'schema' })),
576
+ ...sourceProblems(pkg, files),
515
577
  ...pathProblems(pkg).map((p) => ({ ...p, rule: 'path' })),
516
578
  ...packageFindings(pkg).map((p) => ({ ...p, rule: p.code })),
517
579
  ...checkPackageFolder(pkg, files).map((p) => ({ rule: 'folder', problem: p.problem, instancePath: p.path, message: FOLDER_MESSAGES[p.problem] ?? p.problem })),
@@ -525,6 +587,21 @@ function writeAtomic(file, text) {
525
587
  fs.renameSync(temp, file);
526
588
  }
527
589
 
590
+ /** The names a hand-written secrets[] declares that the source manifest has no secret slot for, in their order. */
591
+ function secretsLacking(draft, secrets) {
592
+ const have = new Set(secrets.map((s) => s.name));
593
+ const lacking = [];
594
+ for (const entry of Array.isArray(draft.secrets) ? draft.secrets : []) {
595
+ const name = isObject(entry) ? entry.name : undefined;
596
+ if (isString(name) && !have.has(name) && !lacking.includes(name)) lacking.push(name);
597
+ }
598
+ return lacking;
599
+ }
600
+
601
+ function lackingMessage(lacking) {
602
+ return `${SOURCE_FILE} declares no secret slot for ${lacking.join(', ')}, which ${PACKAGE_FILE}'s secrets[] names: pack writes secrets[] from the manifest's slots and never drops a declared secret. Add each as a slot of kind secret to ${SOURCE_FILE} (or take it out of secrets[]), then pack again`;
603
+ }
604
+
528
605
  /** Pack `folder`; with `apply` and no problem, fabric.package.json is rewritten. */
529
606
  export function pack(folder, { apply = false } = {}) {
530
607
  const files = readFolder(folder);
@@ -532,8 +609,28 @@ export function pack(folder, { apply = false } = {}) {
532
609
  throw new PackageError(`Write ${PACKAGE_FILE} in the folder first: schema, kind, id, version, publisher, licence and, for an agent, resultSignal and evals with its receipt. pack fills in agentRef, files[] and digest.`);
533
610
  }
534
611
  const draft = parseStrict(files.get(PACKAGE_FILE), PACKAGE_FILE);
612
+ const source = sourceManifest(files);
613
+ const written = [];
614
+ const refused = [];
615
+ if (source !== undefined) {
616
+ // DEC-0149: secrets[] and state[] come from the source manifest's slots; an empty state[] is a copy without state.
617
+ const members = sourceMembers(source);
618
+ const lacking = secretsLacking(draft, members.secrets);
619
+ if (lacking.length > 0) {
620
+ // a declared secret is never dropped: the manifest gains its slot first
621
+ refused.push({ rule: 'source', instancePath: '/secrets', message: lackingMessage(lacking) });
622
+ } else if (members.secrets.length > 0 || Object.hasOwn(draft, 'secrets')) {
623
+ Object.defineProperty(draft, 'secrets', { value: members.secrets, enumerable: true, writable: true, configurable: true });
624
+ written.push('secrets[]');
625
+ }
626
+ if (members.state.length > 0 && !(Array.isArray(draft.state) && draft.state.length === 0)) {
627
+ Object.defineProperty(draft, 'state', { value: members.state, enumerable: true, writable: true, configurable: true });
628
+ written.push('state[]');
629
+ }
630
+ }
535
631
  const { pkg, notes } = compose(draft, files);
536
- const problems = allProblems(pkg, files);
632
+ if (written.length > 0) notes.push(`${written.join(' and ')} written from ${SOURCE_FILE}`);
633
+ const problems = [...refused, ...allProblems(pkg, files)];
537
634
  const suite = isObject(pkg.evals) ? pkg.evals.suite : undefined;
538
635
  if (isString(suite) && files.has(suite) && problems.some((p) => p.instancePath === '/evals/receipt/suite')) notes.push(`the suite packed is ${sha256Of(files.get(suite))}: run it again and write that receipt`);
539
636
  const answer = { ok: problems.length === 0, applied: false, digest: pkg.digest, files: pkg.files.length, problems, notes };
@@ -40,12 +40,14 @@
40
40
  // privilege. The parsers are pure functions over captured text (test/fixtures/peer-owner/).
41
41
 
42
42
  import { execFile } from 'node:child_process';
43
+ import { realpathSync } from 'node:fs';
43
44
  import { open } from 'node:fs/promises';
44
45
  import http from 'node:http';
45
46
  import net from 'node:net';
46
47
  import os from 'node:os';
48
+ import { fileURLToPath } from 'node:url';
47
49
 
48
- export const PEER_OWNER_VERSION = '1.1.0+kit.4'; // the Python twin's, Observatory's 1.1.0 with the kit's differences
50
+ export const PEER_OWNER_VERSION = '1.2.0+kit.6'; // the Python twin's, Observatory's 1.2.0 with the kit's differences
49
51
  export const THIS_ACCOUNT = 'this-account';
50
52
  export const ANOTHER_ACCOUNT = 'another-account';
51
53
  export const SYSTEM = 'system';
@@ -62,6 +64,8 @@ export const LSOF = '/usr/sbin/lsof';
62
64
  // The reason a socket table hides other processes' sockets from this one (FAA-267): macOS 27.2 gives an ad-hoc-signed
63
65
  // runtime an empty netstat.
64
66
  export const TABLE_RESTRICTED = 'table-restricted';
67
+ // A macOS table that lists sockets but not the reader's own connection (DEC-0155 RT-1, FAA-312).
68
+ export const NO_ROW_RESTRICTED = `${TABLE_RESTRICTED}: no row for this connection`;
65
69
  export const SERVER_NOT_OWN = 'server-not-own';
66
70
  export const SERVER_UNVERIFIED = 'server-unverified';
67
71
  export const SERVER_NOT_THIS_WORKSPACE = 'server-not-this-workspace';
@@ -290,10 +294,10 @@ export function runProgram(file, args, timeout = TIMEOUT_MS) {
290
294
  });
291
295
  }
292
296
 
293
- export function method() {
294
- if (process.platform === 'darwin') return 'netstat';
295
- if (process.platform === 'linux') return 'proc';
296
- if (process.platform === 'win32') return 'netstat-windows';
297
+ export function method(platform = process.platform) {
298
+ if (platform === 'darwin') return 'netstat';
299
+ if (platform === 'linux') return 'proc';
300
+ if (platform === 'win32') return 'netstat-windows';
297
301
  return 'none';
298
302
  }
299
303
 
@@ -319,9 +323,18 @@ async function readProc(name) {
319
323
  return Buffer.concat(chunks);
320
324
  }
321
325
 
322
- export async function readTable({ run = runProgram } = {}) {
323
- const how = method();
324
- if (how === 'netstat') return { rows: parseNetstat(await run(NETSTAT, ['-anv', '-p', 'tcp'])), how };
326
+ export async function readTable({ run = runProgram, platform = process.platform } = {}) {
327
+ const how = method(platform);
328
+ if (how === 'netstat') {
329
+ let text;
330
+ try {
331
+ text = await run(NETSTAT, ['-anv', '-p', 'tcp']);
332
+ } catch (err) {
333
+ // A netstat that fails, exits non-zero or times out is a restricted table (DEC-0155 RT-1, FAA-312).
334
+ throw new TableUnreadable(`${TABLE_RESTRICTED}: ${err instanceof TableUnreadable ? err.message : `netstat: ${err?.name ?? 'error'}`}`);
335
+ }
336
+ return { rows: parseNetstat(text), how };
337
+ }
325
338
  if (how === 'proc') {
326
339
  const rows = [];
327
340
  for (const [name, family] of [['/proc/net/tcp', 4], ['/proc/net/tcp6', 6]]) {
@@ -387,6 +400,21 @@ export async function resolve(row, how, { run = runProgram, ownUid: mine, ownSid
387
400
 
388
401
  // -- the questions ----------------------------------------------------------------------------------------------------
389
402
 
403
+ // DEC-0155 RT-1 (FAA-312): a macOS table holding no row of another uid (sysctl, the Python twin's, which carries uids)
404
+ // or of another process (netstat, which carries pids) is restricted, and a missing row proves nothing. Never on Linux
405
+ // or Windows, which carry the owner on each row.
406
+ export function restricted(rows, how, { ownUid: mine, ownPid } = {}) {
407
+ if (how === 'sysctl') {
408
+ const own = mine ?? ownUid();
409
+ return !rows.some((r) => r.uid !== null && r.uid !== undefined && r.uid !== own);
410
+ }
411
+ if (how === 'netstat') {
412
+ const own = ownPid ?? process.pid;
413
+ return !rows.some((r) => r.pid !== null && r.pid !== undefined && r.pid !== own);
414
+ }
415
+ return false;
416
+ }
417
+
390
418
  // The live connected rows with exactly this local and remote address: one, or the caller cannot tell.
391
419
  // A row printed twice is one socket: Linux resumes a /proc read that sockets came and went under by position, and can
392
420
  // print the same row again (0.74.0 seam reading 6); rows equal in every field count once.
@@ -414,13 +442,14 @@ async function ownerOf(local, remote, opts = {}, server = false) {
414
442
  // A /proc read under churn can skip a row or print one twice; one more read before answering unknown.
415
443
  if (table.how !== 'proc' || found.length === 1) break;
416
444
  }
417
- if (found.length === 0) return owner(UNKNOWN, { method: table.how, reason: 'no-row' });
445
+ // A macOS table without the reader's own connection is restricted (DEC-0155 RT-1, FAA-312).
446
+ if (found.length === 0) return owner(UNKNOWN, { method: table.how, reason: ['sysctl', 'netstat'].includes(table.how) ? NO_ROW_RESTRICTED : 'no-row' });
418
447
  if (found.length > 1) return owner(UNKNOWN, { method: table.how, reason: 'several-rows' });
419
448
  if (table.how === 'proc' && server && found[0].uid === null) {
420
449
  // Linux: a connection not yet accept()ed is printed with uid 0 and inode 0; it belongs to the listener on its local
421
450
  // address and dies with it, so that listener answers. An accepted connection's own row carries the uid of the
422
451
  // process that accepted it: it answers itself, so a listener swapped after another account accepted the client
423
- // cannot pass (Project Observatory's 1.2.0 reading, board thread ced00f6e; the contract's pending CT-254, FAA-258).
452
+ // cannot pass (Project Observatory's 1.2.0 reading, board thread ced00f6e; the contract's CT-254, released).
424
453
  // A server row its owner already closed (an orphan) also has inode 0 and reads so: no process holds it, so nothing
425
454
  // sent to it reaches anyone (0.74.0 unit reading 6).
426
455
  return oneOwner(listenersOf(table.rows, found[0].local), table.how, opts, 'not-accepted');
@@ -464,7 +493,7 @@ export function peer(socket, opts) {
464
493
  // For a client's connected socket: who accepted it. The row whose local address is the server's and whose remote
465
494
  // address is this socket's own, the server process's accepted socket, so a listener swapped after listen cannot pass
466
495
  // for the one that answers. On Linux a connection not yet accept()ed has no owner of its own (inode 0): its listener
467
- // answers (the contract's pending CT-254, FAA-258).
496
+ // answers (the contract's CT-254, HA-2's Linux source).
468
497
  export function serverEnd(socket, opts) {
469
498
  if (!socket.remoteAddress || !socket.localAddress) return Promise.resolve(owner(UNKNOWN, { reason: 'socket: not connected' }));
470
499
  return ownerOf(address(socket.remoteAddress, socket.remotePort), address(socket.localAddress, socket.localPort), opts, true);
@@ -491,23 +520,28 @@ export async function listener(port, opts = {}) {
491
520
  const at = now();
492
521
  const hit = injected ? undefined : listeners.get(port);
493
522
  if (hit && at - hit[0] < LISTENER_TTL_MS) return hit[1];
494
- let found;
495
- let how;
496
523
  let table;
497
524
  try {
498
525
  table = await (read ?? readTable)(opts);
499
- how = table.how;
500
- found = pickListener(table.rows, port);
501
526
  } catch (err) {
502
527
  const who = owner(UNKNOWN, { method: method(), reason: err instanceof TableUnreadable ? err.message : `table: ${err.name}` });
503
528
  if (!injected) listeners.set(port, [at, who]);
504
529
  return who;
505
530
  }
506
- const who = await oneOwner(found, how, opts);
531
+ const who = restricted(table.rows, table.how, opts) ? await restrictedListener(table.rows, port, table.how, opts)
532
+ : await oneOwner(pickListener(table.rows, port), table.how, opts);
507
533
  if (!injected) listeners.set(port, [at, who]);
508
534
  return who;
509
535
  }
510
536
 
537
+ // DEC-0155 RT-4 (FAA-312): under a restricted table only this account's own bind on exactly 127.0.0.1 is its own; its
538
+ // wildcard or ::1 listener, root's, and a port with no row are an owner that cannot be told.
539
+ async function restrictedListener(rows, port, how, opts) {
540
+ const who = await oneOwner(rows.filter((r) => r.listening && r.local[0] === '127.0.0.1' && r.local[1] === port), how, opts);
541
+ if (who.kind === THIS_ACCOUNT) return who;
542
+ return owner(UNKNOWN, { method: how, reason: `${TABLE_RESTRICTED}: no listener of this account on exactly 127.0.0.1:${port}` });
543
+ }
544
+
511
545
  // [code, why] when a client must not send on a connection whose server end is `who`, else null.
512
546
  export function refusal(who, port, expectPid = null) {
513
547
  if (who.kind === ANOTHER_ACCOUNT || who.kind === SYSTEM) return [SERVER_NOT_OWN, `the server on port ${port} belongs to ${who.kind}`];
@@ -518,16 +552,20 @@ export function refusal(who, port, expectPid = null) {
518
552
  return null;
519
553
  }
520
554
 
521
- // macOS, when the socket table is restricted (FAA-267): whether process `pid`, this account's, listens on 127.0.0.1:<port>
522
- // or the IPv4 wildcard (IPv4 alone: a v6-only wildcard never vouches for a 127.0.0.1 connection). `lsof -p` asks the process itself, which the restriction does not hide. Only a caller that started
523
- // `pid` itself (a launchd job it installed) may vouch with it.
524
- export async function pidListens(pid, port, { run = runProgram, platform = process.platform, ownUid: mine } = {}) {
555
+ // macOS, when the socket table is restricted (FAA-267, DEC-0155 RT-3): whether process `pid`, this account's, listens on
556
+ // exactly 127.0.0.1:<port> (never the IPv4 wildcard, which lsof prints as it prints an IPv6-only one, nor ::1; FAA-312).
557
+ // `lsof -p` asks the process itself, which the restriction does not hide. Only a caller that started `pid` itself (a
558
+ // launchd job it installed) may vouch with it, and never a set-uid one (its real and effective uid differ, DEC-0155).
559
+ export async function pidListens(pid, port, { run = runProgram, platform = process.platform, ownUid: mine, ownEuid } = {}) {
525
560
  if (platform !== 'darwin' || !Number.isInteger(pid) || pid <= 1) return false;
561
+ const real = mine ?? ownUid();
562
+ const effective = ownEuid ?? (typeof process.geteuid === 'function' ? process.geteuid() : real);
563
+ if (effective !== real) return false;
526
564
  try {
527
565
  const uid = parsePsUid(await run(PS, ['-o', 'uid=', '-p', String(pid)]));
528
- if (uid === null || uid !== (mine ?? ownUid())) return false;
566
+ if (uid === null || uid !== real) return false;
529
567
  const out = await run(LSOF, ['-nP', '-a', '-p', String(pid), '-i4TCP', '-sTCP:LISTEN', '-Fn']);
530
- return parseLsofListeners(out).some(([host, p]) => (host === '127.0.0.1' || host === '*') && p === port);
568
+ return parseLsofListeners(out).some(([host, p]) => host === '127.0.0.1' && p === port);
531
569
  } catch {
532
570
  return false;
533
571
  }
@@ -587,3 +625,32 @@ export class CheckedAgent extends http.Agent {
587
625
  expectPid: this.expectPid, vouchPid: this.vouchPid, ...this.ask }).then((socket) => callback(null, socket), (err) => callback(err));
588
626
  }
589
627
  }
628
+
629
+ const USAGE = 'usage: fabric-peer-owner.mjs listener <port> [--json]\n';
630
+
631
+ // `listener <port> [--json]`: the kind of who listens on 127.0.0.1:<port>, which the dashboard link gate takes as
632
+ // listener_owner (FAA-315), or the whole owner as JSON; 2 on a usage error. `listener` and `write` are the tests'.
633
+ export async function main(argv, { listener: ask = listener, write = (text) => process.stdout.write(text) } = {}) {
634
+ const asJson = argv.length === 3 && argv[2] === '--json';
635
+ const port = Number(argv[1]);
636
+ if (argv.length !== 2 + Number(asJson) || argv[0] !== 'listener' || !/^[0-9]+$/.test(argv[1] ?? '') || port < 1 || port > 65535) {
637
+ process.stderr.write(USAGE);
638
+ return 2;
639
+ }
640
+ const who = await ask(port);
641
+ write(asJson ? `${JSON.stringify({ kind: who.kind, uid: who.uid, pid: who.pid, method: who.method, reason: who.reason })}\n` : `${who.kind}\n`);
642
+ return 0;
643
+ }
644
+
645
+ // Whether node was started on this file, by real paths (a symlinked install folder, as check_dashboard_layout.mjs).
646
+ function startedDirectly() {
647
+ try {
648
+ return Boolean(process.argv[1]) && realpathSync(process.argv[1]) === realpathSync(fileURLToPath(import.meta.url));
649
+ } catch {
650
+ return false;
651
+ }
652
+ }
653
+
654
+ if (startedDirectly()) {
655
+ main(process.argv.slice(2)).then((code) => { process.exitCode = code; }, (err) => { process.stderr.write(`${err?.message ?? err}\n`); process.exitCode = 1; });
656
+ }
@@ -12,7 +12,7 @@ export const PROTOCOL_REVISIONS = new Map(Object.entries({
12
12
  'fabric-interop': ['0.1', '0.2'], 'fabric-local-channel': ['0.1'], 'fabric-local-runner': ['0.1'], 'fabric-machine-report': ['1'], 'fabric-ops-digest': ['0.1'], 'fabric-operator-channel': ['0.1'],
13
13
  'fabric-package': ['1'],
14
14
  'fabric-peer-share': ['1'], 'fabric-peer-share-envelope': ['1'], 'fabric-peer-share-report': ['1'], 'fabric-project-comms': ['0.1'], 'fabric-proposal': ['1'], 'fabric-provider': ['0.1'], 'fabric-relay': ['0.1'],
15
- 'fabric-secret-envelope': ['1'], 'fabric-service': ['0.1'], 'fabric-settings-backup': ['1'], heartbeat: ['1'], 'learning-note': ['1'], 'machine-report': ['1'], mandate: ['1'],
15
+ 'fabric-secret-envelope': ['1'], 'fabric-service': ['0.1'], 'fabric-settings-backup': ['1'], 'fabric-source': ['1'], heartbeat: ['1'], 'learning-note': ['1'], 'machine-report': ['1'], mandate: ['1'],
16
16
  memory: ['0.1'], 'node-summary': ['1'], 'operator-source': ['1'], 'org-adaptation': ['1'], 'org-adaptation-receipt': ['1'], 'org-authority': ['1'], 'org-authority-draft': ['1'],
17
17
  'org-key-set': ['1'], 'org-key-set-draft': ['1'], 'org-membership': ['1'], 'org-root-registration': ['1'], 'pairing-message': ['1'],
18
18
  'peer-share-deletion': ['1'], 'peer-share-frame': ['1'], 'peer-share-revocation': ['1'], pipeline: ['0.1'], 'repair-request': ['1'], 'repair-result': ['1'],