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

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 (127) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/bundle/chunks/chunk-4TRKE3ZC.js +7 -0
  3. package/dist/bundle/chunks/chunk-7IRLFYNM.js +23 -0
  4. package/dist/bundle/chunks/{chunk-FIBIVBQK.js → chunk-7UUOGZKR.js} +1 -1
  5. package/dist/bundle/chunks/{chunk-ONLNXGO3.js → chunk-AVLYZET7.js} +1 -1
  6. package/dist/bundle/chunks/{chunk-VZAYN5SB.js → chunk-CYG4LS4S.js} +1 -1
  7. package/dist/bundle/chunks/chunk-CZYADQ2E.js +8 -0
  8. package/dist/bundle/chunks/{chunk-OA5INWHI.js → chunk-DITSGW4R.js} +68 -8
  9. package/dist/bundle/chunks/{chunk-IELM5U6I.js → chunk-DYWHYQUD.js} +2 -2
  10. package/dist/bundle/chunks/chunk-EWOEVVQT.js +4 -0
  11. package/dist/bundle/chunks/{chunk-RQHK4QZ5.js → chunk-FWXMMMCV.js} +1 -1
  12. package/dist/bundle/chunks/{chunk-HHYSXDYP.js → chunk-GOB5BLDV.js} +1 -1
  13. package/dist/bundle/chunks/{chunk-BRXN5R2F.js → chunk-H6IG2ITJ.js} +1 -1
  14. package/dist/bundle/chunks/{chunk-3N2PQEQV.js → chunk-I5NIJZDY.js} +1 -1
  15. package/dist/bundle/chunks/chunk-II22E3CV.js +3 -0
  16. package/dist/bundle/chunks/chunk-MRN6IKOQ.js +27 -0
  17. package/dist/bundle/chunks/{chunk-QQSCUEYJ.js → chunk-RHHBXIRN.js} +2 -5
  18. package/dist/bundle/chunks/chunk-RVYSJBZ7.js +7 -0
  19. package/dist/bundle/chunks/{chunk-RAIZG6HA.js → chunk-V3CK2AVC.js} +1 -1
  20. package/dist/bundle/chunks/{chunk-HRPMLI7O.js → chunk-VRDU425E.js} +2 -2
  21. package/dist/bundle/chunks/{chunk-OUPD5LWY.js → chunk-XUFUKDKJ.js} +3 -3
  22. package/dist/bundle/chunks/{chunk-QEQG42HL.js → chunk-ZGAB7WQA.js} +1 -1
  23. package/dist/bundle/chunks/cli-main-BOYWBHOB.js +3 -0
  24. package/dist/bundle/chunks/host-command-7MAXSZIH.js +2 -0
  25. package/dist/bundle/chunks/host-lifecycle-U36U7JTE.js +3 -0
  26. package/dist/bundle/chunks/host-lifecycle.js +26 -21
  27. package/dist/bundle/chunks/host-session-command-L2UTORL5.js +20 -0
  28. package/dist/bundle/chunks/{host-socket-request-KKGH3JHV.js → host-socket-request-ZDJGSSZO.js} +1 -1
  29. package/dist/bundle/chunks/interactive-mode-NVBFOE5R.js +2 -0
  30. package/dist/bundle/chunks/{multi-session-host-O5GZQSRL.js → multi-session-host-3TVASSQD.js} +12 -12
  31. package/dist/bundle/chunks/{package-manager-cli-LC5GAGTW.js → package-manager-cli-5BHVDVJC.js} +1 -1
  32. package/dist/bundle/chunks/{role-4G43UDOC.js → role-BHA5ROSF.js} +1 -1
  33. package/dist/bundle/chunks/rpc-mode-LPP46I7H.js +2 -0
  34. package/dist/bundle/chunks/rpc-monitor-source-OL5KU6NO.js +3 -0
  35. package/dist/bundle/chunks/{schedule-command-QNCVE4HM.js → schedule-command-QW67J57R.js} +1 -1
  36. package/dist/bundle/chunks/session-control-endpoint-QQBBY5AT.js +3 -0
  37. package/dist/bundle/chunks/session-worker.js +425 -359
  38. package/dist/bundle/cli.js +2 -2
  39. package/dist/bundle/host-supervisor.js +26 -21
  40. package/dist/bundle/index.js +1 -1
  41. package/dist/bundle/rpc-entry.js +1 -1
  42. package/dist/bundle/runtime-manifest.json +1 -1
  43. package/dist/cli/host-command-args.d.ts +33 -0
  44. package/dist/cli/host-command-args.js +145 -0
  45. package/dist/cli/host-command.d.ts +1 -30
  46. package/dist/cli/host-command.js +5 -137
  47. package/dist/cli/host-session-command.d.ts +1 -0
  48. package/dist/cli/host-session-command.js +5 -3
  49. package/dist/core/extensions/builtin/prompt-preset/contract.js +6 -1
  50. package/dist/core/extensions/builtin/prompt-preset/kimi-k3-1.d.ts +3 -0
  51. package/dist/core/extensions/builtin/prompt-preset/kimi-k3-1.js +93 -0
  52. package/dist/core/extensions/builtin/prompt-preset/matchers.d.ts +2 -0
  53. package/dist/core/extensions/builtin/prompt-preset/matchers.js +9 -0
  54. package/dist/core/extensions/builtin/prompt-preset/presets.js +3 -0
  55. package/dist/core/extensions/builtin/prompt-preset/settings.d.ts +1 -1
  56. package/dist/core/extensions/builtin/prompt-preset/settings.js +1 -0
  57. package/dist/core/extensions/builtin/terminal/rpc-monitor-observer.d.ts +2 -1
  58. package/dist/core/extensions/builtin/terminal/rpc-monitor-observer.js +12 -4
  59. package/dist/modes/rpc/host-admission-proxy.d.ts +2 -1
  60. package/dist/modes/rpc/host-admission-proxy.js +10 -9
  61. package/dist/modes/rpc/host-cli-entry.d.ts +2 -0
  62. package/dist/modes/rpc/host-cli-entry.js +13 -1
  63. package/dist/modes/rpc/host-ensure-client.d.ts +5 -1
  64. package/dist/modes/rpc/host-ensure-client.js +8 -0
  65. package/dist/modes/rpc/host-ensure-types.d.ts +1 -0
  66. package/dist/modes/rpc/host-ensure.js +6 -11
  67. package/dist/modes/rpc/host-gc-pass.js +3 -0
  68. package/dist/modes/rpc/host-gc.js +3 -0
  69. package/dist/modes/rpc/host-internal-transport.d.ts +16 -5
  70. package/dist/modes/rpc/host-internal-transport.js +40 -9
  71. package/dist/modes/rpc/host-lifecycle-proxy.d.ts +4 -8
  72. package/dist/modes/rpc/host-lifecycle-proxy.js +9 -21
  73. package/dist/modes/rpc/host-lifecycle-shutdown.d.ts +2 -0
  74. package/dist/modes/rpc/host-lifecycle-shutdown.js +3 -0
  75. package/dist/modes/rpc/host-lifecycle.js +9 -16
  76. package/dist/modes/rpc/host-observe-request.js +1 -1
  77. package/dist/modes/rpc/host-probe.d.ts +8 -6
  78. package/dist/modes/rpc/host-probe.js +13 -13
  79. package/dist/modes/rpc/host-request-key-root.d.ts +8 -0
  80. package/dist/modes/rpc/host-request-key-root.js +14 -0
  81. package/dist/modes/rpc/host-runner.d.ts +3 -1
  82. package/dist/modes/rpc/host-runner.js +6 -1
  83. package/dist/modes/rpc/host-socket-bind.d.ts +6 -0
  84. package/dist/modes/rpc/host-socket-bind.js +42 -0
  85. package/dist/modes/rpc/host-socket-request.d.ts +3 -2
  86. package/dist/modes/rpc/host-socket-request.js +55 -10
  87. package/dist/modes/rpc/host-status.d.ts +3 -2
  88. package/dist/modes/rpc/host-status.js +5 -0
  89. package/dist/modes/rpc/host-supervisor-log.d.ts +1 -0
  90. package/dist/modes/rpc/host-supervisor-log.js +3 -0
  91. package/dist/modes/rpc/host-watchdog.d.ts +8 -1
  92. package/dist/modes/rpc/host-watchdog.js +37 -12
  93. package/dist/modes/rpc/internal-channel.js +14 -14
  94. package/dist/modes/rpc/multi-session-host.js +18 -46
  95. package/dist/modes/rpc/rpc-auth-client.d.ts +13 -0
  96. package/dist/modes/rpc/rpc-auth-client.js +142 -0
  97. package/dist/modes/rpc/rpc-auth-proof.d.ts +12 -0
  98. package/dist/modes/rpc/rpc-auth-proof.js +27 -0
  99. package/dist/modes/rpc/rpc-client-socket.d.ts +5 -0
  100. package/dist/modes/rpc/rpc-client-socket.js +30 -0
  101. package/dist/modes/rpc/rpc-client.d.ts +2 -0
  102. package/dist/modes/rpc/rpc-client.js +2 -24
  103. package/dist/modes/rpc/rpc-host-command-types.d.ts +4 -0
  104. package/dist/modes/rpc/rpc-protocol-info-types.d.ts +6 -0
  105. package/dist/modes/rpc/rpc-public-auth.d.ts +41 -0
  106. package/dist/modes/rpc/rpc-public-auth.js +237 -0
  107. package/dist/modes/rpc/rpc-public-key.d.ts +28 -0
  108. package/dist/modes/rpc/rpc-public-key.js +193 -0
  109. package/dist/modes/rpc/socket-ownership.d.ts +7 -0
  110. package/dist/modes/rpc/socket-ownership.js +17 -0
  111. package/docs/rpc.md +108 -2
  112. package/docs/settings.md +1 -1
  113. package/package.json +6 -6
  114. package/dist/bundle/chunks/chunk-3474DPZK.js +0 -2
  115. package/dist/bundle/chunks/chunk-64CFJHKT.js +0 -7
  116. package/dist/bundle/chunks/chunk-B6NJHBLH.js +0 -21
  117. package/dist/bundle/chunks/chunk-HZTQHE5O.js +0 -26
  118. package/dist/bundle/chunks/chunk-IBDADH5Z.js +0 -4
  119. package/dist/bundle/chunks/chunk-OJZD7CO7.js +0 -8
  120. package/dist/bundle/chunks/cli-main-4JNPG6LB.js +0 -3
  121. package/dist/bundle/chunks/host-command-I3ZMWQQ3.js +0 -2
  122. package/dist/bundle/chunks/host-lifecycle-QEO422VW.js +0 -3
  123. package/dist/bundle/chunks/host-session-command-INULKX6E.js +0 -20
  124. package/dist/bundle/chunks/interactive-mode-SYWJVPKK.js +0 -2
  125. package/dist/bundle/chunks/rpc-mode-JMCYSP4P.js +0 -2
  126. package/dist/bundle/chunks/rpc-monitor-source-34AQDTZ6.js +0 -3
  127. package/dist/bundle/chunks/session-control-endpoint-IW5X56WN.js +0 -3
@@ -3,17 +3,20 @@
3
3
  * adoption of the public name, and the readiness probes of the private hop. Split out of
4
4
  * `host-lifecycle.ts`, which keeps the supervisor's orchestration (senpi#2566).
5
5
  */
6
- import { chmod, mkdir, rename, stat, unlink } from "node:fs/promises";
6
+ import { chmod, mkdir, stat, unlink } from "node:fs/promises";
7
7
  import { createConnection, createServer } from "node:net";
8
8
  import { dirname } from "node:path";
9
9
  import { proxyWithAdmission } from "./host-admission-proxy.js";
10
10
  import { connectInternalConnection } from "./internal-channel.js";
11
11
  import { attachJsonlLineReader, MAX_RPC_LINE_CHARACTERS } from "./jsonl.js";
12
- import { MAX_SOCKET_PATH_BYTES, socketEntryListenerState, statSocketIdentity, } from "./socket-ownership.js";
12
+ import { PublicRpcConnection } from "./rpc-public-auth.js";
13
+ import { MAX_SOCKET_PATH_BYTES, socketEntryListenerState } from "./socket-ownership.js";
14
+ export { adoptPublicSocket } from "./socket-ownership.js";
13
15
  import { authenticateSocket, ensureSocketSecret, resolveSocketTransportAddress, sendSocketHandshake, socketSecretPath, } from "./socket-transport.js";
14
16
  export function createPublicProxy(options) {
15
17
  const { internalSocket, internalSecret, publicSecret, clients } = options;
16
18
  return createServer((client) => {
19
+ const auth = options.publicEndpoint ? new PublicRpcConnection(client, options.publicEndpoint) : undefined;
17
20
  const accept = async () => {
18
21
  // Teardown is terminal even for an accept that raced socket shielding/server.close.
19
22
  if (!clients.admit(client))
@@ -47,8 +50,10 @@ export function createPublicProxy(options) {
47
50
  hop?.destroy();
48
51
  return;
49
52
  }
50
- proxyWithAdmission(client, hop, options.admission);
51
- client.resume();
53
+ proxyWithAdmission(client, hop, options.admission, auth);
54
+ // An authenticated endpoint resumes the client itself once its key record is published.
55
+ if (!auth)
56
+ client.resume();
52
57
  }
53
58
  catch {
54
59
  detach();
@@ -60,23 +65,6 @@ export function createPublicProxy(options) {
60
65
  void accept();
61
66
  });
62
67
  }
63
- /**
64
- * Takes the public name over with a rename, once - and only while - that name still refers to the
65
- * entry this handoff was decided against. A socket that changed underneath belongs to another
66
- * process now: replacing it would unlink an endpoint this generation cannot prove it owns, so the
67
- * successor aborts instead and leaves both the intruder's socket and its own bind entry alone.
68
- */
69
- export async function adoptPublicSocket(bindSocket, publicSocket, expected) {
70
- if (expected === undefined)
71
- throw new Error(`${publicSocket}: a generation launch must name the entry it replaces`);
72
- const current = await statSocketIdentity(publicSocket);
73
- if (current === undefined || current.dev !== expected.dev || current.ino !== expected.ino) {
74
- throw new Error(`${publicSocket}: owned by another socket entry now; refusing to replace it`);
75
- }
76
- // rename(2) is atomic for readers of the path: every connect either reaches the old entry or
77
- // this one, never nothing. The inode this supervisor bound simply answers to a second name.
78
- await rename(bindSocket, publicSocket);
79
- }
80
68
  /**
81
69
  * Reuses an existing valid secret - including one ensureHost() just wrote - and
82
70
  * creates one (with its parent directories, mode 0600) when it is missing or
@@ -10,6 +10,7 @@ import type { HostStopSender } from "./host-crash-record.ts";
10
10
  import type { HostDaemonPaths, HostGenerationPaths } from "./host-daemon-paths.ts";
11
11
  import type { SupervisorActivity } from "./host-lifecycle-activity.ts";
12
12
  import type { SupervisorDrain } from "./host-lifecycle-drain.ts";
13
+ import type { RpcPublicEndpoint } from "./rpc-public-key.ts";
13
14
  import { type SocketFileIdentity } from "./socket-ownership.ts";
14
15
  export { CHILD_STOP_TIMEOUT_MS } from "./host-stop-intent.ts";
15
16
  export interface SupervisorState {
@@ -34,6 +35,7 @@ export interface SupervisorShutdown {
34
35
  readonly drain: SupervisorDrain;
35
36
  readonly admission: HostAdmission;
36
37
  readonly state: SupervisorState;
38
+ readonly publicEndpoint?: RpcPublicEndpoint;
37
39
  /** Stops the idle ticker, the supersession watch and the win32 identity watchdog. */
38
40
  readonly stopWatchers: () => void;
39
41
  }
@@ -68,6 +68,9 @@ export async function performShutdown(context, reason, exitCode) {
68
68
  }
69
69
  }
70
70
  finally {
71
+ await context.publicEndpoint?.dispose().catch((cause) => {
72
+ supervisorLog(`endpoint key cleanup failed: ${errorMessage(cause)}`);
73
+ });
71
74
  if (hardExit)
72
75
  clearTimeout(hardExit);
73
76
  // Windows named-pipe handles can outlive their JavaScript wrappers, so cleanup failure must
@@ -1,12 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import { randomUUID } from "node:crypto";
3
- import { realpathSync } from "node:fs";
4
3
  import { join } from "node:path";
5
- import { fileURLToPath } from "node:url";
6
4
  import { getAgentDir, isBundledNode, VERSION } from "../../config.js";
7
5
  import { HostAdmission } from "./host-admission.js";
8
6
  import { classifyChildExit, noteChildExit } from "./host-child-exit.js";
9
7
  import { hostCrashCleanupPaths } from "./host-cleanup-paths.js";
8
+ import { isCliModuleEntry } from "./host-cli-entry.js";
10
9
  import { createHostDaemonPaths, generationPaths } from "./host-daemon-paths.js";
11
10
  import { HOST_INSTANCE_ID_ENV } from "./host-identity-env.js";
12
11
  import { SupervisorActivity, SupervisorOwner } from "./host-lifecycle-activity.js";
@@ -18,7 +17,8 @@ import { adoptPublicSocket, createPublicProxy, ensurePublicSocketSecret, listen,
18
17
  import { readSettingsFile, recordChildPid } from "./host-lifecycle-scratch.js";
19
18
  import { performShutdown, registerSupervisorSignals, supervisorSender, } from "./host-lifecycle-shutdown.js";
20
19
  import { startSupervisorTicker } from "./host-lifecycle-ticker.js";
21
- import { errorMessage, writeStderrLine } from "./host-supervisor-log.js";
20
+ import { errorMessage, writeHostReady, writeStderrLine } from "./host-supervisor-log.js";
21
+ import { RpcPublicEndpoint } from "./rpc-public-key.js";
22
22
  import { PUBLIC_SOCKET_IDENTITY_FILE, statSocketIdentity, writeSocketIdentityFile } from "./socket-ownership.js";
23
23
  // The exit verdict, the launch surface and the cold-start/idle-exit policy live in their own modules
24
24
  // (host-child-exit.ts, host-lifecycle-launch.ts, host-lifecycle-policy.ts); they stay exported from
@@ -36,6 +36,7 @@ export async function runHostSupervisor(launch) {
36
36
  const generation = generationPaths(paths, instanceId);
37
37
  const policy = resolveHostPolicy(await readSettingsFile(paths.settingsFile), process.env);
38
38
  const publicSocket = launch.socket;
39
+ const publicEndpoint = process.platform === "win32" ? undefined : new RpcPublicEndpoint(launch.agentDir ?? getAgentDir(), publicSocket);
39
40
  // A successor generation binds its own name and adopts the public one by rename; an ordinary
40
41
  // start binds the public name directly. Everything downstream - the child's environment, the
41
42
  // ownership token, the teardown - is expressed in terms of the PUBLIC path either way.
@@ -119,6 +120,7 @@ export async function runHostSupervisor(launch) {
119
120
  internalSocket,
120
121
  ...hop.transport,
121
122
  ...(publicSecret ? { publicSecret } : {}),
123
+ ...(publicEndpoint ? { publicEndpoint } : {}),
122
124
  clients: activity.clients,
123
125
  admission,
124
126
  onDetach: () => activity.refresh(),
@@ -148,6 +150,7 @@ export async function runHostSupervisor(launch) {
148
150
  drain,
149
151
  admission,
150
152
  state,
153
+ ...(publicEndpoint ? { publicEndpoint } : {}),
151
154
  stopWatchers: () => {
152
155
  for (const stop of watchers.splice(0))
153
156
  stop();
@@ -174,6 +177,7 @@ export async function runHostSupervisor(launch) {
174
177
  await activity.openObserver();
175
178
  await prepareSocketPath(bindSocket);
176
179
  await listen(server, bindSocket, publicSecret);
180
+ await publicEndpoint?.publish();
177
181
  state.publicSocketOwned = true;
178
182
  if (successor)
179
183
  await adoptPublicSocket(bindSocket, publicSocket, launch.replaceIdentity);
@@ -195,21 +199,10 @@ export async function runHostSupervisor(launch) {
195
199
  if (process.platform === "win32") {
196
200
  watchers.push(await watchWin32ChildIdentity(child, () => state.shuttingDown, () => void shutdown("rpc host child exit observed by identity watchdog", 0)));
197
201
  }
198
- writeStderrLine(`senpi rpc host ready on unix://${publicSocket} (coldStart=${policy.coldStart}, idleExitMs=${policy.coldStart === "persistent" ? "never" : String(policy.idleExitMs)})`);
202
+ writeHostReady(publicSocket, policy.coldStart, policy.idleExitMs);
199
203
  await new Promise(() => { });
200
204
  }
201
- function isEntryScript() {
202
- const entry = process.argv[1];
203
- if (!entry)
204
- return false;
205
- try {
206
- return fileURLToPath(import.meta.url) === realpathSync(entry);
207
- }
208
- catch {
209
- return false;
210
- }
211
- }
212
- if (!isBundledNode && isEntryScript()) {
205
+ if (!isBundledNode && isCliModuleEntry(import.meta.url)) {
213
206
  const launch = parseSupervisorArgs(process.argv.slice(2));
214
207
  if (!launch) {
215
208
  writeStderrLine("usage: host-lifecycle.ts --socket <path> [host cli args...]");
@@ -45,7 +45,7 @@ export function isObservingRequest(line) {
45
45
  if (typeof request !== "object" || request === null || Array.isArray(request))
46
46
  return false;
47
47
  const record = request;
48
- if (record.type === "warm")
48
+ if (record.type === "warm" || record.type === "authenticate")
49
49
  return true;
50
50
  return record[OBSERVE_REQUEST_FIELD] === true && OBSERVING_COMMANDS.has(record.type);
51
51
  }
@@ -1,24 +1,26 @@
1
1
  import { type HostAttachHold } from "./host-attach-hold.ts";
2
2
  import { type HostProtocolInfo } from "./host-decision.ts";
3
3
  import { requestOnSocket } from "./host-socket-request.ts";
4
+ import type { RpcKeyOptions } from "./rpc-auth-client.ts";
4
5
  export { requestOnSocket };
5
6
  /** Default probe budget: long enough for a host under load, short enough to keep an ensure moving. */
6
7
  export declare const DEFAULT_PROBE_TIMEOUT_MS = 10000;
7
8
  export interface ProbeHostOptions {
8
9
  readonly socket: string;
9
10
  readonly timeoutMs?: number;
11
+ readonly keyRoot?: string;
10
12
  }
11
13
  /** The running host's identity, or `undefined` when nothing is serving the endpoint. */
12
14
  export declare function probeHost(options: ProbeHostOptions): Promise<HostProtocolInfo | undefined>;
13
- export declare function probeProtocolInfo(socketPath: string, timeoutMs: number): Promise<HostProtocolInfo | undefined>;
15
+ export declare function probeProtocolInfo(socketPath: string, timeoutMs: number, options?: RpcKeyOptions): Promise<HostProtocolInfo | undefined>;
14
16
  /** `probeProtocolInfo` for a read that must not count as host activity, such as `host status`. */
15
- export declare function observeProtocolInfo(socketPath: string, timeoutMs: number): Promise<HostProtocolInfo | undefined>;
17
+ export declare function observeProtocolInfo(socketPath: string, timeoutMs: number, options?: RpcKeyOptions): Promise<HostProtocolInfo | undefined>;
16
18
  /**
17
19
  * The host's identity together with an attach hold on the connection that answered, or `undefined`
18
20
  * when nothing answers. The answer and the claim share one connection, so there is no instant
19
21
  * between "the host is ready" and "the host is held" in which an idle window could close.
20
22
  */
21
- export declare function holdProtocolInfo(socketPath: string, timeoutMs: number): Promise<{
23
+ export declare function holdProtocolInfo(socketPath: string, timeoutMs: number, options?: RpcKeyOptions): Promise<{
22
24
  readonly info: HostProtocolInfo;
23
25
  readonly hold: HostAttachHold;
24
26
  } | undefined>;
@@ -28,12 +30,12 @@ export declare function holdProtocolInfo(socketPath: string, timeoutMs: number):
28
30
  * difference decides whether an ensure may end it: a silent-but-connectable socket has an owner
29
31
  * serving sessions behind it, and killing that owner destroys every one of them.
30
32
  */
31
- export declare function probeSocketReachable(socketPath: string, timeoutMs: number): Promise<boolean>;
33
+ export declare function probeSocketReachable(socketPath: string, timeoutMs: number, options?: RpcKeyOptions): Promise<boolean>;
32
34
  /**
33
35
  * How many sessions the host holds right now, worker sessions included, or `undefined` when it
34
36
  * does not answer. A stop decision needs the number the host reports, not the one a client remembers.
35
37
  */
36
- export declare function probeSessionCount(socketPath: string, timeoutMs: number): Promise<number | undefined>;
38
+ export declare function probeSessionCount(socketPath: string, timeoutMs: number, options?: RpcKeyOptions): Promise<number | undefined>;
37
39
  /**
38
40
  * The session count together with the connection that answered it, kept open so the SAME host can be
39
41
  * asked again after its public path was handed to somebody else: the recount reaches the process that
@@ -45,5 +47,5 @@ export interface HeldSessionCount {
45
47
  recount(timeoutMs: number): Promise<number | undefined>;
46
48
  close(): void;
47
49
  }
48
- export declare function holdSessionCount(socketPath: string, timeoutMs: number): Promise<HeldSessionCount | undefined>;
50
+ export declare function holdSessionCount(socketPath: string, timeoutMs: number, options?: RpcKeyOptions): Promise<HeldSessionCount | undefined>;
49
51
  //# sourceMappingURL=host-probe.d.ts.map
@@ -7,15 +7,15 @@ export { requestOnSocket };
7
7
  export const DEFAULT_PROBE_TIMEOUT_MS = 10_000;
8
8
  /** The running host's identity, or `undefined` when nothing is serving the endpoint. */
9
9
  export function probeHost(options) {
10
- return probeProtocolInfo(options.socket, options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS);
10
+ return probeProtocolInfo(options.socket, options.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS, options);
11
11
  }
12
- export async function probeProtocolInfo(socketPath, timeoutMs) {
13
- const reply = await requestOnSocket(socketPath, { type: "get_protocol_info" }, timeoutMs);
12
+ export async function probeProtocolInfo(socketPath, timeoutMs, options = {}) {
13
+ const reply = await requestOnSocket(socketPath, { type: "get_protocol_info" }, timeoutMs, options);
14
14
  return reply === undefined ? undefined : parseHostProtocolInfo(reply);
15
15
  }
16
16
  /** `probeProtocolInfo` for a read that must not count as host activity, such as `host status`. */
17
- export async function observeProtocolInfo(socketPath, timeoutMs) {
18
- const reply = await requestOnSocket(socketPath, { type: "get_protocol_info", [OBSERVE_REQUEST_FIELD]: true }, timeoutMs);
17
+ export async function observeProtocolInfo(socketPath, timeoutMs, options = {}) {
18
+ const reply = await requestOnSocket(socketPath, { type: "get_protocol_info", [OBSERVE_REQUEST_FIELD]: true }, timeoutMs, options);
19
19
  return reply === undefined ? undefined : parseHostProtocolInfo(reply);
20
20
  }
21
21
  /**
@@ -23,8 +23,8 @@ export async function observeProtocolInfo(socketPath, timeoutMs) {
23
23
  * when nothing answers. The answer and the claim share one connection, so there is no instant
24
24
  * between "the host is ready" and "the host is held" in which an idle window could close.
25
25
  */
26
- export async function holdProtocolInfo(socketPath, timeoutMs) {
27
- const outcome = await connectAndAsk(socketPath, { id: PROBE_REQUEST_ID, type: "get_protocol_info" }, timeoutMs, true);
26
+ export async function holdProtocolInfo(socketPath, timeoutMs, options = {}) {
27
+ const outcome = await connectAndAsk(socketPath, { id: PROBE_REQUEST_ID, type: "get_protocol_info" }, timeoutMs, true, options);
28
28
  const info = outcome.answer === undefined ? undefined : parseHostProtocolInfo(outcome.answer);
29
29
  if (outcome.kept === undefined)
30
30
  return undefined;
@@ -40,18 +40,18 @@ export async function holdProtocolInfo(socketPath, timeoutMs) {
40
40
  * difference decides whether an ensure may end it: a silent-but-connectable socket has an owner
41
41
  * serving sessions behind it, and killing that owner destroys every one of them.
42
42
  */
43
- export async function probeSocketReachable(socketPath, timeoutMs) {
44
- return (await connectAndAsk(socketPath, { id: PROBE_REQUEST_ID, type: "get_protocol_info" }, timeoutMs)).connected;
43
+ export async function probeSocketReachable(socketPath, timeoutMs, options = {}) {
44
+ return (await connectAndAsk(socketPath, { id: PROBE_REQUEST_ID, type: "get_protocol_info" }, timeoutMs, false, options)).connected;
45
45
  }
46
46
  /**
47
47
  * How many sessions the host holds right now, worker sessions included, or `undefined` when it
48
48
  * does not answer. A stop decision needs the number the host reports, not the one a client remembers.
49
49
  */
50
- export async function probeSessionCount(socketPath, timeoutMs) {
51
- return sessionCountOf(await requestOnSocket(socketPath, { type: "list_sessions", include_workers: true }, timeoutMs));
50
+ export async function probeSessionCount(socketPath, timeoutMs, options = {}) {
51
+ return sessionCountOf(await requestOnSocket(socketPath, { type: "list_sessions", include_workers: true }, timeoutMs, options));
52
52
  }
53
- export async function holdSessionCount(socketPath, timeoutMs) {
54
- const outcome = await connectAndAsk(socketPath, { id: PROBE_REQUEST_ID, type: "list_sessions", include_workers: true }, timeoutMs, true);
53
+ export async function holdSessionCount(socketPath, timeoutMs, options = {}) {
54
+ const outcome = await connectAndAsk(socketPath, { id: PROBE_REQUEST_ID, type: "list_sessions", include_workers: true }, timeoutMs, true, options);
55
55
  const kept = outcome.kept;
56
56
  if (kept === undefined)
57
57
  return undefined;
@@ -0,0 +1,8 @@
1
+ import type { HostRequest } from "./host-runner.ts";
2
+ /**
3
+ * Where a host request proves endpoint keys from (senpi#3815): the caller's explicit `--rpc-key-root`, then a gc's
4
+ * explicit target directory (it dials that directory's endpoints, whose hosts published their records there), then
5
+ * an enclosing key root, `SENPI_RPC_KEY_ROOT`, and finally the target's agent dir.
6
+ */
7
+ export declare function hostRequestKeyRoot(request: HostRequest, explicit?: string): string;
8
+ //# sourceMappingURL=host-request-key-root.d.ts.map
@@ -0,0 +1,14 @@
1
+ import { rpcKeyRoot } from "./rpc-auth-client.js";
2
+ /**
3
+ * Where a host request proves endpoint keys from (senpi#3815): the caller's explicit `--rpc-key-root`, then a gc's
4
+ * explicit target directory (it dials that directory's endpoints, whose hosts published their records there), then
5
+ * an enclosing key root, `SENPI_RPC_KEY_ROOT`, and finally the target's agent dir.
6
+ */
7
+ export function hostRequestKeyRoot(request, explicit) {
8
+ if (explicit !== undefined)
9
+ return explicit;
10
+ if (request.action === "gc")
11
+ return request.agentDir;
12
+ return rpcKeyRoot({}, request.action === "shard_path" ? undefined : request.target.agentDir);
13
+ }
14
+ //# sourceMappingURL=host-request-key-root.js.map
@@ -48,5 +48,7 @@ export type HostRequest = {
48
48
  readonly owner: string;
49
49
  readonly root: string;
50
50
  };
51
- export declare function runHostRequest(request: HostRequest): Promise<HostOutcome>;
51
+ export declare function runHostRequest(request: HostRequest, options?: {
52
+ readonly keyRoot?: string;
53
+ }): Promise<HostOutcome>;
52
54
  //# sourceMappingURL=host-runner.d.ts.map
@@ -33,8 +33,13 @@ import { readHostStatus, readSessionCounts } from "./host-status.js";
33
33
  import { readAllHostStatus } from "./host-status-all.js";
34
34
  import { staleStopGeneration, stopHost } from "./host-stop.js";
35
35
  export { HOST_EXIT_ERROR, HOST_EXIT_FALLBACK, HOST_EXIT_OK, HOST_EXIT_REFUSED, HOST_EXIT_USAGE, } from "./host-outcome.js";
36
+ import { hostRequestKeyRoot } from "./host-request-key-root.js";
37
+ import { withRpcKeyRoot } from "./rpc-auth-client.js";
36
38
  const PROBE_TIMEOUT_MS = 10_000;
37
- export async function runHostRequest(request) {
39
+ export async function runHostRequest(request, options = {}) {
40
+ return withRpcKeyRoot(hostRequestKeyRoot(request, options.keyRoot), () => runHostRequestInKeyRoot(request));
41
+ }
42
+ async function runHostRequestInKeyRoot(request) {
38
43
  if (request.action === "ensure" || request.action === "handoff" || request.action === "stop") {
39
44
  const { socket, agentDir } = request.target;
40
45
  if ((await endpointKindOfSocket(socket, agentDir)) === "tui") {
@@ -0,0 +1,6 @@
1
+ import type { Server } from "node:net";
2
+ import { type SocketFileIdentity } from "./socket-ownership.ts";
3
+ export declare function resolveSocketPath(value: string, agentDir: string): string;
4
+ export declare function formatSocketAddress(socketPath: string): string;
5
+ export declare function listen(server: Server, socketPath: string, secret?: Uint8Array): Promise<SocketFileIdentity | undefined>;
6
+ //# sourceMappingURL=host-socket-bind.d.ts.map
@@ -0,0 +1,42 @@
1
+ /** Where a socket host listens and how it binds: the `--listen` address forms and the owned bind (moved out of multi-session-host.ts). */
2
+ import { chmod } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { statSocketIdentity } from "./socket-ownership.js";
5
+ import { resolveSocketTransportAddress } from "./socket-transport.js";
6
+ export function resolveSocketPath(value, agentDir) {
7
+ if (value === "unix://")
8
+ return join(agentDir, "rpc", "rpc.sock");
9
+ if (value.startsWith("unix://")) {
10
+ const path = value.slice("unix://".length);
11
+ if (path.length === 0)
12
+ return join(agentDir, "rpc", "rpc.sock");
13
+ if (path.startsWith("@") && process.platform === "linux")
14
+ return `\0${path.slice(1)}`;
15
+ return path;
16
+ }
17
+ return value;
18
+ }
19
+ export function formatSocketAddress(socketPath) {
20
+ return socketPath.startsWith("\0") ? `unix://@${socketPath.slice(1)}` : `unix://${socketPath}`;
21
+ }
22
+ export function listen(server, socketPath, secret) {
23
+ return new Promise((resolve, reject) => {
24
+ server.once("error", reject);
25
+ server.listen(resolveSocketTransportAddress(socketPath, process.platform, secret), async () => {
26
+ server.off("error", reject);
27
+ try {
28
+ if (process.platform !== "win32" && !socketPath.startsWith("\0")) {
29
+ await chmod(socketPath, 0o600);
30
+ // Record which filesystem entry THIS listener created; shutdown
31
+ // removes the path only while this identity still matches.
32
+ return resolve(await statSocketIdentity(socketPath));
33
+ }
34
+ resolve(undefined);
35
+ }
36
+ catch (cause) {
37
+ reject(cause);
38
+ }
39
+ });
40
+ });
41
+ }
42
+ //# sourceMappingURL=host-socket-bind.js.map
@@ -5,6 +5,7 @@
5
5
  * behind it. `host-probe` builds the typed questions on top of it.
6
6
  */
7
7
  import { type Socket } from "node:net";
8
+ import { type RpcKeyOptions } from "./rpc-auth-client.ts";
8
9
  export declare const PROBE_REQUEST_ID = "ensure-host-probe";
9
10
  /** One probe's outcome: whether the socket accepted a connection, and the answer if one arrived. */
10
11
  export interface ProbeOutcome {
@@ -14,8 +15,8 @@ export interface ProbeOutcome {
14
15
  readonly kept?: Socket;
15
16
  }
16
17
  /** Sends one command and returns its `data`, or `undefined` for any failure to get a usable answer. */
17
- export declare function requestOnSocket(socketPath: string, command: Readonly<Record<string, unknown>>, timeoutMs: number): Promise<unknown>;
18
- export declare function connectAndAsk(socketPath: string, request: Readonly<Record<string, unknown>>, timeoutMs: number, keep?: boolean): Promise<ProbeOutcome>;
18
+ export declare function requestOnSocket(socketPath: string, command: Readonly<Record<string, unknown>>, timeoutMs: number, options?: RpcKeyOptions): Promise<unknown>;
19
+ export declare function connectAndAsk(socketPath: string, request: Readonly<Record<string, unknown>>, timeoutMs: number, keep?: boolean, options?: RpcKeyOptions): Promise<ProbeOutcome>;
19
20
  export declare function readAnswer(text: string, id?: string): unknown;
20
21
  export declare function isRecord(value: unknown): value is Readonly<Record<string, unknown>>;
21
22
  //# sourceMappingURL=host-socket-request.d.ts.map
@@ -5,14 +5,16 @@
5
5
  * behind it. `host-probe` builds the typed questions on top of it.
6
6
  */
7
7
  import { createConnection } from "node:net";
8
+ import { connectAuthenticatedRpcSocket } from "./rpc-auth-client.js";
9
+ import { RpcEndpointAuthError } from "./rpc-auth-proof.js";
8
10
  import { readSocketSecret, resolveSocketTransportAddress, sendSocketHandshake, socketSecretPath, } from "./socket-transport.js";
9
11
  import { socketNeedsHandshake } from "./tui-socket.js";
10
12
  export const PROBE_REQUEST_ID = "ensure-host-probe";
11
13
  /** Sends one command and returns its `data`, or `undefined` for any failure to get a usable answer. */
12
- export async function requestOnSocket(socketPath, command, timeoutMs) {
13
- return (await connectAndAsk(socketPath, { id: PROBE_REQUEST_ID, ...command }, timeoutMs)).answer;
14
+ export async function requestOnSocket(socketPath, command, timeoutMs, options = {}) {
15
+ return (await connectAndAsk(socketPath, { id: PROBE_REQUEST_ID, ...command }, timeoutMs, false, options)).answer;
14
16
  }
15
- export async function connectAndAsk(socketPath, request, timeoutMs, keep = false) {
17
+ export async function connectAndAsk(socketPath, request, timeoutMs, keep = false, options = {}) {
16
18
  let secret;
17
19
  if (socketNeedsHandshake(socketPath)) {
18
20
  try {
@@ -22,16 +24,39 @@ export async function connectAndAsk(socketPath, request, timeoutMs, keep = false
22
24
  return { connected: false, answer: undefined };
23
25
  }
24
26
  }
25
- return new Promise((resolveProbe) => {
26
- const socket = createConnection(resolveSocketTransportAddress(socketPath, process.platform, secret));
27
+ let authenticated;
28
+ const deadline = Date.now() + timeoutMs;
29
+ if (!socketNeedsHandshake(socketPath)) {
30
+ try {
31
+ authenticated = await connectAuthenticatedRpcSocket(socketPath, request.type === "get_protocol_info" ? request : undefined, options, timeoutMs);
32
+ }
33
+ catch (cause) {
34
+ if (cause instanceof RpcEndpointAuthError)
35
+ throw cause;
36
+ return {
37
+ connected: cause instanceof Error && "rpcSocketConnected" in cause && cause.rpcSocketConnected === true,
38
+ answer: undefined,
39
+ };
40
+ }
41
+ if (request.type === "get_protocol_info") {
42
+ const answer = authenticated.protocol.data ?? {};
43
+ if (keep)
44
+ return { connected: true, answer, kept: authenticated.socket };
45
+ authenticated.socket.destroy();
46
+ return { connected: true, answer };
47
+ }
48
+ }
49
+ return new Promise((resolveProbe, rejectProbe) => {
50
+ const socket = authenticated?.socket ?? createConnection(resolveSocketTransportAddress(socketPath, process.platform, secret));
27
51
  let buffer = "";
28
52
  let settled = false;
29
- let connected = false;
53
+ let connected = authenticated !== undefined;
30
54
  const finish = (value) => {
31
55
  if (settled)
32
56
  return;
33
57
  settled = true;
34
58
  clearTimeout(timeout);
59
+ socket.removeAllListeners("data");
35
60
  if (keep && value !== undefined && !socket.destroyed) {
36
61
  resolveProbe({ connected, answer: value, kept: socket });
37
62
  return;
@@ -39,11 +64,11 @@ export async function connectAndAsk(socketPath, request, timeoutMs, keep = false
39
64
  socket.destroy();
40
65
  resolveProbe({ connected, answer: value });
41
66
  };
42
- const timeout = setTimeout(() => finish(), timeoutMs);
43
- socket.once("connect", () => {
67
+ const timeout = setTimeout(() => finish(), Math.max(0, deadline - Date.now()));
68
+ const send = () => {
44
69
  connected = true;
45
70
  socket.write(`${JSON.stringify(request)}\n`);
46
- });
71
+ };
47
72
  socket.on("data", (chunk) => {
48
73
  buffer += chunk.toString("utf8");
49
74
  for (;;) {
@@ -52,9 +77,25 @@ export async function connectAndAsk(socketPath, request, timeoutMs, keep = false
52
77
  return;
53
78
  const line = buffer.slice(0, newline);
54
79
  buffer = buffer.slice(newline + 1);
80
+ let value;
81
+ try {
82
+ value = JSON.parse(line);
83
+ }
84
+ catch {
85
+ continue;
86
+ }
87
+ if (isRecord(value) &&
88
+ value.id === request.id &&
89
+ (value.errorCode === "auth_required" || value.errorCode === "auth_failed")) {
90
+ clearTimeout(timeout);
91
+ settled = true;
92
+ socket.destroy();
93
+ rejectProbe(new RpcEndpointAuthError(value.errorCode, `${socketPath}; set SENPI_RPC_KEY_ROOT to the host's agent dir`));
94
+ return;
95
+ }
55
96
  // A host broadcasts lifecycle records to every connection, so the reply to THIS
56
97
  // request is the line carrying its id - not simply the first line that arrives.
57
- const answer = readAnswer(line);
98
+ const answer = readAnswer(line, typeof request.id === "string" ? request.id : PROBE_REQUEST_ID);
58
99
  if (answer !== undefined)
59
100
  return finish(answer);
60
101
  }
@@ -67,6 +108,10 @@ export async function connectAndAsk(socketPath, request, timeoutMs, keep = false
67
108
  // of becoming the expected "no existing host" result for the next ensure.
68
109
  if (secret)
69
110
  sendSocketHandshake(socket, secret);
111
+ if (authenticated)
112
+ send();
113
+ else
114
+ socket.once("connect", send);
70
115
  });
71
116
  }
72
117
  export function readAnswer(text, id = PROBE_REQUEST_ID) {
@@ -92,11 +92,12 @@ export declare function readHostStatus(options: HostStatusOptions, read?: HostSt
92
92
  * falls back to the RECORDED generation, so only this says whether the socket named an instance - and
93
93
  * `listing` is every row `list_sessions` returned, whether or not the report publishes them.
94
94
  */
95
- export declare function probeHostStatus(options: HostStatusOptions, read?: HostStatusReadOptions): Promise<{
95
+ export declare function probeHostStatus(options: HostStatusOptions, read?: HostStatusReadOptions): Promise<HostStatusProbe>;
96
+ interface HostStatusProbe {
96
97
  readonly report: HostStatusReport;
97
98
  readonly answered: HostProtocolInfo | undefined;
98
99
  readonly listing: readonly HostSessionRow[];
99
- }>;
100
+ }
100
101
  /**
101
102
  * The occupancy a stop decision is made on. A host that does not answer holds nothing a client can
102
103
  * see, and an empty count is what lets a stop proceed against a socket nobody serves.
@@ -20,6 +20,7 @@ import { electPointer } from "./host-pointer-election.js";
20
20
  import { observeProtocolInfo, requestOnSocket } from "./host-probe.js";
21
21
  import { readHostProcessMetrics } from "./host-process-metrics.js";
22
22
  import { parseSessionRows, readClaimRows } from "./host-status-rows.js";
23
+ import { rpcKeyRoot, withRpcKeyRoot } from "./rpc-auth-client.js";
23
24
  const STATUS_PROBE_TIMEOUT_MS = 10_000;
24
25
  export async function readHostStatus(options, read = {}) {
25
26
  return (await probeHostStatus(options, read)).report;
@@ -31,6 +32,10 @@ export async function readHostStatus(options, read = {}) {
31
32
  * `listing` is every row `list_sessions` returned, whether or not the report publishes them.
32
33
  */
33
34
  export async function probeHostStatus(options, read = {}) {
35
+ // The host's key record lives under its own agent dir; a looker that names it proves the key from there.
36
+ return withRpcKeyRoot(rpcKeyRoot({}, options.agentDir), () => probeHostStatusInKeyRoot(options, read));
37
+ }
38
+ async function probeHostStatusInKeyRoot(options, read) {
34
39
  const paths = createHostDaemonPaths({
35
40
  socket: options.socket,
36
41
  ...(options.agentDir ? { agentDir: options.agentDir } : {}),
@@ -4,6 +4,7 @@
4
4
  */
5
5
  export declare function writeStderrLine(text: string): void;
6
6
  export declare function supervisorLog(message: string): void;
7
+ export declare function writeHostReady(socket: string, coldStart: string, idleExitMs: number): void;
7
8
  export declare function logUnknownHostIdentity(record: "legacy_host.pid" | "host.pid" | "session-path-claim", pid: number): void;
8
9
  export declare function errorMessage(cause: unknown): string;
9
10
  //# sourceMappingURL=host-supervisor-log.d.ts.map
@@ -14,6 +14,9 @@ export function writeStderrLine(text) {
14
14
  export function supervisorLog(message) {
15
15
  writeStderrLine(`senpi rpc host supervisor: ${message}`);
16
16
  }
17
+ export function writeHostReady(socket, coldStart, idleExitMs) {
18
+ writeStderrLine(`senpi rpc host ready on unix://${socket} (coldStart=${coldStart}, idleExitMs=${coldStart === "persistent" ? "never" : String(idleExitMs)})`);
19
+ }
17
20
  // Identity refusal loops share one process-local diagnostic budget, not a persisted latch.
18
21
  const loggedUnknownIdentities = new Set();
19
22
  export function logUnknownHostIdentity(record, pid) {
@@ -41,7 +41,14 @@ export interface HostWatchdogConfig {
41
41
  */
42
42
  export declare function readHostWatchdogConfig(env?: Readonly<Record<string, string | undefined>>): HostWatchdogConfig | undefined;
43
43
  /** Same configuration, resolved through the brand-aware env prefixes. */
44
- export declare function readHostWatchdogConfigFromBrandEnv(): HostWatchdogConfig | undefined;
44
+ export declare function readHostWatchdogConfigFromBrandEnv(env?: NodeJS.ProcessEnv): HostWatchdogConfig | undefined;
45
+ /**
46
+ * Reads the supervisor binding once and removes it from `env` (senpi#3815 review L4), so no tool, shell or nested host
47
+ * this host starts inherits a binding to a supervisor that is not its own. The generation identity (instance id,
48
+ * daemon dir) stays: sessions use it to name the host they run in.
49
+ */
50
+ export declare function consumeHostWatchdogEnv(env?: NodeJS.ProcessEnv): HostWatchdogConfig | undefined;
51
+ export declare function scrubHostWatchdogEnv(env?: NodeJS.ProcessEnv): void;
45
52
  /**
46
53
  * Arms the configured watchdog. `onSupervisorGone` receives the reason and is
47
54
  * expected to perform the host's normal clean shutdown; the scratch directory