@zswarm/core 0.2.6 → 0.2.7
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/harness.d.ts +27 -2
- package/dist/harness.js +68 -2
- package/dist/host/cli.js +5 -2
- package/dist/host/relay.d.ts +2 -0
- package/dist/host/relay.js +60 -9
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/ops/dispatch.d.ts +9 -0
- package/dist/ops/dispatch.js +25 -3
- package/dist/ops/restart.d.ts +57 -0
- package/dist/ops/restart.js +416 -0
- package/dist/ops/serve-install.d.ts +8 -1
- package/dist/ops/serve-install.js +160 -73
- package/dist/ops/serve.d.ts +28 -0
- package/dist/ops/serve.js +35 -0
- package/dist/policy.js +1 -0
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +31 -7
- package/package.json +1 -1
|
@@ -7,7 +7,7 @@ import { pathToFileURL } from "node:url";
|
|
|
7
7
|
import { ZellijError } from "../errors.js";
|
|
8
8
|
import { encodePowerShellCommand } from "../zellij/ipc.js";
|
|
9
9
|
import { coverHostReport, hostDoctorRequest, isRequiredFailure, mergeHostReply, } from "./doctor.js";
|
|
10
|
-
import { callServe, isLoopbackHost, parseListenAddress, probeServe, redactServeSecret, SERVE_CORE_VERSION, SERVE_HELLO_TIMEOUT_MS, SERVE_LAUNCH_ID_ENV, SERVE_PROTOCOL, SERVE_TASK_NAME, serveLogonCommand, } from "./serve.js";
|
|
10
|
+
import { callServe, isLoopbackHost, parseListenAddress, probeServe, redactServeSecret, SERVE_CORE_VERSION, SERVE_HELLO_TIMEOUT_MS, SERVE_LAUNCH_ID_ENV, SERVE_PROTOCOL, SERVE_TASK_NAME, serveLogonCommand, serveTaskTarget, } from "./serve.js";
|
|
11
11
|
import { authorizeServeListen, SERVE_BIND_REMEDY, SERVE_TAILSCALE_BIN_ENV, } from "./serve-bind.js";
|
|
12
12
|
import { throwIfAborted } from "./util.js";
|
|
13
13
|
/** Overall install/readiness deadline when `timeoutMs` is omitted. */
|
|
@@ -90,11 +90,45 @@ const PS_PREAMBLE = [
|
|
|
90
90
|
" $json = $obj | ConvertTo-Json -Compress -Depth 6",
|
|
91
91
|
" [Console]::Out.Write($json)",
|
|
92
92
|
"}",
|
|
93
|
-
`function Get-ZswarmTask { Get-ScheduledTask -TaskName '${SERVE_TASK_NAME}' -ErrorAction Stop }`,
|
|
94
93
|
"function Test-ZswarmMissing($err) {",
|
|
95
94
|
" return $err.CategoryInfo.Category -eq 'ObjectNotFound' -or ([string]$err.FullyQualifiedErrorId -match 'ObjectNotFound')",
|
|
96
95
|
"}",
|
|
97
96
|
].join("\n");
|
|
97
|
+
/** A literal `Get-ScheduledTask` lookup; a missing task or folder is `$null`. */
|
|
98
|
+
function scheduledTaskLookup(name, folder) {
|
|
99
|
+
const path = folder && folder !== "\\" ? ` -TaskPath '${folder}'` : "";
|
|
100
|
+
return `Get-ScheduledTask -TaskName '${name}'${path} -ErrorAction SilentlyContinue`;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* WHY: a legacy install lives at the Task Scheduler root as `zswarm-serve` and
|
|
104
|
+
* records the session it serves as `set "ZSWARM_SESSION=<session>"` in its action.
|
|
105
|
+
* A named session may adopt that task, but only for the session it records:
|
|
106
|
+
* without the marker, `--clear --session other` would stop an unrelated serve.
|
|
107
|
+
* The per-session name is always looked up first so a real per-session task wins.
|
|
108
|
+
*/
|
|
109
|
+
function serveTargetResolver(target) {
|
|
110
|
+
const folder = target.folder ?? "";
|
|
111
|
+
const session = target.session ?? "";
|
|
112
|
+
const lines = [
|
|
113
|
+
"function Get-ZswarmTarget {",
|
|
114
|
+
` $found = ${scheduledTaskLookup(target.name, folder)}`,
|
|
115
|
+
" if ($found) { return @{ task = $found; name = [string]$found.TaskName; path = [string]$found.TaskPath; legacy = $false } }",
|
|
116
|
+
];
|
|
117
|
+
if (session) {
|
|
118
|
+
lines.push(` $legacyTask = ${scheduledTaskLookup(SERVE_TASK_NAME, "")}`, ` if ($legacyTask -and ([string]$legacyTask.Actions[0].Arguments).Contains('set "ZSWARM_SESSION=${session}"')) {`, " return @{ task = $legacyTask; name = [string]$legacyTask.TaskName; path = [string]$legacyTask.TaskPath; legacy = $true }", " }");
|
|
119
|
+
}
|
|
120
|
+
lines.push(` return @{ task = $null; name = '${target.name}'; path = '${folder}'; legacy = $false }`, "}");
|
|
121
|
+
return lines;
|
|
122
|
+
}
|
|
123
|
+
/** Preamble + the target-aware resolver; every action resolves before it mutates. */
|
|
124
|
+
function buildPreamble(target) {
|
|
125
|
+
return [PS_PREAMBLE, ...serveTargetResolver(target)].join("\n");
|
|
126
|
+
}
|
|
127
|
+
/** Splat only when the resolved task really sits in a folder. */
|
|
128
|
+
const TASK_PATH_SPLAT = [
|
|
129
|
+
"$taskPath = @{}",
|
|
130
|
+
"if (-not [string]::IsNullOrEmpty($target.path) -and $target.path -ne '\\') { $taskPath = @{ TaskPath = $target.path } }",
|
|
131
|
+
];
|
|
98
132
|
const REMEDY = {
|
|
99
133
|
notReady: "Registration/start was accepted, but authenticated hello and host inspection did not prove this installation is ready. Retry serve --install or inspect with zswarm doctor; serve --clear removes the owned task. Install does not kill unrelated processes.",
|
|
100
134
|
stale: "A listener answered on this port but did not present this installation's launch identity. Install does not kill arbitrary node processes or port owners. Stop the owned zswarm-serve task (serve --clear) or free the port, then retry.",
|
|
@@ -249,19 +283,17 @@ function privilegedSid(sid) {
|
|
|
249
283
|
return WELL_KNOWN_SIDS.has(sid.trim().toUpperCase());
|
|
250
284
|
}
|
|
251
285
|
export function buildServeTaskScript(action, params = {}) {
|
|
286
|
+
const target = params.target ?? serveTaskTarget();
|
|
252
287
|
if (action === "inspect") {
|
|
253
288
|
return [
|
|
254
|
-
|
|
255
|
-
"
|
|
256
|
-
"
|
|
257
|
-
"
|
|
258
|
-
"
|
|
259
|
-
"
|
|
260
|
-
" Write-ZswarmJson @{ exists = $false; currentUser = $identity.name; currentSid = $identity.sid }",
|
|
261
|
-
" exit 0",
|
|
262
|
-
" }",
|
|
263
|
-
" throw",
|
|
289
|
+
buildPreamble(target),
|
|
290
|
+
"$target = Get-ZswarmTarget",
|
|
291
|
+
"if (-not $target.task) {",
|
|
292
|
+
" $identity = Get-ZswarmIdentity",
|
|
293
|
+
" Write-ZswarmJson @{ exists = $false; currentUser = $identity.name; currentSid = $identity.sid; taskName = $target.name; taskPath = $target.path; legacy = $target.legacy }",
|
|
294
|
+
" exit 0",
|
|
264
295
|
"}",
|
|
296
|
+
"$task = $target.task",
|
|
265
297
|
"$identity = Get-ZswarmIdentity",
|
|
266
298
|
"$action = @($task.Actions)[0]",
|
|
267
299
|
"$principal = $task.Principal",
|
|
@@ -270,6 +302,8 @@ export function buildServeTaskScript(action, params = {}) {
|
|
|
270
302
|
" currentUser = $identity.name",
|
|
271
303
|
" currentSid = $identity.sid",
|
|
272
304
|
" taskName = [string]$task.TaskName",
|
|
305
|
+
" taskPath = [string]$task.TaskPath",
|
|
306
|
+
" legacy = $target.legacy",
|
|
273
307
|
" state = [string]$task.State",
|
|
274
308
|
" userId = [string]$principal.UserId",
|
|
275
309
|
" userSid = (Resolve-ZswarmSid ([string]$principal.UserId))",
|
|
@@ -284,96 +318,92 @@ export function buildServeTaskScript(action, params = {}) {
|
|
|
284
318
|
const command = params.command ?? "";
|
|
285
319
|
const commandB64 = Buffer.from(command, "utf8").toString("base64");
|
|
286
320
|
return [
|
|
287
|
-
|
|
321
|
+
buildPreamble(target),
|
|
288
322
|
`$cmd = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${commandB64}'))`,
|
|
289
|
-
"
|
|
290
|
-
"
|
|
323
|
+
"$target = Get-ZswarmTarget",
|
|
324
|
+
"$existing = $target.task",
|
|
325
|
+
"if ($existing) {",
|
|
291
326
|
" $identity = Assert-ZswarmCurrentOwner $existing",
|
|
292
|
-
"}
|
|
293
|
-
" if (-not (Test-ZswarmMissing $_)) { throw }",
|
|
327
|
+
"} else {",
|
|
294
328
|
" $identity = Get-ZswarmIdentity",
|
|
295
329
|
"}",
|
|
296
330
|
"$userId = $identity.name",
|
|
297
331
|
"$action = New-ScheduledTaskAction -Execute 'cmd.exe' -Argument ('/c ' + $cmd)",
|
|
298
332
|
"$principal = New-ScheduledTaskPrincipal -UserId $userId -LogonType Interactive -RunLevel Limited",
|
|
299
333
|
"$trigger = New-ScheduledTaskTrigger -AtLogOn -User $userId",
|
|
300
|
-
|
|
301
|
-
"
|
|
302
|
-
|
|
303
|
-
"' }",
|
|
334
|
+
...TASK_PATH_SPLAT,
|
|
335
|
+
"Register-ScheduledTask -TaskName $target.name @taskPath -Action $action -Principal $principal -Trigger $trigger -Force | Out-Null",
|
|
336
|
+
"Write-ZswarmJson @{ registered = $true; userId = $userId; userSid = $identity.sid; logonType = 'Interactive'; runLevel = 'Limited'; taskName = $target.name; taskPath = $target.path; legacy = $target.legacy }",
|
|
304
337
|
].join("\n");
|
|
305
338
|
}
|
|
306
339
|
if (action === "start") {
|
|
340
|
+
// Start is only ever asked for a task that was just registered or just
|
|
341
|
+
// found, so a missing one is a real failure and keeps the old contract:
|
|
342
|
+
// the strict lookup below throws the cmdlet's own ObjectNotFound.
|
|
307
343
|
return [
|
|
308
|
-
|
|
309
|
-
"
|
|
310
|
-
"
|
|
311
|
-
" [
|
|
312
|
-
"
|
|
313
|
-
" throw",
|
|
344
|
+
buildPreamble(target),
|
|
345
|
+
"$target = Get-ZswarmTarget",
|
|
346
|
+
"if (-not $target.task) {",
|
|
347
|
+
" if ([string]::IsNullOrEmpty($target.path) -or $target.path -eq '\\') { Get-ScheduledTask -TaskName $target.name -ErrorAction Stop | Out-Null }",
|
|
348
|
+
" else { Get-ScheduledTask -TaskName $target.name -TaskPath $target.path -ErrorAction Stop | Out-Null }",
|
|
314
349
|
"}",
|
|
315
|
-
|
|
316
|
-
|
|
350
|
+
"$task = $target.task",
|
|
351
|
+
...TASK_PATH_SPLAT,
|
|
352
|
+
"[void](Assert-ZswarmCurrentOwner $task)",
|
|
353
|
+
"Start-ScheduledTask -TaskName $target.name @taskPath",
|
|
354
|
+
"Write-ZswarmJson @{ started = $true; taskName = $target.name; taskPath = $target.path; legacy = $target.legacy }",
|
|
317
355
|
].join("\n");
|
|
318
356
|
}
|
|
319
357
|
if (action === "stop") {
|
|
320
358
|
return [
|
|
321
|
-
|
|
359
|
+
buildPreamble(target),
|
|
360
|
+
"$target = Get-ZswarmTarget",
|
|
361
|
+
"$task = $target.task",
|
|
362
|
+
...TASK_PATH_SPLAT,
|
|
363
|
+
"if ($task) { [void](Assert-ZswarmCurrentOwner $task) }",
|
|
322
364
|
"try {",
|
|
323
|
-
" $
|
|
324
|
-
" [void](Assert-ZswarmCurrentOwner $task)",
|
|
325
|
-
` Stop-ScheduledTask -TaskName '${SERVE_TASK_NAME}'`,
|
|
365
|
+
" Stop-ScheduledTask -TaskName $target.name @taskPath",
|
|
326
366
|
"} catch {",
|
|
327
367
|
" if (-not (Test-ZswarmMissing $_)) { throw }",
|
|
328
368
|
"}",
|
|
329
|
-
"Write-ZswarmJson @{ stopped = $true; taskName =
|
|
369
|
+
"Write-ZswarmJson @{ stopped = $true; taskName = $target.name; taskPath = $target.path; legacy = $target.legacy }",
|
|
330
370
|
].join("\n");
|
|
331
371
|
}
|
|
332
372
|
return [
|
|
333
|
-
|
|
373
|
+
buildPreamble(target),
|
|
334
374
|
"$identity = Get-ZswarmIdentity",
|
|
335
375
|
"$stopped = $false",
|
|
336
376
|
"$stopFailed = $false",
|
|
337
377
|
"$stopError = $null",
|
|
338
|
-
"
|
|
339
|
-
"
|
|
340
|
-
|
|
341
|
-
"
|
|
342
|
-
"
|
|
343
|
-
|
|
344
|
-
"' }",
|
|
345
|
-
" exit 0",
|
|
346
|
-
" }",
|
|
347
|
-
" throw",
|
|
378
|
+
"$target = Get-ZswarmTarget",
|
|
379
|
+
"$task = $target.task",
|
|
380
|
+
...TASK_PATH_SPLAT,
|
|
381
|
+
"if (-not $task) {",
|
|
382
|
+
" Write-ZswarmJson @{ cleared = $true; missing = $true; stopped = $false; taskName = $target.name; taskPath = $target.path; legacy = $target.legacy }",
|
|
383
|
+
" exit 0",
|
|
348
384
|
"}",
|
|
349
385
|
"[void](Assert-ZswarmCurrentOwner $task)",
|
|
350
386
|
"try {",
|
|
351
|
-
|
|
387
|
+
" Stop-ScheduledTask -TaskName $target.name @taskPath",
|
|
352
388
|
" $stopped = $true",
|
|
353
389
|
"} catch {",
|
|
354
390
|
" if (Test-ZswarmMissing $_) {",
|
|
355
|
-
" Write-ZswarmJson @{ cleared = $true; missing = $true; stopped = $false; taskName =
|
|
356
|
-
SERVE_TASK_NAME +
|
|
357
|
-
"' }",
|
|
391
|
+
" Write-ZswarmJson @{ cleared = $true; missing = $true; stopped = $false; taskName = $target.name; taskPath = $target.path; legacy = $target.legacy }",
|
|
358
392
|
" exit 0",
|
|
359
393
|
" }",
|
|
360
394
|
" $stopFailed = $true",
|
|
361
395
|
" $stopError = [string]$_.Exception.Message",
|
|
362
396
|
"}",
|
|
363
397
|
"try {",
|
|
364
|
-
|
|
398
|
+
" Unregister-ScheduledTask -TaskName $target.name @taskPath -Confirm:$false",
|
|
365
399
|
"} catch {",
|
|
366
400
|
" if (Test-ZswarmMissing $_) {",
|
|
367
|
-
" Write-ZswarmJson @{ cleared = $true; missing = $true; stopped = $stopped; stopFailed = $stopFailed; stopError = $stopError; taskName =
|
|
368
|
-
SERVE_TASK_NAME +
|
|
369
|
-
"' }",
|
|
401
|
+
" Write-ZswarmJson @{ cleared = $true; missing = $true; stopped = $stopped; stopFailed = $stopFailed; stopError = $stopError; taskName = $target.name; taskPath = $target.path; legacy = $target.legacy }",
|
|
370
402
|
" exit 0",
|
|
371
403
|
" }",
|
|
372
404
|
" throw",
|
|
373
405
|
"}",
|
|
374
|
-
"Write-ZswarmJson @{ cleared = $true; stopped = $stopped; stopFailed = $stopFailed; stopError = $stopError; taskName =
|
|
375
|
-
SERVE_TASK_NAME +
|
|
376
|
-
"' }",
|
|
406
|
+
"Write-ZswarmJson @{ cleared = $true; stopped = $stopped; stopFailed = $stopFailed; stopError = $stopError; taskName = $target.name; taskPath = $target.path; legacy = $target.legacy }",
|
|
377
407
|
].join("\n");
|
|
378
408
|
}
|
|
379
409
|
function runPowerShellDefault(script, options) {
|
|
@@ -513,6 +543,11 @@ function launchIdFromCommand(command) {
|
|
|
513
543
|
function asString(value) {
|
|
514
544
|
return typeof value === "string" ? value : "";
|
|
515
545
|
}
|
|
546
|
+
/** Task Scheduler reports the root folder as `\`; our target path is `''` there. */
|
|
547
|
+
function serveTaskFolder(value) {
|
|
548
|
+
const text = asString(value).trim();
|
|
549
|
+
return text === "\\" ? "" : text;
|
|
550
|
+
}
|
|
516
551
|
function liveSessionsFrom(checks) {
|
|
517
552
|
const row = checks.find((item) => item.id === "zellij_sessions");
|
|
518
553
|
const live = row?.detail?.live;
|
|
@@ -570,7 +605,10 @@ function notReadyError(message, details, secrets, code = SERVE_NOT_READY_CODE) {
|
|
|
570
605
|
return new ZellijError(code, scrubDiagnosticText(message, secrets), scrubSecrets(details, secrets));
|
|
571
606
|
}
|
|
572
607
|
async function runTask(action, input) {
|
|
573
|
-
const script = buildServeTaskScript(action, {
|
|
608
|
+
const script = buildServeTaskScript(action, {
|
|
609
|
+
command: input.command,
|
|
610
|
+
target: input.target,
|
|
611
|
+
});
|
|
574
612
|
const result = await input.run(script, {
|
|
575
613
|
timeoutMs: input.timeoutMs,
|
|
576
614
|
signal: input.signal,
|
|
@@ -642,6 +680,10 @@ export async function installServeLogon(input = {}) {
|
|
|
642
680
|
const { host, label } = parseListenAddress(input.listen);
|
|
643
681
|
const token = serveInstallToken(input);
|
|
644
682
|
const secrets = [token];
|
|
683
|
+
// One resolved task identity for the whole install: inspect, register, start and
|
|
684
|
+
// stop must all address the same task, including an adopted legacy one.
|
|
685
|
+
const explicitSession = input.session?.trim() || null;
|
|
686
|
+
const target = serveTaskTarget(explicitSession);
|
|
645
687
|
throwIfAborted(input.signal);
|
|
646
688
|
if (remaining() <= 0) {
|
|
647
689
|
throw notReadyError("serve --install timed out before task mutation", {
|
|
@@ -651,7 +693,9 @@ export async function installServeLogon(input = {}) {
|
|
|
651
693
|
phase: "inspect",
|
|
652
694
|
cause: "timeout",
|
|
653
695
|
listen: label,
|
|
654
|
-
task:
|
|
696
|
+
task: target.name,
|
|
697
|
+
taskPath: target.folder ?? "",
|
|
698
|
+
legacy: false,
|
|
655
699
|
}, secrets, "timeout");
|
|
656
700
|
}
|
|
657
701
|
// Non-loopback: verify Tailscale + OS ownership before any task mutation.
|
|
@@ -675,7 +719,9 @@ export async function installServeLogon(input = {}) {
|
|
|
675
719
|
phase: "verify",
|
|
676
720
|
cause: "timeout",
|
|
677
721
|
listen: label,
|
|
678
|
-
task:
|
|
722
|
+
task: target.name,
|
|
723
|
+
taskPath: target.folder ?? "",
|
|
724
|
+
legacy: false,
|
|
679
725
|
}, secrets, "timeout");
|
|
680
726
|
}
|
|
681
727
|
const launch = resolveServeCliLaunch({
|
|
@@ -688,14 +734,19 @@ export async function installServeLogon(input = {}) {
|
|
|
688
734
|
const probe = input.probeServe ?? probeServe;
|
|
689
735
|
const call = input.callServe ??
|
|
690
736
|
((target, args, options) => callServe(target, args, options));
|
|
691
|
-
const explicitSession = input.session?.trim() || null;
|
|
692
737
|
const requestedLaunchId = input.launchId?.trim() || "";
|
|
738
|
+
// Refined after inspect when a real (possibly adopted legacy) task is found.
|
|
739
|
+
let taskName = target.name;
|
|
740
|
+
let taskPath = target.folder ?? "";
|
|
741
|
+
let legacyAdopted = false;
|
|
693
742
|
const partial = (extra) => {
|
|
694
743
|
const details = scrubSecrets({
|
|
695
744
|
installed: false,
|
|
696
745
|
ready: false,
|
|
697
746
|
running: false,
|
|
698
|
-
task:
|
|
747
|
+
task: taskName,
|
|
748
|
+
taskPath,
|
|
749
|
+
legacy: legacyAdopted,
|
|
699
750
|
listen: label,
|
|
700
751
|
...extra,
|
|
701
752
|
}, secrets);
|
|
@@ -734,8 +785,13 @@ export async function installServeLogon(input = {}) {
|
|
|
734
785
|
timeoutMs: inspectBudget,
|
|
735
786
|
signal: input.signal,
|
|
736
787
|
secrets,
|
|
788
|
+
target,
|
|
737
789
|
});
|
|
738
790
|
const exists = inspected.exists === true;
|
|
791
|
+
// The inspected identity wins: it is the real task (or the adopted legacy one).
|
|
792
|
+
taskName = asString(inspected.taskName) || target.name;
|
|
793
|
+
legacyAdopted = inspected.legacy === true;
|
|
794
|
+
taskPath = legacyAdopted ? "" : serveTaskFolder(inspected.taskPath) || (target.folder ?? "");
|
|
739
795
|
const existingCommand = commandFromArguments(asString(inspected.arguments));
|
|
740
796
|
secrets.push(...tokenFromCommand(existingCommand));
|
|
741
797
|
const ownership = (() => {
|
|
@@ -758,7 +814,7 @@ export async function installServeLogon(input = {}) {
|
|
|
758
814
|
}
|
|
759
815
|
const verified = isWindowsSid(userSid) && sameWindowsSid(currentSid, userSid);
|
|
760
816
|
if (!verified || privilegedSid(userSid) || privilegedAccount(userId)) {
|
|
761
|
-
throw notReadyError(`serve --install will not overwrite ${
|
|
817
|
+
throw notReadyError(`serve --install will not overwrite ${taskName} owned by ${userId || "an unverified account"}`, {
|
|
762
818
|
...partial({
|
|
763
819
|
phase: "inspect",
|
|
764
820
|
cause: SERVE_TASK_OWNED_CODE,
|
|
@@ -774,7 +830,13 @@ export async function installServeLogon(input = {}) {
|
|
|
774
830
|
const execute = asString(inspected.execute);
|
|
775
831
|
const alreadyRunning = exists && taskRunning(asString(inspected.state));
|
|
776
832
|
const existingLaunchId = launchIdFromCommand(existingCommand);
|
|
777
|
-
|
|
833
|
+
// WHY: a legacy task is recognised only by the session its command records. If
|
|
834
|
+
// we re-register it under its own root-folder name, keep the marker, or the
|
|
835
|
+
// next `--clear --session` can no longer recognise (and remove) it.
|
|
836
|
+
const taskEnv = legacyAdopted && explicitSession
|
|
837
|
+
? { ...env, ZSWARM_SESSION: explicitSession }
|
|
838
|
+
: env;
|
|
839
|
+
const desiredBase = serveLogonCommand(launch.execPath, launch.scriptPath, label, token, serveTaskLaunchEnv(taskEnv));
|
|
778
840
|
const configMatches = exists &&
|
|
779
841
|
execute.toLowerCase() === "cmd.exe" &&
|
|
780
842
|
serveCommandFingerprint(existingCommand) === desiredBase &&
|
|
@@ -786,7 +848,7 @@ export async function installServeLogon(input = {}) {
|
|
|
786
848
|
const launchId = reuseExistingLaunch
|
|
787
849
|
? existingLaunchId
|
|
788
850
|
: (requestedLaunchId || randomUUID());
|
|
789
|
-
const childEnv = serveTaskLaunchEnv(
|
|
851
|
+
const childEnv = serveTaskLaunchEnv(taskEnv, launchId);
|
|
790
852
|
const command = serveLogonCommand(launch.execPath, launch.scriptPath, label, token, childEnv);
|
|
791
853
|
const redactedCommand = redactServeSecret(command, token);
|
|
792
854
|
const needsUpdate = !reuseExistingLaunch;
|
|
@@ -806,6 +868,7 @@ export async function installServeLogon(input = {}) {
|
|
|
806
868
|
signal: input.signal,
|
|
807
869
|
secrets,
|
|
808
870
|
command: commandArg,
|
|
871
|
+
target,
|
|
809
872
|
});
|
|
810
873
|
}
|
|
811
874
|
catch (err) {
|
|
@@ -1033,7 +1096,9 @@ export async function installServeLogon(input = {}) {
|
|
|
1033
1096
|
installed: true,
|
|
1034
1097
|
ready: true,
|
|
1035
1098
|
running: true,
|
|
1036
|
-
task:
|
|
1099
|
+
task: taskName,
|
|
1100
|
+
taskPath,
|
|
1101
|
+
legacy: legacyAdopted,
|
|
1037
1102
|
listen: label,
|
|
1038
1103
|
command: redactedCommand,
|
|
1039
1104
|
principal,
|
|
@@ -1055,7 +1120,7 @@ export async function installServeLogon(input = {}) {
|
|
|
1055
1120
|
lastCause === "zellij_wrong_bin" ||
|
|
1056
1121
|
lastCause === "zellij_incompatible";
|
|
1057
1122
|
if (conclusive) {
|
|
1058
|
-
throw notReadyError(`serve --install registered ${
|
|
1123
|
+
throw notReadyError(`serve --install registered ${taskName} but it is not ready (${lastCause})`, known(), secrets);
|
|
1059
1124
|
}
|
|
1060
1125
|
if (remaining() <= 0)
|
|
1061
1126
|
break;
|
|
@@ -1067,7 +1132,7 @@ export async function installServeLogon(input = {}) {
|
|
|
1067
1132
|
const code = lastCause === "cancelled" ? "cancelled" : remaining() <= 0 || lastCause === "timeout" ? "timeout" : SERVE_NOT_READY_CODE;
|
|
1068
1133
|
throw notReadyError(code === "timeout"
|
|
1069
1134
|
? "serve --install timed out waiting for authenticated readiness"
|
|
1070
|
-
: `serve --install registered ${
|
|
1135
|
+
: `serve --install registered ${taskName} but it is not ready (${lastCause})`, known(), secrets, code === SERVE_NOT_READY_CODE ? SERVE_NOT_READY_CODE : code);
|
|
1071
1136
|
}
|
|
1072
1137
|
export async function uninstallServeLogon(input = {}) {
|
|
1073
1138
|
if ((input.platform ?? process.platform) !== "win32") {
|
|
@@ -1080,12 +1145,21 @@ export async function uninstallServeLogon(input = {}) {
|
|
|
1080
1145
|
const token = input.token?.trim() || input.env?.ZSWARM_SERVE_TOKEN?.trim() || "none";
|
|
1081
1146
|
const secrets = token === "none" ? [] : [token];
|
|
1082
1147
|
const run = input.runPowerShell ?? runPowerShellDefault;
|
|
1148
|
+
// `--clear` honours `--session`: a named session removes its per-session task,
|
|
1149
|
+
// or the legacy root task only when that task records the same session.
|
|
1150
|
+
const explicitSession = input.session?.trim() || null;
|
|
1151
|
+
const target = serveTaskTarget(explicitSession);
|
|
1152
|
+
let taskName = target.name;
|
|
1153
|
+
let taskPath = target.folder ?? "";
|
|
1154
|
+
let legacyAdopted = false;
|
|
1083
1155
|
const assertClear = (phase, extra = {}) => {
|
|
1084
1156
|
if (input.signal?.aborted) {
|
|
1085
1157
|
throw notReadyError("serve --clear cancelled", {
|
|
1086
1158
|
installed: extra.installed ?? false,
|
|
1087
1159
|
ready: false,
|
|
1088
|
-
task:
|
|
1160
|
+
task: taskName,
|
|
1161
|
+
taskPath,
|
|
1162
|
+
legacy: legacyAdopted,
|
|
1089
1163
|
phase,
|
|
1090
1164
|
cause: "cancelled",
|
|
1091
1165
|
...extra,
|
|
@@ -1096,7 +1170,9 @@ export async function uninstallServeLogon(input = {}) {
|
|
|
1096
1170
|
throw notReadyError("serve --clear timed out", {
|
|
1097
1171
|
installed: extra.installed ?? false,
|
|
1098
1172
|
ready: false,
|
|
1099
|
-
task:
|
|
1173
|
+
task: taskName,
|
|
1174
|
+
taskPath,
|
|
1175
|
+
legacy: legacyAdopted,
|
|
1100
1176
|
phase,
|
|
1101
1177
|
cause: "timeout",
|
|
1102
1178
|
...extra,
|
|
@@ -1110,7 +1186,11 @@ export async function uninstallServeLogon(input = {}) {
|
|
|
1110
1186
|
timeoutMs: inspectBudget,
|
|
1111
1187
|
signal: input.signal,
|
|
1112
1188
|
secrets,
|
|
1189
|
+
target,
|
|
1113
1190
|
});
|
|
1191
|
+
taskName = asString(inspected.taskName) || target.name;
|
|
1192
|
+
legacyAdopted = inspected.legacy === true;
|
|
1193
|
+
taskPath = legacyAdopted ? "" : serveTaskFolder(inspected.taskPath) || (target.folder ?? "");
|
|
1114
1194
|
secrets.push(...tokenFromCommand(commandFromArguments(asString(inspected.arguments))));
|
|
1115
1195
|
assertClear("unregister", { installed: inspected.exists === true });
|
|
1116
1196
|
if (inspected.exists === true) {
|
|
@@ -1120,10 +1200,12 @@ export async function uninstallServeLogon(input = {}) {
|
|
|
1120
1200
|
const existingUser = asString(inspected.userId);
|
|
1121
1201
|
const verified = isWindowsSid(currentSid) && isWindowsSid(userSid) && sameWindowsSid(currentSid, userSid);
|
|
1122
1202
|
if (!verified || privilegedSid(userSid) || privilegedAccount(existingUser)) {
|
|
1123
|
-
throw notReadyError(`serve --clear will not remove ${
|
|
1203
|
+
throw notReadyError(`serve --clear will not remove ${taskName} owned by ${existingUser || "an unverified account"}`, {
|
|
1124
1204
|
installed: true,
|
|
1125
1205
|
ready: false,
|
|
1126
|
-
task:
|
|
1206
|
+
task: taskName,
|
|
1207
|
+
taskPath,
|
|
1208
|
+
legacy: legacyAdopted,
|
|
1127
1209
|
phase: "inspect",
|
|
1128
1210
|
cause: SERVE_TASK_OWNED_CODE,
|
|
1129
1211
|
remedy: REMEDY.owner,
|
|
@@ -1141,13 +1223,16 @@ export async function uninstallServeLogon(input = {}) {
|
|
|
1141
1223
|
timeoutMs: unregisterBudget,
|
|
1142
1224
|
signal: input.signal,
|
|
1143
1225
|
secrets,
|
|
1226
|
+
target,
|
|
1144
1227
|
});
|
|
1145
1228
|
}
|
|
1146
1229
|
catch (err) {
|
|
1147
1230
|
if (err instanceof ZellijError && (err.code === "cancelled" || err.code === "timeout")) {
|
|
1148
1231
|
throw notReadyError(err.message, {
|
|
1149
1232
|
ready: false,
|
|
1150
|
-
task:
|
|
1233
|
+
task: taskName,
|
|
1234
|
+
taskPath,
|
|
1235
|
+
legacy: legacyAdopted,
|
|
1151
1236
|
phase: "unregister",
|
|
1152
1237
|
cause: err.code,
|
|
1153
1238
|
mutation: "uncertain",
|
|
@@ -1165,7 +1250,9 @@ export async function uninstallServeLogon(input = {}) {
|
|
|
1165
1250
|
};
|
|
1166
1251
|
assertClear("unregister", confirmed);
|
|
1167
1252
|
return {
|
|
1168
|
-
task:
|
|
1253
|
+
task: taskName,
|
|
1254
|
+
taskPath,
|
|
1255
|
+
legacy: legacyAdopted,
|
|
1169
1256
|
...confirmed,
|
|
1170
1257
|
};
|
|
1171
1258
|
}
|
package/dist/ops/serve.d.ts
CHANGED
|
@@ -2,7 +2,35 @@ import { type Server, type Socket } from "node:net";
|
|
|
2
2
|
import { type NetworkInterfacesFn, type ServeBindDeps, type TailscaleStatusRunner } from "./serve-bind.js";
|
|
3
3
|
import type { OpsResult } from "./types.js";
|
|
4
4
|
export declare const DEFAULT_SERVE_LISTEN = "127.0.0.1:9419";
|
|
5
|
+
/**
|
|
6
|
+
* The Windows logon task name used before serve was per session. Installs
|
|
7
|
+
* without a session still use it, at the Task Scheduler root, exactly as they
|
|
8
|
+
* always have; an install with `--session` gets its own task under the folder
|
|
9
|
+
* below, so a second serve cannot overwrite the first.
|
|
10
|
+
*/
|
|
5
11
|
export declare const SERVE_TASK_NAME = "zswarm-serve";
|
|
12
|
+
/** Folder for per-session serve tasks, next to the crew tasks. */
|
|
13
|
+
export declare const SERVE_TASK_FOLDER = "\\zswarm\\";
|
|
14
|
+
export type ServeTaskTarget = {
|
|
15
|
+
/** `null` is the Task Scheduler root, where a sessionless install lives. */
|
|
16
|
+
folder: string | null;
|
|
17
|
+
name: string;
|
|
18
|
+
/** The session this task stands for, or null for the legacy singleton. */
|
|
19
|
+
session: string | null;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* The task one serve install/clear owns: `\zswarm\zswarm-serve-<session>` when a
|
|
23
|
+
* session is named, otherwise the legacy root-folder `zswarm-serve` (so an
|
|
24
|
+
* install that never names a session behaves exactly as it did before).
|
|
25
|
+
*/
|
|
26
|
+
export declare function serveTaskTarget(session?: string | null): ServeTaskTarget;
|
|
27
|
+
/**
|
|
28
|
+
* The session a stored task command says it serves, from the
|
|
29
|
+
* `set "ZSWARM_SESSION=<session>"` an install bakes into the task environment.
|
|
30
|
+
* This is how a legacy root-folder install is recognised as belonging to one
|
|
31
|
+
* session: without it, a `--session` install/clear leaves that task alone.
|
|
32
|
+
*/
|
|
33
|
+
export declare function sessionFromCommand(command: string): string | null;
|
|
6
34
|
/** One JSONL request; a client that never sends newline cannot grow forever. */
|
|
7
35
|
export declare const SERVE_MAX_REQUEST_BYTES: number;
|
|
8
36
|
/**
|
package/dist/ops/serve.js
CHANGED
|
@@ -5,7 +5,42 @@ import { hostname } from "node:os";
|
|
|
5
5
|
import { ZellijError } from "../errors.js";
|
|
6
6
|
import { authorizeServeListen, SERVE_BIND_REMEDY, SERVE_BIND_VERIFY_TIMEOUT_MS, } from "./serve-bind.js";
|
|
7
7
|
export const DEFAULT_SERVE_LISTEN = "127.0.0.1:9419";
|
|
8
|
+
/**
|
|
9
|
+
* The Windows logon task name used before serve was per session. Installs
|
|
10
|
+
* without a session still use it, at the Task Scheduler root, exactly as they
|
|
11
|
+
* always have; an install with `--session` gets its own task under the folder
|
|
12
|
+
* below, so a second serve cannot overwrite the first.
|
|
13
|
+
*/
|
|
8
14
|
export const SERVE_TASK_NAME = "zswarm-serve";
|
|
15
|
+
/** Folder for per-session serve tasks, next to the crew tasks. */
|
|
16
|
+
export const SERVE_TASK_FOLDER = "\\zswarm\\";
|
|
17
|
+
/** Task Scheduler rejects these in a name; sessions get no further validation. */
|
|
18
|
+
const SERVE_TASK_SESSION = /^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/;
|
|
19
|
+
/**
|
|
20
|
+
* The task one serve install/clear owns: `\zswarm\zswarm-serve-<session>` when a
|
|
21
|
+
* session is named, otherwise the legacy root-folder `zswarm-serve` (so an
|
|
22
|
+
* install that never names a session behaves exactly as it did before).
|
|
23
|
+
*/
|
|
24
|
+
export function serveTaskTarget(session) {
|
|
25
|
+
const wanted = session?.trim();
|
|
26
|
+
if (!wanted)
|
|
27
|
+
return { folder: null, name: SERVE_TASK_NAME, session: null };
|
|
28
|
+
if (!SERVE_TASK_SESSION.test(wanted)) {
|
|
29
|
+
throw new ZellijError("bad_arg", `invalid session name ${JSON.stringify(session)}`);
|
|
30
|
+
}
|
|
31
|
+
return { folder: SERVE_TASK_FOLDER, name: `${SERVE_TASK_NAME}-${wanted}`, session: wanted };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* The session a stored task command says it serves, from the
|
|
35
|
+
* `set "ZSWARM_SESSION=<session>"` an install bakes into the task environment.
|
|
36
|
+
* This is how a legacy root-folder install is recognised as belonging to one
|
|
37
|
+
* session: without it, a `--session` install/clear leaves that task alone.
|
|
38
|
+
*/
|
|
39
|
+
export function sessionFromCommand(command) {
|
|
40
|
+
const match = /set "ZSWARM_SESSION=([^"]*)"/.exec(command);
|
|
41
|
+
const session = match?.[1]?.trim();
|
|
42
|
+
return session && SERVE_TASK_SESSION.test(session) ? session : null;
|
|
43
|
+
}
|
|
9
44
|
/** One JSONL request; a client that never sends newline cannot grow forever. */
|
|
10
45
|
export const SERVE_MAX_REQUEST_BYTES = 1024 * 1024;
|
|
11
46
|
/**
|
package/dist/policy.js
CHANGED
package/dist/schema.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* One description of the zswarm surface. The MCP tool schema, the CLI flags,
|
|
3
3
|
* and the CLI help text are all generated from it, so they cannot drift apart.
|
|
4
4
|
*/
|
|
5
|
-
export declare const OP_NAMES: readonly ["list", "sessions", "send", "broadcast", "dump", "tail", "wait", "status", "keys", "interrupt", "spawn", "close", "worktrees", "unworktree", "signal", "signals", "await", "log", "rename", "focus", "tabs", "layout", "stack", "diff", "checkpoint", "bus", "serve", "doctor"];
|
|
5
|
+
export declare const OP_NAMES: readonly ["list", "sessions", "send", "broadcast", "dump", "tail", "wait", "status", "keys", "interrupt", "spawn", "restart", "close", "worktrees", "unworktree", "signal", "signals", "await", "log", "rename", "focus", "tabs", "layout", "stack", "diff", "checkpoint", "bus", "serve", "doctor"];
|
|
6
6
|
export type OpName = (typeof OP_NAMES)[number];
|
|
7
7
|
/** Ops that address an existing pane through `to`. */
|
|
8
8
|
export declare const TARGET_OPS: readonly OpName[];
|