@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.
- package/CHANGELOG.md +37 -0
- package/dist/bundle/chunks/{args-6T5EQVY3.js → args-JXULI4EH.js} +1 -1
- package/dist/bundle/chunks/{chunk-VUKNGMDT.js → chunk-3LNSLJ7N.js} +1 -1
- package/dist/bundle/chunks/chunk-3VOG6WFU.js +27 -0
- package/dist/bundle/chunks/{chunk-HHYSXDYP.js → chunk-42OY5SZZ.js} +2 -2
- package/dist/bundle/chunks/chunk-7IRLFYNM.js +23 -0
- package/dist/bundle/chunks/{chunk-FIBIVBQK.js → chunk-7UUOGZKR.js} +1 -1
- package/dist/bundle/chunks/chunk-BMYNMUVZ.js +7 -0
- package/dist/bundle/chunks/{chunk-JAXZOEJE.js → chunk-BWA4FVFW.js} +1 -1
- package/dist/bundle/chunks/{chunk-AGQSRWNN.js → chunk-EHQXLHRB.js} +1 -1
- package/dist/bundle/chunks/chunk-EWOEVVQT.js +4 -0
- package/dist/bundle/chunks/{chunk-YJA2B7FQ.js → chunk-FDL4CF6F.js} +1 -1
- package/dist/bundle/chunks/{chunk-RQHK4QZ5.js → chunk-FWXMMMCV.js} +1 -1
- package/dist/bundle/chunks/{chunk-OA5INWHI.js → chunk-GVJ76VQI.js} +102 -42
- package/dist/bundle/chunks/{chunk-HCF5M6NA.js → chunk-GXY5JEI5.js} +1 -1
- package/dist/bundle/chunks/{chunk-VZAYN5SB.js → chunk-HKUXKTFW.js} +1 -1
- package/dist/bundle/chunks/chunk-II22E3CV.js +3 -0
- package/dist/bundle/chunks/{chunk-ONLNXGO3.js → chunk-IUHU5PEC.js} +1 -1
- package/dist/bundle/chunks/{chunk-LR6Z6V54.js → chunk-NQC7MI2G.js} +1 -1
- package/dist/bundle/chunks/{chunk-IELM5U6I.js → chunk-PXKF2RHD.js} +2 -2
- package/dist/bundle/chunks/{chunk-BRXN5R2F.js → chunk-QH2HSCMX.js} +1 -1
- package/dist/bundle/chunks/{chunk-QQSCUEYJ.js → chunk-RHHBXIRN.js} +2 -5
- package/dist/bundle/chunks/chunk-RVYSJBZ7.js +7 -0
- package/dist/bundle/chunks/chunk-SGDOZT6B.js +8 -0
- package/dist/bundle/chunks/{chunk-BZ75DRZB.js → chunk-U67ML6CQ.js} +4 -2
- package/dist/bundle/chunks/{chunk-3N2PQEQV.js → chunk-UT6AJHPK.js} +1 -1
- package/dist/bundle/chunks/{chunk-RAIZG6HA.js → chunk-V3CK2AVC.js} +1 -1
- package/dist/bundle/chunks/{chunk-CZR7C6CY.js → chunk-VOBD6FQG.js} +1 -1
- package/dist/bundle/chunks/{chunk-HRPMLI7O.js → chunk-VRDU425E.js} +2 -2
- package/dist/bundle/chunks/{chunk-OUPD5LWY.js → chunk-XUFUKDKJ.js} +3 -3
- package/dist/bundle/chunks/{chunk-SGATKHO4.js → chunk-Y5OTHMXO.js} +1 -1
- package/dist/bundle/chunks/{chunk-QEQG42HL.js → chunk-ZGAB7WQA.js} +1 -1
- package/dist/bundle/chunks/cli-main-UH3MAREK.js +3 -0
- package/dist/bundle/chunks/{dispatch-KQEGIXQW.js → dispatch-DEP4PEDZ.js} +3 -2
- package/dist/bundle/chunks/{help-fast-path-6PVN7WI5.js → help-fast-path-ZHZGXAUG.js} +1 -1
- package/dist/bundle/chunks/host-command-OQR4RDTM.js +2 -0
- package/dist/bundle/chunks/host-lifecycle-U36U7JTE.js +3 -0
- package/dist/bundle/chunks/host-lifecycle.js +26 -21
- package/dist/bundle/chunks/host-session-command-IGB27ZPJ.js +20 -0
- package/dist/bundle/chunks/{host-socket-request-KKGH3JHV.js → host-socket-request-ZDJGSSZO.js} +1 -1
- package/dist/bundle/chunks/interactive-mode-R5RCSJ2F.js +2 -0
- package/dist/bundle/chunks/{list-tips-BO5RP34W.js → list-tips-CREX4IXZ.js} +1 -1
- package/dist/bundle/chunks/{multi-session-host-O5GZQSRL.js → multi-session-host-YZEMYUFY.js} +12 -12
- package/dist/bundle/chunks/{package-manager-cli-LC5GAGTW.js → package-manager-cli-5FOXDKUA.js} +1 -1
- package/dist/bundle/chunks/{role-4G43UDOC.js → role-M2MTVACJ.js} +1 -1
- package/dist/bundle/chunks/{rotation-stream-RXMCJJUL.js → rotation-stream-U7YQMC7H.js} +1 -1
- package/dist/bundle/chunks/rpc-mode-6K62CZBW.js +2 -0
- package/dist/bundle/chunks/rpc-monitor-source-OL5KU6NO.js +3 -0
- package/dist/bundle/chunks/{schedule-command-QNCVE4HM.js → schedule-command-XBW4EOG5.js} +1 -1
- package/dist/bundle/chunks/session-control-endpoint-54TMCS7Q.js +3 -0
- package/dist/bundle/chunks/{session-picker-HIC5ZTE6.js → session-picker-P2VUU2MQ.js} +1 -1
- package/dist/bundle/chunks/session-worker.js +437 -368
- package/dist/bundle/cli.js +2 -2
- package/dist/bundle/host-supervisor.js +26 -21
- package/dist/bundle/index.js +1 -1
- package/dist/bundle/rpc-entry.js +1 -1
- package/dist/bundle/runtime-manifest.json +1 -1
- package/dist/cli/args.d.ts +2 -0
- package/dist/cli/args.js +27 -0
- package/dist/cli/host-command-args.d.ts +33 -0
- package/dist/cli/host-command-args.js +145 -0
- package/dist/cli/host-command.d.ts +1 -30
- package/dist/cli/host-command.js +5 -137
- package/dist/cli/host-session-command.d.ts +1 -0
- package/dist/cli/host-session-command.js +5 -3
- package/dist/core/agent-session.js +1 -1
- package/dist/core/extensions/builtin/anthropic-subscription/index.js +4 -2
- package/dist/core/extensions/builtin/anthropic-subscription/options.js +2 -1
- package/dist/core/extensions/builtin/anthropic-subscription/sdk-boundary.js +9 -2
- package/dist/core/extensions/builtin/anthropic-subscription/session-cwd.d.ts +3 -0
- package/dist/core/extensions/builtin/anthropic-subscription/session-cwd.js +9 -0
- package/dist/core/extensions/builtin/anthropic-subscription/settings.d.ts +1 -1
- package/dist/core/extensions/builtin/anthropic-subscription/settings.js +3 -1
- package/dist/core/extensions/builtin/anthropic-subscription/stream.js +4 -1
- package/dist/core/extensions/builtin/btw/index.js +4 -1
- package/dist/core/extensions/builtin/compaction/lane-policy.js +3 -1
- package/dist/core/extensions/builtin/look-at/runner.js +1 -0
- package/dist/core/extensions/builtin/mcp/connection-types.d.ts +2 -0
- package/dist/core/extensions/builtin/mcp/connection.js +3 -0
- package/dist/core/extensions/builtin/mcp/elicitation.d.ts +5 -2
- package/dist/core/extensions/builtin/mcp/elicitation.js +57 -11
- package/dist/core/extensions/builtin/mcp/index.js +1 -1
- package/dist/core/extensions/builtin/mcp/modern/dispatch.js +6 -9
- package/dist/core/extensions/builtin/mcp/service-connection.d.ts +1 -0
- package/dist/core/extensions/builtin/mcp/service-connection.js +1 -0
- package/dist/core/extensions/builtin/mcp/service.js +1 -0
- package/dist/core/extensions/builtin/mcp/transport-sdk.d.ts +1 -0
- package/dist/core/extensions/builtin/mcp/transport-sdk.js +5 -5
- package/dist/core/extensions/builtin/mcp/transport.d.ts +1 -0
- package/dist/core/extensions/builtin/mcp/transport.js +1 -0
- package/dist/core/extensions/builtin/prompt-preset/contract.js +6 -1
- package/dist/core/extensions/builtin/prompt-preset/kimi-k3-1.d.ts +3 -0
- package/dist/core/extensions/builtin/prompt-preset/kimi-k3-1.js +93 -0
- package/dist/core/extensions/builtin/prompt-preset/matchers.d.ts +2 -0
- package/dist/core/extensions/builtin/prompt-preset/matchers.js +9 -0
- package/dist/core/extensions/builtin/prompt-preset/presets.js +3 -0
- package/dist/core/extensions/builtin/prompt-preset/settings.d.ts +1 -1
- package/dist/core/extensions/builtin/prompt-preset/settings.js +1 -0
- package/dist/core/extensions/builtin/terminal/rpc-monitor-observer.d.ts +2 -1
- package/dist/core/extensions/builtin/terminal/rpc-monitor-observer.js +12 -4
- package/dist/core/external-admission.d.ts +4 -1
- package/dist/core/external-admission.js +18 -2
- package/dist/core/lifecycle-session-binding.js +20 -0
- package/dist/core/sdk.js +1 -0
- package/dist/core/session-spawned-by.d.ts +4 -0
- package/dist/core/session-spawned-by.js +16 -0
- package/dist/main.js +7 -2
- package/dist/modes/rpc/host-admission-proxy.d.ts +2 -1
- package/dist/modes/rpc/host-admission-proxy.js +10 -9
- package/dist/modes/rpc/host-cli-entry.d.ts +2 -0
- package/dist/modes/rpc/host-cli-entry.js +13 -1
- package/dist/modes/rpc/host-ensure-client.d.ts +5 -1
- package/dist/modes/rpc/host-ensure-client.js +8 -0
- package/dist/modes/rpc/host-ensure-types.d.ts +1 -0
- package/dist/modes/rpc/host-ensure.js +6 -11
- package/dist/modes/rpc/host-gc-pass.js +3 -0
- package/dist/modes/rpc/host-gc.js +3 -0
- package/dist/modes/rpc/host-internal-transport.d.ts +16 -5
- package/dist/modes/rpc/host-internal-transport.js +40 -9
- package/dist/modes/rpc/host-lifecycle-proxy.d.ts +4 -8
- package/dist/modes/rpc/host-lifecycle-proxy.js +9 -21
- package/dist/modes/rpc/host-lifecycle-shutdown.d.ts +2 -0
- package/dist/modes/rpc/host-lifecycle-shutdown.js +3 -0
- package/dist/modes/rpc/host-lifecycle.js +9 -16
- package/dist/modes/rpc/host-observe-request.js +1 -1
- package/dist/modes/rpc/host-probe.d.ts +8 -6
- package/dist/modes/rpc/host-probe.js +13 -13
- package/dist/modes/rpc/host-request-key-root.d.ts +8 -0
- package/dist/modes/rpc/host-request-key-root.js +14 -0
- package/dist/modes/rpc/host-runner.d.ts +3 -1
- package/dist/modes/rpc/host-runner.js +6 -1
- package/dist/modes/rpc/host-socket-bind.d.ts +6 -0
- package/dist/modes/rpc/host-socket-bind.js +42 -0
- package/dist/modes/rpc/host-socket-request.d.ts +3 -2
- package/dist/modes/rpc/host-socket-request.js +55 -10
- package/dist/modes/rpc/host-status.d.ts +3 -2
- package/dist/modes/rpc/host-status.js +5 -0
- package/dist/modes/rpc/host-supervisor-log.d.ts +1 -0
- package/dist/modes/rpc/host-supervisor-log.js +3 -0
- package/dist/modes/rpc/host-watchdog.d.ts +8 -1
- package/dist/modes/rpc/host-watchdog.js +37 -12
- package/dist/modes/rpc/internal-channel.js +14 -14
- package/dist/modes/rpc/multi-session-host.js +18 -46
- package/dist/modes/rpc/rpc-auth-client.d.ts +13 -0
- package/dist/modes/rpc/rpc-auth-client.js +142 -0
- package/dist/modes/rpc/rpc-auth-proof.d.ts +12 -0
- package/dist/modes/rpc/rpc-auth-proof.js +27 -0
- package/dist/modes/rpc/rpc-client-socket.d.ts +5 -0
- package/dist/modes/rpc/rpc-client-socket.js +30 -0
- package/dist/modes/rpc/rpc-client.d.ts +2 -0
- package/dist/modes/rpc/rpc-client.js +2 -24
- package/dist/modes/rpc/rpc-host-command-types.d.ts +4 -0
- package/dist/modes/rpc/rpc-protocol-info-types.d.ts +6 -0
- package/dist/modes/rpc/rpc-public-auth.d.ts +41 -0
- package/dist/modes/rpc/rpc-public-auth.js +237 -0
- package/dist/modes/rpc/rpc-public-key.d.ts +28 -0
- package/dist/modes/rpc/rpc-public-key.js +193 -0
- package/dist/modes/rpc/session-router-open-profile.js +2 -10
- package/dist/modes/rpc/socket-ownership.d.ts +7 -0
- package/dist/modes/rpc/socket-ownership.js +17 -0
- package/docs/rpc.md +108 -2
- package/docs/settings.md +1 -1
- package/docs/usage.md +14 -0
- package/package.json +6 -6
- package/dist/bundle/chunks/chunk-3474DPZK.js +0 -2
- package/dist/bundle/chunks/chunk-64CFJHKT.js +0 -7
- package/dist/bundle/chunks/chunk-B6NJHBLH.js +0 -21
- package/dist/bundle/chunks/chunk-HZTQHE5O.js +0 -26
- package/dist/bundle/chunks/chunk-IBDADH5Z.js +0 -4
- package/dist/bundle/chunks/chunk-OJZD7CO7.js +0 -8
- package/dist/bundle/chunks/cli-main-4JNPG6LB.js +0 -3
- package/dist/bundle/chunks/host-command-I3ZMWQQ3.js +0 -2
- package/dist/bundle/chunks/host-lifecycle-QEO422VW.js +0 -3
- package/dist/bundle/chunks/host-session-command-INULKX6E.js +0 -20
- package/dist/bundle/chunks/interactive-mode-SYWJVPKK.js +0 -2
- package/dist/bundle/chunks/rpc-mode-JMCYSP4P.js +0 -2
- package/dist/bundle/chunks/rpc-monitor-source-34AQDTZ6.js +0 -3
- package/dist/bundle/chunks/session-control-endpoint-IW5X56WN.js +0 -3
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { type Server } from "node:net";
|
|
2
2
|
import type { HostAdmission } from "./host-admission.ts";
|
|
3
3
|
import type { ClientOccupancy } from "./host-client-occupancy.ts";
|
|
4
|
-
import
|
|
4
|
+
import type { RpcPublicEndpoint } from "./rpc-public-key.ts";
|
|
5
|
+
import { type ListenerState } from "./socket-ownership.ts";
|
|
6
|
+
export { adoptPublicSocket } from "./socket-ownership.ts";
|
|
5
7
|
export interface PublicProxyOptions {
|
|
6
8
|
readonly internalSocket: string;
|
|
7
9
|
readonly internalSecret?: Buffer;
|
|
8
10
|
readonly internalKey?: Buffer;
|
|
9
11
|
readonly publicSecret?: Buffer;
|
|
12
|
+
readonly publicEndpoint?: RpcPublicEndpoint;
|
|
10
13
|
readonly clients: ClientOccupancy;
|
|
11
14
|
/** Shared authority for stopping, protocol replies and new-session admission. */
|
|
12
15
|
readonly admission: HostAdmission;
|
|
@@ -16,13 +19,6 @@ export interface PublicProxyOptions {
|
|
|
16
19
|
readonly onAdmit?: () => void;
|
|
17
20
|
}
|
|
18
21
|
export declare function createPublicProxy(options: PublicProxyOptions): Server;
|
|
19
|
-
/**
|
|
20
|
-
* Takes the public name over with a rename, once - and only while - that name still refers to the
|
|
21
|
-
* entry this handoff was decided against. A socket that changed underneath belongs to another
|
|
22
|
-
* process now: replacing it would unlink an endpoint this generation cannot prove it owns, so the
|
|
23
|
-
* successor aborts instead and leaves both the intruder's socket and its own bind entry alone.
|
|
24
|
-
*/
|
|
25
|
-
export declare function adoptPublicSocket(bindSocket: string, publicSocket: string, expected: SocketFileIdentity | undefined): Promise<void>;
|
|
26
22
|
/**
|
|
27
23
|
* Reuses an existing valid secret - including one ensureHost() just wrote - and
|
|
28
24
|
* creates one (with its parent directories, mode 0600) when it is missing or
|
|
@@ -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,
|
|
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 {
|
|
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.
|
|
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
|
-
|
|
202
|
+
writeHostReady(publicSocket, policy.coldStart, policy.idleExitMs);
|
|
199
203
|
await new Promise(() => { });
|
|
200
204
|
}
|
|
201
|
-
|
|
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
|
|
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
|
-
|
|
26
|
-
|
|
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 =
|
|
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(),
|
|
43
|
-
|
|
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
|