harnery 0.45.0 → 0.45.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commander.js +0 -1
- package/dist/commands/files.d.ts +2 -2
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +4 -7
- package/dist/commands/servers.d.ts.map +1 -1
- package/dist/commands/servers.js +17 -11
- package/dist/commands/tunnel.d.ts +0 -15
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +153 -453
- package/dist/core/agents/live-authority-v3.d.ts +0 -1
- package/dist/core/agents/live-authority-v3.d.ts.map +1 -1
- package/dist/core/agents/live-authority-v3.js +0 -11
- package/dist/core/agents/live-lifecycle-v3.d.ts +2 -4
- package/dist/core/agents/live-lifecycle-v3.d.ts.map +1 -1
- package/dist/core/agents/live-lifecycle-v3.js +7 -28
- package/dist/core/agents/state/heartbeat-writer.d.ts.map +1 -1
- package/dist/core/agents/state/heartbeat-writer.js +14 -25
- package/dist/core/agents/state/live-coordination-writer.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-writer.js +0 -5
- package/dist/core/agents/state/pidmap.d.ts.map +1 -1
- package/dist/core/agents/state/pidmap.js +0 -5
- package/dist/core/agents/state/stale-sweep.d.ts.map +1 -1
- package/dist/core/agents/state/stale-sweep.js +70 -180
- package/dist/core/artifacts/activity.d.ts +4 -1
- package/dist/core/artifacts/activity.d.ts.map +1 -1
- package/dist/core/artifacts/activity.js +8 -2
- package/dist/core/artifacts/delivery-card.d.ts.map +1 -1
- package/dist/core/artifacts/delivery-card.js +1 -2
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +38 -12
- package/dist/core/events/v3/authority-outbox.d.ts.map +1 -1
- package/dist/core/events/v3/authority-outbox.js +0 -29
- package/dist/core/events/v3/producers/coordination-recorder.d.ts +0 -1
- package/dist/core/events/v3/producers/coordination-recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/coordination-recorder.js +0 -7
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +64 -77
- package/dist/core/hooks/cli.js +78 -48
- package/dist/core/servers/index.d.ts +13 -10
- package/dist/core/servers/index.d.ts.map +1 -1
- package/dist/core/servers/index.js +97 -192
- package/dist/core/servers/tunnels.d.ts +3 -5
- package/dist/core/servers/tunnels.d.ts.map +1 -1
- package/dist/core/servers/tunnels.js +2 -45
- package/dist/core/storage/durable-history.js +56 -22
- package/dist/core/storage/segments.d.ts.map +1 -1
- package/dist/core/storage/segments.js +73 -17
- package/dist/core/work/state.js +7 -7
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +21 -65
- package/dist/core/workflow/live-session-v3.d.ts +0 -6
- package/dist/core/workflow/live-session-v3.d.ts.map +1 -1
- package/dist/core/workflow/live-session-v3.js +5 -126
- package/dist/core/workflow/proof.d.ts +4 -4
- package/dist/core/workflow/proof.d.ts.map +1 -1
- package/dist/core/workflow/proof.js +4 -12
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +0 -19
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +0 -13
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +0 -19
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
- package/dist/core/workflow/spawn-opencode.js +0 -14
- package/dist/core/workflow/types.d.ts +11 -54
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/lib/identities/assume.d.ts.map +1 -1
- package/dist/lib/identities/assume.js +0 -4
- package/dist/lib/tunnel/state.d.ts +6 -13
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +12 -58
- package/package.json +1 -6
- package/src/commander.ts +0 -10
- package/src/commands/files.ts +5 -13
- package/src/commands/servers.ts +23 -12
- package/src/commands/tunnel.ts +166 -533
- package/src/core/agents/live-authority-v3.ts +0 -17
- package/src/core/agents/live-lifecycle-v3.ts +14 -52
- package/src/core/agents/state/heartbeat-writer.ts +12 -29
- package/src/core/agents/state/live-coordination-writer.ts +0 -29
- package/src/core/agents/state/pidmap.ts +0 -13
- package/src/core/agents/state/stale-sweep.ts +82 -247
- package/src/core/artifacts/activity.ts +9 -2
- package/src/core/artifacts/delivery-card.ts +1 -7
- package/src/core/artifacts/index.ts +45 -12
- package/src/core/events/v3/authority-outbox.ts +0 -56
- package/src/core/events/v3/producers/coordination-recorder.ts +0 -15
- package/src/core/events/v3/producers/recorder.ts +68 -94
- package/src/core/hooks/cli.ts +85 -47
- package/src/core/servers/index.ts +106 -228
- package/src/core/servers/tunnels.ts +3 -55
- package/src/core/storage/durable-history.ts +52 -20
- package/src/core/storage/segments.ts +79 -17
- package/src/core/work/state.ts +7 -7
- package/src/core/workflow/engine.ts +21 -73
- package/src/core/workflow/live-session-v3.ts +5 -146
- package/src/core/workflow/proof.ts +5 -21
- package/src/core/workflow/spawn-claude.ts +0 -18
- package/src/core/workflow/spawn-codex.ts +0 -13
- package/src/core/workflow/spawn-cursor.ts +0 -18
- package/src/core/workflow/spawn-opencode.ts +0 -14
- package/src/core/workflow/types.ts +11 -41
- package/src/lib/identities/assume.ts +0 -4
- package/src/lib/tunnel/state.ts +15 -70
- package/dist/commands/turn-rollback.d.ts +0 -5
- package/dist/commands/turn-rollback.d.ts.map +0 -1
- package/dist/commands/turn-rollback.js +0 -76
- package/dist/core/agents/state/unchanged-file.d.ts +0 -44
- package/dist/core/agents/state/unchanged-file.d.ts.map +0 -1
- package/dist/core/agents/state/unchanged-file.js +0 -390
- package/dist/core/events/v3/generation-custody.d.ts +0 -5
- package/dist/core/events/v3/generation-custody.d.ts.map +0 -1
- package/dist/core/events/v3/generation-custody.js +0 -50
- package/dist/core/turn-rollback/content.d.ts +0 -53
- package/dist/core/turn-rollback/content.d.ts.map +0 -1
- package/dist/core/turn-rollback/content.js +0 -396
- package/dist/core/turn-rollback/index.d.ts +0 -72
- package/dist/core/turn-rollback/index.d.ts.map +0 -1
- package/dist/core/turn-rollback/index.js +0 -443
- package/dist/core/workflow/failure-evidence.d.ts +0 -16
- package/dist/core/workflow/failure-evidence.d.ts.map +0 -1
- package/dist/core/workflow/failure-evidence.js +0 -114
- package/src/commands/turn-rollback.ts +0 -111
- package/src/core/agents/state/unchanged-file.ts +0 -456
- package/src/core/events/v3/generation-custody.ts +0 -59
- package/src/core/turn-rollback/content.ts +0 -499
- package/src/core/turn-rollback/index.ts +0 -580
- package/src/core/workflow/failure-evidence.ts +0 -141
package/dist/core/hooks/cli.js
CHANGED
|
@@ -631,6 +631,9 @@ async function main() {
|
|
|
631
631
|
if (eventName === "runtime-context-retry") {
|
|
632
632
|
return runRuntimeContextRetryWorker(coordRoot, adapter, extra);
|
|
633
633
|
}
|
|
634
|
+
if (eventName === SESSION_MAINTENANCE_EVENT) {
|
|
635
|
+
return runSessionMaintenanceWorker(coordRoot, adapter);
|
|
636
|
+
}
|
|
634
637
|
const ledgerRoute = resolveLiveEventLedgerRouteV3(coordRoot);
|
|
635
638
|
// Always log a breadcrumb, useful when an event_type maps to null or owner
|
|
636
639
|
// resolution fails. Stays cheap (one append) and self-prunes via repo log
|
|
@@ -1007,7 +1010,7 @@ function recordContextTelemetry(run) {
|
|
|
1007
1010
|
}
|
|
1008
1011
|
}
|
|
1009
1012
|
async function handleSessionStarted(run) {
|
|
1010
|
-
const { coordRoot, adapter,
|
|
1013
|
+
const { coordRoot, adapter, payload, owner, sessionId, data } = run;
|
|
1011
1014
|
// A configured backup runs out of process and is freshness-gated per host.
|
|
1012
1015
|
// Startup never waits for restic or a remote provider; the child owns no
|
|
1013
1016
|
// hook stdio, so the harness is not held open until it finishes. A failed
|
|
@@ -1019,53 +1022,10 @@ async function handleSessionStarted(run) {
|
|
|
1019
1022
|
catch (err) {
|
|
1020
1023
|
logError(coordRoot, err, { phase: "backup-schedule" });
|
|
1021
1024
|
}
|
|
1022
|
-
//
|
|
1023
|
-
//
|
|
1024
|
-
//
|
|
1025
|
-
|
|
1026
|
-
journalJanitor(coordRoot);
|
|
1027
|
-
imageJanitor(coordRoot);
|
|
1028
|
-
// Session start is the only caller of the artifact janitor and the storage
|
|
1029
|
-
// maintenance pass, so both load here instead of on every tool call.
|
|
1030
|
-
const { autoCleanArtifacts } = await import("../artifacts/index.js");
|
|
1031
|
-
const { autoCleanEventV3Archives } = await import("../events/v3/archive-retention.js");
|
|
1032
|
-
const { createAutomaticMaintenanceComposition, runAutomaticMaintenancePass } = await import("../storage/maintenance-providers.js");
|
|
1033
|
-
// Effect: throttled hourly artifact sweep (see autoCleanArtifacts). It
|
|
1034
|
-
// deletes expired workspaces, plus unheld workspaces over the per-bundle
|
|
1035
|
-
// ceiling or repository budget once they have been idle past
|
|
1036
|
-
// artifacts.idle_grace_hours. Each deletion is rechecked first and recorded
|
|
1037
|
-
// in .harnery/artifact-deletions.jsonl. Best-effort like every janitor
|
|
1038
|
-
// here: a failure logs and never blocks session start.
|
|
1039
|
-
try {
|
|
1040
|
-
autoCleanArtifacts(coordRoot);
|
|
1041
|
-
}
|
|
1042
|
-
catch (err) {
|
|
1043
|
-
logError(coordRoot, err, { phase: "artifact-auto-clean" });
|
|
1044
|
-
}
|
|
1045
|
-
// Effect: apply the same daily throttle to closed V3 epochs. The newest
|
|
1046
|
-
// recovery epochs remain protected by the archive retention policy.
|
|
1047
|
-
try {
|
|
1048
|
-
autoCleanEventV3Archives(coordRoot);
|
|
1049
|
-
}
|
|
1050
|
-
catch (err) {
|
|
1051
|
-
logError(coordRoot, err, { phase: "event-v3-archive-auto-clean" });
|
|
1052
|
-
}
|
|
1053
|
-
// Plan one bounded, claim-first global slice from cached pressure only.
|
|
1054
|
-
// Existing owner janitors above remain the only active executors until
|
|
1055
|
-
// each provider receives a separate production activation.
|
|
1056
|
-
try {
|
|
1057
|
-
await runAutomaticMaintenancePass(createAutomaticMaintenanceComposition(coordRoot, {
|
|
1058
|
-
journal: () => {
|
|
1059
|
-
if (behavior.journalMaintenance)
|
|
1060
|
-
journalJanitor(coordRoot);
|
|
1061
|
-
},
|
|
1062
|
-
images: () => imageJanitor(coordRoot),
|
|
1063
|
-
artifacts: () => autoCleanArtifacts(coordRoot),
|
|
1064
|
-
}));
|
|
1065
|
-
}
|
|
1066
|
-
catch (err) {
|
|
1067
|
-
logError(coordRoot, err, { phase: "storage-maintenance-auto" });
|
|
1068
|
-
}
|
|
1025
|
+
// Storage maintenance can walk large stores. It runs in a detached worker so
|
|
1026
|
+
// session start emits its context without waiting for it; an adapter that
|
|
1027
|
+
// stops a slow hook would otherwise drop that context entirely.
|
|
1028
|
+
scheduleSessionMaintenance(coordRoot, adapter);
|
|
1069
1029
|
let recovery = null;
|
|
1070
1030
|
if (payload?.source === "compact") {
|
|
1071
1031
|
try {
|
|
@@ -1518,6 +1478,76 @@ async function handleCompletedResponse(run) {
|
|
|
1518
1478
|
});
|
|
1519
1479
|
return 0;
|
|
1520
1480
|
}
|
|
1481
|
+
const SESSION_MAINTENANCE_EVENT = "session-maintenance";
|
|
1482
|
+
function scheduleSessionMaintenance(coordRoot, adapter) {
|
|
1483
|
+
const modulePath = process.argv[1];
|
|
1484
|
+
if (!modulePath)
|
|
1485
|
+
return;
|
|
1486
|
+
try {
|
|
1487
|
+
const child = spawn(process.execPath, [modulePath, SESSION_MAINTENANCE_EVENT, "--adapter", adapter], {
|
|
1488
|
+
cwd: coordRoot,
|
|
1489
|
+
detached: true,
|
|
1490
|
+
stdio: "ignore",
|
|
1491
|
+
env: childEnv(coordRoot),
|
|
1492
|
+
});
|
|
1493
|
+
child.unref();
|
|
1494
|
+
}
|
|
1495
|
+
catch (err) {
|
|
1496
|
+
logError(coordRoot, err, { phase: "session-maintenance-spawn" });
|
|
1497
|
+
}
|
|
1498
|
+
}
|
|
1499
|
+
/**
|
|
1500
|
+
* Session-start storage maintenance, run out of the hook's process. Every step
|
|
1501
|
+
* is best-effort and throttled by its own stamp: a failure logs and never
|
|
1502
|
+
* reaches the session.
|
|
1503
|
+
*/
|
|
1504
|
+
async function runSessionMaintenanceWorker(coordRoot, adapter) {
|
|
1505
|
+
const journalMaintenance = adapter ? adapterBehavior(adapter).journalMaintenance : false;
|
|
1506
|
+
// Effect (journal-maintaining adapters): prune stale journal archives +
|
|
1507
|
+
// sweep orphans. The recovery cue is read separately at session start.
|
|
1508
|
+
if (journalMaintenance)
|
|
1509
|
+
journalJanitor(coordRoot);
|
|
1510
|
+
imageJanitor(coordRoot);
|
|
1511
|
+
const { autoCleanArtifacts } = await import("../artifacts/index.js");
|
|
1512
|
+
const { autoCleanEventV3Archives } = await import("../events/v3/archive-retention.js");
|
|
1513
|
+
const { createAutomaticMaintenanceComposition, runAutomaticMaintenancePass } = await import("../storage/maintenance-providers.js");
|
|
1514
|
+
// Effect: throttled hourly artifact sweep (see autoCleanArtifacts). It
|
|
1515
|
+
// deletes expired workspaces, plus unheld workspaces over the per-bundle
|
|
1516
|
+
// ceiling or repository budget once they have been idle past
|
|
1517
|
+
// artifacts.idle_grace_hours. Each deletion is rechecked first and recorded
|
|
1518
|
+
// in .harnery/artifact-deletions.jsonl.
|
|
1519
|
+
try {
|
|
1520
|
+
autoCleanArtifacts(coordRoot);
|
|
1521
|
+
}
|
|
1522
|
+
catch (err) {
|
|
1523
|
+
logError(coordRoot, err, { phase: "artifact-auto-clean" });
|
|
1524
|
+
}
|
|
1525
|
+
// Effect: apply the same daily throttle to closed V3 epochs. The newest
|
|
1526
|
+
// recovery epochs remain protected by the archive retention policy.
|
|
1527
|
+
try {
|
|
1528
|
+
autoCleanEventV3Archives(coordRoot);
|
|
1529
|
+
}
|
|
1530
|
+
catch (err) {
|
|
1531
|
+
logError(coordRoot, err, { phase: "event-v3-archive-auto-clean" });
|
|
1532
|
+
}
|
|
1533
|
+
// Plan one bounded, claim-first global slice from cached pressure only.
|
|
1534
|
+
// Existing owner janitors above remain the only active executors until
|
|
1535
|
+
// each provider receives a separate production activation.
|
|
1536
|
+
try {
|
|
1537
|
+
await runAutomaticMaintenancePass(createAutomaticMaintenanceComposition(coordRoot, {
|
|
1538
|
+
journal: () => {
|
|
1539
|
+
if (journalMaintenance)
|
|
1540
|
+
journalJanitor(coordRoot);
|
|
1541
|
+
},
|
|
1542
|
+
images: () => imageJanitor(coordRoot),
|
|
1543
|
+
artifacts: () => autoCleanArtifacts(coordRoot),
|
|
1544
|
+
}));
|
|
1545
|
+
}
|
|
1546
|
+
catch (err) {
|
|
1547
|
+
logError(coordRoot, err, { phase: "storage-maintenance-auto" });
|
|
1548
|
+
}
|
|
1549
|
+
return 0;
|
|
1550
|
+
}
|
|
1521
1551
|
function scheduleRuntimeContextRetry(coordRoot, adapter, nativeSessionId) {
|
|
1522
1552
|
const modulePath = process.argv[1];
|
|
1523
1553
|
if (!modulePath)
|
|
@@ -24,15 +24,14 @@ import { type ScanSupport } from "./net.js";
|
|
|
24
24
|
export { processStartToken, signalIfSameGeneration, verifyProcessGeneration, } from "../agents/state/proc-start.js";
|
|
25
25
|
export { acquireNoClobberLease } from "../workflow/workspaces/leases.js";
|
|
26
26
|
export { ancestry, establishedConnectionCounts, listListeningPorts, listListeningSockets, lsofConnectionCounts, lsofListeningSockets, parseElapsed, parseLsofListen, parseProcNetTcp, processStartedAt, psProcessInfo, readProcessInfo, scanSupport, } from "./net.js";
|
|
27
|
-
export {
|
|
28
|
-
export declare const SERVER_RECORD_SCHEMA_VERSION =
|
|
27
|
+
export { tunnelServerId, tunnelServerInput } from "./tunnels.js";
|
|
28
|
+
export declare const SERVER_RECORD_SCHEMA_VERSION = 1;
|
|
29
29
|
export declare const SERVER_DEFAULT_IDLE_HOURS = 2;
|
|
30
30
|
export declare const SERVER_DEFAULT_OWNER_STALE_HOURS = 24;
|
|
31
31
|
export type ServerKind = "service" | "session";
|
|
32
32
|
export interface ServerRecord {
|
|
33
33
|
schema_version: typeof SERVER_RECORD_SCHEMA_VERSION;
|
|
34
34
|
id: string;
|
|
35
|
-
registration_id: string;
|
|
36
35
|
kind: ServerKind;
|
|
37
36
|
/** Free-form family name: `preview`, `tunnel`, `dashboard`, ... */
|
|
38
37
|
type: string;
|
|
@@ -122,11 +121,13 @@ export declare function isValidServerId(id: string): boolean;
|
|
|
122
121
|
export declare function registerServer(input: RegisterServerInput, options?: ServerOptions): ServerRecord | null;
|
|
123
122
|
/** Best-effort registration that never throws (for server start paths). */
|
|
124
123
|
export declare function tryRegisterServer(input: RegisterServerInput, options?: ServerOptions): ServerRecord | null;
|
|
125
|
-
/**
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
export declare function
|
|
124
|
+
/**
|
|
125
|
+
* Remove a server's record. With `pid`, only a record still naming that pid
|
|
126
|
+
* is removed, so a replacement that re-registered the same id survives.
|
|
127
|
+
*/
|
|
128
|
+
export declare function unregisterServer(id: string, options?: ServerOptions & {
|
|
129
|
+
pid?: number;
|
|
130
|
+
}): boolean;
|
|
130
131
|
export declare function readServer(id: string, options?: ServerOptions): ServerRecord | null;
|
|
131
132
|
/** Mark a server as used now. */
|
|
132
133
|
export declare function touchServer(id: string, options?: ServerOptions & {
|
|
@@ -184,11 +185,13 @@ export interface StopResult {
|
|
|
184
185
|
}
|
|
185
186
|
export interface ServerProcessDependencies extends ProcessGenerationSignalDependencies {
|
|
186
187
|
probePid?: (pid: number) => boolean;
|
|
188
|
+
/** Numeric signal, used for the process group and in tests. */
|
|
189
|
+
kill?: (pid: number, signal: NodeJS.Signals) => void;
|
|
187
190
|
}
|
|
188
191
|
/**
|
|
189
192
|
* Stop a verified server with SIGTERM, then SIGKILL after the grace period.
|
|
190
|
-
*
|
|
191
|
-
*
|
|
193
|
+
* Each signal, including the process group and auxiliary processes, requires
|
|
194
|
+
* a fresh generation check. Unknown or different generations retain the record.
|
|
192
195
|
*/
|
|
193
196
|
export declare function stopServer(record: ServerRecord, options?: ServerOptions & {
|
|
194
197
|
graceMs?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/servers/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAQH,OAAO,EACL,KAAK,mCAAmC,EACxC,KAAK,wBAAwB,EAI9B,MAAM,+BAA+B,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/core/servers/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* which `gc` never stops; a record without an owner is never stopped
|
|
20
20
|
* automatically.
|
|
21
21
|
*/
|
|
22
|
-
import { createHash
|
|
22
|
+
import { createHash } from "node:crypto";
|
|
23
23
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
24
24
|
import { hostname } from "node:os";
|
|
25
25
|
import { basename, isAbsolute, join, relative, resolve } from "node:path";
|
|
@@ -31,13 +31,13 @@ import { readCoordinationViewV3, requireAuthoritySafeCoordinationViewV3, } from
|
|
|
31
31
|
import { liveInstanceIdV3 } from "../events/v3/live-route-observer.js";
|
|
32
32
|
import { resolveOwner } from "../hooks/resolve/owner.js";
|
|
33
33
|
import { writePrivateJsonAtomic } from "../storage/atomic-json.js";
|
|
34
|
-
import { acquireNoClobberLease } from "../workflow/workspaces/leases.js";
|
|
35
34
|
import { ancestry, establishedConnectionCounts, listListeningPorts, listListeningSockets, processStartedAt, readProcessInfo, scanSupport, } from "./net.js";
|
|
35
|
+
// Host supervisors verify and signal their own children with the same checks.
|
|
36
36
|
export { processStartToken, signalIfSameGeneration, verifyProcessGeneration, } from "../agents/state/proc-start.js";
|
|
37
37
|
export { acquireNoClobberLease } from "../workflow/workspaces/leases.js";
|
|
38
38
|
export { ancestry, establishedConnectionCounts, listListeningPorts, listListeningSockets, lsofConnectionCounts, lsofListeningSockets, parseElapsed, parseLsofListen, parseProcNetTcp, processStartedAt, psProcessInfo, readProcessInfo, scanSupport, } from "./net.js";
|
|
39
|
-
export {
|
|
40
|
-
export const SERVER_RECORD_SCHEMA_VERSION =
|
|
39
|
+
export { tunnelServerId, tunnelServerInput } from "./tunnels.js";
|
|
40
|
+
export const SERVER_RECORD_SCHEMA_VERSION = 1;
|
|
41
41
|
export const SERVER_DEFAULT_IDLE_HOURS = 2;
|
|
42
42
|
export const SERVER_DEFAULT_OWNER_STALE_HOURS = 24;
|
|
43
43
|
const TOUCH_INTERVAL_MS = 60_000;
|
|
@@ -86,54 +86,40 @@ export function registerServer(input, options) {
|
|
|
86
86
|
!(Number.isInteger(input.port) && input.port > 0 && input.port < 65536))
|
|
87
87
|
throw new Error(`invalid server port: ${input.port}`);
|
|
88
88
|
const id = input.id ?? serverId(input.type, input.scope);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
verifyProcessGeneration(pid, input.start_token) !== "same")
|
|
124
|
-
throw new Error(`pid ${pid} generation could not be verified before registration`);
|
|
125
|
-
if (existsSync(path)) {
|
|
126
|
-
const existing = readRecord(path);
|
|
127
|
-
if (!existing)
|
|
128
|
-
throw new Error(`registration ${id} custody is unknown: invalid or older server record retained`);
|
|
129
|
-
if (sameProcessCustody(existing, record) && serverState(existing) === "running")
|
|
130
|
-
return existing;
|
|
131
|
-
if (existing.host !== hostname() || !allProcessesAbsent(existing))
|
|
132
|
-
throw new Error(`registration ${id} custody is held by a live or unverifiable server`);
|
|
133
|
-
}
|
|
134
|
-
writePrivateJsonAtomic(path, record);
|
|
135
|
-
return record;
|
|
136
|
-
});
|
|
89
|
+
const now = new Date().toISOString();
|
|
90
|
+
const startToken = input.start_token ?? processStartToken(pid);
|
|
91
|
+
const pids = [...new Set(input.pids?.filter((p) => Number.isSafeInteger(p) && p > 0))];
|
|
92
|
+
const pidsStartTokens = {};
|
|
93
|
+
for (const auxiliaryPid of pids) {
|
|
94
|
+
const token = processStartToken(auxiliaryPid);
|
|
95
|
+
if (token)
|
|
96
|
+
pidsStartTokens[auxiliaryPid] = token;
|
|
97
|
+
}
|
|
98
|
+
const owner = input.owner === undefined ? callingOwner(coordRoot) : (input.owner ?? undefined);
|
|
99
|
+
const record = {
|
|
100
|
+
schema_version: SERVER_RECORD_SCHEMA_VERSION,
|
|
101
|
+
id,
|
|
102
|
+
kind: input.kind,
|
|
103
|
+
type: input.type,
|
|
104
|
+
label: input.label ?? id,
|
|
105
|
+
...(input.url ? { url: input.url } : {}),
|
|
106
|
+
...(input.port ? { port: input.port } : {}),
|
|
107
|
+
pid,
|
|
108
|
+
...(startToken ? { start_token: startToken } : {}),
|
|
109
|
+
...(pids.length ? { pids, pids_start_tokens: pidsStartTokens } : {}),
|
|
110
|
+
host: hostname(),
|
|
111
|
+
...(input.scope ? { scope: resolve(input.scope) } : {}),
|
|
112
|
+
...(input.cwd ? { cwd: resolve(input.cwd) } : {}),
|
|
113
|
+
...(input.log ? { log: resolve(input.log) } : {}),
|
|
114
|
+
...(owner ? { owner } : {}),
|
|
115
|
+
started_at: input.started_at ?? now,
|
|
116
|
+
registered_at: now,
|
|
117
|
+
last_active_at: now,
|
|
118
|
+
};
|
|
119
|
+
if (input.start_token !== undefined && verifyProcessGeneration(pid, input.start_token) !== "same")
|
|
120
|
+
throw new Error(`pid ${pid} generation could not be verified before registration`);
|
|
121
|
+
writePrivateJsonAtomic(recordPath(coordRoot, id), record);
|
|
122
|
+
return record;
|
|
137
123
|
}
|
|
138
124
|
/** Best-effort registration that never throws (for server start paths). */
|
|
139
125
|
export function tryRegisterServer(input, options) {
|
|
@@ -156,106 +142,43 @@ function callingOwner(coordRoot) {
|
|
|
156
142
|
return undefined;
|
|
157
143
|
}
|
|
158
144
|
}
|
|
159
|
-
/**
|
|
160
|
-
|
|
145
|
+
/**
|
|
146
|
+
* Remove a server's record. With `pid`, only a record still naming that pid
|
|
147
|
+
* is removed, so a replacement that re-registered the same id survives.
|
|
148
|
+
*/
|
|
149
|
+
export function unregisterServer(id, options) {
|
|
161
150
|
const coordRoot = rootOf(options);
|
|
162
|
-
if (!coordRoot || !
|
|
151
|
+
if (!coordRoot || !isValidServerId(id))
|
|
163
152
|
return false;
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
throw new Error("registration custody root is unavailable");
|
|
170
|
-
const lease = serverMutationLease(coordRoot, record.id);
|
|
171
|
-
try {
|
|
172
|
-
const current = readServer(record.id, { coordRoot });
|
|
173
|
-
if (!current ||
|
|
174
|
-
current.registration_id !== record.registration_id ||
|
|
175
|
-
!sameProcessCustody(current, record))
|
|
176
|
-
throw new Error(`registration ${record.id} custody changed`);
|
|
177
|
-
const result = effect();
|
|
178
|
-
if (result !== null &&
|
|
179
|
-
(typeof result === "object" || typeof result === "function") &&
|
|
180
|
-
typeof result.then === "function")
|
|
181
|
-
return Promise.resolve(result).finally(() => lease.release());
|
|
182
|
-
lease.release();
|
|
183
|
-
return result;
|
|
184
|
-
}
|
|
185
|
-
catch (error) {
|
|
186
|
-
lease.release();
|
|
187
|
-
throw error;
|
|
153
|
+
const path = recordPath(coordRoot, id);
|
|
154
|
+
if (options?.pid !== undefined) {
|
|
155
|
+
const record = readRecord(path);
|
|
156
|
+
if (record && record.pid !== options.pid)
|
|
157
|
+
return false;
|
|
188
158
|
}
|
|
189
|
-
|
|
190
|
-
function removeServerReceipt(coordRoot, record) {
|
|
191
|
-
const path = recordPath(coordRoot, record.id);
|
|
192
|
-
const current = readRecord(path);
|
|
193
|
-
if (!current ||
|
|
194
|
-
current.registration_id !== record.registration_id ||
|
|
195
|
-
!sameProcessCustody(current, record))
|
|
159
|
+
if (!existsSync(path))
|
|
196
160
|
return false;
|
|
197
|
-
rmSync(path);
|
|
161
|
+
rmSync(path, { force: true });
|
|
198
162
|
return true;
|
|
199
163
|
}
|
|
200
|
-
function serverMutationLease(coordRoot, id) {
|
|
201
|
-
recordPath(coordRoot, id);
|
|
202
|
-
const scope = `server-registration-${id}`;
|
|
203
|
-
return acquireNoClobberLease({
|
|
204
|
-
path: join(serversDir(coordRoot), ".custody", id),
|
|
205
|
-
scope,
|
|
206
|
-
authoritySha256: createHash("sha256").update(scope).digest("hex"),
|
|
207
|
-
staleAfterMs: 1,
|
|
208
|
-
});
|
|
209
|
-
}
|
|
210
|
-
function withServerMutation(coordRoot, id, operation) {
|
|
211
|
-
const lease = serverMutationLease(coordRoot, id);
|
|
212
|
-
try {
|
|
213
|
-
return operation();
|
|
214
|
-
}
|
|
215
|
-
finally {
|
|
216
|
-
lease.release();
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
function sameProcessCustody(a, b) {
|
|
220
|
-
const helpers = (record) => [...new Set(record.pids ?? [])]
|
|
221
|
-
.sort((x, y) => x - y)
|
|
222
|
-
.map((pid) => [pid, record.pids_start_tokens?.[pid] ?? null]);
|
|
223
|
-
return (a.schema_version === SERVER_RECORD_SCHEMA_VERSION &&
|
|
224
|
-
b.schema_version === SERVER_RECORD_SCHEMA_VERSION &&
|
|
225
|
-
a.id === b.id &&
|
|
226
|
-
a.pid === b.pid &&
|
|
227
|
-
a.start_token === b.start_token &&
|
|
228
|
-
a.host === b.host &&
|
|
229
|
-
a.kind === b.kind &&
|
|
230
|
-
a.type === b.type &&
|
|
231
|
-
a.scope === b.scope &&
|
|
232
|
-
a.owner?.instance_id === b.owner?.instance_id &&
|
|
233
|
-
JSON.stringify(helpers(a)) === JSON.stringify(helpers(b)));
|
|
234
|
-
}
|
|
235
164
|
export function readServer(id, options) {
|
|
236
165
|
const coordRoot = rootOf(options);
|
|
237
166
|
if (!coordRoot || !isValidServerId(id))
|
|
238
167
|
return null;
|
|
239
|
-
|
|
240
|
-
const record = readRecord(path);
|
|
241
|
-
if (!record && existsSync(path))
|
|
242
|
-
throw new Error(`server record ${id} is invalid or uses an older schema; retained without custody`);
|
|
243
|
-
return record;
|
|
168
|
+
return readRecord(recordPath(coordRoot, id));
|
|
244
169
|
}
|
|
245
170
|
/** Mark a server as used now. */
|
|
246
171
|
export function touchServer(id, options) {
|
|
247
172
|
const coordRoot = rootOf(options);
|
|
248
173
|
if (!coordRoot || !isValidServerId(id))
|
|
249
174
|
return;
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
last_active_at: (options?.now ?? new Date()).toISOString(),
|
|
258
|
-
});
|
|
175
|
+
const path = recordPath(coordRoot, id);
|
|
176
|
+
const record = readRecord(path);
|
|
177
|
+
if (!record)
|
|
178
|
+
return;
|
|
179
|
+
writePrivateJsonAtomic(path, {
|
|
180
|
+
...record,
|
|
181
|
+
last_active_at: (options?.now ?? new Date()).toISOString(),
|
|
259
182
|
});
|
|
260
183
|
}
|
|
261
184
|
/**
|
|
@@ -284,8 +207,6 @@ function readRecord(path) {
|
|
|
284
207
|
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
285
208
|
if (value?.schema_version !== SERVER_RECORD_SCHEMA_VERSION ||
|
|
286
209
|
typeof value.id !== "string" ||
|
|
287
|
-
typeof value.registration_id !== "string" ||
|
|
288
|
-
!/^[0-9a-f-]{36}$/.test(value.registration_id) ||
|
|
289
210
|
!Number.isSafeInteger(value.pid) ||
|
|
290
211
|
(value.kind !== "service" && value.kind !== "session"))
|
|
291
212
|
return null;
|
|
@@ -304,12 +225,8 @@ export function readServerRecords(options) {
|
|
|
304
225
|
return [];
|
|
305
226
|
return readdirSync(dir)
|
|
306
227
|
.filter((name) => name.endsWith(".json"))
|
|
307
|
-
.map((name) =>
|
|
308
|
-
|
|
309
|
-
if (!record)
|
|
310
|
-
throw new Error(`server record ${name} is invalid or uses an older schema; retained without custody`);
|
|
311
|
-
return record;
|
|
312
|
-
})
|
|
228
|
+
.map((name) => readRecord(join(dir, name)))
|
|
229
|
+
.filter((record) => record !== null)
|
|
313
230
|
.sort((a, b) => a.kind.localeCompare(b.kind) || a.type.localeCompare(b.type) || a.id.localeCompare(b.id));
|
|
314
231
|
}
|
|
315
232
|
export function pidAlive(pid) {
|
|
@@ -380,7 +297,7 @@ export function listServers(options) {
|
|
|
380
297
|
records.forEach((record, i) => {
|
|
381
298
|
const state = states[i];
|
|
382
299
|
if (state === "dead" && allProcessesAbsent(record) && options?.prune !== false) {
|
|
383
|
-
unregisterServer(record, { coordRoot });
|
|
300
|
+
unregisterServer(record.id, { coordRoot, pid: record.pid });
|
|
384
301
|
pruned.push(record.id);
|
|
385
302
|
return;
|
|
386
303
|
}
|
|
@@ -511,51 +428,28 @@ export function planServerGc(views, policy) {
|
|
|
511
428
|
};
|
|
512
429
|
});
|
|
513
430
|
}
|
|
514
|
-
/**
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
if (!coordRoot)
|
|
522
|
-
return {
|
|
523
|
-
id: record.id,
|
|
524
|
-
stopped: false,
|
|
525
|
-
method: "refused",
|
|
526
|
-
generation: "unknown",
|
|
527
|
-
detail: "registration custody root is unavailable",
|
|
528
|
-
};
|
|
529
|
-
let lease;
|
|
431
|
+
/** Recheck the generation, then signal. A supplied `kill` replaces the handle path. */
|
|
432
|
+
function signalVerified(pid, token, name, dependencies) {
|
|
433
|
+
if (!dependencies.kill)
|
|
434
|
+
return signalIfSameGeneration(pid, token, name, dependencies);
|
|
435
|
+
const verdict = verifyProcessGeneration(pid, token, dependencies.readToken);
|
|
436
|
+
if (verdict !== "same")
|
|
437
|
+
return { sent: false, verdict };
|
|
530
438
|
try {
|
|
531
|
-
|
|
439
|
+
dependencies.kill(pid, name);
|
|
440
|
+
return { sent: true, verdict };
|
|
532
441
|
}
|
|
533
442
|
catch {
|
|
534
|
-
return {
|
|
535
|
-
id: record.id,
|
|
536
|
-
stopped: false,
|
|
537
|
-
method: "refused",
|
|
538
|
-
generation: "unknown",
|
|
539
|
-
detail: "registration custody is unavailable",
|
|
540
|
-
};
|
|
541
|
-
}
|
|
542
|
-
try {
|
|
543
|
-
const current = readServer(record.id, { coordRoot });
|
|
544
|
-
if (!current || current.registration_id !== record.registration_id)
|
|
545
|
-
return {
|
|
546
|
-
id: record.id,
|
|
547
|
-
stopped: false,
|
|
548
|
-
method: "refused",
|
|
549
|
-
generation: "unknown",
|
|
550
|
-
detail: "registration custody changed; retained current server",
|
|
551
|
-
};
|
|
552
|
-
return await stopServerWithCustody(record, coordRoot, options);
|
|
553
|
-
}
|
|
554
|
-
finally {
|
|
555
|
-
lease.release();
|
|
443
|
+
return { sent: false, verdict };
|
|
556
444
|
}
|
|
557
445
|
}
|
|
558
|
-
|
|
446
|
+
/**
|
|
447
|
+
* Stop a verified server with SIGTERM, then SIGKILL after the grace period.
|
|
448
|
+
* Each signal, including the process group and auxiliary processes, requires
|
|
449
|
+
* a fresh generation check. Unknown or different generations retain the record.
|
|
450
|
+
*/
|
|
451
|
+
export async function stopServer(record, options) {
|
|
452
|
+
const coordRoot = rootOf(options);
|
|
559
453
|
const graceMs = options?.graceMs ?? 5_000;
|
|
560
454
|
const dependencies = options?.signalDependencies ?? {};
|
|
561
455
|
const targets = [
|
|
@@ -573,7 +467,7 @@ async function stopServerWithCustody(record, coordRoot, options) {
|
|
|
573
467
|
});
|
|
574
468
|
const finish = (result) => {
|
|
575
469
|
if (result.stopped || result.method === "not-running")
|
|
576
|
-
|
|
470
|
+
unregisterServer(record.id, { coordRoot, pid: record.pid });
|
|
577
471
|
return result;
|
|
578
472
|
};
|
|
579
473
|
if (record.host !== hostname())
|
|
@@ -595,9 +489,6 @@ async function stopServerWithCustody(record, coordRoot, options) {
|
|
|
595
489
|
}
|
|
596
490
|
if (targets.every((target) => processPresence(target.pid, dependencies.probePid) === "absent"))
|
|
597
491
|
return finish({ id: record.id, stopped: false, method: "not-running" });
|
|
598
|
-
const current = readServer(record.id, { coordRoot });
|
|
599
|
-
if (!current || !sameProcessCustody(current, record))
|
|
600
|
-
return refuse(record.pid, "unknown");
|
|
601
492
|
for (const name of ["SIGTERM", "SIGKILL"]) {
|
|
602
493
|
for (const target of targets) {
|
|
603
494
|
const presence = processPresence(target.pid, dependencies.probePid);
|
|
@@ -605,9 +496,11 @@ async function stopServerWithCustody(record, coordRoot, options) {
|
|
|
605
496
|
continue;
|
|
606
497
|
if (presence === "unknown")
|
|
607
498
|
return refuse(target.pid, "unknown");
|
|
608
|
-
const result =
|
|
609
|
-
|
|
610
|
-
|
|
499
|
+
const result = target.pid === record.pid
|
|
500
|
+
? signalServerLeader(target.pid, target.token, name, dependencies)
|
|
501
|
+
: signalVerified(target.pid, target.token, name, dependencies);
|
|
502
|
+
if (result.verdict !== "same")
|
|
503
|
+
return refuse(target.pid, result.verdict);
|
|
611
504
|
}
|
|
612
505
|
const observed = await waitForExit(targets, name === "SIGTERM" ? graceMs : 2_000, dependencies);
|
|
613
506
|
if (observed.refused)
|
|
@@ -623,6 +516,18 @@ async function stopServerWithCustody(record, coordRoot, options) {
|
|
|
623
516
|
detail: "process survived SIGKILL",
|
|
624
517
|
};
|
|
625
518
|
}
|
|
519
|
+
function signalServerLeader(pid, token, name, dependencies) {
|
|
520
|
+
// The leader's generation authorizes its group. A failed group signal must
|
|
521
|
+
// not reuse that observation for the direct-PID fallback.
|
|
522
|
+
const kill = dependencies.kill ?? process.kill;
|
|
523
|
+
const group = signalVerified(pid, token, name, {
|
|
524
|
+
readToken: dependencies.readToken,
|
|
525
|
+
kill: (leader, signal) => kill(-leader, signal),
|
|
526
|
+
});
|
|
527
|
+
return group.sent || group.verdict !== "same"
|
|
528
|
+
? group
|
|
529
|
+
: signalVerified(pid, token, name, dependencies);
|
|
530
|
+
}
|
|
626
531
|
async function waitForExit(targets, waitMs, dependencies) {
|
|
627
532
|
const deadline = Date.now() + waitMs;
|
|
628
533
|
while (true) {
|
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
* Tunnels as registry services.
|
|
3
3
|
*
|
|
4
4
|
* `tunnel up` and `tunnel reload` register the new gate; `tunnel down` removes
|
|
5
|
-
* its record.
|
|
5
|
+
* its record. Older tunnels need an explicit adoption of an observed listener.
|
|
6
6
|
*/
|
|
7
|
-
import { type
|
|
8
|
-
import {
|
|
9
|
-
/** Verify only witnesses saved by the original tunnel registration. */
|
|
10
|
-
export declare function createTunnelReadinessVerifier(options: ServerOptions): TunnelReadinessVerifier;
|
|
7
|
+
import { type TunnelState } from "../../lib/tunnel/state.js";
|
|
8
|
+
import type { RegisterServerInput } from "./index.js";
|
|
11
9
|
export declare function tunnelServerId(name: string): string;
|
|
12
10
|
/** Register the new gate; saved helper PIDs have no generation witnesses. */
|
|
13
11
|
export declare function tunnelServerInput(state: TunnelState, coordRoot: string): RegisterServerInput;
|