@code-yeongyu/senpi 2026.10.24-2 → 2026.10.24-4

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 (178) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/bundle/chunks/{args-6T5EQVY3.js → args-JXULI4EH.js} +1 -1
  3. package/dist/bundle/chunks/{chunk-VUKNGMDT.js → chunk-3LNSLJ7N.js} +1 -1
  4. package/dist/bundle/chunks/chunk-3VOG6WFU.js +27 -0
  5. package/dist/bundle/chunks/{chunk-HHYSXDYP.js → chunk-42OY5SZZ.js} +2 -2
  6. package/dist/bundle/chunks/chunk-7IRLFYNM.js +23 -0
  7. package/dist/bundle/chunks/{chunk-FIBIVBQK.js → chunk-7UUOGZKR.js} +1 -1
  8. package/dist/bundle/chunks/chunk-BMYNMUVZ.js +7 -0
  9. package/dist/bundle/chunks/{chunk-JAXZOEJE.js → chunk-BWA4FVFW.js} +1 -1
  10. package/dist/bundle/chunks/{chunk-AGQSRWNN.js → chunk-EHQXLHRB.js} +1 -1
  11. package/dist/bundle/chunks/chunk-EWOEVVQT.js +4 -0
  12. package/dist/bundle/chunks/{chunk-YJA2B7FQ.js → chunk-FDL4CF6F.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-RQHK4QZ5.js → chunk-FWXMMMCV.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-OA5INWHI.js → chunk-GVJ76VQI.js} +102 -42
  15. package/dist/bundle/chunks/{chunk-HCF5M6NA.js → chunk-GXY5JEI5.js} +1 -1
  16. package/dist/bundle/chunks/{chunk-VZAYN5SB.js → chunk-HKUXKTFW.js} +1 -1
  17. package/dist/bundle/chunks/chunk-II22E3CV.js +3 -0
  18. package/dist/bundle/chunks/{chunk-ONLNXGO3.js → chunk-IUHU5PEC.js} +1 -1
  19. package/dist/bundle/chunks/{chunk-LR6Z6V54.js → chunk-NQC7MI2G.js} +1 -1
  20. package/dist/bundle/chunks/{chunk-IELM5U6I.js → chunk-PXKF2RHD.js} +2 -2
  21. package/dist/bundle/chunks/{chunk-BRXN5R2F.js → chunk-QH2HSCMX.js} +1 -1
  22. package/dist/bundle/chunks/{chunk-QQSCUEYJ.js → chunk-RHHBXIRN.js} +2 -5
  23. package/dist/bundle/chunks/chunk-RVYSJBZ7.js +7 -0
  24. package/dist/bundle/chunks/chunk-SGDOZT6B.js +8 -0
  25. package/dist/bundle/chunks/{chunk-BZ75DRZB.js → chunk-U67ML6CQ.js} +4 -2
  26. package/dist/bundle/chunks/{chunk-3N2PQEQV.js → chunk-UT6AJHPK.js} +1 -1
  27. package/dist/bundle/chunks/{chunk-RAIZG6HA.js → chunk-V3CK2AVC.js} +1 -1
  28. package/dist/bundle/chunks/{chunk-CZR7C6CY.js → chunk-VOBD6FQG.js} +1 -1
  29. package/dist/bundle/chunks/{chunk-HRPMLI7O.js → chunk-VRDU425E.js} +2 -2
  30. package/dist/bundle/chunks/{chunk-OUPD5LWY.js → chunk-XUFUKDKJ.js} +3 -3
  31. package/dist/bundle/chunks/{chunk-SGATKHO4.js → chunk-Y5OTHMXO.js} +1 -1
  32. package/dist/bundle/chunks/{chunk-QEQG42HL.js → chunk-ZGAB7WQA.js} +1 -1
  33. package/dist/bundle/chunks/cli-main-UH3MAREK.js +3 -0
  34. package/dist/bundle/chunks/{dispatch-KQEGIXQW.js → dispatch-DEP4PEDZ.js} +3 -2
  35. package/dist/bundle/chunks/{help-fast-path-6PVN7WI5.js → help-fast-path-ZHZGXAUG.js} +1 -1
  36. package/dist/bundle/chunks/host-command-OQR4RDTM.js +2 -0
  37. package/dist/bundle/chunks/host-lifecycle-U36U7JTE.js +3 -0
  38. package/dist/bundle/chunks/host-lifecycle.js +26 -21
  39. package/dist/bundle/chunks/host-session-command-IGB27ZPJ.js +20 -0
  40. package/dist/bundle/chunks/{host-socket-request-KKGH3JHV.js → host-socket-request-ZDJGSSZO.js} +1 -1
  41. package/dist/bundle/chunks/interactive-mode-R5RCSJ2F.js +2 -0
  42. package/dist/bundle/chunks/{list-tips-BO5RP34W.js → list-tips-CREX4IXZ.js} +1 -1
  43. package/dist/bundle/chunks/{multi-session-host-O5GZQSRL.js → multi-session-host-YZEMYUFY.js} +12 -12
  44. package/dist/bundle/chunks/{package-manager-cli-LC5GAGTW.js → package-manager-cli-5FOXDKUA.js} +1 -1
  45. package/dist/bundle/chunks/{role-4G43UDOC.js → role-M2MTVACJ.js} +1 -1
  46. package/dist/bundle/chunks/{rotation-stream-RXMCJJUL.js → rotation-stream-U7YQMC7H.js} +1 -1
  47. package/dist/bundle/chunks/rpc-mode-6K62CZBW.js +2 -0
  48. package/dist/bundle/chunks/rpc-monitor-source-OL5KU6NO.js +3 -0
  49. package/dist/bundle/chunks/{schedule-command-QNCVE4HM.js → schedule-command-XBW4EOG5.js} +1 -1
  50. package/dist/bundle/chunks/session-control-endpoint-54TMCS7Q.js +3 -0
  51. package/dist/bundle/chunks/{session-picker-HIC5ZTE6.js → session-picker-P2VUU2MQ.js} +1 -1
  52. package/dist/bundle/chunks/session-worker.js +437 -368
  53. package/dist/bundle/cli.js +2 -2
  54. package/dist/bundle/host-supervisor.js +26 -21
  55. package/dist/bundle/index.js +1 -1
  56. package/dist/bundle/rpc-entry.js +1 -1
  57. package/dist/bundle/runtime-manifest.json +1 -1
  58. package/dist/cli/args.d.ts +2 -0
  59. package/dist/cli/args.js +27 -0
  60. package/dist/cli/host-command-args.d.ts +33 -0
  61. package/dist/cli/host-command-args.js +145 -0
  62. package/dist/cli/host-command.d.ts +1 -30
  63. package/dist/cli/host-command.js +5 -137
  64. package/dist/cli/host-session-command.d.ts +1 -0
  65. package/dist/cli/host-session-command.js +5 -3
  66. package/dist/core/agent-session.js +1 -1
  67. package/dist/core/extensions/builtin/anthropic-subscription/index.js +4 -2
  68. package/dist/core/extensions/builtin/anthropic-subscription/options.js +2 -1
  69. package/dist/core/extensions/builtin/anthropic-subscription/sdk-boundary.js +9 -2
  70. package/dist/core/extensions/builtin/anthropic-subscription/session-cwd.d.ts +3 -0
  71. package/dist/core/extensions/builtin/anthropic-subscription/session-cwd.js +9 -0
  72. package/dist/core/extensions/builtin/anthropic-subscription/settings.d.ts +1 -1
  73. package/dist/core/extensions/builtin/anthropic-subscription/settings.js +3 -1
  74. package/dist/core/extensions/builtin/anthropic-subscription/stream.js +4 -1
  75. package/dist/core/extensions/builtin/btw/index.js +4 -1
  76. package/dist/core/extensions/builtin/compaction/lane-policy.js +3 -1
  77. package/dist/core/extensions/builtin/look-at/runner.js +1 -0
  78. package/dist/core/extensions/builtin/mcp/connection-types.d.ts +2 -0
  79. package/dist/core/extensions/builtin/mcp/connection.js +3 -0
  80. package/dist/core/extensions/builtin/mcp/elicitation.d.ts +5 -2
  81. package/dist/core/extensions/builtin/mcp/elicitation.js +57 -11
  82. package/dist/core/extensions/builtin/mcp/index.js +1 -1
  83. package/dist/core/extensions/builtin/mcp/modern/dispatch.js +6 -9
  84. package/dist/core/extensions/builtin/mcp/service-connection.d.ts +1 -0
  85. package/dist/core/extensions/builtin/mcp/service-connection.js +1 -0
  86. package/dist/core/extensions/builtin/mcp/service.js +1 -0
  87. package/dist/core/extensions/builtin/mcp/transport-sdk.d.ts +1 -0
  88. package/dist/core/extensions/builtin/mcp/transport-sdk.js +5 -5
  89. package/dist/core/extensions/builtin/mcp/transport.d.ts +1 -0
  90. package/dist/core/extensions/builtin/mcp/transport.js +1 -0
  91. package/dist/core/extensions/builtin/prompt-preset/contract.js +6 -1
  92. package/dist/core/extensions/builtin/prompt-preset/kimi-k3-1.d.ts +3 -0
  93. package/dist/core/extensions/builtin/prompt-preset/kimi-k3-1.js +93 -0
  94. package/dist/core/extensions/builtin/prompt-preset/matchers.d.ts +2 -0
  95. package/dist/core/extensions/builtin/prompt-preset/matchers.js +9 -0
  96. package/dist/core/extensions/builtin/prompt-preset/presets.js +3 -0
  97. package/dist/core/extensions/builtin/prompt-preset/settings.d.ts +1 -1
  98. package/dist/core/extensions/builtin/prompt-preset/settings.js +1 -0
  99. package/dist/core/extensions/builtin/terminal/rpc-monitor-observer.d.ts +2 -1
  100. package/dist/core/extensions/builtin/terminal/rpc-monitor-observer.js +12 -4
  101. package/dist/core/external-admission.d.ts +4 -1
  102. package/dist/core/external-admission.js +18 -2
  103. package/dist/core/lifecycle-session-binding.js +20 -0
  104. package/dist/core/sdk.js +1 -0
  105. package/dist/core/session-spawned-by.d.ts +4 -0
  106. package/dist/core/session-spawned-by.js +16 -0
  107. package/dist/main.js +7 -2
  108. package/dist/modes/rpc/host-admission-proxy.d.ts +2 -1
  109. package/dist/modes/rpc/host-admission-proxy.js +10 -9
  110. package/dist/modes/rpc/host-cli-entry.d.ts +2 -0
  111. package/dist/modes/rpc/host-cli-entry.js +13 -1
  112. package/dist/modes/rpc/host-ensure-client.d.ts +5 -1
  113. package/dist/modes/rpc/host-ensure-client.js +8 -0
  114. package/dist/modes/rpc/host-ensure-types.d.ts +1 -0
  115. package/dist/modes/rpc/host-ensure.js +6 -11
  116. package/dist/modes/rpc/host-gc-pass.js +3 -0
  117. package/dist/modes/rpc/host-gc.js +3 -0
  118. package/dist/modes/rpc/host-internal-transport.d.ts +16 -5
  119. package/dist/modes/rpc/host-internal-transport.js +40 -9
  120. package/dist/modes/rpc/host-lifecycle-proxy.d.ts +4 -8
  121. package/dist/modes/rpc/host-lifecycle-proxy.js +9 -21
  122. package/dist/modes/rpc/host-lifecycle-shutdown.d.ts +2 -0
  123. package/dist/modes/rpc/host-lifecycle-shutdown.js +3 -0
  124. package/dist/modes/rpc/host-lifecycle.js +9 -16
  125. package/dist/modes/rpc/host-observe-request.js +1 -1
  126. package/dist/modes/rpc/host-probe.d.ts +8 -6
  127. package/dist/modes/rpc/host-probe.js +13 -13
  128. package/dist/modes/rpc/host-request-key-root.d.ts +8 -0
  129. package/dist/modes/rpc/host-request-key-root.js +14 -0
  130. package/dist/modes/rpc/host-runner.d.ts +3 -1
  131. package/dist/modes/rpc/host-runner.js +6 -1
  132. package/dist/modes/rpc/host-socket-bind.d.ts +6 -0
  133. package/dist/modes/rpc/host-socket-bind.js +42 -0
  134. package/dist/modes/rpc/host-socket-request.d.ts +3 -2
  135. package/dist/modes/rpc/host-socket-request.js +55 -10
  136. package/dist/modes/rpc/host-status.d.ts +3 -2
  137. package/dist/modes/rpc/host-status.js +5 -0
  138. package/dist/modes/rpc/host-supervisor-log.d.ts +1 -0
  139. package/dist/modes/rpc/host-supervisor-log.js +3 -0
  140. package/dist/modes/rpc/host-watchdog.d.ts +8 -1
  141. package/dist/modes/rpc/host-watchdog.js +37 -12
  142. package/dist/modes/rpc/internal-channel.js +14 -14
  143. package/dist/modes/rpc/multi-session-host.js +18 -46
  144. package/dist/modes/rpc/rpc-auth-client.d.ts +13 -0
  145. package/dist/modes/rpc/rpc-auth-client.js +142 -0
  146. package/dist/modes/rpc/rpc-auth-proof.d.ts +12 -0
  147. package/dist/modes/rpc/rpc-auth-proof.js +27 -0
  148. package/dist/modes/rpc/rpc-client-socket.d.ts +5 -0
  149. package/dist/modes/rpc/rpc-client-socket.js +30 -0
  150. package/dist/modes/rpc/rpc-client.d.ts +2 -0
  151. package/dist/modes/rpc/rpc-client.js +2 -24
  152. package/dist/modes/rpc/rpc-host-command-types.d.ts +4 -0
  153. package/dist/modes/rpc/rpc-protocol-info-types.d.ts +6 -0
  154. package/dist/modes/rpc/rpc-public-auth.d.ts +41 -0
  155. package/dist/modes/rpc/rpc-public-auth.js +237 -0
  156. package/dist/modes/rpc/rpc-public-key.d.ts +28 -0
  157. package/dist/modes/rpc/rpc-public-key.js +193 -0
  158. package/dist/modes/rpc/session-router-open-profile.js +2 -10
  159. package/dist/modes/rpc/socket-ownership.d.ts +7 -0
  160. package/dist/modes/rpc/socket-ownership.js +17 -0
  161. package/docs/rpc.md +108 -2
  162. package/docs/settings.md +1 -1
  163. package/docs/usage.md +14 -0
  164. package/package.json +6 -6
  165. package/dist/bundle/chunks/chunk-3474DPZK.js +0 -2
  166. package/dist/bundle/chunks/chunk-64CFJHKT.js +0 -7
  167. package/dist/bundle/chunks/chunk-B6NJHBLH.js +0 -21
  168. package/dist/bundle/chunks/chunk-HZTQHE5O.js +0 -26
  169. package/dist/bundle/chunks/chunk-IBDADH5Z.js +0 -4
  170. package/dist/bundle/chunks/chunk-OJZD7CO7.js +0 -8
  171. package/dist/bundle/chunks/cli-main-4JNPG6LB.js +0 -3
  172. package/dist/bundle/chunks/host-command-I3ZMWQQ3.js +0 -2
  173. package/dist/bundle/chunks/host-lifecycle-QEO422VW.js +0 -3
  174. package/dist/bundle/chunks/host-session-command-INULKX6E.js +0 -20
  175. package/dist/bundle/chunks/interactive-mode-SYWJVPKK.js +0 -2
  176. package/dist/bundle/chunks/rpc-mode-JMCYSP4P.js +0 -2
  177. package/dist/bundle/chunks/rpc-monitor-source-34AQDTZ6.js +0 -3
  178. package/dist/bundle/chunks/session-control-endpoint-IW5X56WN.js +0 -3
@@ -16,7 +16,7 @@
16
16
  import { rmSync } from "node:fs";
17
17
  import { rm } from "node:fs/promises";
18
18
  import { Socket } from "node:net";
19
- import { envValue } from "../../core/brand.js";
19
+ import { brandEnvNames, envValue } from "../../core/brand.js";
20
20
  import { recordHostCrash } from "./host-crash-record.js";
21
21
  import { HOST_DAEMON_DIR_ENV } from "./host-daemon-paths.js";
22
22
  import { HOST_INSTANCE_ID_ENV } from "./host-identity-env.js";
@@ -71,24 +71,49 @@ export function readHostWatchdogConfig(env = process.env) {
71
71
  };
72
72
  }
73
73
  /** Same configuration, resolved through the brand-aware env prefixes. */
74
- export function readHostWatchdogConfigFromBrandEnv() {
74
+ export function readHostWatchdogConfigFromBrandEnv(env = process.env) {
75
75
  // The lifecycle supervisor intentionally uses the canonical SENPI_RPC_HOST_*
76
76
  // names when spawning a child. envValue() resolves branded aliases for normal
77
77
  // launches, but must not hide the canonical variables from the supervisor
78
78
  // handoff or the lifetime binding is silently disabled.
79
79
  return readHostWatchdogConfig({
80
- [HOST_WATCH_FD_ENV]: process.env[HOST_WATCH_FD_ENV] ?? envValue("RPC_HOST_WATCH_FD"),
81
- [HOST_WATCH_PPID_ENV]: process.env[HOST_WATCH_PPID_ENV] ?? envValue("RPC_HOST_WATCH_PPID"),
82
- [HOST_WATCH_PSTART_ENV]: process.env[HOST_WATCH_PSTART_ENV] ?? envValue("RPC_HOST_WATCH_PSTART"),
83
- [HOST_WATCH_PSTART_TICKS_ENV]: process.env[HOST_WATCH_PSTART_TICKS_ENV] ?? envValue("RPC_HOST_WATCH_PSTART_TICKS"),
84
- [HOST_WATCH_LIFETIME_FIFO_ENV]: process.env[HOST_WATCH_LIFETIME_FIFO_ENV] ?? envValue("RPC_HOST_LIFETIME_FIFO"),
85
- [HOST_SCRATCH_DIR_ENV]: process.env[HOST_SCRATCH_DIR_ENV] ?? envValue("RPC_HOST_SCRATCH_DIR"),
86
- [HOST_PUBLIC_SOCKET_ENV]: process.env[HOST_PUBLIC_SOCKET_ENV] ?? envValue("RPC_HOST_PUBLIC_SOCKET"),
87
- [HOST_CLEANUP_PATHS_ENV]: process.env[HOST_CLEANUP_PATHS_ENV] ?? envValue("RPC_HOST_CLEANUP_PATHS"),
88
- [HOST_DAEMON_DIR_ENV]: process.env[HOST_DAEMON_DIR_ENV],
89
- [HOST_INSTANCE_ID_ENV]: process.env[HOST_INSTANCE_ID_ENV],
80
+ [HOST_WATCH_FD_ENV]: env[HOST_WATCH_FD_ENV] ?? envValue("RPC_HOST_WATCH_FD", env),
81
+ [HOST_WATCH_PPID_ENV]: env[HOST_WATCH_PPID_ENV] ?? envValue("RPC_HOST_WATCH_PPID", env),
82
+ [HOST_WATCH_PSTART_ENV]: env[HOST_WATCH_PSTART_ENV] ?? envValue("RPC_HOST_WATCH_PSTART", env),
83
+ [HOST_WATCH_PSTART_TICKS_ENV]: env[HOST_WATCH_PSTART_TICKS_ENV] ?? envValue("RPC_HOST_WATCH_PSTART_TICKS", env),
84
+ [HOST_WATCH_LIFETIME_FIFO_ENV]: env[HOST_WATCH_LIFETIME_FIFO_ENV] ?? envValue("RPC_HOST_LIFETIME_FIFO", env),
85
+ [HOST_SCRATCH_DIR_ENV]: env[HOST_SCRATCH_DIR_ENV] ?? envValue("RPC_HOST_SCRATCH_DIR", env),
86
+ [HOST_PUBLIC_SOCKET_ENV]: env[HOST_PUBLIC_SOCKET_ENV] ?? envValue("RPC_HOST_PUBLIC_SOCKET", env),
87
+ [HOST_CLEANUP_PATHS_ENV]: env[HOST_CLEANUP_PATHS_ENV] ?? envValue("RPC_HOST_CLEANUP_PATHS", env),
88
+ [HOST_DAEMON_DIR_ENV]: env[HOST_DAEMON_DIR_ENV],
89
+ [HOST_INSTANCE_ID_ENV]: env[HOST_INSTANCE_ID_ENV],
90
90
  });
91
91
  }
92
+ const WATCH_ENV_SUFFIXES = [
93
+ "RPC_HOST_WATCH_FD",
94
+ "RPC_HOST_WATCH_PPID",
95
+ "RPC_HOST_WATCH_PSTART",
96
+ "RPC_HOST_WATCH_PSTART_TICKS",
97
+ "RPC_HOST_LIFETIME_FIFO",
98
+ "RPC_HOST_SCRATCH_DIR",
99
+ "RPC_HOST_PUBLIC_SOCKET",
100
+ "RPC_HOST_CLEANUP_PATHS",
101
+ ];
102
+ /**
103
+ * Reads the supervisor binding once and removes it from `env` (senpi#3815 review L4), so no tool, shell or nested host
104
+ * this host starts inherits a binding to a supervisor that is not its own. The generation identity (instance id,
105
+ * daemon dir) stays: sessions use it to name the host they run in.
106
+ */
107
+ export function consumeHostWatchdogEnv(env = process.env) {
108
+ const config = readHostWatchdogConfigFromBrandEnv(env);
109
+ scrubHostWatchdogEnv(env);
110
+ return config;
111
+ }
112
+ export function scrubHostWatchdogEnv(env = process.env) {
113
+ for (const suffix of WATCH_ENV_SUFFIXES)
114
+ for (const name of [`SENPI_${suffix}`, ...brandEnvNames(suffix)])
115
+ delete env[name];
116
+ }
92
117
  /**
93
118
  * Arms the configured watchdog. `onSupervisorGone` receives the reason and is
94
119
  * expected to perform the host's normal clean shutdown; the scratch directory
@@ -1,14 +1,14 @@
1
- import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
1
+ import { randomBytes } from "node:crypto";
2
2
  import { createInternalRecordStream } from "./internal-record-stream.js";
3
3
  import { MAX_RPC_LINE_CHARACTERS } from "./jsonl.js";
4
- const HEX = /^[0-9a-f]{64}$/;
4
+ import { RPC_AUTH_HEX, rpcAuthProof, rpcProofMatches } from "./rpc-auth-proof.js";
5
+ const INTERNAL_LAUNCH_ID = "internal";
6
+ /** One HMAC and constant-time comparison for both endpoints (wire contract v1.1); the hop's launch id is "internal". */
5
7
  function proof(key, label, first, second) {
6
- return createHmac("sha256", key).update(`${label}\ninternal\n${first}\n${second}`).digest("hex");
8
+ return rpcAuthProof(key, label, INTERNAL_LAUNCH_ID, first, second);
7
9
  }
8
10
  function matches(actual, expected) {
9
- return (typeof actual === "string" &&
10
- HEX.test(actual) &&
11
- timingSafeEqual(Buffer.from(actual, "hex"), Buffer.from(expected, "hex")));
11
+ return rpcProofMatches(expected, actual);
12
12
  }
13
13
  function object(value) {
14
14
  if (!isObject(value))
@@ -19,7 +19,7 @@ function isObject(value) {
19
19
  return value !== null && typeof value === "object" && !Array.isArray(value);
20
20
  }
21
21
  function nonce(value) {
22
- if (typeof value !== "string" || !HEX.test(value))
22
+ if (typeof value !== "string" || !RPC_AUTH_HEX.test(value))
23
23
  throw new Error("auth_failed");
24
24
  return value;
25
25
  }
@@ -117,15 +117,15 @@ export function connectInternalConnection(socket, key, options = {}) {
117
117
  const serverNonce = nonce(auth.server_nonce);
118
118
  if (response.id !== "internal-protocol" ||
119
119
  response.success !== true ||
120
- auth.launch_id !== "internal" ||
121
- !matches(auth.server_proof, proof(key, "senpi-rpc-server", clientNonce, serverNonce)))
120
+ auth.launch_id !== INTERNAL_LAUNCH_ID ||
121
+ !matches(auth.server_proof, proof(key, "server", clientNonce, serverNonce)))
122
122
  throw new Error("host_identity_unproven");
123
123
  const [result] = await Promise.all([
124
124
  read(),
125
125
  send({
126
126
  id: "internal-auth",
127
127
  type: "authenticate",
128
- client_proof: proof(key, "senpi-rpc-client", serverNonce, clientNonce),
128
+ client_proof: proof(key, "client", serverNonce, clientNonce),
129
129
  }),
130
130
  ]);
131
131
  if (result.id !== "internal-auth" || result.command !== "authenticate" || result.success !== true)
@@ -150,7 +150,7 @@ export function acceptInternalConnection(socket, key, protocol = {}) {
150
150
  };
151
151
  const first = await read();
152
152
  const offered = isObject(first.auth) ? first.auth.client_nonce : undefined;
153
- if (first.type !== "get_protocol_info" || typeof offered !== "string" || !HEX.test(offered))
153
+ if (first.type !== "get_protocol_info" || typeof offered !== "string" || !RPC_AUTH_HEX.test(offered))
154
154
  return refuse(first);
155
155
  const clientNonce = offered;
156
156
  const serverNonce = randomBytes(32).toString("hex");
@@ -165,16 +165,16 @@ export function acceptInternalConnection(socket, key, protocol = {}) {
165
165
  ...protocol,
166
166
  capabilities: [...(Array.isArray(protocol.capabilities) ? protocol.capabilities : []), "socket_auth"],
167
167
  auth: {
168
- launch_id: "internal",
168
+ launch_id: INTERNAL_LAUNCH_ID,
169
169
  server_nonce: serverNonce,
170
- server_proof: proof(key, "senpi-rpc-server", clientNonce, serverNonce),
170
+ server_proof: proof(key, "server", clientNonce, serverNonce),
171
171
  },
172
172
  },
173
173
  }),
174
174
  ]);
175
175
  if (frame.type !== "authenticate")
176
176
  return refuse(frame);
177
- const valid = matches(frame.client_proof, proof(key, "senpi-rpc-client", serverNonce, clientNonce));
177
+ const valid = matches(frame.client_proof, proof(key, "client", serverNonce, clientNonce));
178
178
  await send({
179
179
  id: frame.id,
180
180
  type: "response",
@@ -1,4 +1,3 @@
1
- import { chmod } from "node:fs/promises";
2
1
  import { createServer } from "node:net";
3
2
  import { basename, dirname, join } from "node:path";
4
3
  import { envValue } from "../../core/brand.js";
@@ -16,14 +15,14 @@ import { reopenHandoverParkedSessions } from "./host-handover-reopen.js";
16
15
  import { performIdleHandover } from "./host-handover-wire.js";
17
16
  import { EXPECTED_RUNTIME_BUILD_ID_ENV } from "./host-idle-handover.js";
18
17
  import { RPC_CLOSE_GRACE_MS_ENV, resolveHostIdlePolicy } from "./host-idle-policy.js";
19
- import { internalHostTransport } from "./host-internal-transport.js";
18
+ import { hostWatchdogBinding, internalHostTransport } from "./host-internal-transport.js";
20
19
  import { parseIdleExitMs } from "./host-lifecycle.js";
21
20
  import { prepareSocketPath } from "./host-lifecycle-proxy.js";
22
21
  import { createZeroSessionTrim, startHostObservers } from "./host-observers.js";
23
22
  import { runAsHostGenerationProcess } from "./host-process-role.js";
24
23
  import { createEndpointReservations } from "./host-reservations.js";
25
24
  import { hostChildDrainRequestFile, watchDrainRequest } from "./host-stop-intent.js";
26
- import { armHostWatchdog, readHostWatchdogConfigFromBrandEnv } from "./host-watchdog.js";
25
+ import { armHostWatchdog } from "./host-watchdog.js";
27
26
  import { attachJsonlLineReader, MAX_RPC_LINE_CHARACTERS } from "./jsonl.js";
28
27
  import { hostGeneration, hostInstanceId, protocolIdentity } from "./protocol-identity.js";
29
28
  import { rpcCommandShapeError } from "./rpc-input-validation.js";
@@ -32,9 +31,10 @@ import { SessionCommandRouter } from "./session-command-router.js";
32
31
  import { SessionEventWriter } from "./session-event-writer.js";
33
32
  import { canonicalSessionPath } from "./session-path-key.js";
34
33
  import { RpcSessionRegistry, RpcSessionRegistryError } from "./session-registry.js";
35
- import { PUBLIC_SOCKET_IDENTITY_FILE, readSocketIdentityFile, shieldSocketDuringClose, socketEntryReplaced, statSocketIdentity, unlinkOwnedSocket, waitForSocketIdentityFile, } from "./socket-ownership.js";
34
+ import { PUBLIC_SOCKET_IDENTITY_FILE, readSocketIdentityFile, shieldSocketDuringClose, socketEntryReplaced, unlinkOwnedSocket, waitForSocketIdentityFile, } from "./socket-ownership.js";
35
+ import { formatSocketAddress, listen, resolveSocketPath } from "./host-socket-bind.js";
36
36
  import { socketSink } from "./socket-sink.js";
37
- import { ensureSocketSecret, resolveSocketTransportAddress, SOCKET_SECRET_FILE_ENV, socketSecretPath, } from "./socket-transport.js";
37
+ import { ensureSocketSecret, SOCKET_SECRET_FILE_ENV, socketSecretPath } from "./socket-transport.js";
38
38
  import { WorkerSessionRegistry } from "./worker-session-registry.js";
39
39
  export { DEFAULT_HOST_EMPTY_EXIT_MS, DEFAULT_SESSION_IDLE_EVICTION_MS, RPC_HOST_EMPTY_EXIT_MS_ENV, RPC_SESSION_IDLE_EVICTION_MS_ENV, resolveHostIdlePolicy, } from "./host-idle-policy.js";
40
40
  /** Win32 named-pipe close can leave libuv's server callback pending after handles are destroyed. */
@@ -239,7 +239,7 @@ async function runSocketHost(options, socketPath) {
239
239
  ...(canHandOver && {
240
240
  endpoint: {
241
241
  agentDir: options.agentDir,
242
- socket: readHostWatchdogConfigFromBrandEnv()?.publicSocket ?? socketPath,
242
+ socket: hostWatchdogBinding()?.publicSocket ?? socketPath,
243
243
  },
244
244
  handover: {
245
245
  identity: () => ({
@@ -249,7 +249,7 @@ async function runSocketHost(options, socketPath) {
249
249
  }),
250
250
  perform: (request) => performIdleHandover({
251
251
  request,
252
- socket: readHostWatchdogConfigFromBrandEnv()?.publicSocket ?? socketPath,
252
+ socket: hostWatchdogBinding()?.publicSocket ?? socketPath,
253
253
  agentDir: options.agentDir,
254
254
  }),
255
255
  log: hostLog,
@@ -273,7 +273,12 @@ async function runSocketHost(options, socketPath) {
273
273
  const secret = process.platform === "win32"
274
274
  ? await ensureSocketSecret(process.env[SOCKET_SECRET_FILE_ENV] ?? socketSecretPath(socketPath))
275
275
  : undefined;
276
- const { watchdogConfig, admit } = internalHostTransport(readHostWatchdogConfigFromBrandEnv(), secret);
276
+ const transport = internalHostTransport(hostWatchdogBinding(), secret, {
277
+ agentDir: options.agentDir,
278
+ socket: socketPath,
279
+ log: hostLog,
280
+ });
281
+ const { watchdogConfig, admit } = transport;
277
282
  // This host's OWN copy of the crash-path cleanup list. A drain empties it: once a successor
278
283
  // generation is registered, the daemon state files under those paths describe the successor,
279
284
  // and a crash of this (already replaced) host must not take them with it.
@@ -393,6 +398,7 @@ async function runSocketHost(options, socketPath) {
393
398
  await watchdogCleanup;
394
399
  }
395
400
  finally {
401
+ await transport.dispose();
396
402
  // Explicitly terminate after every shutdown trigger. Windows named-pipe
397
403
  // handles are not fully controllable from JS, and an unresolved cleanup
398
404
  // must not leave this daemon or its public endpoint alive.
@@ -464,6 +470,8 @@ async function runSocketHost(options, socketPath) {
464
470
  setImmediate(killTrackedDetachedChildren);
465
471
  });
466
472
  boundIdentity = await listen(server, socketPath, secret);
473
+ // A key record that cannot be published safely ends this host: held connections are refused, never served keyless.
474
+ await transport.publish(() => shutdown(1));
467
475
  if (supervisorPublicOwnerFile) {
468
476
  // The supervisor publishes its public-socket token after this internal
469
477
  // listener is ready, so a short bounded wait keeps the lifecycles in step
@@ -513,7 +521,7 @@ function errorMessage(cause) {
513
521
  * (abstract sockets, a supervised win32 host, whose supervisor publishes none).
514
522
  */
515
523
  function hostSessionContext(socketPath) {
516
- const supervised = readHostWatchdogConfigFromBrandEnv();
524
+ const supervised = hostWatchdogBinding();
517
525
  const endpoint = supervised === undefined ? socketPath : supervised.publicSocket;
518
526
  if (endpoint === undefined || endpoint.startsWith("\0"))
519
527
  return { host_instance: hostInstanceId() };
@@ -532,47 +540,11 @@ function listenSocketPath(options) {
532
540
  * socket or every generation hashes a different directory (senpi#3282). Bare hosts bind the path.
533
541
  */
534
542
  function publicEndpointSocket(options) {
535
- const supervised = readHostWatchdogConfigFromBrandEnv();
543
+ const supervised = hostWatchdogBinding();
536
544
  if (supervised !== undefined)
537
545
  return supervised.publicSocket;
538
546
  return listenSocketPath(options);
539
547
  }
540
- function resolveSocketPath(value, agentDir) {
541
- if (value === "unix://")
542
- return join(agentDir, "rpc", "rpc.sock");
543
- if (value.startsWith("unix://")) {
544
- const path = value.slice("unix://".length);
545
- if (path.length === 0)
546
- return join(agentDir, "rpc", "rpc.sock");
547
- if (path.startsWith("@") && process.platform === "linux")
548
- return `\0${path.slice(1)}`;
549
- return path;
550
- }
551
- return value;
552
- }
553
- function formatSocketAddress(socketPath) {
554
- return socketPath.startsWith("\0") ? `unix://@${socketPath.slice(1)}` : `unix://${socketPath}`;
555
- }
556
- function listen(server, socketPath, secret) {
557
- return new Promise((resolve, reject) => {
558
- server.once("error", reject);
559
- server.listen(resolveSocketTransportAddress(socketPath, process.platform, secret), async () => {
560
- server.off("error", reject);
561
- try {
562
- if (process.platform !== "win32" && !socketPath.startsWith("\0")) {
563
- await chmod(socketPath, 0o600);
564
- // Record which filesystem entry THIS listener created; shutdown
565
- // removes the path only while this identity still matches.
566
- return resolve(await statSocketIdentity(socketPath));
567
- }
568
- resolve(undefined);
569
- }
570
- catch (cause) {
571
- reject(cause);
572
- }
573
- });
574
- });
575
- }
576
548
  function closeServer(server) {
577
549
  return new Promise((resolve, reject) => {
578
550
  server.close((cause) => (cause ? reject(cause) : resolve()));
@@ -0,0 +1,13 @@
1
+ import { type Socket } from "node:net";
2
+ export interface RpcKeyOptions {
3
+ /** Credential lookup only; never selects the session's configuration profile. */
4
+ readonly keyRoot?: string;
5
+ }
6
+ export declare function withRpcKeyRoot<T>(root: string, operation: () => T): T;
7
+ export declare function rpcKeyRoot(options?: RpcKeyOptions, defaultRoot?: string): string;
8
+ /** A fresh connection per attempt, including the single refresh after a host's auth_failed. */
9
+ export declare function connectAuthenticatedRpcSocket(path: string, first?: Readonly<Record<string, unknown>>, options?: RpcKeyOptions, timeoutMs?: number): Promise<{
10
+ readonly socket: Socket;
11
+ readonly protocol: Readonly<Record<string, unknown>>;
12
+ }>;
13
+ //# sourceMappingURL=rpc-auth-client.d.ts.map
@@ -0,0 +1,142 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { randomBytes } from "node:crypto";
3
+ import { createConnection } from "node:net";
4
+ import { getAgentDir } from "../../config.js";
5
+ import { ORIGIN_HOST_INSTANCE_FIELD, originHostInstance } from "./host-observe-request.js";
6
+ import { attachJsonlLineReader, serializeJsonLine } from "./jsonl.js";
7
+ import { authRecord, RPC_AUTH_HEX, RPC_LAUNCH_ID, RpcEndpointAuthError, rpcAuthProof, rpcProofMatches, } from "./rpc-auth-proof.js";
8
+ import { readRpcKeyRecord } from "./rpc-public-key.js";
9
+ import { SOCKET_HANDSHAKE_TIMEOUT_MS } from "./socket-transport.js";
10
+ const keyRoots = new AsyncLocalStorage();
11
+ export function withRpcKeyRoot(root, operation) {
12
+ return keyRoots.run(root, operation);
13
+ }
14
+ export function rpcKeyRoot(options = {}, defaultRoot) {
15
+ return options.keyRoot ?? keyRoots.getStore() ?? process.env.SENPI_RPC_KEY_ROOT ?? defaultRoot ?? getAgentDir();
16
+ }
17
+ /** A fresh connection per attempt, including the single refresh after a host's auth_failed. */
18
+ export async function connectAuthenticatedRpcSocket(path, first = {
19
+ type: "get_protocol_info",
20
+ id: "rpc-auth-protocol",
21
+ observe: true,
22
+ [ORIGIN_HOST_INSTANCE_FIELD]: originHostInstance(),
23
+ }, options = {}, timeoutMs = SOCKET_HANDSHAKE_TIMEOUT_MS) {
24
+ const deadline = Date.now() + timeoutMs;
25
+ for (let attempt = 0;; attempt++) {
26
+ const socket = createConnection(path);
27
+ // A failed attempt is destroyed below; an error its queued write reports later (Bun does, for a missing
28
+ // socket) must not surface as an uncaught event. The listener goes once the socket is handed over.
29
+ let earlyError;
30
+ const ignoreLateError = (error) => {
31
+ earlyError ??= error;
32
+ };
33
+ socket.on("error", ignoreLateError);
34
+ let connected = false;
35
+ socket.once("connect", () => {
36
+ connected = true;
37
+ });
38
+ // Install error/close handlers before any write, including connect failures.
39
+ try {
40
+ const protocol = await authenticate(socket, path, first, rpcKeyRoot(options), Math.max(1, deadline - Date.now()));
41
+ socket.off("error", ignoreLateError);
42
+ return { socket, protocol };
43
+ }
44
+ catch (cause) {
45
+ socket.destroy();
46
+ if (attempt === 0 && cause instanceof RpcEndpointAuthError && cause.code === "auth_failed")
47
+ continue;
48
+ // The socket's own failure (ENOENT, ECONNREFUSED) is how callers tell an unreachable endpoint apart.
49
+ const failure = !(cause instanceof RpcEndpointAuthError) && earlyError !== undefined ? earlyError : cause;
50
+ if (failure instanceof Error)
51
+ Object.assign(failure, { rpcSocketConnected: connected });
52
+ throw failure;
53
+ }
54
+ }
55
+ }
56
+ async function authenticate(socket, path, first, root, timeoutMs) {
57
+ let pending;
58
+ const reject = (error) => pending?.reject(error);
59
+ // The socket's own error (ENOENT, ECONNREFUSED) is what callers classify an unreachable endpoint by; a close
60
+ // that a runtime reports before it is answered a turn later, so the error wins when there is one.
61
+ let socketError;
62
+ const onClose = () => setImmediate(() => reject(socketError ?? new Error(`RPC socket closed during authentication: ${path}`)));
63
+ const onError = (error) => {
64
+ socketError ??= error;
65
+ reject(error);
66
+ };
67
+ socket.on("error", onError);
68
+ socket.on("close", onClose);
69
+ const detach = attachJsonlLineReader(socket, (line) => {
70
+ let response;
71
+ try {
72
+ response = authRecord(JSON.parse(line));
73
+ }
74
+ catch {
75
+ return;
76
+ }
77
+ // Matched by id alone, as the plain probe always did: a host's broadcast records never carry a request id.
78
+ if (response === undefined || pending === undefined || response.id !== pending.id)
79
+ return;
80
+ if (response.success === true)
81
+ pending?.resolve(response);
82
+ else {
83
+ const code = response.errorCode;
84
+ reject(code === "auth_failed" || code === "auth_required"
85
+ ? new RpcEndpointAuthError(code, `${path}; set SENPI_RPC_KEY_ROOT to the host's agent dir (${root})`)
86
+ : new Error(`RPC authentication probe refused on ${path}: ${String(response.error)}`));
87
+ }
88
+ }, { maxLineLength: 1024 * 1024, onOversizedLine: () => socket.destroy() });
89
+ const exchange = (request) => new Promise((resolve, reject) => {
90
+ if (socket.destroyed) {
91
+ // Bun destroys a socket whose connect failed before it emits the error; wait a turn for it.
92
+ setImmediate(() => reject(socketError ?? new Error(`RPC socket closed during authentication: ${path}`)));
93
+ return;
94
+ }
95
+ pending = { id: request.id, resolve, reject };
96
+ // net.Socket queues this write until connect, preserving the caller's own first frame.
97
+ socket.write(serializeJsonLine(request));
98
+ });
99
+ const timer = setTimeout(() => {
100
+ reject(new Error(`RPC authentication timed out on ${path}`));
101
+ socket.destroy();
102
+ }, timeoutMs);
103
+ try {
104
+ const nonce = randomBytes(32).toString("hex");
105
+ const response = await exchange({ ...first, auth: { client_nonce: nonce } });
106
+ const data = authRecord(response.data);
107
+ if (data?.auth === undefined)
108
+ return response;
109
+ const auth = authRecord(data.auth);
110
+ if (typeof auth?.launch_id !== "string" ||
111
+ !RPC_LAUNCH_ID.test(auth.launch_id) ||
112
+ typeof auth.server_nonce !== "string" ||
113
+ !RPC_AUTH_HEX.test(auth.server_nonce))
114
+ throw new RpcEndpointAuthError("host_identity_unproven", path);
115
+ const record = await readRpcKeyRecord(root, auth.launch_id);
116
+ if (socket.destroyed)
117
+ throw new Error(`RPC authentication timed out on ${path}`);
118
+ if (record === undefined) {
119
+ process.stderr.write(`senpi rpc client: no key record for launch ${auth.launch_id} under ${root}; continuing unauthenticated (refused from the next release; set SENPI_RPC_KEY_ROOT to the host's agent dir)\n`);
120
+ return response;
121
+ }
122
+ // readRpcKeyRecord validates this field at the file boundary.
123
+ if (typeof record.key !== "string")
124
+ throw new RpcEndpointAuthError("unsafe_key_record", root);
125
+ const key = Buffer.from(record.key, "hex");
126
+ if (!rpcProofMatches(rpcAuthProof(key, "server", auth.launch_id, nonce, auth.server_nonce), auth.server_proof))
127
+ throw new RpcEndpointAuthError("host_identity_unproven", path);
128
+ await exchange({
129
+ id: "rpc-authenticate",
130
+ type: "authenticate",
131
+ client_proof: rpcAuthProof(key, "client", auth.launch_id, auth.server_nonce, nonce),
132
+ });
133
+ return response;
134
+ }
135
+ finally {
136
+ clearTimeout(timer);
137
+ detach();
138
+ socket.off("error", onError);
139
+ socket.off("close", onClose);
140
+ }
141
+ }
142
+ //# sourceMappingURL=rpc-auth-client.js.map
@@ -0,0 +1,12 @@
1
+ export declare const RPC_AUTH_CAPABILITY = "socket_auth";
2
+ export declare const RPC_AUTH_HEX: RegExp;
3
+ export declare const RPC_LAUNCH_ID: RegExp;
4
+ export declare function rpcAuthProof(key: Uint8Array, label: "server" | "client", ...parts: readonly string[]): string;
5
+ export declare function rpcProofMatches(expected: string, received: unknown): boolean;
6
+ export type RpcEndpointAuthErrorCode = "unsafe_key_record" | "host_identity_unproven" | "key_publication_failed" | "auth_required" | "auth_failed";
7
+ export declare class RpcEndpointAuthError extends Error {
8
+ readonly code: RpcEndpointAuthErrorCode;
9
+ constructor(code: RpcEndpointAuthErrorCode, detail: string, options?: ErrorOptions);
10
+ }
11
+ export declare function authRecord(value: unknown): Readonly<Record<string, unknown>> | undefined;
12
+ //# sourceMappingURL=rpc-auth-proof.d.ts.map
@@ -0,0 +1,27 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ export const RPC_AUTH_CAPABILITY = "socket_auth";
3
+ export const RPC_AUTH_HEX = /^[a-f0-9]{64}$/;
4
+ export const RPC_LAUNCH_ID = /^[a-f0-9]{8}-[a-f0-9]{4}-4[a-f0-9]{3}-[89ab][a-f0-9]{3}-[a-f0-9]{12}$/;
5
+ export function rpcAuthProof(key, label, ...parts) {
6
+ return createHmac("sha256", key)
7
+ .update([`senpi-rpc-${label}`, ...parts].join("\n"), "utf8")
8
+ .digest("hex");
9
+ }
10
+ export function rpcProofMatches(expected, received) {
11
+ return (typeof received === "string" &&
12
+ RPC_AUTH_HEX.test(received) &&
13
+ timingSafeEqual(Buffer.from(expected, "hex"), Buffer.from(received, "hex")));
14
+ }
15
+ export class RpcEndpointAuthError extends Error {
16
+ constructor(code, detail, options) {
17
+ super(`${code}: ${detail}`, options);
18
+ this.code = code;
19
+ this.name = "RpcEndpointAuthError";
20
+ }
21
+ }
22
+ export function authRecord(value) {
23
+ return typeof value === "object" && value !== null && !Array.isArray(value)
24
+ ? value
25
+ : undefined;
26
+ }
27
+ //# sourceMappingURL=rpc-auth-proof.js.map
@@ -0,0 +1,5 @@
1
+ import { type Socket } from "node:net";
2
+ import { type RpcKeyOptions } from "./rpc-auth-client.ts";
3
+ /** Public POSIX authentication and the unchanged TUI/Windows raw-secret connection path. */
4
+ export declare function connectRpcClientSocket(path: string, options: RpcKeyOptions): Promise<Socket>;
5
+ //# sourceMappingURL=rpc-client-socket.d.ts.map
@@ -0,0 +1,30 @@
1
+ import { createConnection } from "node:net";
2
+ import { connectAuthenticatedRpcSocket } from "./rpc-auth-client.js";
3
+ import { readSocketSecret, resolveSocketTransportAddress, sendSocketHandshake, socketSecretPath, } from "./socket-transport.js";
4
+ import { socketNeedsHandshake } from "./tui-socket.js";
5
+ /** Public POSIX authentication and the unchanged TUI/Windows raw-secret connection path. */
6
+ export async function connectRpcClientSocket(path, options) {
7
+ if (!socketNeedsHandshake(path))
8
+ return (await connectAuthenticatedRpcSocket(path, undefined, options)).socket;
9
+ const secret = await readSocketSecret(socketSecretPath(path));
10
+ const socket = createConnection(resolveSocketTransportAddress(path, process.platform, secret));
11
+ await new Promise((resolve, reject) => {
12
+ const cleanup = () => {
13
+ socket.off("connect", onConnect);
14
+ socket.off("error", onError);
15
+ };
16
+ const onConnect = () => {
17
+ cleanup();
18
+ resolve();
19
+ };
20
+ const onError = (error) => {
21
+ cleanup();
22
+ reject(error);
23
+ };
24
+ socket.once("connect", onConnect);
25
+ socket.once("error", onError);
26
+ });
27
+ sendSocketHandshake(socket, secret);
28
+ return socket;
29
+ }
30
+ //# sourceMappingURL=rpc-client-socket.js.map
@@ -27,6 +27,8 @@ import type { EditAssistantMessageResult, EditUserMessageResult, RpcAccountFailo
27
27
  export interface RpcClientOptions {
28
28
  /** Connect to an existing multi-session Unix socket instead of spawning a child. */
29
29
  socketPath?: string;
30
+ /** Endpoint credential lookup only; does not change the client's configuration profile. */
31
+ keyRoot?: string;
30
32
  /** Called once when an established transport disconnects. */
31
33
  onDisconnect?: (error: RpcTransportGoneError) => void;
32
34
  /** Path to the CLI entry point (default: searches for dist/cli.js) */
@@ -4,7 +4,6 @@
4
4
  * Spawns the agent in RPC mode and provides a typed API for all operations.
5
5
  */
6
6
  import { spawn } from "node:child_process";
7
- import { createConnection } from "node:net";
8
7
  import { clientMessageIdentity } from "../../core/client-message-identity.js";
9
8
  import { MissingSessionCwdError } from "../../core/session-cwd.js";
10
9
  import { unknownCommandErrorFromWire } from "../../core/unknown-command.js";
@@ -21,10 +20,9 @@ export class RpcCommandError extends Error {
21
20
  }
22
21
  }
23
22
  import { attachJsonlLineReader, serializeJsonLine } from "./jsonl.js";
23
+ import { connectRpcClientSocket } from "./rpc-client-socket.js";
24
24
  import { armRequestDeadline, OPEN_AFTER_QUEUED_DEADLINE_MS, openStalledMessage, PROMPT_ACK_MAX_WAIT_MS, PROMPT_AFTER_QUEUED_DEADLINE_MS, PROMPT_COMPACTION_DEADLINE_MS, promptStalledMessage, REQUEST_DEADLINE_MS, RpcRequestTimeoutError, requestDeadlineMs, } from "./rpc-request-deadline.js";
25
25
  import { RPC_ERROR_SESSION_NOT_ATTACHED, RPC_ERROR_UNKNOWN_COMMAND } from "./rpc-types.js";
26
- import { readSocketSecret, resolveSocketTransportAddress, sendSocketHandshake, socketSecretPath, } from "./socket-transport.js";
27
- import { socketNeedsHandshake } from "./tui-socket.js";
28
26
  function isProviderAccountEvent(event) {
29
27
  return event.type === "auth_accounts_changed" || event.type === "account_failover";
30
28
  }
@@ -244,28 +242,8 @@ export class RpcClient {
244
242
  this.pendingRequests.clear();
245
243
  }
246
244
  async startSocket(path) {
247
- const secret = socketNeedsHandshake(path) ? await readSocketSecret(socketSecretPath(path)) : undefined;
248
- const socket = createConnection(resolveSocketTransportAddress(path, process.platform, secret));
245
+ const socket = await connectRpcClientSocket(path, this.options);
249
246
  this.socket = socket;
250
- await new Promise((resolve, reject) => {
251
- const onConnect = () => {
252
- cleanup();
253
- resolve();
254
- };
255
- const onError = (error) => {
256
- cleanup();
257
- this.socket = null;
258
- reject(error);
259
- };
260
- const cleanup = () => {
261
- socket.off("connect", onConnect);
262
- socket.off("error", onError);
263
- };
264
- socket.once("connect", onConnect);
265
- socket.once("error", onError);
266
- });
267
- if (secret)
268
- sendSocketHandshake(socket, secret);
269
247
  this.stopReadingStdout = attachJsonlLineReader(socket, (line) => this.handleLine(line));
270
248
  socket.once("close", () => {
271
249
  if (this.socket !== socket)
@@ -14,6 +14,10 @@ export type RpcCommand = RpcAttachmentCommand | (RpcSessionCommand & {
14
14
  }) | {
15
15
  id?: string;
16
16
  type: "get_protocol_info";
17
+ /** Public POSIX endpoint challenge (wire contract v1.1); ignored by hosts that predate authentication. */
18
+ auth?: {
19
+ readonly client_nonce: string;
20
+ };
17
21
  /** An observing read: this connection never counts as host activity (docs/rpc.md, idle exit). */
18
22
  observe?: boolean;
19
23
  } | {
@@ -34,6 +34,12 @@ export interface RpcProtocolIdentity {
34
34
  readonly agentDir: string;
35
35
  }
36
36
  export interface RpcProtocolInfo extends RpcProtocolIdentity {
37
+ /** Present only on a public POSIX endpoint's answer to a challenge (wire contract v1.1). */
38
+ readonly auth?: {
39
+ readonly launch_id: string;
40
+ readonly server_nonce: string;
41
+ readonly server_proof: string;
42
+ };
37
43
  /** Supervised socket hosts expose admission state independently of child responsiveness. */
38
44
  readonly lifecycle?: "ready" | "stopping";
39
45
  readonly protocolVersion: 1;
@@ -0,0 +1,41 @@
1
+ import type { Socket } from "node:net";
2
+ import { Duplex, PassThrough } from "node:stream";
3
+ import type { RpcPublicEndpoint } from "./rpc-public-key.ts";
4
+ /** Filters public input before it can reach either the proxy or the bare host's router. */
5
+ export declare class PublicRpcConnection {
6
+ readonly input: PassThrough;
7
+ private readonly raw;
8
+ private readonly timer;
9
+ private bytes;
10
+ private active;
11
+ private failed;
12
+ private first;
13
+ private challenge;
14
+ private protocolPending;
15
+ /**
16
+ * The admitted connection as one duplex for a router that reads and writes a single stream: it reads only
17
+ * frames this filter released and writes through `response()`, so the challenge answer gains its proof.
18
+ */
19
+ get stream(): Duplex;
20
+ private view;
21
+ private readonly socket;
22
+ private readonly endpoint;
23
+ /** Enforcement: refuse an unauthenticated client. Off by default for one release of logged grace. */
24
+ private readonly requireToken;
25
+ constructor(socket: Socket, endpoint: RpcPublicEndpoint, requireToken?: boolean);
26
+ private duplex;
27
+ private line;
28
+ private forward;
29
+ private grace;
30
+ /** Every pre-auth drop says why on stderr, by reason code only: never a frame, a nonce or key material. */
31
+ private logDrop;
32
+ private drop;
33
+ private refuse;
34
+ /**
35
+ * Writer/proxy batches contain whole JSONL records; ordinary authenticated bytes stay untouched. Before
36
+ * admission only the challenge answer leaves an enforcing host; a grace host keeps delivering records (the
37
+ * lifecycle broadcasts a passive legacy observer relies on).
38
+ */
39
+ response(chunk: string): string;
40
+ }
41
+ //# sourceMappingURL=rpc-public-auth.d.ts.map