harnery 0.15.0 → 0.17.0
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/README.md +1 -1
- package/dist/commands/supervisor.d.ts.map +1 -1
- package/dist/commands/supervisor.js +393 -4
- package/dist/core/supervisor/index.d.ts +4 -0
- package/dist/core/supervisor/index.d.ts.map +1 -1
- package/dist/core/supervisor/index.js +4 -0
- package/dist/core/supervisor/plan-api.d.ts +16 -0
- package/dist/core/supervisor/plan-api.d.ts.map +1 -0
- package/dist/core/supervisor/plan-api.js +34 -0
- package/dist/core/supervisor/plan-read.d.ts +4 -0
- package/dist/core/supervisor/plan-read.d.ts.map +1 -0
- package/dist/core/supervisor/plan-read.js +269 -0
- package/dist/core/supervisor/plan-types.d.ts +113 -0
- package/dist/core/supervisor/plan-types.d.ts.map +1 -0
- package/dist/core/supervisor/plan-types.js +14 -0
- package/dist/core/supervisor/planning.d.ts +26 -0
- package/dist/core/supervisor/planning.d.ts.map +1 -0
- package/dist/core/supervisor/planning.js +486 -0
- package/dist/core/supervisor/read.d.ts +4 -0
- package/dist/core/supervisor/read.d.ts.map +1 -1
- package/dist/core/supervisor/read.js +3 -0
- package/dist/core/supervisor/runner.d.ts +3 -0
- package/dist/core/supervisor/runner.d.ts.map +1 -1
- package/dist/core/supervisor/runner.js +37 -7
- package/dist/core/supervisor/service-read.d.ts +6 -0
- package/dist/core/supervisor/service-read.d.ts.map +1 -0
- package/dist/core/supervisor/service-read.js +127 -0
- package/dist/core/supervisor/service.d.ts +114 -0
- package/dist/core/supervisor/service.d.ts.map +1 -0
- package/dist/core/supervisor/service.js +706 -0
- package/dist/core/supervisor/state.d.ts +11 -1
- package/dist/core/supervisor/state.d.ts.map +1 -1
- package/dist/core/supervisor/state.js +186 -7
- package/dist/lib/tunnel/gate.d.ts.map +1 -1
- package/dist/lib/tunnel/gate.js +6 -1
- package/package.json +1 -1
- package/src/commands/supervisor.ts +490 -3
- package/src/core/supervisor/index.ts +45 -0
- package/src/core/supervisor/plan-api.ts +47 -0
- package/src/core/supervisor/plan-read.ts +299 -0
- package/src/core/supervisor/plan-types.ts +144 -0
- package/src/core/supervisor/planning.ts +576 -0
- package/src/core/supervisor/read.ts +30 -0
- package/src/core/supervisor/runner.ts +39 -2
- package/src/core/supervisor/service-read.ts +144 -0
- package/src/core/supervisor/service.ts +927 -0
- package/src/core/supervisor/state.ts +235 -6
- package/src/lib/tunnel/gate.ts +7 -1
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { hostname } from "node:os";
|
|
3
|
+
import { join, resolve } from "node:path";
|
|
4
|
+
import type {
|
|
5
|
+
SupervisorServiceConfig,
|
|
6
|
+
SupervisorServiceRuntime,
|
|
7
|
+
SupervisorServiceStatus,
|
|
8
|
+
SupervisorServiceStatusRecord,
|
|
9
|
+
} from "./service.ts";
|
|
10
|
+
|
|
11
|
+
const MAX_FILE_BYTES = 512 * 1024;
|
|
12
|
+
const MAX_GOALS = 100;
|
|
13
|
+
const FOREIGN_STATUS_STALE_MS = 30_000;
|
|
14
|
+
|
|
15
|
+
export function readSupervisorServiceConfig(coordRootRaw: string): SupervisorServiceConfig {
|
|
16
|
+
const coordRoot = resolve(coordRootRaw);
|
|
17
|
+
const config = readBoundedJson<SupervisorServiceConfig>(
|
|
18
|
+
join(serviceDir(coordRoot), "config.json"),
|
|
19
|
+
"supervisor service configuration",
|
|
20
|
+
);
|
|
21
|
+
if (
|
|
22
|
+
config.schema_version !== 1 ||
|
|
23
|
+
!validTimestamp(config.created_at) ||
|
|
24
|
+
!Array.isArray(config.goal_ids) ||
|
|
25
|
+
config.goal_ids.length < 1 ||
|
|
26
|
+
config.goal_ids.length > MAX_GOALS ||
|
|
27
|
+
config.goal_ids.some((goalId) => typeof goalId !== "string" || !goalId.trim()) ||
|
|
28
|
+
!positive(config.wake_interval_ms) ||
|
|
29
|
+
!positive(config.heartbeat_interval_ms) ||
|
|
30
|
+
!positive(config.error_backoff_base_ms) ||
|
|
31
|
+
!positive(config.error_backoff_max_ms) ||
|
|
32
|
+
config.error_backoff_max_ms < config.error_backoff_base_ms ||
|
|
33
|
+
!config.engine ||
|
|
34
|
+
typeof config.engine !== "object" ||
|
|
35
|
+
typeof config.engine.subscription_only !== "boolean" ||
|
|
36
|
+
typeof config.engine.allow_api_billing !== "boolean"
|
|
37
|
+
) {
|
|
38
|
+
throw new Error("supervisor service configuration has an unsupported schema");
|
|
39
|
+
}
|
|
40
|
+
return config;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function readSupervisorServiceRuntime(
|
|
44
|
+
coordRootRaw: string,
|
|
45
|
+
): SupervisorServiceRuntime | undefined {
|
|
46
|
+
const coordRoot = resolve(coordRootRaw);
|
|
47
|
+
const path = join(serviceDir(coordRoot), "runtime.json");
|
|
48
|
+
if (!existsSync(path)) return undefined;
|
|
49
|
+
const runtime = readBoundedJson<SupervisorServiceRuntime>(path, "supervisor service runtime");
|
|
50
|
+
if (
|
|
51
|
+
runtime.schema_version !== 1 ||
|
|
52
|
+
!validTimestamp(runtime.config_created_at) ||
|
|
53
|
+
!validTimestamp(runtime.updated_at) ||
|
|
54
|
+
!runtime.goals ||
|
|
55
|
+
typeof runtime.goals !== "object" ||
|
|
56
|
+
Array.isArray(runtime.goals)
|
|
57
|
+
) {
|
|
58
|
+
throw new Error("supervisor service runtime has an unsupported schema");
|
|
59
|
+
}
|
|
60
|
+
return runtime;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function readSupervisorServiceStatus(coordRootRaw: string): SupervisorServiceStatus {
|
|
64
|
+
const coordRoot = resolve(coordRootRaw);
|
|
65
|
+
const record = readStatusRecord(coordRoot);
|
|
66
|
+
let config: SupervisorServiceConfig | undefined;
|
|
67
|
+
let runtime: SupervisorServiceRuntime | undefined;
|
|
68
|
+
try {
|
|
69
|
+
config = readSupervisorServiceConfig(coordRoot);
|
|
70
|
+
} catch {
|
|
71
|
+
// Unconfigured or corrupt service state is represented without throwing.
|
|
72
|
+
}
|
|
73
|
+
try {
|
|
74
|
+
runtime = readSupervisorServiceRuntime(coordRoot);
|
|
75
|
+
} catch {
|
|
76
|
+
// Runtime is recoverable and must not make the dashboard unreadable.
|
|
77
|
+
}
|
|
78
|
+
if (!record) return { running: false, stale: false, config, runtime };
|
|
79
|
+
const running = statusOwnerIsLive(record);
|
|
80
|
+
return { running, stale: !running && record.state !== "stopped", record, config, runtime };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function supervisorServiceLogPath(coordRoot: string): string {
|
|
84
|
+
return join(serviceDir(resolve(coordRoot)), "service.log");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function readStatusRecord(coordRoot: string): SupervisorServiceStatusRecord | undefined {
|
|
88
|
+
const path = join(serviceDir(coordRoot), "status.json");
|
|
89
|
+
if (!existsSync(path)) return undefined;
|
|
90
|
+
try {
|
|
91
|
+
const value = readBoundedJson<SupervisorServiceStatusRecord>(path, "supervisor service status");
|
|
92
|
+
if (
|
|
93
|
+
value.schema_version !== 1 ||
|
|
94
|
+
!Number.isSafeInteger(value.pid) ||
|
|
95
|
+
value.pid < 1 ||
|
|
96
|
+
typeof value.host !== "string" ||
|
|
97
|
+
typeof value.nonce !== "string" ||
|
|
98
|
+
!validTimestamp(value.started_at) ||
|
|
99
|
+
!validTimestamp(value.heartbeat_at)
|
|
100
|
+
) {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
return value;
|
|
104
|
+
} catch {
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function statusOwnerIsLive(record: SupervisorServiceStatusRecord): boolean {
|
|
110
|
+
if (record.state === "stopped") return false;
|
|
111
|
+
if (record.host === hostname()) {
|
|
112
|
+
try {
|
|
113
|
+
process.kill(record.pid, 0);
|
|
114
|
+
return true;
|
|
115
|
+
} catch (error) {
|
|
116
|
+
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const age = Date.now() - Date.parse(record.heartbeat_at);
|
|
120
|
+
return Number.isFinite(age) && age < FOREIGN_STATUS_STALE_MS;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function readBoundedJson<T>(path: string, label: string): T {
|
|
124
|
+
if (!existsSync(path)) throw new Error(`${label} does not exist`);
|
|
125
|
+
const size = statSync(path).size;
|
|
126
|
+
if (size <= 0 || size > MAX_FILE_BYTES) throw new Error(`${label} has invalid size ${size}`);
|
|
127
|
+
try {
|
|
128
|
+
return JSON.parse(readFileSync(path, "utf8")) as T;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
throw new Error(`cannot parse ${label}: ${(error as Error).message}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function serviceDir(coordRoot: string): string {
|
|
135
|
+
return join(coordRoot, ".harnery", "supervisor-service");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function positive(value: unknown): value is number {
|
|
139
|
+
return Number.isSafeInteger(value) && (value as number) > 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function validTimestamp(value: unknown): value is string {
|
|
143
|
+
return typeof value === "string" && value.length <= 40 && Number.isFinite(Date.parse(value));
|
|
144
|
+
}
|