@vtxmacro/cli 2026.9.51 → 2026.9.52
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 +5 -3
- package/bin/vtx-service-bootstrap.js +118 -44
- package/bin/vtx-service-launcher.js +1389 -0
- package/bin/vtx.js +436 -293
- package/package.json +1 -1
|
@@ -0,0 +1,1389 @@
|
|
|
1
|
+
// lib/runtime-policy.generated.json
|
|
2
|
+
var runtime_policy_generated_default = {
|
|
3
|
+
ui: {
|
|
4
|
+
performance_defaults: {
|
|
5
|
+
default_timeframe: "30d"
|
|
6
|
+
},
|
|
7
|
+
execution_metadata_retry: {
|
|
8
|
+
max_attempts: 3,
|
|
9
|
+
base_delay_milliseconds: 2e3,
|
|
10
|
+
max_delay_milliseconds: 3e4
|
|
11
|
+
},
|
|
12
|
+
auth_bootstrap_retry: {
|
|
13
|
+
max_attempts: 3,
|
|
14
|
+
jitter_milliseconds: 750,
|
|
15
|
+
base_delay_milliseconds: 500,
|
|
16
|
+
min_delay_milliseconds: 250,
|
|
17
|
+
max_delay_milliseconds: 1e4
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
ai_dashboard: {
|
|
21
|
+
market_news_bounds: {
|
|
22
|
+
max_items_min: 1,
|
|
23
|
+
max_items_max: 100,
|
|
24
|
+
lookback_hours_min: 1,
|
|
25
|
+
lookback_hours_max: 168,
|
|
26
|
+
max_items_steps: [
|
|
27
|
+
1,
|
|
28
|
+
2,
|
|
29
|
+
3,
|
|
30
|
+
4,
|
|
31
|
+
5,
|
|
32
|
+
6,
|
|
33
|
+
7,
|
|
34
|
+
8,
|
|
35
|
+
9,
|
|
36
|
+
10,
|
|
37
|
+
15,
|
|
38
|
+
20,
|
|
39
|
+
25,
|
|
40
|
+
30,
|
|
41
|
+
40,
|
|
42
|
+
50,
|
|
43
|
+
60,
|
|
44
|
+
75,
|
|
45
|
+
90,
|
|
46
|
+
100
|
|
47
|
+
],
|
|
48
|
+
lookback_hours_steps: [
|
|
49
|
+
1,
|
|
50
|
+
2,
|
|
51
|
+
4,
|
|
52
|
+
8,
|
|
53
|
+
12,
|
|
54
|
+
24,
|
|
55
|
+
48,
|
|
56
|
+
72,
|
|
57
|
+
168
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
trading: {
|
|
62
|
+
engine: {
|
|
63
|
+
client_runtime: {
|
|
64
|
+
recovery: {
|
|
65
|
+
resume_retry_base_delay_ms: 1e3,
|
|
66
|
+
resume_retry_max_delay_ms: 3e4,
|
|
67
|
+
transient_load_min_delay_ms: 3e4,
|
|
68
|
+
transient_load_max_delay_ms: 12e4,
|
|
69
|
+
rate_limit_buffer_ms: 1e3,
|
|
70
|
+
rate_limit_max_delay_ms: 3e5,
|
|
71
|
+
engine_stop_settle_retry_delay_ms: 250,
|
|
72
|
+
recoverable_reset_settle_retry_delay_ms: 1500,
|
|
73
|
+
local_ai_preflight_timeout_ms: 3e3,
|
|
74
|
+
surge_stagger_max_ms: 6e4,
|
|
75
|
+
surge_stagger_min_ms: 2e3,
|
|
76
|
+
surge_stagger_min_candidates: 4,
|
|
77
|
+
stagger_deadline_ttl_ms: 3e5,
|
|
78
|
+
server_active_reprobe_delay_ms: 6e4,
|
|
79
|
+
transient_load_jitter_ms: 1e4,
|
|
80
|
+
cross_host_completed_ttl_ms: 15e3,
|
|
81
|
+
telemetry_dedupe_ttl_ms: 5e3,
|
|
82
|
+
browser_presence_heartbeat_interval_ms: 3e4,
|
|
83
|
+
host_liveness_heartbeat_interval_ms: 3e4,
|
|
84
|
+
host_liveness_recovery_nudge_ttl_ms: 1e4
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
external_inference: {
|
|
90
|
+
service: {
|
|
91
|
+
update_check_interval_ms: 3e5,
|
|
92
|
+
update_registry_timeout_ms: 15e3,
|
|
93
|
+
bootstrap_readiness_timeout_ms: 6e5,
|
|
94
|
+
bootstrap_stop_timeout_ms: 1e4,
|
|
95
|
+
startup_max_attempts: 40,
|
|
96
|
+
startup_poll_interval_ms: 250,
|
|
97
|
+
restart_stable_uptime_ms: 6e4,
|
|
98
|
+
restart_backoff_base_ms: 1e3,
|
|
99
|
+
restart_backoff_max_ms: 3e4,
|
|
100
|
+
restart_exponent_max: 5
|
|
101
|
+
},
|
|
102
|
+
claim_diagnostics: {
|
|
103
|
+
event_loop_sample_interval_ms: 100
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
desktop: {
|
|
107
|
+
runtime_watchdog: {
|
|
108
|
+
ping_interval_milliseconds: 15e3,
|
|
109
|
+
telemetry_interval_milliseconds: 3e5
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
screener: {
|
|
113
|
+
resource_scheduler: {
|
|
114
|
+
client_lock_ttl_milliseconds: 12e4,
|
|
115
|
+
client_poll_interval_milliseconds: 250
|
|
116
|
+
}
|
|
117
|
+
},
|
|
118
|
+
deployment: {
|
|
119
|
+
cutover_buffer: {
|
|
120
|
+
target_timeout_ms: 3e3,
|
|
121
|
+
retired_timeout_ms: 45e3,
|
|
122
|
+
max_body_bytes: 33554432,
|
|
123
|
+
max_inflight: 16,
|
|
124
|
+
max_queue: 64
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
exchange: {
|
|
128
|
+
hyperliquid: {
|
|
129
|
+
client_public_read_budget: {
|
|
130
|
+
max_weight: 900,
|
|
131
|
+
window_seconds: 60,
|
|
132
|
+
cooldown_seconds: 60,
|
|
133
|
+
metadata_ttl_seconds: 300,
|
|
134
|
+
user_fees_ttl_seconds: 300,
|
|
135
|
+
account_mode_ttl_seconds: 30,
|
|
136
|
+
default_request_weight: 20,
|
|
137
|
+
cache_max_entries: 512
|
|
138
|
+
},
|
|
139
|
+
read_budget: {
|
|
140
|
+
weights: {
|
|
141
|
+
exchangeStatus: 2,
|
|
142
|
+
clearinghouseState: 2,
|
|
143
|
+
spotClearinghouseState: 2,
|
|
144
|
+
l2Book: 2,
|
|
145
|
+
allMids: 2,
|
|
146
|
+
userFills: 120,
|
|
147
|
+
userFillsByTime: 120,
|
|
148
|
+
historicalOrders: 120,
|
|
149
|
+
twapHistory: 120,
|
|
150
|
+
userTwapSliceFills: 120,
|
|
151
|
+
userFunding: 45,
|
|
152
|
+
userNonFundingLedgerUpdates: 45,
|
|
153
|
+
candleSnapshot: 104,
|
|
154
|
+
userRole: 60,
|
|
155
|
+
userAbstraction: 20,
|
|
156
|
+
userDexAbstraction: 20,
|
|
157
|
+
userActiveAssetData: 20,
|
|
158
|
+
activeAssetData: 20,
|
|
159
|
+
openOrders: 20,
|
|
160
|
+
orderStatus: 2,
|
|
161
|
+
portfolio: 20,
|
|
162
|
+
userFees: 20,
|
|
163
|
+
metaAndAssetCtxs: 20,
|
|
164
|
+
meta: 20,
|
|
165
|
+
perpDexs: 20,
|
|
166
|
+
spotMeta: 20,
|
|
167
|
+
approvedAgents: 20,
|
|
168
|
+
authorizedAgents: 20,
|
|
169
|
+
agents: 20,
|
|
170
|
+
apiWallets: 20
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// lib/inference-host/service-launcher.ts
|
|
178
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
179
|
+
import { appendFile, readFile as readFile3, rm as rm3 } from "node:fs/promises";
|
|
180
|
+
import { resolve as resolve3 } from "node:path";
|
|
181
|
+
import { fileURLToPath } from "node:url";
|
|
182
|
+
|
|
183
|
+
// lib/inference-host/config.ts
|
|
184
|
+
import { randomBytes } from "node:crypto";
|
|
185
|
+
import { spawn } from "node:child_process";
|
|
186
|
+
import { constants } from "node:fs";
|
|
187
|
+
import { lstat, mkdir, open, readFile, realpath, rename, rm } from "node:fs/promises";
|
|
188
|
+
import { homedir } from "node:os";
|
|
189
|
+
import { dirname, isAbsolute, join, relative, resolve, win32 as windowsPath } from "node:path";
|
|
190
|
+
var MAX_INFERENCE_PRIVATE_FILE_BYTES = 16 * 1024 * 1024;
|
|
191
|
+
var WINDOWS_PRIVATE_ACL_BROKER_SCRIPT = `$ErrorActionPreference='Stop'
|
|
192
|
+
$ProgressPreference='SilentlyContinue'
|
|
193
|
+
while(($line=[Console]::In.ReadLine()) -ne $null) {
|
|
194
|
+
$requestId=$null
|
|
195
|
+
$failureStage='request_validation'
|
|
196
|
+
try {
|
|
197
|
+
if($line.Length -lt 2 -or $line.Length -gt 65536) { throw 'invalid request size' }
|
|
198
|
+
$request=$line | ConvertFrom-Json
|
|
199
|
+
$names=@($request.PSObject.Properties.Name | Sort-Object)
|
|
200
|
+
if(($names -join ',') -ne 'id,kind,mode,path_b64') { throw 'invalid request shape' }
|
|
201
|
+
if($request.id -isnot [long] -and $request.id -isnot [int]) { throw 'invalid request id' }
|
|
202
|
+
$requestId=[long]$request.id
|
|
203
|
+
if($requestId -lt 1) { throw 'invalid request id' }
|
|
204
|
+
$kind=[string]$request.kind
|
|
205
|
+
$mode=[string]$request.mode
|
|
206
|
+
if($kind -ne 'directory' -and $kind -ne 'file') { throw 'invalid path kind' }
|
|
207
|
+
if($mode -ne 'harden' -and $mode -ne 'verify' -and $mode -ne 'verify-owner') { throw 'invalid ACL mode' }
|
|
208
|
+
$path=[Text.Encoding]::UTF8.GetString([Convert]::FromBase64String([string]$request.path_b64))
|
|
209
|
+
if([string]::IsNullOrWhiteSpace($path) -or $path.Length -gt 32767 -or $path.Contains([char]0)) { throw 'invalid path' }
|
|
210
|
+
$failureStage='path_lookup'
|
|
211
|
+
if(-not (Test-Path -LiteralPath $path)) {
|
|
212
|
+
[Console]::Out.WriteLine((@{id=$requestId;status='missing'} | ConvertTo-Json -Compress))
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
$failureStage='principal_resolution'
|
|
216
|
+
$currentIdentity=[Security.Principal.WindowsIdentity]::GetCurrent()
|
|
217
|
+
$current=$currentIdentity.User
|
|
218
|
+
$system=New-Object Security.Principal.SecurityIdentifier('S-1-5-18')
|
|
219
|
+
$admins=New-Object Security.Principal.SecurityIdentifier('S-1-5-32-544')
|
|
220
|
+
$currentIsAdmin=(New-Object Security.Principal.WindowsPrincipal($currentIdentity)).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
221
|
+
$allowed=@($current.Value,$system.Value,$admins.Value)
|
|
222
|
+
if($mode -eq 'harden') {
|
|
223
|
+
$failureStage='acl_read'
|
|
224
|
+
$acl=Get-Acl -LiteralPath $path
|
|
225
|
+
$originalAccess=$acl.GetSecurityDescriptorSddlForm([Security.AccessControl.AccessControlSections]::Access)
|
|
226
|
+
$failureStage='owner_validation'
|
|
227
|
+
$existingOwner=(New-Object Security.Principal.NTAccount($acl.Owner)).Translate([Security.Principal.SecurityIdentifier]).Value
|
|
228
|
+
if($existingOwner -ne $current.Value -and ($existingOwner -ne $admins.Value -or -not $currentIsAdmin)) { throw 'private ACL owner is invalid' }
|
|
229
|
+
$failureStage='acl_mutation'
|
|
230
|
+
$acl.SetAccessRuleProtection($true,$false)
|
|
231
|
+
foreach($existingRule in @($acl.Access)) { [void]$acl.RemoveAccessRuleSpecific($existingRule) }
|
|
232
|
+
$inherit=if($kind -eq 'directory') {
|
|
233
|
+
[Security.AccessControl.InheritanceFlags]'ContainerInherit,ObjectInherit'
|
|
234
|
+
} else { [Security.AccessControl.InheritanceFlags]::None }
|
|
235
|
+
foreach($sid in @($current,$system,$admins)) {
|
|
236
|
+
$rule=New-Object Security.AccessControl.FileSystemAccessRule(
|
|
237
|
+
$sid,
|
|
238
|
+
[Security.AccessControl.FileSystemRights]::FullControl,
|
|
239
|
+
$inherit,
|
|
240
|
+
[Security.AccessControl.PropagationFlags]::None,
|
|
241
|
+
[Security.AccessControl.AccessControlType]::Allow
|
|
242
|
+
)
|
|
243
|
+
[void]$acl.AddAccessRule($rule)
|
|
244
|
+
}
|
|
245
|
+
$failureStage='acl_write'
|
|
246
|
+
if($originalAccess -ne $acl.GetSecurityDescriptorSddlForm([Security.AccessControl.AccessControlSections]::Access)) {
|
|
247
|
+
(Get-Item -LiteralPath $path).SetAccessControl($acl)
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
$failureStage='acl_readback'
|
|
251
|
+
$actual=Get-Acl -LiteralPath $path
|
|
252
|
+
$failureStage='acl_verification'
|
|
253
|
+
$owner=(New-Object Security.Principal.NTAccount($actual.Owner)).Translate([Security.Principal.SecurityIdentifier]).Value
|
|
254
|
+
if($owner -ne $current.Value -and ($owner -ne $admins.Value -or -not $currentIsAdmin)) { throw 'private ACL owner is invalid' }
|
|
255
|
+
if($mode -ne 'verify-owner') {
|
|
256
|
+
if(-not $actual.AreAccessRulesProtected) { throw 'private ACL inheritance is invalid' }
|
|
257
|
+
$userFull=$false
|
|
258
|
+
foreach($rule in $actual.Access) {
|
|
259
|
+
$sid=$rule.IdentityReference.Translate([Security.Principal.SecurityIdentifier]).Value
|
|
260
|
+
if($rule.IsInherited -or $allowed -notcontains $sid -or $rule.AccessControlType -ne [Security.AccessControl.AccessControlType]::Allow) {
|
|
261
|
+
throw 'private ACL contains an unauthorized access rule'
|
|
262
|
+
}
|
|
263
|
+
if($sid -eq $current.Value -and (($rule.FileSystemRights -band [Security.AccessControl.FileSystemRights]::FullControl) -eq [Security.AccessControl.FileSystemRights]::FullControl)) { $userFull=$true }
|
|
264
|
+
}
|
|
265
|
+
if(-not $userFull) { throw 'private ACL does not grant the current user full control' }
|
|
266
|
+
}
|
|
267
|
+
[Console]::Out.WriteLine((@{id=$requestId;status='ok'} | ConvertTo-Json -Compress))
|
|
268
|
+
} catch {
|
|
269
|
+
[Console]::Out.WriteLine((@{id=$requestId;status='error';error_code=$failureStage} | ConvertTo-Json -Compress))
|
|
270
|
+
}
|
|
271
|
+
}`;
|
|
272
|
+
var WINDOWS_IDENTITY_COMMAND_TIMEOUT_MS = 15e3;
|
|
273
|
+
var WINDOWS_PRIVATE_ACL_BROKER_STARTUP_TIMEOUT_MS = 9e4;
|
|
274
|
+
var SMALL_IDENTITY_COMMAND_TIMEOUT_MS = 5e3;
|
|
275
|
+
var windowsPowerShellEnvironment = (extra = {}) => {
|
|
276
|
+
const sanitizedExtra = { ...extra };
|
|
277
|
+
delete sanitizedExtra.PSModulePath;
|
|
278
|
+
const windowsRoot = process.env.SystemRoot ?? process.env.WINDIR;
|
|
279
|
+
return {
|
|
280
|
+
...sanitizedExtra,
|
|
281
|
+
NODE_ENV: process.env.NODE_ENV,
|
|
282
|
+
SystemRoot: process.env.SystemRoot,
|
|
283
|
+
WINDIR: process.env.WINDIR,
|
|
284
|
+
ComSpec: process.env.ComSpec,
|
|
285
|
+
PATHEXT: process.env.PATHEXT,
|
|
286
|
+
PSModulePath: windowsRoot ? windowsPath.join(windowsRoot, "System32", "WindowsPowerShell", "v1.0", "Modules") : void 0,
|
|
287
|
+
TEMP: process.env.TEMP,
|
|
288
|
+
TMP: process.env.TMP
|
|
289
|
+
};
|
|
290
|
+
};
|
|
291
|
+
var windowsPrivateAclBrokerInvocation = () => ({
|
|
292
|
+
args: [
|
|
293
|
+
"-NoLogo",
|
|
294
|
+
"-NoProfile",
|
|
295
|
+
"-NonInteractive",
|
|
296
|
+
"-EncodedCommand",
|
|
297
|
+
Buffer.from(WINDOWS_PRIVATE_ACL_BROKER_SCRIPT, "utf16le").toString("base64")
|
|
298
|
+
],
|
|
299
|
+
env: windowsPowerShellEnvironment()
|
|
300
|
+
});
|
|
301
|
+
var WindowsPrivateAclBroker = class {
|
|
302
|
+
constructor(dependencies = {}) {
|
|
303
|
+
this.child = null;
|
|
304
|
+
this.requestSequence = 0;
|
|
305
|
+
this.queue = Promise.resolve();
|
|
306
|
+
this.pending = null;
|
|
307
|
+
this.outputBuffer = "";
|
|
308
|
+
this.idleTimer = null;
|
|
309
|
+
this.run = async (path, kind, mode) => {
|
|
310
|
+
const operation = this.queue.then(
|
|
311
|
+
() => this.dispatch(path, kind, mode),
|
|
312
|
+
() => this.dispatch(path, kind, mode)
|
|
313
|
+
);
|
|
314
|
+
this.queue = operation.then(() => void 0, () => void 0);
|
|
315
|
+
await operation;
|
|
316
|
+
};
|
|
317
|
+
this.requestTimeoutMs = dependencies.requestTimeoutMs ?? 15e3;
|
|
318
|
+
this.startupRequestTimeoutMs = dependencies.startupRequestTimeoutMs ?? WINDOWS_PRIVATE_ACL_BROKER_STARTUP_TIMEOUT_MS;
|
|
319
|
+
this.idleShutdownMs = dependencies.idleShutdownMs ?? 3e4;
|
|
320
|
+
this.spawnProcess = dependencies.spawnProcess ?? (() => {
|
|
321
|
+
const invocation = windowsPrivateAclBrokerInvocation();
|
|
322
|
+
return spawn("powershell.exe", [...invocation.args], {
|
|
323
|
+
env: invocation.env,
|
|
324
|
+
windowsHide: true,
|
|
325
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
scheduleIdleShutdown() {
|
|
330
|
+
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
331
|
+
this.idleTimer = setTimeout(() => {
|
|
332
|
+
this.idleTimer = null;
|
|
333
|
+
const child = this.child;
|
|
334
|
+
if (!child || this.pending) return;
|
|
335
|
+
this.child = null;
|
|
336
|
+
this.outputBuffer = "";
|
|
337
|
+
child.stdin.end();
|
|
338
|
+
const force = setTimeout(() => child.kill("SIGKILL"), 1e3);
|
|
339
|
+
force.unref();
|
|
340
|
+
child.once("exit", () => clearTimeout(force));
|
|
341
|
+
}, this.idleShutdownMs);
|
|
342
|
+
this.idleTimer.unref();
|
|
343
|
+
}
|
|
344
|
+
failChild(child, error) {
|
|
345
|
+
if (this.child !== child) return;
|
|
346
|
+
this.child = null;
|
|
347
|
+
this.outputBuffer = "";
|
|
348
|
+
if (this.idleTimer) {
|
|
349
|
+
clearTimeout(this.idleTimer);
|
|
350
|
+
this.idleTimer = null;
|
|
351
|
+
}
|
|
352
|
+
const pending = this.pending;
|
|
353
|
+
this.pending = null;
|
|
354
|
+
if (pending) {
|
|
355
|
+
clearTimeout(pending.timer);
|
|
356
|
+
pending.reject(error);
|
|
357
|
+
}
|
|
358
|
+
child.kill("SIGKILL");
|
|
359
|
+
}
|
|
360
|
+
acceptLine(child, line) {
|
|
361
|
+
const pending = this.pending;
|
|
362
|
+
if (!pending || !line || line.length > 4096) {
|
|
363
|
+
this.failChild(child, new Error("Windows private ACL broker returned malformed output."));
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
let response;
|
|
367
|
+
try {
|
|
368
|
+
const parsed = JSON.parse(line);
|
|
369
|
+
const status = String(parsed.status);
|
|
370
|
+
const expectedKeys = status === "error" ? "error_code\0id\0status" : "id\0status";
|
|
371
|
+
if (Object.keys(parsed).sort().join("\0") !== expectedKeys || parsed.id !== pending.id || !["ok", "missing", "error"].includes(status) || status === "error" && ![
|
|
372
|
+
"request_validation",
|
|
373
|
+
"path_lookup",
|
|
374
|
+
"principal_resolution",
|
|
375
|
+
"acl_read",
|
|
376
|
+
"owner_validation",
|
|
377
|
+
"acl_mutation",
|
|
378
|
+
"acl_write",
|
|
379
|
+
"acl_readback",
|
|
380
|
+
"acl_verification"
|
|
381
|
+
].includes(String(parsed.error_code))) throw new Error("invalid response");
|
|
382
|
+
response = parsed;
|
|
383
|
+
} catch {
|
|
384
|
+
this.failChild(child, new Error("Windows private ACL broker returned malformed output."));
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
this.pending = null;
|
|
388
|
+
clearTimeout(pending.timer);
|
|
389
|
+
if (response.status === "ok") {
|
|
390
|
+
pending.resolve();
|
|
391
|
+
} else if (response.status === "missing") {
|
|
392
|
+
const error = new Error("Windows private path no longer exists.");
|
|
393
|
+
error.code = "ENOENT";
|
|
394
|
+
pending.reject(error);
|
|
395
|
+
} else {
|
|
396
|
+
pending.reject(new Error(
|
|
397
|
+
`Windows private ACL ${pending.mode} failed (${response.error_code}).`
|
|
398
|
+
));
|
|
399
|
+
}
|
|
400
|
+
this.scheduleIdleShutdown();
|
|
401
|
+
}
|
|
402
|
+
ensureChild() {
|
|
403
|
+
if (this.child && this.child.exitCode === null && !this.child.killed) {
|
|
404
|
+
return { child: this.child, startedNow: false };
|
|
405
|
+
}
|
|
406
|
+
const child = this.spawnProcess();
|
|
407
|
+
this.child = child;
|
|
408
|
+
this.outputBuffer = "";
|
|
409
|
+
child.stdout.setEncoding("utf8");
|
|
410
|
+
child.stdout.on("data", (chunk) => {
|
|
411
|
+
if (this.child !== child) return;
|
|
412
|
+
this.outputBuffer += chunk;
|
|
413
|
+
if (this.outputBuffer.length > 65536) {
|
|
414
|
+
this.failChild(child, new Error("Windows private ACL broker exceeded its output limit."));
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
while (this.child === child) {
|
|
418
|
+
const newline = this.outputBuffer.indexOf("\n");
|
|
419
|
+
if (newline < 0) break;
|
|
420
|
+
const line = this.outputBuffer.slice(0, newline).trim();
|
|
421
|
+
this.outputBuffer = this.outputBuffer.slice(newline + 1);
|
|
422
|
+
this.acceptLine(child, line);
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
child.stderr.on("data", () => void 0);
|
|
426
|
+
child.once("error", () => {
|
|
427
|
+
this.failChild(child, new Error("Windows private ACL broker failed to start."));
|
|
428
|
+
});
|
|
429
|
+
child.once("exit", () => {
|
|
430
|
+
this.failChild(child, new Error("Windows private ACL broker exited before confirmation."));
|
|
431
|
+
});
|
|
432
|
+
child.unref();
|
|
433
|
+
for (const stream of [child.stdin, child.stdout, child.stderr]) {
|
|
434
|
+
stream.unref?.();
|
|
435
|
+
}
|
|
436
|
+
return { child, startedNow: true };
|
|
437
|
+
}
|
|
438
|
+
async dispatch(path, kind, mode) {
|
|
439
|
+
if (this.idleTimer) {
|
|
440
|
+
clearTimeout(this.idleTimer);
|
|
441
|
+
this.idleTimer = null;
|
|
442
|
+
}
|
|
443
|
+
const id = ++this.requestSequence;
|
|
444
|
+
const request = `${JSON.stringify({
|
|
445
|
+
id,
|
|
446
|
+
kind,
|
|
447
|
+
mode,
|
|
448
|
+
path_b64: Buffer.from(path, "utf8").toString("base64")
|
|
449
|
+
})}
|
|
450
|
+
`;
|
|
451
|
+
if (Buffer.byteLength(request, "utf8") > 65536) {
|
|
452
|
+
throw new Error("Windows private ACL broker request exceeds its input limit.");
|
|
453
|
+
}
|
|
454
|
+
const { child, startedNow } = this.ensureChild();
|
|
455
|
+
const timeoutMs = startedNow ? this.startupRequestTimeoutMs : this.requestTimeoutMs;
|
|
456
|
+
await new Promise((resolvePromise, reject) => {
|
|
457
|
+
const timer = setTimeout(() => {
|
|
458
|
+
this.failChild(child, new Error("Windows private ACL broker request timed out."));
|
|
459
|
+
}, timeoutMs);
|
|
460
|
+
this.pending = { id, mode, resolve: resolvePromise, reject, timer };
|
|
461
|
+
child.stdin.write(request, "utf8", (error) => {
|
|
462
|
+
if (error) {
|
|
463
|
+
this.failChild(child, new Error("Windows private ACL broker request failed."));
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
close() {
|
|
469
|
+
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
470
|
+
this.idleTimer = null;
|
|
471
|
+
const child = this.child;
|
|
472
|
+
this.child = null;
|
|
473
|
+
this.outputBuffer = "";
|
|
474
|
+
if (child) child.stdin.end();
|
|
475
|
+
}
|
|
476
|
+
};
|
|
477
|
+
var defaultWindowsPrivateAclBroker = new WindowsPrivateAclBroker();
|
|
478
|
+
var runWindowsPrivateAcl = async (path, kind, mode) => await defaultWindowsPrivateAclBroker.run(path, kind, mode);
|
|
479
|
+
var windowsAclCache = /* @__PURE__ */ new Map();
|
|
480
|
+
var windowsAclIdentity = async (path) => {
|
|
481
|
+
const metadata = await lstat(path, { bigint: true });
|
|
482
|
+
return {
|
|
483
|
+
dev: metadata.dev,
|
|
484
|
+
ino: metadata.ino,
|
|
485
|
+
ctimeNs: metadata.ctimeNs,
|
|
486
|
+
mtimeNs: metadata.mtimeNs,
|
|
487
|
+
mode: metadata.mode,
|
|
488
|
+
size: metadata.size
|
|
489
|
+
};
|
|
490
|
+
};
|
|
491
|
+
var aclCacheKey = (path, kind, mode) => `${kind}:${mode}:${resolve(path)}`;
|
|
492
|
+
var secureWindowsPrivatePath = async (path, kind, mode, platform = process.platform, runner = runWindowsPrivateAcl) => {
|
|
493
|
+
if (platform !== "win32") return;
|
|
494
|
+
const key = aclCacheKey(path, kind, mode);
|
|
495
|
+
const before = await windowsAclIdentity(path);
|
|
496
|
+
const cached = windowsAclCache.get(key);
|
|
497
|
+
if (cached && cached.dev === before.dev && cached.ino === before.ino && cached.ctimeNs === before.ctimeNs && cached.mtimeNs === before.mtimeNs && cached.mode === before.mode && cached.size === before.size) return;
|
|
498
|
+
await runner(path, kind, mode);
|
|
499
|
+
windowsAclCache.set(key, await windowsAclIdentity(path));
|
|
500
|
+
};
|
|
501
|
+
var invalidateWindowsAclCache = (path) => {
|
|
502
|
+
for (const kind of ["file", "directory"]) {
|
|
503
|
+
for (const mode of ["harden", "verify", "verify-owner"]) {
|
|
504
|
+
windowsAclCache.delete(aclCacheKey(path, kind, mode));
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
};
|
|
508
|
+
var rebindHardenedWindowsAclAfterRename = async (source, destination, kind, expected) => {
|
|
509
|
+
const resolvedSource = resolve(source);
|
|
510
|
+
const resolvedDestination = resolve(destination);
|
|
511
|
+
if (resolvedSource === resolvedDestination || dirname(resolvedSource) !== dirname(resolvedDestination)) {
|
|
512
|
+
throw new Error("Windows private ACL cache rename boundary is invalid.");
|
|
513
|
+
}
|
|
514
|
+
const published = await windowsAclIdentity(destination);
|
|
515
|
+
if (published.dev !== expected.dev || published.ino !== expected.ino || published.mode !== expected.mode || published.mtimeNs !== expected.mtimeNs || published.size !== expected.size) {
|
|
516
|
+
invalidateWindowsAclCache(source);
|
|
517
|
+
invalidateWindowsAclCache(destination);
|
|
518
|
+
throw new Error("Windows private file identity changed during atomic publication.");
|
|
519
|
+
}
|
|
520
|
+
invalidateWindowsAclCache(source);
|
|
521
|
+
invalidateWindowsAclCache(destination);
|
|
522
|
+
for (const mode of ["harden", "verify", "verify-owner"]) {
|
|
523
|
+
windowsAclCache.set(aclCacheKey(destination, kind, mode), published);
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
var rebindHardenedWindowsDirectoryAfterOwnedMutation = async (path, expected) => {
|
|
527
|
+
const current = await windowsAclIdentity(path);
|
|
528
|
+
if (current.dev !== expected.dev || current.ino !== expected.ino || current.mode !== expected.mode) {
|
|
529
|
+
invalidateWindowsAclCache(path);
|
|
530
|
+
throw new Error("Windows private directory identity changed during atomic publication.");
|
|
531
|
+
}
|
|
532
|
+
invalidateWindowsAclCache(path);
|
|
533
|
+
for (const mode of ["harden", "verify", "verify-owner"]) {
|
|
534
|
+
windowsAclCache.set(aclCacheKey(path, "directory", mode), current);
|
|
535
|
+
}
|
|
536
|
+
};
|
|
537
|
+
var isDefaultWindowsPrivatePath = (path, root = resolve(homedir(), ".vtx", "inference-host")) => {
|
|
538
|
+
const child = relative(root, resolve(path));
|
|
539
|
+
return child === "" || !child.startsWith("..") && !child.includes(":");
|
|
540
|
+
};
|
|
541
|
+
var secureExistingWindowsPath = async (path, kind, platform = process.platform, runner = runWindowsPrivateAcl, defaultRoot = resolve(homedir(), ".vtx", "inference-host")) => {
|
|
542
|
+
if (platform !== "win32") return;
|
|
543
|
+
if (!isDefaultWindowsPrivatePath(path, defaultRoot)) {
|
|
544
|
+
await secureWindowsPrivatePath(path, kind, "verify", platform, runner);
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
await secureWindowsPrivatePath(path, kind, "harden", platform, runner);
|
|
548
|
+
};
|
|
549
|
+
var linuxProcessIdentity = async (pid) => {
|
|
550
|
+
try {
|
|
551
|
+
const [bootId, statLine] = await Promise.all([
|
|
552
|
+
readFile("/proc/sys/kernel/random/boot_id", "utf8"),
|
|
553
|
+
readFile(`/proc/${pid}/stat`, "utf8")
|
|
554
|
+
]);
|
|
555
|
+
const fields = statLine.slice(statLine.lastIndexOf(")") + 2).trim().split(/\s+/u);
|
|
556
|
+
const startTicks = fields[19];
|
|
557
|
+
return startTicks ? `linux:${bootId.trim()}:${startTicks}` : null;
|
|
558
|
+
} catch {
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
var windowsProcessIdentityInvocation = (pid) => {
|
|
563
|
+
const script = `$ErrorActionPreference='Stop';$p=[Diagnostics.Process]::GetProcessById([int]$env:VTX_PROCESS_PID);[Console]::Out.Write($p.StartTime.ToUniversalTime().ToFileTimeUtc())`;
|
|
564
|
+
return {
|
|
565
|
+
args: [
|
|
566
|
+
"-NoLogo",
|
|
567
|
+
"-NoProfile",
|
|
568
|
+
"-NonInteractive",
|
|
569
|
+
"-EncodedCommand",
|
|
570
|
+
Buffer.from(script, "utf16le").toString("base64")
|
|
571
|
+
],
|
|
572
|
+
env: windowsPowerShellEnvironment({ VTX_PROCESS_PID: String(pid) }),
|
|
573
|
+
timeoutMs: WINDOWS_IDENTITY_COMMAND_TIMEOUT_MS
|
|
574
|
+
};
|
|
575
|
+
};
|
|
576
|
+
var windowsBootIdentityInvocation = () => {
|
|
577
|
+
const script = `$ErrorActionPreference='Stop';$s=[System.Management.ManagementObjectSearcher]::new('SELECT LastBootUpTime FROM Win32_OperatingSystem');try{$c=$s.Get();$e=$c.GetEnumerator();if(-not $e.MoveNext()){throw 'boot identity unavailable'};$raw=[string]$e.Current['LastBootUpTime'];$v=[System.Management.ManagementDateTimeConverter]::ToDateTime($raw).ToUniversalTime().ToFileTimeUtc();[Console]::Out.Write($v)}finally{if($c){$c.Dispose()};$s.Dispose()}`;
|
|
578
|
+
return {
|
|
579
|
+
args: [
|
|
580
|
+
"-NoLogo",
|
|
581
|
+
"-NoProfile",
|
|
582
|
+
"-NonInteractive",
|
|
583
|
+
"-EncodedCommand",
|
|
584
|
+
Buffer.from(script, "utf16le").toString("base64")
|
|
585
|
+
],
|
|
586
|
+
env: windowsPowerShellEnvironment(),
|
|
587
|
+
timeoutMs: WINDOWS_IDENTITY_COMMAND_TIMEOUT_MS
|
|
588
|
+
};
|
|
589
|
+
};
|
|
590
|
+
var windowsProcessIdentity = async (pid) => await new Promise((resolvePromise) => {
|
|
591
|
+
const invocation = windowsProcessIdentityInvocation(pid);
|
|
592
|
+
const child = spawn("powershell.exe", [...invocation.args], {
|
|
593
|
+
windowsHide: true,
|
|
594
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
595
|
+
env: invocation.env
|
|
596
|
+
});
|
|
597
|
+
const output = [];
|
|
598
|
+
let settled = false;
|
|
599
|
+
const settle = (value) => {
|
|
600
|
+
if (settled) return;
|
|
601
|
+
settled = true;
|
|
602
|
+
clearTimeout(timer);
|
|
603
|
+
resolvePromise(value);
|
|
604
|
+
};
|
|
605
|
+
const timer = setTimeout(() => {
|
|
606
|
+
child.kill("SIGKILL");
|
|
607
|
+
settle(null);
|
|
608
|
+
}, invocation.timeoutMs);
|
|
609
|
+
child.stdout.on("data", (chunk) => {
|
|
610
|
+
if (output.reduce((total, item) => total + item.byteLength, 0) + Buffer.byteLength(chunk) > 1024) {
|
|
611
|
+
child.kill("SIGKILL");
|
|
612
|
+
settle(null);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
output.push(Buffer.from(chunk));
|
|
616
|
+
});
|
|
617
|
+
child.once("error", () => settle(null));
|
|
618
|
+
child.once("exit", (code) => {
|
|
619
|
+
const started = Buffer.concat(output).toString("utf8").trim();
|
|
620
|
+
settle(code === 0 && /^\d+$/u.test(started) ? `win32:${started}` : null);
|
|
621
|
+
});
|
|
622
|
+
});
|
|
623
|
+
var darwinProcessIdentity = async (pid) => await new Promise((resolvePromise) => {
|
|
624
|
+
const child = spawn("ps", ["-p", String(pid), "-o", "lstart="], {
|
|
625
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
626
|
+
});
|
|
627
|
+
const output = [];
|
|
628
|
+
let settled = false;
|
|
629
|
+
const settle = (value) => {
|
|
630
|
+
if (settled) return;
|
|
631
|
+
settled = true;
|
|
632
|
+
clearTimeout(timer);
|
|
633
|
+
resolvePromise(value);
|
|
634
|
+
};
|
|
635
|
+
const timer = setTimeout(() => {
|
|
636
|
+
child.kill("SIGKILL");
|
|
637
|
+
settle(null);
|
|
638
|
+
}, 5e3);
|
|
639
|
+
child.stdout.on("data", (chunk) => output.push(Buffer.from(chunk)));
|
|
640
|
+
child.once("error", () => settle(null));
|
|
641
|
+
child.once("exit", (code) => {
|
|
642
|
+
const started = Date.parse(Buffer.concat(output).toString("utf8").trim());
|
|
643
|
+
settle(code === 0 && Number.isFinite(started) ? `darwin:${Math.floor(started / 1e3)}` : null);
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
var runSmallIdentityCommand = async (command, args, env, timeoutMs = SMALL_IDENTITY_COMMAND_TIMEOUT_MS) => await new Promise((resolvePromise) => {
|
|
647
|
+
const child = spawn(command, [...args], {
|
|
648
|
+
windowsHide: true,
|
|
649
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
650
|
+
env
|
|
651
|
+
});
|
|
652
|
+
const output = [];
|
|
653
|
+
let bytes = 0;
|
|
654
|
+
let settled = false;
|
|
655
|
+
const settle = (value) => {
|
|
656
|
+
if (settled) return;
|
|
657
|
+
settled = true;
|
|
658
|
+
clearTimeout(timer);
|
|
659
|
+
resolvePromise(value);
|
|
660
|
+
};
|
|
661
|
+
const timer = setTimeout(() => {
|
|
662
|
+
child.kill("SIGKILL");
|
|
663
|
+
settle(null);
|
|
664
|
+
}, timeoutMs);
|
|
665
|
+
child.stdout.on("data", (chunk) => {
|
|
666
|
+
bytes += Buffer.byteLength(chunk);
|
|
667
|
+
if (bytes > 4096) {
|
|
668
|
+
child.kill("SIGKILL");
|
|
669
|
+
settle(null);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
output.push(Buffer.from(chunk));
|
|
673
|
+
});
|
|
674
|
+
child.once("error", () => settle(null));
|
|
675
|
+
child.once("exit", (code) => settle(
|
|
676
|
+
code === 0 ? Buffer.concat(output).toString("utf8").trim() || null : null
|
|
677
|
+
));
|
|
678
|
+
});
|
|
679
|
+
var cachedSystemBootIdentity = null;
|
|
680
|
+
var readInferenceSystemBootIdentity = async (platform = process.platform) => {
|
|
681
|
+
if (platform === process.platform && cachedSystemBootIdentity) {
|
|
682
|
+
return await cachedSystemBootIdentity;
|
|
683
|
+
}
|
|
684
|
+
const read = async () => {
|
|
685
|
+
let identity = null;
|
|
686
|
+
if (platform === "linux") {
|
|
687
|
+
try {
|
|
688
|
+
identity = (await readFile("/proc/sys/kernel/random/boot_id", "utf8")).trim();
|
|
689
|
+
} catch {
|
|
690
|
+
identity = null;
|
|
691
|
+
}
|
|
692
|
+
} else if (platform === "win32") {
|
|
693
|
+
const invocation = windowsBootIdentityInvocation();
|
|
694
|
+
identity = await runSmallIdentityCommand(
|
|
695
|
+
"powershell.exe",
|
|
696
|
+
invocation.args,
|
|
697
|
+
invocation.env,
|
|
698
|
+
invocation.timeoutMs
|
|
699
|
+
);
|
|
700
|
+
} else if (platform === "darwin") {
|
|
701
|
+
identity = await runSmallIdentityCommand("sysctl", ["-n", "kern.boottime"], void 0);
|
|
702
|
+
}
|
|
703
|
+
if (!identity || /[\r\n\0]/u.test(identity)) {
|
|
704
|
+
throw new Error("Inference host could not establish the current system boot identity.");
|
|
705
|
+
}
|
|
706
|
+
return `${platform}:${identity}`;
|
|
707
|
+
};
|
|
708
|
+
const pending = read();
|
|
709
|
+
if (platform === process.platform) cachedSystemBootIdentity = pending;
|
|
710
|
+
try {
|
|
711
|
+
return await pending;
|
|
712
|
+
} catch (error) {
|
|
713
|
+
if (platform === process.platform) cachedSystemBootIdentity = null;
|
|
714
|
+
throw error;
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
var defaultProcessIdentity = async (pid) => {
|
|
718
|
+
if (process.platform === "linux") return await linuxProcessIdentity(pid);
|
|
719
|
+
if (process.platform === "win32") return await windowsProcessIdentity(pid);
|
|
720
|
+
if (process.platform === "darwin") return await darwinProcessIdentity(pid);
|
|
721
|
+
return null;
|
|
722
|
+
};
|
|
723
|
+
var defaultCurrentProcessIdentity = async () => {
|
|
724
|
+
const identity = await defaultProcessIdentity(process.pid);
|
|
725
|
+
if (!identity) throw new Error("Inference host could not establish its process-start identity.");
|
|
726
|
+
return identity;
|
|
727
|
+
};
|
|
728
|
+
var processLockObservationCache = /* @__PURE__ */ new Map();
|
|
729
|
+
var inferenceProcessIdentitiesMatch = (stored, observed) => stored === observed;
|
|
730
|
+
async function ensureInferencePrivateDirectory(path, dependencies = {}) {
|
|
731
|
+
const platform = dependencies.platform ?? process.platform;
|
|
732
|
+
const runner = dependencies.runner ?? runWindowsPrivateAcl;
|
|
733
|
+
const defaultRoot = dependencies.defaultRoot ?? resolve(homedir(), ".vtx", "inference-host");
|
|
734
|
+
let created = false;
|
|
735
|
+
try {
|
|
736
|
+
const before = await lstat(path);
|
|
737
|
+
if (before.isSymbolicLink() || !before.isDirectory()) {
|
|
738
|
+
throw new Error("Inference host state directory is unsafe.");
|
|
739
|
+
}
|
|
740
|
+
} catch (error) {
|
|
741
|
+
if (error.code !== "ENOENT") throw error;
|
|
742
|
+
await mkdir(path, { recursive: true, mode: 448 });
|
|
743
|
+
created = true;
|
|
744
|
+
}
|
|
745
|
+
const after = await lstat(path);
|
|
746
|
+
if (after.isSymbolicLink() || !after.isDirectory()) {
|
|
747
|
+
throw new Error("Inference host state directory is unsafe.");
|
|
748
|
+
}
|
|
749
|
+
if (platform === "win32") {
|
|
750
|
+
if (created) {
|
|
751
|
+
await secureWindowsPrivatePath(path, "directory", "harden", platform, runner);
|
|
752
|
+
} else {
|
|
753
|
+
await secureExistingWindowsPath(
|
|
754
|
+
path,
|
|
755
|
+
"directory",
|
|
756
|
+
platform,
|
|
757
|
+
runner,
|
|
758
|
+
defaultRoot
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
if ((after.mode & 63) !== 0) {
|
|
764
|
+
throw new Error("Inference host state directory must have mode 0700.");
|
|
765
|
+
}
|
|
766
|
+
if (typeof process.getuid === "function" && after.uid !== process.getuid()) {
|
|
767
|
+
throw new Error("Inference host state directory must be owned by the current user.");
|
|
768
|
+
}
|
|
769
|
+
const canonical = await realpath(path);
|
|
770
|
+
if (canonical !== resolve(path)) {
|
|
771
|
+
throw new Error("Inference host state directory may not traverse symbolic links.");
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
async function syncInferenceDirectory(path, platform = process.platform) {
|
|
775
|
+
if (platform === "win32") return;
|
|
776
|
+
const handle = await open(path, constants.O_RDONLY | constants.O_DIRECTORY);
|
|
777
|
+
try {
|
|
778
|
+
await handle.sync();
|
|
779
|
+
} finally {
|
|
780
|
+
await handle.close();
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
var WINDOWS_PRIVATE_FILE_REPLACE_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
784
|
+
"EACCES",
|
|
785
|
+
"EBUSY",
|
|
786
|
+
"EEXIST",
|
|
787
|
+
"ENOTEMPTY",
|
|
788
|
+
"EPERM"
|
|
789
|
+
]);
|
|
790
|
+
async function replaceAtomicInferencePrivateFile(temporaryPath, destinationPath, options = {}) {
|
|
791
|
+
const platform = options.platform ?? process.platform;
|
|
792
|
+
const renameFile = options.renameFile ?? rename;
|
|
793
|
+
const sleep = options.sleep ?? ((milliseconds) => new Promise((resolve4) => setTimeout(resolve4, milliseconds)));
|
|
794
|
+
const maxAttempts = Math.max(1, options.maxAttempts ?? 40);
|
|
795
|
+
const retryDelayMs = Math.max(0, options.retryDelayMs ?? 25);
|
|
796
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
797
|
+
try {
|
|
798
|
+
await renameFile(temporaryPath, destinationPath);
|
|
799
|
+
return;
|
|
800
|
+
} catch (error) {
|
|
801
|
+
const code = String(error.code ?? "");
|
|
802
|
+
if (platform !== "win32" || !WINDOWS_PRIVATE_FILE_REPLACE_ERROR_CODES.has(code) || attempt === maxAttempts - 1) {
|
|
803
|
+
throw error;
|
|
804
|
+
}
|
|
805
|
+
await sleep(retryDelayMs);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
async function writeAtomicInferencePrivateFile(path, contents, dependencies = {}) {
|
|
810
|
+
const platform = dependencies.platform ?? process.platform;
|
|
811
|
+
const runner = dependencies.runner ?? runWindowsPrivateAcl;
|
|
812
|
+
const defaultRoot = dependencies.defaultRoot ?? resolve(homedir(), ".vtx", "inference-host");
|
|
813
|
+
const directory = dirname(path);
|
|
814
|
+
await ensureInferencePrivateDirectory(directory, dependencies);
|
|
815
|
+
const hardenedDirectoryIdentity = platform === "win32" && isDefaultWindowsPrivatePath(directory, defaultRoot) ? windowsAclCache.get(aclCacheKey(directory, "directory", "harden")) : void 0;
|
|
816
|
+
if (platform === "win32" && isDefaultWindowsPrivatePath(directory, defaultRoot) && !hardenedDirectoryIdentity) {
|
|
817
|
+
throw new Error("Windows private directory ACL hardening was not durably verified.");
|
|
818
|
+
}
|
|
819
|
+
const temporary = join(
|
|
820
|
+
directory,
|
|
821
|
+
`.${process.pid}.${randomBytes(12).toString("hex")}.tmp`
|
|
822
|
+
);
|
|
823
|
+
const handle = await open(
|
|
824
|
+
temporary,
|
|
825
|
+
constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW,
|
|
826
|
+
384
|
|
827
|
+
);
|
|
828
|
+
try {
|
|
829
|
+
await handle.writeFile(contents, "utf8");
|
|
830
|
+
await handle.sync();
|
|
831
|
+
} catch (error) {
|
|
832
|
+
await handle.close().catch(() => void 0);
|
|
833
|
+
await rm(temporary, { force: true }).catch(() => void 0);
|
|
834
|
+
throw error;
|
|
835
|
+
}
|
|
836
|
+
await handle.close();
|
|
837
|
+
await secureWindowsPrivatePath(temporary, "file", "harden", platform, runner);
|
|
838
|
+
const hardenedIdentity = platform === "win32" ? windowsAclCache.get(aclCacheKey(temporary, "file", "harden")) : void 0;
|
|
839
|
+
if (platform === "win32" && !hardenedIdentity) {
|
|
840
|
+
await rm(temporary, { force: true }).catch(() => void 0);
|
|
841
|
+
throw new Error("Windows private ACL hardening was not durably verified.");
|
|
842
|
+
}
|
|
843
|
+
try {
|
|
844
|
+
invalidateWindowsAclCache(path);
|
|
845
|
+
await replaceAtomicInferencePrivateFile(temporary, path, {
|
|
846
|
+
platform,
|
|
847
|
+
renameFile: dependencies.renameFile,
|
|
848
|
+
sleep: dependencies.sleep,
|
|
849
|
+
maxAttempts: dependencies.replaceMaxAttempts,
|
|
850
|
+
retryDelayMs: dependencies.replaceRetryDelayMs
|
|
851
|
+
});
|
|
852
|
+
if (platform === "win32" && hardenedIdentity) {
|
|
853
|
+
await rebindHardenedWindowsAclAfterRename(
|
|
854
|
+
temporary,
|
|
855
|
+
path,
|
|
856
|
+
"file",
|
|
857
|
+
hardenedIdentity
|
|
858
|
+
);
|
|
859
|
+
} else {
|
|
860
|
+
invalidateWindowsAclCache(temporary);
|
|
861
|
+
}
|
|
862
|
+
await syncInferenceDirectory(directory, platform);
|
|
863
|
+
if (platform === "win32" && hardenedDirectoryIdentity) {
|
|
864
|
+
await rebindHardenedWindowsDirectoryAfterOwnedMutation(
|
|
865
|
+
directory,
|
|
866
|
+
hardenedDirectoryIdentity
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
} catch (error) {
|
|
870
|
+
await rm(temporary, { force: true }).catch(() => void 0);
|
|
871
|
+
invalidateWindowsAclCache(temporary);
|
|
872
|
+
throw error;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
async function acquireInferenceHostProcessLock(path, dependencies = {}) {
|
|
876
|
+
const directory = dirname(path);
|
|
877
|
+
await ensureInferencePrivateDirectory(directory);
|
|
878
|
+
const ownerToken = randomBytes(24).toString("base64url");
|
|
879
|
+
const observeIdentity = dependencies.processIdentity ?? defaultProcessIdentity;
|
|
880
|
+
const currentBootIdentity = dependencies.currentBootIdentity ?? (() => readInferenceSystemBootIdentity());
|
|
881
|
+
const isProcessAlive = dependencies.processAlive ?? ((pid) => {
|
|
882
|
+
try {
|
|
883
|
+
process.kill(pid, 0);
|
|
884
|
+
return true;
|
|
885
|
+
} catch (error) {
|
|
886
|
+
if (error.code === "ESRCH") return false;
|
|
887
|
+
throw error;
|
|
888
|
+
}
|
|
889
|
+
});
|
|
890
|
+
let handle;
|
|
891
|
+
for (let acquisition = 0; acquisition < 2; acquisition += 1) {
|
|
892
|
+
try {
|
|
893
|
+
handle = await open(
|
|
894
|
+
path,
|
|
895
|
+
constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW,
|
|
896
|
+
384
|
|
897
|
+
);
|
|
898
|
+
break;
|
|
899
|
+
} catch (error) {
|
|
900
|
+
if (error.code !== "EEXIST") throw error;
|
|
901
|
+
if (acquisition > 0) {
|
|
902
|
+
throw new Error("Another inference host process already owns this local host.");
|
|
903
|
+
}
|
|
904
|
+
let before;
|
|
905
|
+
try {
|
|
906
|
+
before = await lstat(path);
|
|
907
|
+
} catch (error2) {
|
|
908
|
+
if (error2.code === "ENOENT") continue;
|
|
909
|
+
throw error2;
|
|
910
|
+
}
|
|
911
|
+
if (before.isSymbolicLink() || !before.isFile() || process.platform !== "win32" && (before.mode & 63) !== 0 || typeof process.getuid === "function" && before.uid !== process.getuid()) {
|
|
912
|
+
throw new Error("Inference host process lock is unsafe.");
|
|
913
|
+
}
|
|
914
|
+
const reclaimPublicationResidue = async () => {
|
|
915
|
+
let current2;
|
|
916
|
+
try {
|
|
917
|
+
current2 = await lstat(path);
|
|
918
|
+
} catch (error2) {
|
|
919
|
+
if (error2.code === "ENOENT") return false;
|
|
920
|
+
throw error2;
|
|
921
|
+
}
|
|
922
|
+
if (current2.isSymbolicLink() || !current2.isFile() || current2.dev !== before.dev || current2.ino !== before.ino || process.platform !== "win32" && (current2.mode & 63) !== 0 || typeof process.getuid === "function" && current2.uid !== process.getuid()) {
|
|
923
|
+
throw new Error("Inference host process lock publication residue changed or is unsafe.");
|
|
924
|
+
}
|
|
925
|
+
try {
|
|
926
|
+
await rm(path);
|
|
927
|
+
} catch (error2) {
|
|
928
|
+
if (error2.code === "ENOENT") return false;
|
|
929
|
+
throw error2;
|
|
930
|
+
}
|
|
931
|
+
processLockObservationCache.delete(resolve(path));
|
|
932
|
+
await syncInferenceDirectory(directory);
|
|
933
|
+
return true;
|
|
934
|
+
};
|
|
935
|
+
try {
|
|
936
|
+
await secureExistingWindowsPath(path, "file");
|
|
937
|
+
} catch (error2) {
|
|
938
|
+
if (error2.code === "ENOENT") continue;
|
|
939
|
+
throw error2;
|
|
940
|
+
}
|
|
941
|
+
let staleOwner;
|
|
942
|
+
try {
|
|
943
|
+
staleOwner = JSON.parse(await readFile(path, "utf8"));
|
|
944
|
+
} catch (error2) {
|
|
945
|
+
if (error2.code === "ENOENT") continue;
|
|
946
|
+
if (Date.now() - before.mtimeMs < 1e4) {
|
|
947
|
+
throw new Error("Another inference host process already owns this local host.");
|
|
948
|
+
}
|
|
949
|
+
await reclaimPublicationResidue();
|
|
950
|
+
continue;
|
|
951
|
+
}
|
|
952
|
+
if (staleOwner.schema_version !== 2 || !Number.isSafeInteger(staleOwner.pid) || Number(staleOwner.pid) < 1 || typeof staleOwner.owner_token !== "string" || !staleOwner.owner_token || typeof staleOwner.process_identity !== "string" || !staleOwner.process_identity || typeof staleOwner.boot_identity !== "string" || !staleOwner.boot_identity) {
|
|
953
|
+
if (Date.now() - before.mtimeMs < 1e4) {
|
|
954
|
+
throw new Error("Another inference host process already owns this local host.");
|
|
955
|
+
}
|
|
956
|
+
await reclaimPublicationResidue();
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
const stalePid = Number(staleOwner.pid);
|
|
960
|
+
const bootIdentity2 = await currentBootIdentity();
|
|
961
|
+
if (bootIdentity2 === staleOwner.boot_identity && isProcessAlive(stalePid)) {
|
|
962
|
+
const cached = processLockObservationCache.get(resolve(path));
|
|
963
|
+
const observedIdentity = cached && cached.dev === before.dev && cached.ino === before.ino && cached.pid === stalePid && cached.storedIdentity === staleOwner.process_identity && Date.now() - cached.observedAtMs < 5e3 ? cached.observedIdentity : await observeIdentity(stalePid);
|
|
964
|
+
if (!observedIdentity) {
|
|
965
|
+
throw new Error("Inference host could not verify the existing lock owner identity.");
|
|
966
|
+
}
|
|
967
|
+
processLockObservationCache.set(resolve(path), {
|
|
968
|
+
dev: before.dev,
|
|
969
|
+
ino: before.ino,
|
|
970
|
+
pid: stalePid,
|
|
971
|
+
storedIdentity: staleOwner.process_identity,
|
|
972
|
+
observedIdentity,
|
|
973
|
+
observedAtMs: Date.now()
|
|
974
|
+
});
|
|
975
|
+
if (inferenceProcessIdentitiesMatch(staleOwner.process_identity, observedIdentity)) {
|
|
976
|
+
throw new Error("Another inference host process already owns this local host.");
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
let current;
|
|
980
|
+
try {
|
|
981
|
+
current = await lstat(path);
|
|
982
|
+
} catch (error2) {
|
|
983
|
+
if (error2.code === "ENOENT") continue;
|
|
984
|
+
throw error2;
|
|
985
|
+
}
|
|
986
|
+
if (current.dev !== before.dev || current.ino !== before.ino) {
|
|
987
|
+
throw new Error("Inference host process lock ownership changed.");
|
|
988
|
+
}
|
|
989
|
+
await rm(path);
|
|
990
|
+
processLockObservationCache.delete(resolve(path));
|
|
991
|
+
await syncInferenceDirectory(directory);
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
if (!handle) throw new Error("Inference host process lock could not be acquired.");
|
|
995
|
+
let ownerIdentity;
|
|
996
|
+
let bootIdentity;
|
|
997
|
+
try {
|
|
998
|
+
[ownerIdentity, bootIdentity] = await Promise.all([
|
|
999
|
+
(dependencies.currentProcessIdentity ?? defaultCurrentProcessIdentity)(),
|
|
1000
|
+
currentBootIdentity()
|
|
1001
|
+
]);
|
|
1002
|
+
} catch (error) {
|
|
1003
|
+
await handle.close().catch(() => void 0);
|
|
1004
|
+
await rm(path, { force: true }).catch(() => void 0);
|
|
1005
|
+
throw error;
|
|
1006
|
+
}
|
|
1007
|
+
await handle.writeFile(
|
|
1008
|
+
`${JSON.stringify({
|
|
1009
|
+
schema_version: 2,
|
|
1010
|
+
pid: process.pid,
|
|
1011
|
+
owner_token: ownerToken,
|
|
1012
|
+
process_identity: ownerIdentity,
|
|
1013
|
+
boot_identity: bootIdentity
|
|
1014
|
+
})}
|
|
1015
|
+
`,
|
|
1016
|
+
"utf8"
|
|
1017
|
+
);
|
|
1018
|
+
await handle.sync();
|
|
1019
|
+
await handle.close();
|
|
1020
|
+
await secureWindowsPrivatePath(path, "file", "harden");
|
|
1021
|
+
await syncInferenceDirectory(directory);
|
|
1022
|
+
let released = false;
|
|
1023
|
+
return {
|
|
1024
|
+
path,
|
|
1025
|
+
ownerToken,
|
|
1026
|
+
release: async () => {
|
|
1027
|
+
if (released) return;
|
|
1028
|
+
const metadata = await lstat(path);
|
|
1029
|
+
if (metadata.isSymbolicLink() || !metadata.isFile()) {
|
|
1030
|
+
throw new Error("Inference host process lock is unsafe.");
|
|
1031
|
+
}
|
|
1032
|
+
const parsed = JSON.parse(await readFile(path, "utf8"));
|
|
1033
|
+
if (parsed.owner_token !== ownerToken) {
|
|
1034
|
+
throw new Error("Inference host process lock ownership changed.");
|
|
1035
|
+
}
|
|
1036
|
+
await rm(path);
|
|
1037
|
+
processLockObservationCache.delete(resolve(path));
|
|
1038
|
+
await syncInferenceDirectory(directory);
|
|
1039
|
+
released = true;
|
|
1040
|
+
}
|
|
1041
|
+
};
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// lib/inference-host/service-launcher-state.ts
|
|
1045
|
+
import { access, readFile as readFile2, rm as rm2 } from "node:fs/promises";
|
|
1046
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2, relative as relative2, resolve as resolve2 } from "node:path";
|
|
1047
|
+
var launcherSibling = (manifest, suffix) => {
|
|
1048
|
+
if (!manifest.endsWith(".service.json")) throw new Error("Invalid service manifest path.");
|
|
1049
|
+
return `${manifest.slice(0, -".service.json".length)}${suffix}`;
|
|
1050
|
+
};
|
|
1051
|
+
var within = (parent, child) => {
|
|
1052
|
+
const part = relative2(resolve2(parent), resolve2(child));
|
|
1053
|
+
return Boolean(part) && !part.startsWith("..") && !isAbsolute2(part);
|
|
1054
|
+
};
|
|
1055
|
+
var launcherSelection = (manifest, value) => {
|
|
1056
|
+
if (!value || typeof value !== "object") return null;
|
|
1057
|
+
const r = value;
|
|
1058
|
+
if (r.schema_version !== "vtx_inference_service_release_v1" || r.package_name !== "@vtxmacro/cli" || typeof r.package_version !== "string" || !/^\d{4}\.\d{1,2}\.\d{1,3}$/u.test(r.package_version) || typeof r.integrity !== "string" || typeof r.selected_at !== "string" || !Number.isFinite(Date.parse(r.selected_at)) || typeof r.script !== "string" || !isAbsolute2(r.script) || /[\r\n\0]/u.test(r.script) || !within(launcherSibling(manifest, ".service-releases"), r.script)) return null;
|
|
1059
|
+
return r;
|
|
1060
|
+
};
|
|
1061
|
+
var readLauncherSelection = async (manifest) => {
|
|
1062
|
+
try {
|
|
1063
|
+
return launcherSelection(manifest, JSON.parse(await readFile2(launcherSibling(manifest, ".service-release.json"), "utf8")));
|
|
1064
|
+
} catch (error) {
|
|
1065
|
+
if (error.code === "ENOENT") return null;
|
|
1066
|
+
throw error;
|
|
1067
|
+
}
|
|
1068
|
+
};
|
|
1069
|
+
var writeLauncherSelection = async (manifest, selection) => {
|
|
1070
|
+
const path = launcherSibling(manifest, ".service-release.json");
|
|
1071
|
+
if (selection) await writeAtomicInferencePrivateFile(path, `${JSON.stringify(selection)}
|
|
1072
|
+
`);
|
|
1073
|
+
else await rm2(path, { force: true });
|
|
1074
|
+
};
|
|
1075
|
+
var readLauncherState = async (manifest) => {
|
|
1076
|
+
let value;
|
|
1077
|
+
try {
|
|
1078
|
+
value = JSON.parse(await readFile2(launcherSibling(manifest, ".service-launcher.json"), "utf8"));
|
|
1079
|
+
} catch (error) {
|
|
1080
|
+
if (error.code === "ENOENT") return null;
|
|
1081
|
+
throw error;
|
|
1082
|
+
}
|
|
1083
|
+
if (!value || typeof value !== "object") throw new Error("Invalid service launcher state.");
|
|
1084
|
+
const r = value;
|
|
1085
|
+
const target = (v) => {
|
|
1086
|
+
if (!v || typeof v !== "object") return false;
|
|
1087
|
+
const t = v;
|
|
1088
|
+
return typeof t.package_version === "string" && /^\d{4}\.\d{1,2}\.\d{1,3}$/u.test(t.package_version) && typeof t.script === "string" && isAbsolute2(t.script) && !/[\r\n\0]/u.test(t.script) && (within(launcherSibling(manifest, ".service-releases"), t.script) || resolve2(t.script) === resolve2(launcherSibling(manifest, ".service-bootstrap.previous.js"))) && (t.selection === null || (() => {
|
|
1089
|
+
const selected = launcherSelection(manifest, t.selection);
|
|
1090
|
+
return Boolean(selected && selected.package_version === t.package_version && (resolve2(t.script) === resolve2(join2(dirname2(selected.script), "vtx-service-bootstrap.js")) || resolve2(t.script) === resolve2(launcherSibling(manifest, ".service-bootstrap.previous.js"))));
|
|
1091
|
+
})());
|
|
1092
|
+
};
|
|
1093
|
+
if (r.schema_version !== "vtx_inference_service_launcher_v1" || !target(r.active) || r.previous !== null && !target(r.previous) || typeof r.pending !== "boolean") {
|
|
1094
|
+
throw new Error("Invalid service launcher recovery targets.");
|
|
1095
|
+
}
|
|
1096
|
+
return r;
|
|
1097
|
+
};
|
|
1098
|
+
var writeLauncherState = async (manifest, state) => {
|
|
1099
|
+
await writeAtomicInferencePrivateFile(launcherSibling(manifest, ".service-launcher.json"), `${JSON.stringify(state)}
|
|
1100
|
+
`);
|
|
1101
|
+
};
|
|
1102
|
+
var targetForSelection = async (selection) => {
|
|
1103
|
+
const script = join2(dirname2(selection.script), "vtx-service-bootstrap.js");
|
|
1104
|
+
await access(script);
|
|
1105
|
+
return { package_version: selection.package_version, script, selection };
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
// lib/inference-host/service-launcher.ts
|
|
1109
|
+
var runInferenceHostServiceLauncher = async (manifestPathValue, lockPathValue, dependencies = {}) => {
|
|
1110
|
+
const manifestPath = resolve3(manifestPathValue);
|
|
1111
|
+
const lock = await acquireInferenceHostProcessLock(launcherSibling(manifestPath, ".service-launcher.lock"));
|
|
1112
|
+
const lifetime = new AbortController();
|
|
1113
|
+
let child = null;
|
|
1114
|
+
const stop = () => {
|
|
1115
|
+
lifetime.abort();
|
|
1116
|
+
child?.requestDrain();
|
|
1117
|
+
};
|
|
1118
|
+
dependencies.signal?.addEventListener("abort", stop, { once: true });
|
|
1119
|
+
process.on("SIGTERM", stop);
|
|
1120
|
+
process.on("SIGINT", stop);
|
|
1121
|
+
if (dependencies.signal?.aborted) stop();
|
|
1122
|
+
const desired = async () => {
|
|
1123
|
+
if (lifetime.signal.aborted) return false;
|
|
1124
|
+
const value = JSON.parse(await readFile3(launcherSibling(manifestPath, ".service-desired.json"), "utf8"));
|
|
1125
|
+
return value.schema_version === "vtx_inference_service_desired_v1" && value.desired_running === true;
|
|
1126
|
+
};
|
|
1127
|
+
let manifest;
|
|
1128
|
+
const log = async (event, version) => {
|
|
1129
|
+
await appendFile(manifest.log_path, `${JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString(), event, package_version: version })}
|
|
1130
|
+
`, { mode: 384 }).catch(() => void 0);
|
|
1131
|
+
};
|
|
1132
|
+
const runCore = dependencies.runCore ?? ((executable, target, args) => {
|
|
1133
|
+
const processChild = spawn2(executable, [target.script, ...args], {
|
|
1134
|
+
windowsHide: true,
|
|
1135
|
+
stdio: ["ignore", "inherit", "inherit", "ipc"],
|
|
1136
|
+
env: { ...process.env, VTX_INFERENCE_HOST_LAUNCHER_PROTOCOL: "1" }
|
|
1137
|
+
});
|
|
1138
|
+
const ready = new Promise((readyResolve) => {
|
|
1139
|
+
processChild.on("message", (message) => {
|
|
1140
|
+
if (message && typeof message === "object" && message.type === "vtx-inference-host-bootstrap-ready-v1" && message.package_version === target.package_version) readyResolve();
|
|
1141
|
+
});
|
|
1142
|
+
});
|
|
1143
|
+
const completion = new Promise((complete) => {
|
|
1144
|
+
processChild.once("error", () => complete(1));
|
|
1145
|
+
processChild.once("exit", (code) => complete(code ?? 1));
|
|
1146
|
+
});
|
|
1147
|
+
return { completion, ready, requestDrain: () => {
|
|
1148
|
+
if (processChild.connected) processChild.send({ type: "vtx-inference-host-launcher-stop-v1" }, () => void 0);
|
|
1149
|
+
}, terminate: () => {
|
|
1150
|
+
processChild.kill("SIGKILL");
|
|
1151
|
+
} };
|
|
1152
|
+
});
|
|
1153
|
+
const drainCore = async (running) => {
|
|
1154
|
+
running.requestDrain();
|
|
1155
|
+
let timer;
|
|
1156
|
+
const exited = await Promise.race([
|
|
1157
|
+
running.completion.then((code) => ({ code })),
|
|
1158
|
+
new Promise((done) => {
|
|
1159
|
+
timer = setTimeout(
|
|
1160
|
+
() => done(null),
|
|
1161
|
+
dependencies.stopTimeoutMs ?? runtime_policy_generated_default.external_inference.service.bootstrap_stop_timeout_ms
|
|
1162
|
+
);
|
|
1163
|
+
})
|
|
1164
|
+
]).finally(() => clearTimeout(timer));
|
|
1165
|
+
if (exited) return exited.code;
|
|
1166
|
+
running.terminate();
|
|
1167
|
+
return await running.completion;
|
|
1168
|
+
};
|
|
1169
|
+
try {
|
|
1170
|
+
manifest = JSON.parse(await readFile3(manifestPath, "utf8"));
|
|
1171
|
+
let state = await readLauncherState(manifestPath);
|
|
1172
|
+
if (!state) throw new Error("Service launcher recovery state is missing.");
|
|
1173
|
+
const rollback = async (reason) => {
|
|
1174
|
+
if (!state.previous || !await desired()) return false;
|
|
1175
|
+
const failed = state.active;
|
|
1176
|
+
await writeLauncherSelection(manifestPath, state.previous.selection);
|
|
1177
|
+
await writeAtomicInferencePrivateFile(launcherSibling(manifestPath, ".service-release-rejection.json"), `${JSON.stringify({
|
|
1178
|
+
schema_version: "vtx_inference_service_release_rejection_v1",
|
|
1179
|
+
package_version: failed.package_version,
|
|
1180
|
+
rejected_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1181
|
+
reason
|
|
1182
|
+
})}
|
|
1183
|
+
`);
|
|
1184
|
+
state = { ...state, active: state.previous, previous: null, pending: false };
|
|
1185
|
+
await writeLauncherState(manifestPath, state);
|
|
1186
|
+
await log("service_launcher_rolled_back", failed.package_version);
|
|
1187
|
+
return true;
|
|
1188
|
+
};
|
|
1189
|
+
if (state.pending && state.previous && await desired()) await rollback("bootstrap_interrupted_before_readiness");
|
|
1190
|
+
while (await desired()) {
|
|
1191
|
+
state.pending = true;
|
|
1192
|
+
await writeLauncherState(manifestPath, state);
|
|
1193
|
+
if (!await desired()) return 0;
|
|
1194
|
+
const launchedAt = Date.now();
|
|
1195
|
+
const runtimePath = launcherSibling(manifestPath, ".service-runtime.json");
|
|
1196
|
+
await rm3(runtimePath, { force: true });
|
|
1197
|
+
if (!await desired()) return 0;
|
|
1198
|
+
child = runCore(manifest.executable, state.active, ["--service-manifest", manifestPath, "--service-lock", resolve3(lockPathValue)]);
|
|
1199
|
+
const readiness = new AbortController();
|
|
1200
|
+
const watchingStop = new AbortController();
|
|
1201
|
+
const stopped = (async () => {
|
|
1202
|
+
while (!watchingStop.signal.aborted) {
|
|
1203
|
+
if (!await desired()) return { kind: "stop", code: 0 };
|
|
1204
|
+
await new Promise((done) => {
|
|
1205
|
+
if (watchingStop.signal.aborted) {
|
|
1206
|
+
done();
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
const timer2 = setTimeout(finish, runtime_policy_generated_default.external_inference.service.startup_poll_interval_ms);
|
|
1210
|
+
function finish() {
|
|
1211
|
+
clearTimeout(timer2);
|
|
1212
|
+
watchingStop.signal.removeEventListener("abort", finish);
|
|
1213
|
+
done();
|
|
1214
|
+
}
|
|
1215
|
+
watchingStop.signal.addEventListener("abort", finish, { once: true });
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
return await new Promise(() => void 0);
|
|
1219
|
+
})();
|
|
1220
|
+
const legacyReady = async () => {
|
|
1221
|
+
if (resolve3(state.active.script) !== resolve3(launcherSibling(manifestPath, ".service-bootstrap.previous.js"))) {
|
|
1222
|
+
return await new Promise(() => void 0);
|
|
1223
|
+
}
|
|
1224
|
+
while (!readiness.signal.aborted) {
|
|
1225
|
+
try {
|
|
1226
|
+
const currentManifest = JSON.parse(await readFile3(manifestPath, "utf8"));
|
|
1227
|
+
const runtime = JSON.parse(await readFile3(runtimePath, "utf8"));
|
|
1228
|
+
const names = (currentManifest.workers ?? [{ instance_name: "default" }]).map((w) => w.instance_name).sort();
|
|
1229
|
+
if (runtime.schema_version === "vtx_inference_service_runtime_v1" && runtime.manifest_generation === currentManifest.generation && Date.parse(runtime.updated_at) >= launchedAt && Array.isArray(runtime.workers) && runtime.workers.length > 0 && runtime.workers.every((w) => w.state === "running") && JSON.stringify(runtime.workers.map((w) => w.instance_name).sort()) === JSON.stringify(names)) return;
|
|
1230
|
+
} catch {
|
|
1231
|
+
}
|
|
1232
|
+
await new Promise((r) => {
|
|
1233
|
+
if (readiness.signal.aborted) {
|
|
1234
|
+
r();
|
|
1235
|
+
return;
|
|
1236
|
+
}
|
|
1237
|
+
const timer2 = setTimeout(done, 100);
|
|
1238
|
+
function done() {
|
|
1239
|
+
clearTimeout(timer2);
|
|
1240
|
+
readiness.signal.removeEventListener("abort", done);
|
|
1241
|
+
r();
|
|
1242
|
+
}
|
|
1243
|
+
readiness.signal.addEventListener("abort", done, { once: true });
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
return await new Promise(() => void 0);
|
|
1247
|
+
};
|
|
1248
|
+
let timer;
|
|
1249
|
+
let first;
|
|
1250
|
+
let code;
|
|
1251
|
+
try {
|
|
1252
|
+
first = await Promise.race([
|
|
1253
|
+
Promise.race([child.ready, legacyReady()]).then(() => ({ kind: "ready", code: 0 })),
|
|
1254
|
+
child.completion.then((code2) => ({ kind: "exit", code: code2 })),
|
|
1255
|
+
stopped,
|
|
1256
|
+
new Promise((r) => {
|
|
1257
|
+
timer = setTimeout(() => r({ kind: "timeout", code: 1 }), dependencies.readyTimeoutMs ?? runtime_policy_generated_default.external_inference.service.bootstrap_readiness_timeout_ms);
|
|
1258
|
+
})
|
|
1259
|
+
]).finally(() => {
|
|
1260
|
+
clearTimeout(timer);
|
|
1261
|
+
readiness.abort();
|
|
1262
|
+
});
|
|
1263
|
+
if (first.kind === "ready") {
|
|
1264
|
+
const selected = await readLauncherSelection(manifestPath);
|
|
1265
|
+
if (selected?.package_version === state.active.package_version) state.active = { ...state.active, selection: selected };
|
|
1266
|
+
state.pending = false;
|
|
1267
|
+
await writeLauncherState(manifestPath, state);
|
|
1268
|
+
await log("service_launcher_ready", state.active.package_version);
|
|
1269
|
+
}
|
|
1270
|
+
if (first.kind === "exit") code = first.code;
|
|
1271
|
+
else if (first.kind !== "ready") code = await drainCore(child);
|
|
1272
|
+
else {
|
|
1273
|
+
const finished = await Promise.race([child.completion.then((code2) => ({ kind: "exit", code: code2 })), stopped]);
|
|
1274
|
+
code = finished.kind === "exit" ? finished.code : await drainCore(child);
|
|
1275
|
+
}
|
|
1276
|
+
} finally {
|
|
1277
|
+
watchingStop.abort();
|
|
1278
|
+
readiness.abort();
|
|
1279
|
+
clearTimeout(timer);
|
|
1280
|
+
}
|
|
1281
|
+
child = null;
|
|
1282
|
+
const supervisorLockPath = resolve3(lockPathValue).replace(/\.service-bootstrap$/u, ".service");
|
|
1283
|
+
if (supervisorLockPath !== resolve3(lockPathValue)) {
|
|
1284
|
+
while (true) {
|
|
1285
|
+
try {
|
|
1286
|
+
const supervisorLock = await acquireInferenceHostProcessLock(supervisorLockPath);
|
|
1287
|
+
await supervisorLock.release();
|
|
1288
|
+
break;
|
|
1289
|
+
} catch (error) {
|
|
1290
|
+
if (!/already owns/iu.test(error instanceof Error ? error.message : "")) throw error;
|
|
1291
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
if (!await desired()) return 0;
|
|
1296
|
+
if (code === 75) {
|
|
1297
|
+
const selected = await readLauncherSelection(manifestPath);
|
|
1298
|
+
if (selected && selected.package_version !== state.active.package_version) {
|
|
1299
|
+
let next;
|
|
1300
|
+
try {
|
|
1301
|
+
next = await targetForSelection(selected);
|
|
1302
|
+
} catch {
|
|
1303
|
+
await writeLauncherSelection(manifestPath, state.active.selection);
|
|
1304
|
+
await writeAtomicInferencePrivateFile(launcherSibling(manifestPath, ".service-release-rejection.json"), `${JSON.stringify({
|
|
1305
|
+
schema_version: "vtx_inference_service_release_rejection_v1",
|
|
1306
|
+
package_version: selected.package_version,
|
|
1307
|
+
rejected_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1308
|
+
reason: "bootstrap_core_missing"
|
|
1309
|
+
})}
|
|
1310
|
+
`);
|
|
1311
|
+
state.pending = false;
|
|
1312
|
+
await writeLauncherState(manifestPath, state);
|
|
1313
|
+
continue;
|
|
1314
|
+
}
|
|
1315
|
+
state = { schema_version: state.schema_version, active: next, previous: state.active, pending: false };
|
|
1316
|
+
await writeLauncherState(manifestPath, state);
|
|
1317
|
+
await log("service_launcher_handoff", next.package_version);
|
|
1318
|
+
continue;
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
if (!await rollback(first.kind === "timeout" ? "bootstrap_readiness_timeout" : "bootstrap_exited")) return code || 1;
|
|
1322
|
+
}
|
|
1323
|
+
return 0;
|
|
1324
|
+
} finally {
|
|
1325
|
+
lifetime.abort();
|
|
1326
|
+
if (child) await drainCore(child).catch(() => 1);
|
|
1327
|
+
dependencies.signal?.removeEventListener("abort", stop);
|
|
1328
|
+
process.removeListener("SIGTERM", stop);
|
|
1329
|
+
process.removeListener("SIGINT", stop);
|
|
1330
|
+
await lock.release();
|
|
1331
|
+
}
|
|
1332
|
+
};
|
|
1333
|
+
var invokedPath = process.argv[1] ? resolve3(process.argv[1]) : "";
|
|
1334
|
+
if (invokedPath === fileURLToPath(import.meta.url)) {
|
|
1335
|
+
const manifestIndex = process.argv.indexOf("--service-manifest");
|
|
1336
|
+
const lockIndex = process.argv.indexOf("--service-lock");
|
|
1337
|
+
const manifest = manifestIndex >= 0 ? process.argv[manifestIndex + 1] : null;
|
|
1338
|
+
const lock = lockIndex >= 0 ? process.argv[lockIndex + 1] : null;
|
|
1339
|
+
if (!manifest || !lock) {
|
|
1340
|
+
process.stderr.write("Internal service manifest and lock paths are required.\n");
|
|
1341
|
+
process.exitCode = 1;
|
|
1342
|
+
} else process.exitCode = await runInferenceHostServiceLauncher(manifest, lock).catch((error) => {
|
|
1343
|
+
process.stderr.write(`${error instanceof Error ? error.message : "Service launcher failed."}
|
|
1344
|
+
`);
|
|
1345
|
+
return 1;
|
|
1346
|
+
});
|
|
1347
|
+
}
|
|
1348
|
+
export {
|
|
1349
|
+
runInferenceHostServiceLauncher
|
|
1350
|
+
};
|
|
1351
|
+
/*! Bundled license information:
|
|
1352
|
+
|
|
1353
|
+
smol-toml/dist/date.js:
|
|
1354
|
+
smol-toml/dist/error.js:
|
|
1355
|
+
smol-toml/dist/util.js:
|
|
1356
|
+
smol-toml/dist/primitive.js:
|
|
1357
|
+
smol-toml/dist/extract.js:
|
|
1358
|
+
smol-toml/dist/struct.js:
|
|
1359
|
+
smol-toml/dist/parse.js:
|
|
1360
|
+
smol-toml/dist/stringify.js:
|
|
1361
|
+
smol-toml/dist/index.js:
|
|
1362
|
+
(*!
|
|
1363
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
1364
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
1365
|
+
*
|
|
1366
|
+
* Redistribution and use in source and binary forms, with or without
|
|
1367
|
+
* modification, are permitted provided that the following conditions are met:
|
|
1368
|
+
*
|
|
1369
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
1370
|
+
* list of conditions and the following disclaimer.
|
|
1371
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
1372
|
+
* this list of conditions and the following disclaimer in the
|
|
1373
|
+
* documentation and/or other materials provided with the distribution.
|
|
1374
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
1375
|
+
* may be used to endorse or promote products derived from this software without
|
|
1376
|
+
* specific prior written permission.
|
|
1377
|
+
*
|
|
1378
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
1379
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
1380
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
1381
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
1382
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
1383
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
1384
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
1385
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
1386
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
1387
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
1388
|
+
*)
|
|
1389
|
+
*/
|