@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,6 +1,9 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
1
2
  // Which OS account owns each end of a loopback TCP connection (DEC-0154 HA-2, FAA-226, layer L0); the twin of
2
- // fabric_peer_owner.py, which is Project Observatory's peer_owner 1.1.0 vendored by commit. Node 18+, no dependency;
3
- // nothing here imports another kit module.
3
+ // fabric_peer_owner.py, which is Project Observatory's peer_owner vendored by commit (its 1.3.1 line). Node 18+, no
4
+ // dependency; the one kit module it may load is fabric-socket-proof.mjs beside it (DEC-0155 RT-2, FAA-292): on a
5
+ // restricted macOS table a server end or listener one of this account's own processes holds is this account's, and
6
+ // without that file this one answers as before.
4
7
  //
5
8
  // WHY. Loopback is one per computer, not one per OS account: every account's processes reach every listener on
6
9
  // 127.0.0.1, so a loopback socket proves nothing about who is on the other end. A client about to send a credential
@@ -47,7 +50,7 @@ import net from 'node:net';
47
50
  import os from 'node:os';
48
51
  import { fileURLToPath } from 'node:url';
49
52
 
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
53
+ export const PEER_OWNER_VERSION = '1.3.1+kit.8'; // the Python twin's, Observatory's 1.3.1 line with the kit's differences
51
54
  export const THIS_ACCOUNT = 'this-account';
52
55
  export const ANOTHER_ACCOUNT = 'another-account';
53
56
  export const SYSTEM = 'system';
@@ -478,10 +481,36 @@ async function oneOwner(found, how, opts, noneReason = null) {
478
481
  return owner(UNKNOWN, { method: how, reason: 'several-rows' });
479
482
  }
480
483
 
484
+ // fabric-socket-proof.mjs beside this file, loaded once (DEC-0155 RT-2, FAA-292); null when it is not there.
485
+ let socketProof;
486
+
487
+ // `fallback`, unless it is a restricted macOS table's unknown and fabric-socket-proof finds the socket (`accepted`) or
488
+ // the 127.0.0.1 listener (`ownListener`, FAA-272) in this account's own processes; else it stands. A test that injects
489
+ // a table (`read`, `run`) reaches this host's processes only through the `socketProof` it names.
490
+ async function proved(ask, args, fallback, opts = {}) {
491
+ if (fallback.kind !== UNKNOWN || !String(fallback.reason ?? '').startsWith(TABLE_RESTRICTED)) return fallback;
492
+ if ((opts.platform ?? process.platform) !== 'darwin') return fallback;
493
+ let module = opts.socketProof;
494
+ if (module === undefined) {
495
+ if (opts.read || opts.run) return fallback;
496
+ if (socketProof === undefined) socketProof = await import('./fabric-socket-proof.mjs').catch(() => null);
497
+ module = socketProof;
498
+ }
499
+ let p = null;
500
+ try {
501
+ p = module ? await module[ask](...args) : null;
502
+ } catch {
503
+ p = null; // a proof that fails proves nothing; the table's answer stands (fail closed)
504
+ }
505
+ return p?.proved ? owner(THIS_ACCOUNT, { uid: opts.ownUid ?? ownUid(), pid: p.pid, method: p.method, reason: p.reason }) : fallback;
506
+ }
507
+
481
508
  // The owner of the CLIENT socket of the connection between `server` and `client` ([host, port] each), as a server sees
482
509
  // them: the row whose local address is the client's and whose remote address is the server's.
483
- export function peerAt(server, client, opts) {
484
- return ownerOf(address(client[0], client[1]), address(server[0], server[1]), opts);
510
+ export async function peerAt(server, client, opts) {
511
+ const c = address(client[0], client[1]);
512
+ const s = address(server[0], server[1]);
513
+ return proved('accepted', [c, s, { wait: false }], await ownerOf(c, s, opts), opts);
485
514
  }
486
515
 
487
516
  // For a server's accepted socket: who connected. Reads nothing from the socket.
@@ -494,9 +523,11 @@ export function peer(socket, opts) {
494
523
  // address is this socket's own, the server process's accepted socket, so a listener swapped after listen cannot pass
495
524
  // for the one that answers. On Linux a connection not yet accept()ed has no owner of its own (inode 0): its listener
496
525
  // answers (the contract's CT-254, HA-2's Linux source).
497
- export function serverEnd(socket, opts) {
498
- if (!socket.remoteAddress || !socket.localAddress) return Promise.resolve(owner(UNKNOWN, { reason: 'socket: not connected' }));
499
- return ownerOf(address(socket.remoteAddress, socket.remotePort), address(socket.localAddress, socket.localPort), opts, true);
526
+ export async function serverEnd(socket, opts) {
527
+ if (!socket.remoteAddress || !socket.localAddress) return owner(UNKNOWN, { reason: 'socket: not connected' });
528
+ const s = address(socket.remoteAddress, socket.remotePort);
529
+ const own = address(socket.localAddress, socket.localPort);
530
+ return proved('accepted', [s, own, { wait: true }], await ownerOf(s, own, opts, true), opts);
500
531
  }
501
532
 
502
533
  const LISTEN_ORDER = ['127.0.0.1', '*', '::1'];
@@ -524,12 +555,12 @@ export async function listener(port, opts = {}) {
524
555
  try {
525
556
  table = await (read ?? readTable)(opts);
526
557
  } catch (err) {
527
- const who = owner(UNKNOWN, { method: method(), reason: err instanceof TableUnreadable ? err.message : `table: ${err.name}` });
558
+ const who = await proved('ownListener', [port], owner(UNKNOWN, { method: method(), reason: err instanceof TableUnreadable ? err.message : `table: ${err.name}` }), opts);
528
559
  if (!injected) listeners.set(port, [at, who]);
529
560
  return who;
530
561
  }
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);
562
+ const who = await proved('ownListener', [port], restricted(table.rows, table.how, opts)
563
+ ? await restrictedListener(table.rows, port, table.how, opts) : await oneOwner(pickListener(table.rows, port), table.how, opts), opts);
533
564
  if (!injected) listeners.set(port, [at, who]);
534
565
  return who;
535
566
  }
@@ -552,18 +583,23 @@ export function refusal(who, port, expectPid = null) {
552
583
  return null;
553
584
  }
554
585
 
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).
586
+ // macOS, when the socket table is restricted (FAA-267, DEC-0155 RT-3): whether process `pid` listens on exactly
587
+ // 127.0.0.1:<port> (never the IPv4 wildcard, which lsof prints as it prints an IPv6-only one, nor ::1; FAA-312).
557
588
  // `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 } = {}) {
589
+ // launchd job it installed) may vouch with it, and never a set-uid one for this account (its real and effective uid
590
+ // differ, DEC-0155). `owner`: the uid `pid` must run as, this account's by default, or null when the caller checks it
591
+ // itself (a LaunchDaemon's UserName, FAA-286); anything else is a RangeError.
592
+ export async function pidListens(pid, port, { run = runProgram, platform = process.platform, ownUid: mine, ownEuid, owner: want = THIS_ACCOUNT } = {}) {
593
+ if (want !== null && want !== THIS_ACCOUNT && !(Number.isInteger(want) && want >= 0)) throw new RangeError('owner is this-account, a uid or null');
560
594
  if (platform !== 'darwin' || !Number.isInteger(pid) || pid <= 1) return false;
561
595
  const real = mine ?? ownUid();
562
596
  const effective = ownEuid ?? (typeof process.geteuid === 'function' ? process.geteuid() : real);
563
- if (effective !== real) return false;
597
+ if (want === THIS_ACCOUNT && effective !== real) return false;
564
598
  try {
565
- const uid = parsePsUid(await run(PS, ['-o', 'uid=', '-p', String(pid)]));
566
- if (uid === null || uid !== real) return false;
599
+ if (want !== null) {
600
+ const uid = parsePsUid(await run(PS, ['-o', 'uid=', '-p', String(pid)]));
601
+ if (uid === null || uid !== (want === THIS_ACCOUNT ? real : want)) return false;
602
+ }
567
603
  const out = await run(LSOF, ['-nP', '-a', '-p', String(pid), '-i4TCP', '-sTCP:LISTEN', '-Fn']);
568
604
  return parseLsofListeners(out).some(([host, p]) => host === '127.0.0.1' && p === port);
569
605
  } catch {
@@ -591,8 +627,9 @@ export function checkedSocket(port, { host = '127.0.0.1', timeoutMs = 10000, exp
591
627
  // A reader that throws (an injected one; the readers themselves answer unknown) fails closed.
592
628
  who = owner(UNKNOWN, { method: method(), reason: `check: ${err?.name ?? 'Error'}` });
593
629
  }
630
+ // The vouch proves a listener on exactly 127.0.0.1, so only a connection that reached it (FAA-273 item 12).
594
631
  if (who.kind === UNKNOWN && String(who.reason ?? '').startsWith(TABLE_RESTRICTED) && vouchPid
595
- && await pidListens(vouchPid, port, ask)) {
632
+ && normHost(socket.remoteAddress) === '127.0.0.1' && await pidListens(vouchPid, port, ask)) {
596
633
  // The table hides other processes (FAA-267); the process this caller started holds the listener.
597
634
  who = owner(THIS_ACCOUNT, { uid: ask.ownUid ?? ownUid(), pid: vouchPid, method: 'lsof-pid' });
598
635
  }
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
1
2
  // Windows, where the kit needs it (DEC-0032): reading a file's ACL to refuse a token file others can read.
2
3
  // Part of the Node service kit (FAA-65, layer L1); the twin of fabric_platform.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
  // Runner and model profiles in the kit (Fabric Agent Contract DEC-0046, Fabric ADR-0147). A port of the contract's
2
3
  // `src/profile-rules.ts` at the pin — resolveModelProfile and the FAC-SEM-058…060 checks — held to the contract's own
3
4
  // answers by test/fixtures/contract/profile-vectors.json; plus the doctor's runner and model checks (runners.md C2) and
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
1
2
  // Protocol revisions (DEC-0067) and an event's machine-readable data (DEC-0070, the feed-data protocols).
2
3
  // Part of the Node service kit (FAA-65, layer L3); the twin of fabric_protocols.py. Node 20+, no dependency.
3
4
  import { credentialName, secretArgument, secretUrl, secretValue } from './fabric-secrets.mjs';
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
1
2
  // The contract's one credential detector (Fabric Agent Contract DEC-0044, `src/secret-shape.ts` at the pin): a name
2
3
  // that can only carry a credential, a value shaped like one, a launch argument that carries one, and a URL that does
3
4
  // (LC-21). A port held to the contract's own answers by test/fixtures/contract/profile-vectors.json. No dependencies.
@@ -124,12 +125,25 @@ const REPOSITORY_AUTHORITY = /^[A-Za-z][A-Za-z0-9+.-]*:\/\//;
124
125
  // boundary, so composed after a separator look-behind it would miss a key after `-`, `.` or `_`.
125
126
  const REPOSITORY_KEYS = 'sk-[A-Za-z0-9_-]{16,}|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,}';
126
127
  const KEY_AT_A_WORD = new RegExp(`(?:^|(?<=[._-]))(?:${REPOSITORY_KEYS})`);
127
- const JWT_AT_A_WORD = new RegExp(`(?:^|(?<=[._-]))${JWT_SHAPE.source}`);
128
+ /** Whether JWT_SHAPE matches at a word's start: the text's start, or after a `.`, `_` or `-` (FAA-321), read in one
129
+ * pass as jwtSpans: only a run's first `eyJ` at a word's start is tried (fabric_secrets.jwt_at_a_word). */
130
+ export function jwtAtAWord(text) {
131
+ for (let at = text.indexOf('eyJ'); at >= 0;) {
132
+ if (at > 0 && !'._-'.includes(text[at - 1])) { at = text.indexOf('eyJ', at + 1); continue; }
133
+ const one = runEnd(text, at + 3);
134
+ if (one - at - 3 >= 2 && text[one] === '.') {
135
+ const two = runEnd(text, one + 1);
136
+ if (two - one - 1 >= 2 && text[two] === '.') return true;
137
+ }
138
+ at = text.indexOf('eyJ', one);
139
+ }
140
+ return false;
141
+ }
128
142
  const SPECIAL_QUERY = /^(?:https?|wss?|ftp|file):/i; // whose query also encodes `'`, as the URL standard does
129
143
 
130
144
  /** Whether a path segment reads as a repository's name: lower case, or letters with no digit, with no provider key's
131
145
  * or JWT's shape at its start or after a `.`, `_` or `-` (0.62.0 readings 9 and 10), so a letters-only key is a key. */
132
- export const repositoryName = (piece) => REPOSITORY_NAME.test(piece) && !KEY_AT_A_WORD.test(piece) && !JWT_AT_A_WORD.test(piece);
146
+ export const repositoryName = (piece) => REPOSITORY_NAME.test(piece) && !KEY_AT_A_WORD.test(piece) && !jwtAtAWord(piece);
133
147
 
134
148
  /** Every `%XX` escape of an ASCII character decoded, the rest as written (fabric_secrets.ascii_decoded, readings 11). */
135
149
  export const asciiDecoded = (text) => text.replace(/%([0-7][0-9A-Fa-f])/g, (_, hex) => String.fromCharCode(parseInt(hex, 16)));
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
1
2
  // Reference kit for the fabric-service/0.1 local service extension, Node: the facade (the twin of fabric_service.py).
2
3
  // Since 0.40.0 (FAA-65) the kit is a set of modules and this file re-exports everything it exported at 0.39.1, so
3
4
  // `import * as k from './fabric-service.mjs'` keeps working. Install it with `fabric_kit.py install <dir> --lang node`,
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
1
2
  // Share grants into an organization and their registrations (contract DEC-0086), the Node kit; the twin of
2
3
  // fabric_share.py, answer for answer (test/fixtures/contract/share-vectors.json). Node built-ins only.
3
4
  // FAC-SEM-117 (a share grant's id, window and agentRef, with parent), FAC-SEM-118 (a share-registration/1 document) and
@@ -0,0 +1,181 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
2
+ // This OS account's own TCP sockets, read from its own processes (layer L0; DEC-0155 RT-2, FAA-292; FAA-272); the twin
3
+ // of fabric_socket_proof.py. Node 18+, no dependency; nothing here imports another kit module.
4
+ //
5
+ // macOS 27.2 shows an ad-hoc-signed runtime (Homebrew's Node) only its own sockets, so fabric-peer-owner reads such a
6
+ // table as table-restricted and cannot tell who accepted a connection. The question can still be asked of this
7
+ // account's own processes, which the restriction leaves readable: whether one of them holds the very socket that
8
+ // accepted the connection (RT-2, the accepted-socket proof), or a listener bound exactly to 127.0.0.1:<port> (HA-2's
9
+ // macOS row, FAA-272). Another account's process is not listed, so a proof only ever says "this account's" or "not
10
+ // proved"; never another account's, the system's or nobody's.
11
+ //
12
+ // await accepted(server, client, { wait }) the socket at `server` connected to `client` is in one of this
13
+ // account's processes (`wait`: read again for ACCEPT_WAIT_MS until the
14
+ // server has called accept())
15
+ // await ownListener(port) a listener of this account on exactly 127.0.0.1:<port>
16
+ //
17
+ // Node has no ctypes, so the twin reads RT-2's other named source, `lsof -nP -a -u <uid> -iTCP:<port> -sTCP:<state>
18
+ // -F pun`, matching the exact address pair and the numeric uid, where the Python twin reads the same records through
19
+ // libproc (lsof only when libproc is unavailable). The twins agree on answers, not on the program they run (DEC-0155
20
+ // Compatibility); test/fixtures/peer-owner/socket-proof-cases.json holds both. The proof is "a process of this uid
21
+ // holds the descriptor", weaker than a table's so_uid: a socket passed with SCM_RIGHTS to a process of this uid reads
22
+ // as this account's, which a caller that knows the server's pid closes with expectPid. One socket several of this
23
+ // account's processes hold is proved with pid null, reason several-processes. A process whose real and effective uids
24
+ // differ is refused (setuid-context). Reasons: table-restricted (read, nothing of this account matches) or
25
+ // `table-restricted: <detail>` (the proof could not be read). Bounded by TIMEOUT_MS and MAX_OUTPUT_BYTES.
26
+
27
+ import { execFile } from 'node:child_process';
28
+ import net from 'node:net';
29
+
30
+ export const TABLE_RESTRICTED = 'table-restricted';
31
+ export const LSOF_METHOD = 'lsof';
32
+ export const LSOF = '/usr/sbin/lsof';
33
+ export const TIMEOUT_MS = 2000;
34
+ export const ACCEPT_WAIT_MS = 500;
35
+ export const MAX_OUTPUT_BYTES = 4 * 1024 * 1024;
36
+
37
+ const proof = (proved, pid, method, reason) => Object.freeze({ proved, pid, method, reason });
38
+ const digits = (text) => /^[0-9]+$/.test(text);
39
+
40
+ // One spelling per address, as fabric-peer-owner's normHost (the same rule, kept here so this module stands alone): an
41
+ // IPv4-mapped IPv6 address is its IPv4 form, the unspecified address `*`, an IPv6 address RFC 5952's.
42
+ function host(text) {
43
+ let h = String(text ?? '').replace(/^\[(.*)\]$/, '$1');
44
+ if (h === '*' || h === '') return '*';
45
+ if (h === 'localhost') return '127.0.0.1';
46
+ h = h.split('%', 1)[0];
47
+ if (net.isIPv4(h)) return h === '0.0.0.0' ? '*' : h.split('.').map((x) => String(Number(x))).join('.');
48
+ if (!net.isIPv6(h)) return h;
49
+ const t = new URL(`http://[${h}]/`).hostname.slice(1, -1);
50
+ if (t === '::') return '*';
51
+ const mapped = /^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/.exec(t);
52
+ if (!mapped) return t;
53
+ const hi = parseInt(mapped[1], 16);
54
+ const lo = parseInt(mapped[2], 16);
55
+ const v4 = [hi >> 8, hi & 255, lo >> 8, lo & 255].join('.');
56
+ return v4 === '0.0.0.0' ? '*' : v4;
57
+ }
58
+
59
+ // Whether `new URL(...).hostname` names this computer's loopback: localhost or a name under .localhost, ::1, or an IPv4
60
+ // address in 127/8 (new URL has already read 127.1, 2130706433, 0x7f.1 and a trailing dot as 127.x.y.z); never an
61
+ // IPv4-mapped address, which new URL writes in hex. The one rule fabric-http's checkUrl and fabric-delegation's key set
62
+ // source read (FAA-187 (15)); the twin of fabric_socket_proof.loopback_host.
63
+ export function loopbackHost(hostname) {
64
+ const h = String(hostname ?? '').toLowerCase().replace(/^\[(.*)\]$/, '$1');
65
+ if (h === 'localhost' || h.endsWith('.localhost') || h === '::1') return true;
66
+ return /^127\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$/.test(h) && h.split('.').every((x) => Number(x) <= 255);
67
+ }
68
+
69
+ function lsofAddress(text) {
70
+ const at = text.lastIndexOf(':');
71
+ if (at < 0) return null;
72
+ const port = text.slice(at + 1);
73
+ if (!digits(port) || Number(port) > 65535) return null;
74
+ return [host(text.slice(0, at)), Number(port)];
75
+ }
76
+
77
+ // `lsof -nP -a -u <uid> -iTCP:<port> -sTCP:<state> -F pun`: the `n` lines of every process block whose `u` line is
78
+ // exactly `uid` (a block of another uid, or with no uid, is not read). A name with `->` is a connection, one without a
79
+ // listener. The same socket listed by several processes (one address pair) is one socket with pid null.
80
+ export function parseLsofSockets(text, uid) {
81
+ const found = new Map();
82
+ let pid = null;
83
+ let owner = null;
84
+ for (const line of String(text).split(/\r?\n/)) {
85
+ const tag = line.slice(0, 1);
86
+ const value = line.slice(1);
87
+ if (tag === 'p') {
88
+ pid = digits(value) ? Number(value) : null;
89
+ owner = null;
90
+ } else if (tag === 'u') {
91
+ owner = digits(value) ? Number(value) : null;
92
+ } else if (tag === 'n' && pid !== null && owner === uid) {
93
+ const arrow = value.indexOf('->');
94
+ const local = lsofAddress(arrow < 0 ? value : value.slice(0, arrow));
95
+ const remote = arrow < 0 ? ['*', 0] : lsofAddress(value.slice(arrow + 2));
96
+ if (!local || !remote) continue;
97
+ const key = JSON.stringify([local, remote, arrow < 0]);
98
+ if (!found.has(key)) found.set(key, { local, remote, listening: arrow < 0, pids: [] });
99
+ found.get(key).pids.push(pid);
100
+ }
101
+ }
102
+ return [...found.values()].map(({ local, remote, listening, pids }) => ({
103
+ local, remote, listening, pid: new Set(pids).size === 1 ? pids[0] : null,
104
+ }));
105
+ }
106
+
107
+ const same = (a, b) => a[0] === b[0] && a[1] === b[1];
108
+
109
+ function proved(hits, how) {
110
+ if (hits.length === 0) return proof(false, null, how, TABLE_RESTRICTED);
111
+ const pids = new Set(hits.map((s) => s.pid));
112
+ if (pids.size === 1 && !pids.has(null)) return proof(true, hits[0].pid, how, null);
113
+ return proof(true, null, how, 'several-processes');
114
+ }
115
+
116
+ // RT-2 over this account's sockets: the one at `server` connected to `client`; two with that pair are several-rows.
117
+ export function acceptedIn(sockets, server, client, how) {
118
+ const hits = sockets.filter((s) => !s.listening && same(s.local, server) && same(s.remote, client));
119
+ if (hits.length > 1) return proof(false, null, how, 'several-rows');
120
+ return proved(hits, how);
121
+ }
122
+
123
+ // FAA-272 over this account's sockets: a listener bound exactly to 127.0.0.1:<port> (DEC-0155 RT-4: our wildcard or
124
+ // ::1 listener, and root's, cannot be told from a hidden one).
125
+ export function listenerIn(sockets, port, how) {
126
+ return proved(sockets.filter((s) => s.listening && same(s.local, ['127.0.0.1', port])), how);
127
+ }
128
+
129
+ class ProofUnreadable extends Error {}
130
+
131
+ // One bounded run of lsof; its exit 1 with nothing printed is "no socket matched", not a failure.
132
+ export function runLsof(file, args, timeout = TIMEOUT_MS) {
133
+ return new Promise((resolve, reject) => {
134
+ execFile(file, args, { timeout, maxBuffer: MAX_OUTPUT_BYTES, encoding: 'utf8' }, (err, stdout) => {
135
+ if (!err) return resolve(stdout);
136
+ if (err.code === 1 && !String(stdout ?? '').trim()) return resolve('');
137
+ const why = typeof err.code === 'number' ? `exit ${err.code}` : (err.killed ? 'timeout' : (err.code ?? 'error'));
138
+ reject(new ProofUnreadable(`lsof: ${err.code === 'ERR_CHILD_PROCESS_STDIO_MAXBUFFER' ? `output over ${MAX_OUTPUT_BYTES} bytes` : why}`));
139
+ });
140
+ });
141
+ }
142
+
143
+ // The reader a question uses: lsof on macOS, none elsewhere.
144
+ export const method = (platform = process.platform) => (platform === 'darwin' ? LSOF_METHOD : 'none');
145
+
146
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
147
+
148
+ async function ask(port, state, pick, waitMs, { platform = process.platform, uid, euid, run = runLsof, now = () => Date.now() } = {}) {
149
+ const start = now();
150
+ if (platform !== 'darwin') return proof(false, null, 'none', `${TABLE_RESTRICTED}: unsupported-platform`);
151
+ const real = uid ?? process.getuid();
152
+ const effective = euid ?? (typeof process.geteuid === 'function' ? process.geteuid() : real);
153
+ if (real !== effective) return proof(false, null, LSOF_METHOD, `${TABLE_RESTRICTED}: setuid-context`);
154
+ let pause = 5;
155
+ for (;;) {
156
+ let have;
157
+ try {
158
+ have = parseLsofSockets(await run(LSOF, ['-nP', '-a', '-u', String(real), `-iTCP:${port}`, `-sTCP:${state}`, '-F', 'pun']), real);
159
+ } catch (err) {
160
+ return proof(false, null, LSOF_METHOD, `${TABLE_RESTRICTED}: ${err?.message ?? 'lsof: error'}`);
161
+ }
162
+ const answer = pick(have);
163
+ if (answer.proved || now() + pause > start + Math.min(waitMs, TIMEOUT_MS)) return answer;
164
+ await sleep(pause);
165
+ pause = Math.min(pause * 2, 50);
166
+ }
167
+ }
168
+
169
+ // RT-2: whether the socket at `server` connected to `client` ([host, port] each) is in one of this account's processes.
170
+ // `wait`: read again for ACCEPT_WAIT_MS, since the server's accepted socket exists only once it calls accept().
171
+ // `platform`, `uid`, `euid`, `run` and `now` are the tests'.
172
+ export function accepted(server, client, { wait = false, ...opts } = {}) {
173
+ const s = [host(server[0]), Number(server[1])];
174
+ const c = [host(client[0]), Number(client[1])];
175
+ return ask(s[1], 'ESTABLISHED', (have) => acceptedIn(have, s, c, LSOF_METHOD), wait ? ACCEPT_WAIT_MS : 0, opts);
176
+ }
177
+
178
+ // FAA-272: whether one of this account's processes listens on exactly 127.0.0.1:<port>.
179
+ export function ownListener(port, opts = {}) {
180
+ return ask(Number(port), 'LISTEN', (have) => listenerIn(have, Number(port), LSOF_METHOD), 0, opts);
181
+ }
@@ -1,3 +1,4 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
1
2
  // Supervisors (DEC-0032, LC-14): systemd user units, Task Scheduler tasks and launchd plists, installed and removed without touching what the operator turned off; release pruning (LC-11, LC-15).
2
3
  // Part of the Node service kit (FAA-65, layer L3); the twin of fabric_supervisors.py. Node 20+, no dependency.
3
4
  import fs from 'node:fs';
@@ -9,8 +10,9 @@ import { DRAIN_SECONDS, SUPERVISOR_ENV, ServiceError, TASK, UNIT } from './fabri
9
10
  import { atomicWrite, instanceDirs } from './fabric-paths.mjs';
10
11
  import { SUPERVISOR_ARG, markerArguments } from './fabric-lock.mjs';
11
12
  import { URI } from './fabric-backups.mjs';
12
- import { CheckedAgent, LSOF, ListenerRefused, PS, SERVER_NOT_OWN, TABLE_RESTRICTED, UNKNOWN, parseLsofListeners, parsePsUid,
13
+ import { CheckedAgent, ListenerRefused, PS, SERVER_NOT_OWN, TABLE_RESTRICTED, UNKNOWN, parsePsUid, pidListens,
13
14
  runProgram, serverEnd } from './fabric-peer-owner.mjs';
15
+ import { loopbackHost } from './fabric-socket-proof.mjs';
14
16
  import http from 'node:http';
15
17
  import net from 'node:net';
16
18
 
@@ -60,7 +62,7 @@ function noMarkerArgument(argv) {
60
62
  // An environment value: read path-aware (secretArgument) only when it reads as a path (it starts as one, has at least two
61
63
  // separators and a lower-case word between them), so a release folder under Application Support is no key (FAA-266) while
62
64
  // a base64 key whose `/` splits it stays one; any other value keeps the strict secretValue.
63
- const secretEnvValue = (value) => {
65
+ export const secretEnvValue = (value) => {
64
66
  const pieces = value.split(/[\\/]/);
65
67
  const path = /^(?:[/~.]|[A-Za-z]:[\\/])/.test(value) && pieces.length >= 3 && pieces.some((x) => /^[a-z][a-z0-9_-]{2,}$/.test(x));
66
68
  return path ? secretArgument(value) : secretValue(value);
@@ -560,20 +562,30 @@ export class ForeignUserAnswer {
560
562
  // The well-known answer at a loopback origin, read only on a connection whose server end is this OS account's process
561
563
  // (DEC-0154 HA-2): null when nothing answers or the answer is not JSON, a ForeignUserAnswer when the listener is another
562
564
  // account's or its owner cannot be told.
563
- // Every loopback spelling (127.0.0.1, another 127/8 address, localhost, [::1]) goes on a checked connection, never the
565
+ // Every loopback spelling (127/8 in any form, localhost or a name under it, [::1]) goes on a checked connection, never the
564
566
  // unchecked way (FAA-257): the host, without brackets, and the port, or null.
565
567
  function loopbackOrigin(origin) {
566
568
  let url;
567
569
  try { url = new URL(String(origin)); } catch { return null; }
568
570
  const host = url.hostname.replace(/^\[|\]$/g, '');
569
- const loop = host === 'localhost' || host === '::1' || (net.isIPv4(host) && host.startsWith('127.'));
570
- if (url.protocol !== 'http:' || !loop) return null;
571
+ // the kit's one loopback rule (FAA-326): a name under .localhost too, as the Python twin reads it
572
+ if (url.protocol !== 'http:' || !loopbackHost(host)) return null;
571
573
  // new URL drops a default port: http://127.0.0.1:80 has port '', read as 80 as Python's urlsplit reads it (0.79.0 seam)
572
574
  return { host, port: Number(url.port || 80) };
573
575
  }
574
576
 
577
+ // An IPv4-mapped 127/8 origin (`new URL` writes it ::ffff:7f00:1): loopback the kit's rule does not take, which fetch would
578
+ // reach unchecked, so it is refused unread, as the Python twin refuses it (0.84.0 seam reading).
579
+ function mappedLoopback(origin) {
580
+ let host;
581
+ try { host = new URL(String(origin)).hostname; } catch { return false; }
582
+ const m = /^\[::ffff:([0-9a-f]{1,4}):[0-9a-f]{1,4}\]$/i.exec(host);
583
+ return !!m && parseInt(m[1], 16) >> 8 === 127;
584
+ }
585
+
575
586
  export async function wellKnownOf(origin, { vouchPid = null, ask = undefined } = {}) {
576
587
  const m = loopbackOrigin(origin);
588
+ if (!m && mappedLoopback(origin)) return null;
577
589
  if (!m) {
578
590
  try {
579
591
  const response = await fetch(`${origin}/.well-known/fabric-service`, { signal: AbortSignal.timeout(2000) });
@@ -609,16 +621,9 @@ function loopbackWellKnown(port, agent, host = '127.0.0.1') {
609
621
  }
610
622
 
611
623
  // Whether launchd's job `pid` listens on exactly 127.0.0.1:<port>, never the wildcard, asked of the process itself (`lsof -p`),
612
- // which a restricted socket table does not hide. Only for a LaunchDaemon root bootstrapped, whose pid launchd named.
613
- async function jobListens(pid, port, run = runProgram, platform = process.platform) {
614
- if (platform !== 'darwin' || !Number.isInteger(pid) || pid <= 1) return false;
615
- try {
616
- const out = await run(LSOF, ['-nP', '-a', '-p', String(pid), '-i4TCP', '-sTCP:LISTEN', '-Fn']);
617
- return parseLsofListeners(out).some(([host, p]) => host === '127.0.0.1' && p === port); // exact bind, as RT-3
618
- } catch {
619
- return false;
620
- }
621
- }
624
+ // which a restricted socket table does not hide. Only for a LaunchDaemon root bootstrapped, whose pid launchd named. The
625
+ // peer owner's one lsof reader, the uid left to the caller, which checks the job's UserName itself (FAA-286 item 1).
626
+ const jobListens = (pid, port, run = runProgram, platform = process.platform) => pidListens(pid, port, { run, platform, owner: null });
622
627
 
623
628
  // The uid account `name` has on this machine (`id -u`), or null when it has none.
624
629
  function accountUid(name, run) {