@alan-ai-hq/agent-manager 0.1.94 → 0.1.95

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.
Files changed (2) hide show
  1. package/dist/index.cjs +521 -219
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -3404,7 +3404,7 @@ var require_websocket = __commonJS({
3404
3404
  var http2 = require("http");
3405
3405
  var net = require("net");
3406
3406
  var tls = require("tls");
3407
- var { randomBytes: randomBytes13, createHash: createHash18 } = require("crypto");
3407
+ var { randomBytes: randomBytes13, createHash: createHash19 } = require("crypto");
3408
3408
  var { Duplex, Readable: Readable2 } = require("stream");
3409
3409
  var { URL: URL2 } = require("url");
3410
3410
  var PerMessageDeflate = require_permessage_deflate();
@@ -4061,7 +4061,7 @@ var require_websocket = __commonJS({
4061
4061
  abortHandshake(websocket, socket, "Invalid Upgrade header");
4062
4062
  return;
4063
4063
  }
4064
- const digest = createHash18("sha1").update(key + GUID).digest("base64");
4064
+ const digest = createHash19("sha1").update(key + GUID).digest("base64");
4065
4065
  if (res.headers["sec-websocket-accept"] !== digest) {
4066
4066
  abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
4067
4067
  return;
@@ -4428,7 +4428,7 @@ var require_websocket_server = __commonJS({
4428
4428
  var EventEmitter = require("events");
4429
4429
  var http2 = require("http");
4430
4430
  var { Duplex } = require("stream");
4431
- var { createHash: createHash18 } = require("crypto");
4431
+ var { createHash: createHash19 } = require("crypto");
4432
4432
  var extension = require_extension();
4433
4433
  var PerMessageDeflate = require_permessage_deflate();
4434
4434
  var subprotocol = require_subprotocol();
@@ -4725,7 +4725,7 @@ var require_websocket_server = __commonJS({
4725
4725
  );
4726
4726
  }
4727
4727
  if (this._state > RUNNING) return abortHandshake(socket, 503);
4728
- const digest = createHash18("sha1").update(key + GUID).digest("base64");
4728
+ const digest = createHash19("sha1").update(key + GUID).digest("base64");
4729
4729
  const headers = [
4730
4730
  "HTTP/1.1 101 Switching Protocols",
4731
4731
  "Upgrade: websocket",
@@ -15451,7 +15451,7 @@ function describeError(error61) {
15451
15451
  }
15452
15452
 
15453
15453
  // src/version.ts
15454
- var AGENT_VERSION = "0.1.94";
15454
+ var AGENT_VERSION = "0.1.95";
15455
15455
 
15456
15456
  // src/daemon-worktree.ts
15457
15457
  var import_node_child_process3 = require("child_process");
@@ -49610,6 +49610,16 @@ async function probeLocalDaemon(port, token, timeoutMs = 750) {
49610
49610
  const probe = await probeLocalDaemonEndpoint(port, token, timeoutMs);
49611
49611
  return probe.state === "running" ? probe.status : null;
49612
49612
  }
49613
+ function requestLocalDaemonShutdown(port, token) {
49614
+ return fetch(`http://127.0.0.1:${port}/shutdown?force=1`, {
49615
+ method: "POST",
49616
+ headers: {
49617
+ authorization: `Bearer ${token}`,
49618
+ "x-alan-shutdown-reason": "runtime_shutdown"
49619
+ },
49620
+ signal: AbortSignal.timeout(5e3)
49621
+ });
49622
+ }
49613
49623
  async function adoptLocalDaemonControl(input2) {
49614
49624
  const timeoutMs = input2.timeoutMs ?? 750;
49615
49625
  try {
@@ -76687,13 +76697,13 @@ var RuntimeRunJournal = class {
76687
76697
  * Keep the exact interruption outcome until a durable authenticated control
76688
76698
  * channel can prove both identity and ownership.
76689
76699
  */
76690
- assessRecovery(runId, isProcessAlive) {
76700
+ assessRecovery(runId, isProcessAlive2) {
76691
76701
  const entry = this.entries.get(runId);
76692
76702
  if (!entry) return void 0;
76693
76703
  if (entry.providerPid === void 0) {
76694
76704
  return { reattachable: false, reason: "provider_not_recorded" };
76695
76705
  }
76696
- if (!isProcessAlive(entry.providerPid)) {
76706
+ if (!isProcessAlive2(entry.providerPid)) {
76697
76707
  return {
76698
76708
  reattachable: false,
76699
76709
  reason: "provider_not_running",
@@ -80440,6 +80450,17 @@ async function runLocalDaemonRecoveryLadder(input2) {
80440
80450
  error: "The local daemon exhausted its reconnect and safe-restart recovery budget."
80441
80451
  };
80442
80452
  }
80453
+ function writeRegisteredRuntimeConfig(args, previousConfig, registered) {
80454
+ writeConfig({
80455
+ ...registered,
80456
+ localApiPort: getLocalApiPort(args, {}),
80457
+ localApiToken: previousConfig.localApiToken ?? (0, import_node_crypto19.randomBytes)(24).toString("hex"),
80458
+ localServiceId: previousConfig.localServiceId ?? (0, import_node_crypto19.randomUUID)(),
80459
+ localServiceSecret: previousConfig.localServiceSecret ?? (0, import_node_crypto19.randomBytes)(32).toString("base64url"),
80460
+ eventSpoolKey: previousConfig.eventSpoolKey ?? (0, import_node_crypto19.randomBytes)(32).toString("base64url"),
80461
+ daemonEpoch: previousConfig.daemonEpoch
80462
+ });
80463
+ }
80443
80464
  async function setupDaemon(args) {
80444
80465
  bindConfigPath(args);
80445
80466
  const previousConfig = readConfigForRegistration();
@@ -80500,7 +80521,7 @@ async function setupDaemon(args) {
80500
80521
  credentialGeneration: body2.credentialGeneration,
80501
80522
  configPath: getConfigPath()
80502
80523
  });
80503
- return { daemonAlreadyRunning: true };
80524
+ return body2.runtimeId !== liveConfig.runtimeId;
80504
80525
  }
80505
80526
  if (readLocalDaemonOwnerLease(getConfigPath())) {
80506
80527
  throw new Error(
@@ -80514,23 +80535,17 @@ async function setupDaemon(args) {
80514
80535
  if (setupToken && error61 instanceof RuntimeSetupGrantExpiredError) {
80515
80536
  console.info("[alan-agent] Setup link expired; continuing with browser authorization");
80516
80537
  await loginWithDeviceCode(args);
80517
- return { daemonAlreadyRunning: false };
80538
+ return false;
80518
80539
  }
80519
80540
  throw error61;
80520
80541
  }
80521
- writeConfig({
80542
+ writeRegisteredRuntimeConfig(args, previousConfig, {
80522
80543
  apiUrl,
80523
80544
  wsUrl,
80524
80545
  endpointProfile,
80525
80546
  runtimeId: body.runtimeId,
80526
80547
  runtimeToken: body.runtimeToken,
80527
80548
  runtimeRenewalToken: body.runtimeRenewalToken,
80528
- localApiPort: getLocalApiPort(args, {}),
80529
- localApiToken: previousConfig.localApiToken ?? (0, import_node_crypto19.randomBytes)(24).toString("hex"),
80530
- localServiceId: previousConfig.localServiceId ?? (0, import_node_crypto19.randomUUID)(),
80531
- localServiceSecret: previousConfig.localServiceSecret ?? (0, import_node_crypto19.randomBytes)(32).toString("base64url"),
80532
- eventSpoolKey: previousConfig.eventSpoolKey ?? (0, import_node_crypto19.randomBytes)(32).toString("base64url"),
80533
- daemonEpoch: previousConfig.daemonEpoch,
80534
80549
  displayName: body.displayName,
80535
80550
  installationId
80536
80551
  });
@@ -80539,7 +80554,7 @@ async function setupDaemon(args) {
80539
80554
  runtimeId: body.runtimeId,
80540
80555
  configPath: getConfigPath()
80541
80556
  });
80542
- return { daemonAlreadyRunning: false };
80557
+ return false;
80543
80558
  }
80544
80559
  async function loginWithDeviceCode(args) {
80545
80560
  bindConfigPath(args);
@@ -80628,19 +80643,13 @@ async function loginWithDeviceCode(args) {
80628
80643
  if (ownedPending?.operationId !== operation.id) {
80629
80644
  throw new Error("browser authorization sidecar ownership was lost before finalization");
80630
80645
  }
80631
- writeConfig({
80646
+ writeRegisteredRuntimeConfig(args, previousConfig, {
80632
80647
  apiUrl,
80633
80648
  wsUrl,
80634
80649
  endpointProfile,
80635
80650
  runtimeId: body.runtime.id,
80636
80651
  runtimeToken: body.runtimeToken,
80637
80652
  runtimeRenewalToken: body.runtimeRenewalToken,
80638
- localApiPort: getLocalApiPort(args, {}),
80639
- localApiToken: previousConfig.localApiToken ?? (0, import_node_crypto19.randomBytes)(24).toString("hex"),
80640
- localServiceId: previousConfig.localServiceId ?? (0, import_node_crypto19.randomUUID)(),
80641
- localServiceSecret: previousConfig.localServiceSecret ?? (0, import_node_crypto19.randomBytes)(32).toString("base64url"),
80642
- eventSpoolKey: previousConfig.eventSpoolKey ?? (0, import_node_crypto19.randomBytes)(32).toString("base64url"),
80643
- daemonEpoch: previousConfig.daemonEpoch,
80644
80653
  displayName: body.runtime.displayName ?? displayName,
80645
80654
  installationId
80646
80655
  });
@@ -80679,14 +80688,7 @@ async function stopDaemon(args) {
80679
80688
  console.info("[alan-agent] No daemon listening", { port: localApiPort });
80680
80689
  return;
80681
80690
  }
80682
- const response = await fetch(`http://127.0.0.1:${localApiPort}/shutdown?force=1`, {
80683
- method: "POST",
80684
- headers: {
80685
- authorization: `Bearer ${localApiToken}`,
80686
- "x-alan-shutdown-reason": "runtime_shutdown"
80687
- },
80688
- signal: AbortSignal.timeout(5e3)
80689
- });
80691
+ const response = await requestLocalDaemonShutdown(localApiPort, localApiToken);
80690
80692
  if (!response.ok) {
80691
80693
  throw new Error(`Failed to stop daemon: HTTP ${response.status}`);
80692
80694
  }
@@ -80831,14 +80833,7 @@ async function drainAndStopLocalDaemonBeforeCredentialCleanup(args, stored) {
80831
80833
  `Logout could not safely drain the local daemon (HTTP ${prepare.status}). Stop or update the local service before removing credentials.`
80832
80834
  );
80833
80835
  }
80834
- const shutdown = await fetch(`http://127.0.0.1:${localApiPort}/shutdown?force=1`, {
80835
- method: "POST",
80836
- headers: {
80837
- authorization: `Bearer ${localApiToken}`,
80838
- "x-alan-shutdown-reason": "runtime_shutdown"
80839
- },
80840
- signal: AbortSignal.timeout(5e3)
80841
- });
80836
+ const shutdown = await requestLocalDaemonShutdown(localApiPort, localApiToken);
80842
80837
  if (!shutdown.ok) {
80843
80838
  throw new Error(`Logout could not stop the local daemon (HTTP ${shutdown.status}).`);
80844
80839
  }
@@ -80999,6 +80994,9 @@ async function printDoctor(args = []) {
80999
80994
  );
81000
80995
  }
81001
80996
 
80997
+ // src/install-command.ts
80998
+ var import_promises6 = require("timers/promises");
80999
+
81002
81000
  // src/mcp-config-adapters.ts
81003
81001
  var import_node_crypto20 = require("crypto");
81004
81002
  var import_node_fs23 = require("fs");
@@ -82383,12 +82381,14 @@ function runMcpIntegrationCommand(args, options = {}) {
82383
82381
 
82384
82382
  // src/service-manager.ts
82385
82383
  var import_node_child_process9 = require("child_process");
82384
+ var import_node_crypto21 = require("crypto");
82386
82385
  var import_node_fs25 = require("fs");
82387
82386
  var import_node_os16 = require("os");
82388
82387
  var import_node_path27 = require("path");
82389
82388
  var SERVICE_LABEL = "ai.tryalan.agent";
82390
82389
  var SYSTEMD_UNIT = "alan-agent.service";
82391
82390
  var WINDOWS_TASK = "Alan Agent";
82391
+ var STOP_POLL_INTERVAL_MS = 200;
82392
82392
  function xml(value2) {
82393
82393
  return value2.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&apos;");
82394
82394
  }
@@ -82398,6 +82398,18 @@ function systemdArg(value2) {
82398
82398
  function powershellLiteral(value2) {
82399
82399
  return `'${value2.replaceAll("'", "''")}'`;
82400
82400
  }
82401
+ function powershellCommand(script, tolerateFailure = false) {
82402
+ return {
82403
+ command: "powershell.exe",
82404
+ args: ["-NoProfile", "-NonInteractive", "-Command", script],
82405
+ tolerateFailure
82406
+ };
82407
+ }
82408
+ function windowsCommandLineArgument(value2) {
82409
+ if (value2 && !/[\s"]/u.test(value2)) return value2;
82410
+ const escaped = value2.replace(/(\\*)"/gu, (_match, slashes) => `${slashes}${slashes}\\"`).replace(/\\+$/u, (slashes) => `${slashes}${slashes}`);
82411
+ return `"${escaped}"`;
82412
+ }
82401
82413
  function serviceNames(profile) {
82402
82414
  if (profile === "production") {
82403
82415
  return { launchd: SERVICE_LABEL, systemd: SYSTEMD_UNIT, windows: WINDOWS_TASK };
@@ -82419,7 +82431,7 @@ function buildDaemonServicePlan(input2) {
82419
82431
  const domain2 = `gui/${userId}`;
82420
82432
  const serviceTarget = `${domain2}/${names.launchd}`;
82421
82433
  const manifestPath = (0, import_node_path27.join)(input2.homeDir, "Library", "LaunchAgents", `${names.launchd}.plist`);
82422
- const logDir = (0, import_node_path27.join)(resolveAgentConfigDirectory(input2.profile, input2.homeDir), "logs");
82434
+ const logDir2 = (0, import_node_path27.join)(resolveAgentConfigDirectory(input2.profile, input2.homeDir), "logs");
82423
82435
  const programArguments = daemonArgs.map((arg) => ` <string>${xml(arg)}</string>`).join("\n");
82424
82436
  const environmentEntries = Object.entries(input2.environment ?? {}).map(([key, value2]) => ` <key>${xml(key)}</key>
82425
82437
  <string>${xml(value2)}</string>`).join("\n");
@@ -82448,9 +82460,9 @@ ${environmentBlock} <key>RunAtLoad</key>
82448
82460
  <key>ThrottleInterval</key>
82449
82461
  <integer>5</integer>
82450
82462
  <key>StandardOutPath</key>
82451
- <string>${xml((0, import_node_path27.join)(logDir, "daemon.log"))}</string>
82463
+ <string>${xml((0, import_node_path27.join)(logDir2, "daemon.log"))}</string>
82452
82464
  <key>StandardErrorPath</key>
82453
- <string>${xml((0, import_node_path27.join)(logDir, "daemon-error.log"))}</string>
82465
+ <string>${xml((0, import_node_path27.join)(logDir2, "daemon-error.log"))}</string>
82454
82466
  </dict>
82455
82467
  </plist>
82456
82468
  `;
@@ -82458,7 +82470,7 @@ ${environmentBlock} <key>RunAtLoad</key>
82458
82470
  platform: "darwin",
82459
82471
  manifestPath,
82460
82472
  manifest,
82461
- logDirectory: logDir,
82473
+ logDirectory: logDir2,
82462
82474
  loadDefinitionCommands: [
82463
82475
  {
82464
82476
  command: "launchctl",
@@ -82499,23 +82511,6 @@ ${environmentBlock} <key>RunAtLoad</key>
82499
82511
  tolerateFailure: true
82500
82512
  }
82501
82513
  ],
82502
- replaceCommands: [
82503
- {
82504
- command: "launchctl",
82505
- args: ["bootout", domain2, manifestPath],
82506
- tolerateFailure: true
82507
- },
82508
- {
82509
- command: "launchctl",
82510
- args: ["bootstrap", domain2, manifestPath],
82511
- tolerateFailure: false
82512
- },
82513
- {
82514
- command: "launchctl",
82515
- args: ["kickstart", "-k", serviceTarget],
82516
- tolerateFailure: false
82517
- }
82518
- ],
82519
82514
  uninstallCommands: [
82520
82515
  {
82521
82516
  command: "launchctl",
@@ -82532,8 +82527,7 @@ ${environmentBlock} <key>RunAtLoad</key>
82532
82527
  command: "launchctl",
82533
82528
  args: ["print", serviceTarget],
82534
82529
  tolerateFailure: true
82535
- },
82536
- lingerStatusCommand: null
82530
+ }
82537
82531
  };
82538
82532
  }
82539
82533
  if (input2.platform === "linux") {
@@ -82596,18 +82590,6 @@ WantedBy=default.target
82596
82590
  tolerateFailure: true
82597
82591
  }
82598
82592
  ],
82599
- replaceCommands: [
82600
- {
82601
- command: "systemctl",
82602
- args: ["--user", "daemon-reload"],
82603
- tolerateFailure: false
82604
- },
82605
- {
82606
- command: "systemctl",
82607
- args: ["--user", "restart", names.systemd],
82608
- tolerateFailure: false
82609
- }
82610
- ],
82611
82593
  uninstallCommands: [
82612
82594
  {
82613
82595
  command: "systemctl",
@@ -82629,117 +82611,179 @@ WantedBy=default.target
82629
82611
  command: "systemctl",
82630
82612
  args: ["--user", "is-active", names.systemd],
82631
82613
  tolerateFailure: true
82632
- },
82633
- lingerStatusCommand: {
82634
- command: "loginctl",
82635
- args: ["show-user", "$(id -un)", "-p", "Linger"],
82636
- tolerateFailure: true
82637
82614
  }
82638
82615
  };
82639
82616
  }
82640
- const environmentPrefix = Object.entries(input2.environment ?? {}).map(([key, value2]) => `$env:${key} = ${powershellLiteral(value2)}`).join("; ");
82641
- const directArgument = [input2.cliEntry, "daemon", "--profile", input2.profile].map((part) => part.includes(" ") ? `"${part}"` : part).join(" ");
82642
- const executable = powershellLiteral(environmentPrefix ? "powershell.exe" : input2.nodeExecutable);
82643
- const argument = powershellLiteral(
82644
- environmentPrefix ? `-NoProfile -NonInteractive -WindowStyle Hidden -Command "${environmentPrefix}; & ${powershellLiteral(input2.nodeExecutable)} ${powershellLiteral(input2.cliEntry)} daemon --profile ${input2.profile}"` : directArgument
82617
+ const logDir = (0, import_node_path27.join)(resolveAgentConfigDirectory(input2.profile, input2.homeDir), "logs");
82618
+ const legacyEnvironmentPrefix = Object.entries(input2.environment ?? {}).map(([key, value2]) => `$env:${key} = ${powershellLiteral(value2)}`).join("; ");
82619
+ const legacyDirectArguments = [input2.cliEntry, "daemon", "--profile", input2.profile].map((part) => part.includes(" ") ? `"${part}"` : part).join(" ");
82620
+ const legacyExecutable = legacyEnvironmentPrefix ? "powershell.exe" : input2.nodeExecutable;
82621
+ const legacyArguments = legacyEnvironmentPrefix ? `-NoProfile -NonInteractive -WindowStyle Hidden -Command "${legacyEnvironmentPrefix}; & ${powershellLiteral(input2.nodeExecutable)} ${powershellLiteral(input2.cliEntry)} daemon --profile ${input2.profile}"` : legacyDirectArguments;
82622
+ const daemonArguments = [input2.cliEntry, "daemon", "--profile", input2.profile].map(windowsCommandLineArgument).join(" ");
82623
+ const environmentStatements = Object.entries(input2.environment ?? {}).map(
82624
+ ([key, value2]) => `$startInfo.EnvironmentVariables[${powershellLiteral(key)}] = ${powershellLiteral(value2)}`
82645
82625
  );
82626
+ const launcherScript = [
82627
+ "$ErrorActionPreference = 'Stop'",
82628
+ "$startInfo = [System.Diagnostics.ProcessStartInfo]::new()",
82629
+ `$startInfo.FileName = ${powershellLiteral(input2.nodeExecutable)}`,
82630
+ `$startInfo.Arguments = ${powershellLiteral(daemonArguments)}`,
82631
+ `$startInfo.WorkingDirectory = ${powershellLiteral((0, import_node_path27.dirname)(input2.cliEntry))}`,
82632
+ "$startInfo.UseShellExecute = $false",
82633
+ "$startInfo.CreateNoWindow = $true",
82634
+ "$startInfo.RedirectStandardOutput = $true",
82635
+ "$startInfo.RedirectStandardError = $true",
82636
+ ...environmentStatements,
82637
+ `$stdout = [System.IO.File]::Open(${powershellLiteral((0, import_node_path27.join)(logDir, "daemon-launcher.log"))}, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write, [System.IO.FileShare]::ReadWrite)`,
82638
+ `$stderr = [System.IO.File]::Open(${powershellLiteral((0, import_node_path27.join)(logDir, "daemon-launcher-error.log"))}, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write, [System.IO.FileShare]::ReadWrite)`,
82639
+ "$process = [System.Diagnostics.Process]::new()",
82640
+ "$process.StartInfo = $startInfo",
82641
+ "try { if (-not $process.Start()) { throw 'Alan daemon process did not start' }; $stdoutCopy = $process.StandardOutput.BaseStream.CopyToAsync($stdout); $stderrCopy = $process.StandardError.BaseStream.CopyToAsync($stderr); $process.WaitForExit(); [System.Threading.Tasks.Task]::WaitAll([System.Threading.Tasks.Task[]]@($stdoutCopy, $stderrCopy)); $exitCode = $process.ExitCode } finally { $stdout.Dispose(); $stderr.Dispose(); $process.Dispose() }",
82642
+ "exit $exitCode"
82643
+ ].join("; ");
82644
+ const executable = powershellLiteral("powershell.exe");
82645
+ const launcherArguments = `-NoProfile -NonInteractive -WindowStyle Hidden -EncodedCommand ${Buffer.from(launcherScript, "utf16le").toString("base64")}`;
82646
+ const settingsScript = "New-ScheduledTaskSettingsSet -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero) -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries";
82647
+ const ownerMarker = `Alan daemon owner v1 ${(0, import_node_crypto21.createHash)("sha256").update(
82648
+ JSON.stringify({
82649
+ executable: input2.nodeExecutable,
82650
+ cliEntry: input2.cliEntry,
82651
+ profile: input2.profile,
82652
+ taskBaseName: names.windows
82653
+ })
82654
+ ).digest("hex")}`;
82655
+ const definitionMarker = `${ownerMarker}; definition v3 ${(0, import_node_crypto21.createHash)("sha256").update(
82656
+ JSON.stringify({
82657
+ executable: "powershell.exe",
82658
+ launcherArguments,
82659
+ workingDirectory: (0, import_node_path27.dirname)(input2.cliEntry),
82660
+ settingsScript,
82661
+ taskBaseName: names.windows
82662
+ })
82663
+ ).digest("hex")}`;
82664
+ const windowsDefinitionContext = [
82665
+ `$expectedLauncherArguments = ${powershellLiteral(launcherArguments)}`,
82666
+ `$expectedWorkingDirectory = ${powershellLiteral((0, import_node_path27.dirname)(input2.cliEntry))}`,
82667
+ `$expectedOwnerMarker = ${powershellLiteral(`${ownerMarker};`)}`,
82668
+ `$expectedDefinitionMarker = ${powershellLiteral(definitionMarker)}`,
82669
+ `$expectedLegacyExecutable = ${powershellLiteral(legacyExecutable)}`,
82670
+ `$expectedLegacyArguments = ${powershellLiteral(legacyArguments)}`
82671
+ ].join("; ");
82672
+ const taskContext = [
82673
+ "$currentIdentity = [System.Security.Principal.WindowsIdentity]::GetCurrent()",
82674
+ "$currentUser = $currentIdentity.Name",
82675
+ "$currentSid = $currentIdentity.User.Value",
82676
+ `$taskName = ${powershellLiteral(names.windows)} + ' ' + $currentSid`
82677
+ ].join("; ");
82678
+ const resolveTaskSid = "function Resolve-TaskSid([string]$value) { try { return ([System.Security.Principal.NTAccount]$value).Translate([System.Security.Principal.SecurityIdentifier]).Value } catch { try { return [System.Security.Principal.SecurityIdentifier]::new($value).Value } catch { return '' } } }";
82679
+ const currentLookup = "$task = @(Get-ScheduledTask -ErrorAction Stop | Where-Object { $_.TaskPath -eq '\\' -and $_.TaskName -eq $taskName })[0]";
82680
+ const checkCurrentOwnership = [
82681
+ currentLookup,
82682
+ "$currentOwned = $false",
82683
+ "if ($null -ne $task) { try {",
82684
+ "$taskAction = @($task.Actions)[0]",
82685
+ "$currentOwned = @($task.Actions).Count -eq 1 -and (Resolve-TaskSid ([string]$task.Principal.UserId)) -eq $currentSid -and (([string]$task.Description).StartsWith($expectedOwnerMarker, [System.StringComparison]::Ordinal) -or (([string]$taskAction.Execute) -ieq 'powershell.exe' -and ([string]$taskAction.Arguments) -ceq $expectedLauncherArguments -and ([string]$taskAction.WorkingDirectory) -ieq $expectedWorkingDirectory))",
82686
+ "} catch { $currentOwned = $false } }"
82687
+ ].join("; ");
82688
+ const legacyLookup = `$legacyTask = @(Get-ScheduledTask -ErrorAction Stop | Where-Object { $_.TaskPath -eq '\\' -and $_.TaskName -eq ${powershellLiteral(names.windows)} })[0]`;
82689
+ const checkLegacyOwnership = [
82690
+ legacyLookup,
82691
+ "$legacyOwned = $false",
82692
+ "$legacyState = 'Missing'",
82693
+ "if ($null -ne $legacyTask) { try {",
82694
+ "$legacyState = [string]$legacyTask.State",
82695
+ "$legacySid = Resolve-TaskSid ([string]$legacyTask.Principal.UserId)",
82696
+ "$legacyAction = @($legacyTask.Actions)[0]",
82697
+ "if ($null -ne $legacyAction) {",
82698
+ "$legacyOwned = $legacySid -eq $currentSid -and @($legacyTask.Actions).Count -eq 1 -and ((([string]$legacyAction.Execute) -ieq $expectedLegacyExecutable -and ([string]$legacyAction.Arguments) -ieq $expectedLegacyArguments -and ([string]$legacyAction.WorkingDirectory) -eq '') -or (([string]$legacyAction.Execute) -ieq 'powershell.exe' -and ([string]$legacyAction.Arguments) -ceq $expectedLauncherArguments -and ([string]$legacyAction.WorkingDirectory) -ieq $expectedWorkingDirectory))",
82699
+ "}",
82700
+ "} catch { $legacyOwned = $false } }"
82701
+ ].join("; ");
82702
+ const assertOwnedTasks = [
82703
+ checkCurrentOwnership,
82704
+ checkLegacyOwnership,
82705
+ `if ($null -ne $task -and -not $currentOwned) { throw ${powershellLiteral(`${names.windows} task is not owned by this Alan installation`)} }`,
82706
+ `if ($null -ne $legacyTask -and -not $legacyOwned) { throw ${powershellLiteral(`Legacy ${names.windows} task is not owned by this Alan installation`)} }`,
82707
+ "if ($null -ne $task -and @('Ready', 'Disabled', 'Queued') -notcontains ([string]$task.State)) { throw 'Alan daemon task became active or has unknown state' }",
82708
+ "if ($null -ne $legacyTask -and @('Ready', 'Disabled', 'Queued') -notcontains ([string]$legacyTask.State)) { throw 'Legacy Alan daemon task became active or has unknown state' }"
82709
+ ].join("; ");
82710
+ const cleanupLegacyTask = [
82711
+ checkLegacyOwnership,
82712
+ `if ($null -ne $legacyTask -and -not $legacyOwned) { throw ${powershellLiteral(`Legacy ${names.windows} task is not owned by this Alan installation`)} }`,
82713
+ "if ($null -ne $legacyTask -and @('Ready', 'Disabled', 'Queued') -notcontains ([string]$legacyTask.State)) { throw 'Legacy Alan daemon task became active or has unknown state' }",
82714
+ `if ($null -ne $legacyTask) { Unregister-ScheduledTask -TaskName ${powershellLiteral(names.windows)} -TaskPath '\\' -Confirm:$false -ErrorAction Stop }`,
82715
+ `$legacyRemaining = @(Get-ScheduledTask -ErrorAction Stop | Where-Object { $_.TaskPath -eq '\\' -and $_.TaskName -eq ${powershellLiteral(names.windows)} })[0]`,
82716
+ "if ($null -ne $legacyRemaining) { throw 'Legacy Alan daemon task still exists after migration' }"
82717
+ ].join("; ");
82646
82718
  const registerScript = [
82647
- "$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name",
82648
- `$action = New-ScheduledTaskAction -Execute ${executable} -Argument ${argument}`,
82719
+ taskContext,
82720
+ resolveTaskSid,
82721
+ windowsDefinitionContext,
82722
+ `$action = New-ScheduledTaskAction -Execute ${executable} -Argument $expectedLauncherArguments -WorkingDirectory $expectedWorkingDirectory`,
82649
82723
  "$trigger = New-ScheduledTaskTrigger -AtLogOn -User $currentUser",
82650
82724
  "$principal = New-ScheduledTaskPrincipal -UserId $currentUser -LogonType Interactive -RunLevel Limited",
82651
- "$settings = New-ScheduledTaskSettingsSet -RestartCount 3 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit ([TimeSpan]::Zero)",
82652
- `Register-ScheduledTask -TaskName ${powershellLiteral(names.windows)} -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force | Out-Null`
82725
+ `$settings = ${settingsScript}`,
82726
+ assertOwnedTasks,
82727
+ "Register-ScheduledTask -TaskName $taskName -Description $expectedDefinitionMarker -Action $action -Trigger $trigger -Principal $principal -Settings $settings -Force -ErrorAction Stop | Out-Null",
82728
+ cleanupLegacyTask,
82729
+ "$registeredTask = @(Get-ScheduledTask -ErrorAction Stop | Where-Object { $_.TaskPath -eq '\\' -and $_.TaskName -eq $taskName })[0]",
82730
+ "if ($null -eq $registeredTask -or $registeredTask.Description -cne $expectedDefinitionMarker) { throw 'Alan daemon task definition was not applied' }"
82731
+ ].join("; ");
82732
+ const uninstallScript = [
82733
+ taskContext,
82734
+ resolveTaskSid,
82735
+ windowsDefinitionContext,
82736
+ assertOwnedTasks,
82737
+ "if ($null -ne $legacyTask) { Unregister-ScheduledTask -TaskName $legacyTask.TaskName -TaskPath $legacyTask.TaskPath -Confirm:$false -ErrorAction Stop }",
82738
+ "$task = @(Get-ScheduledTask -ErrorAction Stop | Where-Object { $_.TaskPath -eq '\\' -and $_.TaskName -eq $taskName })[0]",
82739
+ "if ($null -ne $task) { Unregister-ScheduledTask -TaskName $taskName -TaskPath '\\' -Confirm:$false -ErrorAction Stop }",
82740
+ "$remaining = @(Get-ScheduledTask -ErrorAction Stop | Where-Object { $_.TaskPath -eq '\\' -and ($_.TaskName -eq $taskName -or $_.TaskName -eq $legacyTask.TaskName) })",
82741
+ "if ($remaining.Count -gt 0) { throw 'Alan daemon task still exists after unregister' }"
82653
82742
  ].join("; ");
82743
+ const statusScript = [
82744
+ taskContext,
82745
+ resolveTaskSid,
82746
+ windowsDefinitionContext,
82747
+ checkCurrentOwnership,
82748
+ checkLegacyOwnership,
82749
+ "$definitionMatches = $false",
82750
+ "if ($null -eq $task) { Write-Output 'State: Missing' } else {",
82751
+ "$taskTrigger = @($task.Triggers)[0]",
82752
+ "$definitionMatches = @($task.Actions).Count -eq 1 -and @($task.Triggers).Count -eq 1 -and $task.Description -ceq $expectedDefinitionMarker -and (Resolve-TaskSid ([string]$task.Principal.UserId)) -eq $currentSid -and [string]$task.Principal.LogonType -eq 'Interactive' -and [string]$task.Principal.RunLevel -eq 'Limited' -and [string]$taskAction.Execute -ieq 'powershell.exe' -and [string]$taskAction.Arguments -ceq $expectedLauncherArguments -and [string]$taskAction.WorkingDirectory -ieq $expectedWorkingDirectory -and $taskTrigger.CimClass.CimClassName -eq 'MSFT_TaskLogonTrigger' -and (Resolve-TaskSid ([string]$taskTrigger.UserId)) -eq $currentSid -and -not $task.Settings.DisallowStartIfOnBatteries -and -not $task.Settings.StopIfGoingOnBatteries -and $task.Settings.RestartCount -eq 3 -and [string]$task.Settings.RestartInterval -eq 'PT1M' -and [string]$task.Settings.ExecutionTimeLimit -eq 'PT0S'",
82753
+ "Write-Output ('TaskName: ' + $task.TaskName); Write-Output ('State: ' + $task.State); Write-Output ('Definition: ' + $task.Description)",
82754
+ "}",
82755
+ "Write-Output ('DefinitionMatch: ' + $definitionMatches)",
82756
+ "Write-Output ('CurrentOwned: ' + $currentOwned)",
82757
+ "$legacyPresent = $null -ne $legacyTask",
82758
+ "Write-Output ('LegacyTask: ' + $legacyPresent)",
82759
+ "Write-Output ('LegacyOwned: ' + $legacyOwned)",
82760
+ "Write-Output ('LegacyState: ' + $legacyState)"
82761
+ ].join("; ");
82762
+ const activeScript = [
82763
+ taskContext,
82764
+ "$task = @(Get-ScheduledTask -ErrorAction Stop | Where-Object { $_.TaskPath -eq '\\' -and $_.TaskName -eq $taskName })[0]",
82765
+ "if ($null -eq $task) { Write-Output 'Missing' } else { Write-Output $task.State }"
82766
+ ].join("; ");
82767
+ const registerCommand = powershellCommand(registerScript);
82654
82768
  return {
82655
82769
  platform: "win32",
82656
82770
  manifestPath: null,
82657
82771
  manifest: null,
82658
- logDirectory: null,
82659
- loadDefinitionCommands: [
82660
- {
82661
- command: "powershell.exe",
82662
- args: ["-NoProfile", "-NonInteractive", "-Command", registerScript],
82663
- tolerateFailure: false
82664
- }
82665
- ],
82666
- reloadDefinitionCommands: [
82667
- {
82668
- command: "powershell.exe",
82669
- args: ["-NoProfile", "-NonInteractive", "-Command", registerScript],
82670
- tolerateFailure: false
82671
- }
82672
- ],
82772
+ logDirectory: logDir,
82773
+ loadDefinitionCommands: [registerCommand],
82774
+ reloadDefinitionCommands: [registerCommand],
82673
82775
  enableCommands: [],
82674
- startCommands: [
82675
- {
82676
- command: "powershell.exe",
82677
- args: [
82678
- "-NoProfile",
82679
- "-NonInteractive",
82680
- "-Command",
82681
- `Start-ScheduledTask -TaskName ${powershellLiteral(names.windows)}`
82682
- ],
82683
- tolerateFailure: false
82684
- }
82685
- ],
82776
+ startCommands: [powershellCommand(`${taskContext}; Start-ScheduledTask -TaskName $taskName`)],
82686
82777
  stopCommands: [
82687
- {
82688
- command: "powershell.exe",
82689
- args: [
82690
- "-NoProfile",
82691
- "-NonInteractive",
82692
- "-Command",
82693
- `Stop-ScheduledTask -TaskName ${powershellLiteral(names.windows)} -ErrorAction SilentlyContinue`
82694
- ],
82695
- tolerateFailure: true
82696
- }
82697
- ],
82698
- replaceCommands: [
82699
- {
82700
- command: "powershell.exe",
82701
- args: [
82702
- "-NoProfile",
82703
- "-NonInteractive",
82704
- "-Command",
82705
- `${registerScript}; Start-ScheduledTask -TaskName ${powershellLiteral(names.windows)}`
82706
- ],
82707
- tolerateFailure: false
82708
- }
82709
- ],
82710
- uninstallCommands: [
82711
- {
82712
- command: "powershell.exe",
82713
- args: [
82714
- "-NoProfile",
82715
- "-NonInteractive",
82716
- "-Command",
82717
- `Unregister-ScheduledTask -TaskName ${powershellLiteral(names.windows)} -Confirm:$false -ErrorAction SilentlyContinue`
82718
- ],
82719
- tolerateFailure: true
82720
- }
82778
+ powershellCommand(
82779
+ `${taskContext}; Stop-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue`,
82780
+ true
82781
+ )
82721
82782
  ],
82722
- statusCommand: {
82723
- command: "powershell.exe",
82724
- args: [
82725
- "-NoProfile",
82726
- "-NonInteractive",
82727
- "-Command",
82728
- `Get-ScheduledTask -TaskName ${powershellLiteral(names.windows)} | Format-List TaskName,State`
82729
- ],
82730
- tolerateFailure: true
82731
- },
82732
- isActiveCommand: {
82733
- command: "powershell.exe",
82734
- args: [
82735
- "-NoProfile",
82736
- "-NonInteractive",
82737
- "-Command",
82738
- `(Get-ScheduledTask -TaskName ${powershellLiteral(names.windows)} -ErrorAction SilentlyContinue).State`
82739
- ],
82740
- tolerateFailure: true
82741
- },
82742
- lingerStatusCommand: null
82783
+ uninstallCommands: [powershellCommand(uninstallScript)],
82784
+ statusCommand: powershellCommand(statusScript, true),
82785
+ isActiveCommand: powershellCommand(activeScript, true),
82786
+ definitionMarker
82743
82787
  };
82744
82788
  }
82745
82789
  function resolveCurrentPlatform() {
@@ -82749,14 +82793,14 @@ function resolveCurrentPlatform() {
82749
82793
  }
82750
82794
  return currentPlatform;
82751
82795
  }
82752
- function currentServicePlan(args, runtime) {
82753
- const currentPlatform = resolveCurrentPlatform();
82796
+ function currentServicePlan(args, runtime, options = {}) {
82797
+ const currentPlatform = options.platform ?? resolveCurrentPlatform();
82754
82798
  const profile = resolveEndpointProfileFromArgs(args);
82755
82799
  const cliEntry = runtime?.cliEntry ?? process.argv[1];
82756
82800
  if (!cliEntry) throw new Error("Cannot determine the alan-agent executable path");
82757
82801
  return buildDaemonServicePlan({
82758
82802
  platform: currentPlatform,
82759
- homeDir: (0, import_node_os16.homedir)(),
82803
+ homeDir: options.homeDir ?? (0, import_node_os16.homedir)(),
82760
82804
  nodeExecutable: runtime?.executable ?? process.execPath,
82761
82805
  cliEntry: (0, import_node_path27.resolve)(cliEntry),
82762
82806
  profile,
@@ -82779,9 +82823,13 @@ function defaultReadManifest(path2) {
82779
82823
  return (0, import_node_fs25.readFileSync)(path2, "utf8");
82780
82824
  }
82781
82825
  function runServiceCommand(command) {
82782
- const result = (0, import_node_child_process9.spawnSync)(command.command, command.args, { encoding: "utf8", shell: false });
82826
+ const result = (0, import_node_child_process9.spawnSync)(command.command, command.args, {
82827
+ encoding: "utf8",
82828
+ shell: false,
82829
+ ...(0, import_node_os16.platform)() === "win32" ? { timeout: 15e3, windowsHide: true } : {}
82830
+ });
82783
82831
  const status = result.status ?? 1;
82784
- const output2 = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
82832
+ const output2 = `${result.stdout ?? ""}${result.stderr ?? ""}${result.error?.message ?? ""}`.trim();
82785
82833
  if (status !== 0 && !command.tolerateFailure) {
82786
82834
  throw new Error(
82787
82835
  `Service command failed (${command.command} ${command.args.join(" ")}): ${output2 || `exit ${status}`}`
@@ -82794,6 +82842,23 @@ function defaultSleep(ms) {
82794
82842
  setTimeout(resolveSleep, ms);
82795
82843
  });
82796
82844
  }
82845
+ function isProcessAlive(pid) {
82846
+ if (pid <= 0) return false;
82847
+ try {
82848
+ process.kill(pid, 0);
82849
+ return true;
82850
+ } catch (error61) {
82851
+ return error61.code === "EPERM";
82852
+ }
82853
+ }
82854
+ async function requestWindowsDaemonShutdown(owner) {
82855
+ try {
82856
+ const response = await requestLocalDaemonShutdown(owner.localApiPort, owner.localApiToken);
82857
+ return response.ok ? "accepted" : "rejected";
82858
+ } catch {
82859
+ return "unreachable";
82860
+ }
82861
+ }
82797
82862
  function parseServiceHostState(servicePlatform, result) {
82798
82863
  const output2 = result.output;
82799
82864
  if (servicePlatform === "linux") {
@@ -82819,9 +82884,62 @@ function parseServiceHostState(servicePlatform, result) {
82819
82884
  const stateMatch = output2.match(/\b(?:State|state)\s*[:=]\s*(\w+)/);
82820
82885
  const state = (stateMatch?.[1] ?? output2.trim()).toLowerCase();
82821
82886
  if (state === "running") return "running";
82822
- if (state === "ready" || state === "disabled" || state === "queued") return "stopped";
82887
+ if (state === "queued") return "starting";
82888
+ if (state === "ready" || state === "disabled") return "stopped";
82823
82889
  return "unknown";
82824
82890
  }
82891
+ function parseWindowsTaskInspection(result) {
82892
+ if (result.status !== 0) {
82893
+ throw new Error("Cannot inspect Windows daemon task definition");
82894
+ }
82895
+ const value2 = (name) => {
82896
+ const match = result.output.match(new RegExp(`^${name}:\\s*(.*)$`, "imu"));
82897
+ if (!match) throw new Error(`Incomplete Windows daemon task inspection: missing ${name}`);
82898
+ return match[1]?.trim() ?? "";
82899
+ };
82900
+ const boolean4 = (name) => {
82901
+ const parsed = value2(name).toLowerCase();
82902
+ if (parsed === "true") return true;
82903
+ if (parsed === "false") return false;
82904
+ throw new Error(`Incomplete Windows daemon task inspection: invalid ${name}`);
82905
+ };
82906
+ const currentPresent = /^TaskName:\s*.+$/imu.test(result.output);
82907
+ const currentStateValue = value2("State");
82908
+ const currentOwned = boolean4("CurrentOwned");
82909
+ const definitionMatches = boolean4("DefinitionMatch");
82910
+ const legacyPresent = boolean4("LegacyTask");
82911
+ const legacyOwned = boolean4("LegacyOwned");
82912
+ const legacyStateValue = value2("LegacyState");
82913
+ if (currentPresent !== (currentStateValue.toLowerCase() !== "missing")) {
82914
+ throw new Error("Incomplete Windows daemon task inspection: inconsistent current task state");
82915
+ }
82916
+ if (!currentPresent && (currentOwned || definitionMatches)) {
82917
+ throw new Error(
82918
+ "Incomplete Windows daemon task inspection: absent current task is marked owned"
82919
+ );
82920
+ }
82921
+ if (legacyPresent !== (legacyStateValue.toLowerCase() !== "missing") || !legacyPresent && legacyOwned) {
82922
+ throw new Error("Incomplete Windows daemon task inspection: inconsistent legacy task state");
82923
+ }
82924
+ return {
82925
+ currentPresent,
82926
+ currentOwned,
82927
+ currentState: currentPresent ? parseServiceHostState("win32", { status: 0, output: currentStateValue }) : "stopped",
82928
+ definitionMatches,
82929
+ definitionMarker: result.output.match(/^Definition:\s*(.+)$/imu)?.[1]?.trim(),
82930
+ legacyPresent,
82931
+ legacyOwned,
82932
+ legacyState: legacyPresent ? parseServiceHostState("win32", { status: 0, output: legacyStateValue }) : "stopped"
82933
+ };
82934
+ }
82935
+ function assertWindowsTaskOwnership(tasks) {
82936
+ if (tasks.currentPresent && !tasks.currentOwned) {
82937
+ throw new Error("Windows daemon task is not owned by this Alan installation");
82938
+ }
82939
+ if (tasks.legacyPresent && !tasks.legacyOwned) {
82940
+ throw new Error("Legacy Windows daemon task is not owned by this Alan installation");
82941
+ }
82942
+ }
82825
82943
  function resolveEnsureDefinitionOutcome(input2) {
82826
82944
  if (!input2.definitionChanged) {
82827
82945
  return { changed: false, updatePending: false };
@@ -82831,9 +82949,6 @@ function resolveEnsureDefinitionOutcome(input2) {
82831
82949
  }
82832
82950
  return { changed: true, updatePending: false };
82833
82951
  }
82834
- function shouldStartOnInstall(hostState) {
82835
- return hostState === "stopped";
82836
- }
82837
82952
  function resolveUsername(options) {
82838
82953
  if (options?.username) return options.username;
82839
82954
  try {
@@ -82842,17 +82957,14 @@ function resolveUsername(options) {
82842
82957
  return process.env.USER || process.env.USERNAME || "unknown";
82843
82958
  }
82844
82959
  }
82845
- function buildLingerStatusCommand(username) {
82846
- return {
82847
- command: "loginctl",
82848
- args: ["show-user", username, "-p", "Linger"],
82849
- tolerateFailure: true
82850
- };
82851
- }
82852
82960
  function assertSystemdUserLingerEnabled(options = {}) {
82853
82961
  const username = resolveUsername(options);
82854
82962
  const run = options.runCommand ?? runServiceCommand;
82855
- const result = run(buildLingerStatusCommand(username));
82963
+ const result = run({
82964
+ command: "loginctl",
82965
+ args: ["show-user", username, "-p", "Linger"],
82966
+ tolerateFailure: true
82967
+ });
82856
82968
  if (result.status !== 0) {
82857
82969
  throw new Error(
82858
82970
  `Cannot verify systemd user linger for ${username} (loginctl failed). For durable headless VM boot run: sudo loginctl enable-linger ${username}`
@@ -82866,9 +82978,9 @@ function assertSystemdUserLingerEnabled(options = {}) {
82866
82978
  }
82867
82979
  function createHostContext(options = {}) {
82868
82980
  const args = options.args ?? [];
82869
- const plan = currentServicePlan(args, options.runtime);
82981
+ const plan = currentServicePlan(args, options.runtime, options);
82870
82982
  const run = options.runCommand ?? runServiceCommand;
82871
- const sleep4 = options.sleep ?? defaultSleep;
82983
+ const sleep5 = options.sleep ?? defaultSleep;
82872
82984
  const nowMs = options.nowMs ?? Date.now;
82873
82985
  const readManifest = options.readManifest ?? defaultReadManifest;
82874
82986
  const writeManifest = options.writeManifest ?? writeManifestAtomic;
@@ -82880,13 +82992,20 @@ function createHostContext(options = {}) {
82880
82992
  args,
82881
82993
  plan,
82882
82994
  run,
82883
- sleep: sleep4,
82995
+ sleep: sleep5,
82884
82996
  nowMs,
82885
82997
  readManifest,
82886
82998
  writeManifest,
82887
82999
  removeManifest,
82888
83000
  ensureLogDir,
82889
- profile: resolveEndpointProfileFromArgs(args)
83001
+ configPath: options.configPath ?? resolveAgentConfigPath({ profile: resolveEndpointProfileFromArgs(args) }),
83002
+ readOwner: options.readOwner ?? readLocalDaemonOwnerLease,
83003
+ isProcessAlive: options.isProcessAlive ?? isProcessAlive,
83004
+ probeEndpoint: options.probeEndpoint ?? probeLocalDaemonEndpoint,
83005
+ requestWindowsShutdown: options.requestWindowsShutdown ?? requestWindowsDaemonShutdown,
83006
+ inspectJournal: options.inspectJournal ?? inspectLocalDaemonRunJournal,
83007
+ acquireMaintenance: options.acquireMaintenance ?? acquireLocalDaemonMaintenanceLease,
83008
+ releaseMaintenance: options.releaseMaintenance ?? releaseLocalDaemonMaintenanceLease
82890
83009
  };
82891
83010
  }
82892
83011
  function inspectDaemonService(options = {}) {
@@ -82902,8 +83021,9 @@ function inspectDaemonService(options = {}) {
82902
83021
  }
82903
83022
  function ensureDaemonServiceDefinition(options = {}) {
82904
83023
  const ctx = createHostContext(options);
82905
- const { plan, run, readManifest, writeManifest, ensureLogDir } = ctx;
83024
+ const { plan, run, nowMs, readManifest, writeManifest, ensureLogDir } = ctx;
82906
83025
  let definitionChanged = false;
83026
+ let inspection;
82907
83027
  if (plan.manifestPath && plan.manifest) {
82908
83028
  ensureLogDir();
82909
83029
  const existing = readManifest(plan.manifestPath);
@@ -82912,20 +83032,38 @@ function ensureDaemonServiceDefinition(options = {}) {
82912
83032
  writeManifest(plan.manifestPath, plan.manifest);
82913
83033
  }
82914
83034
  } else if (plan.platform === "win32") {
82915
- definitionChanged = true;
83035
+ ensureLogDir();
83036
+ const result = run(plan.statusCommand);
83037
+ const tasks = parseWindowsTaskInspection(result);
83038
+ assertWindowsTaskOwnership(tasks);
83039
+ if (tasks.currentPresent && tasks.currentState === "unknown" || tasks.legacyPresent && tasks.legacyState === "unknown") {
83040
+ throw new Error("Cannot safely update a Windows daemon task with unknown state");
83041
+ }
83042
+ definitionChanged = tasks.definitionMarker !== plan.definitionMarker || !tasks.definitionMatches || tasks.legacyPresent;
83043
+ const running = tasks.currentState === "running" || tasks.legacyState === "running";
83044
+ inspection = {
83045
+ state: running ? "running" : tasks.currentState,
83046
+ platform: plan.platform,
83047
+ detail: result.output || void 0,
83048
+ observedAtMs: nowMs()
83049
+ };
82916
83050
  }
82917
- const inspection = inspectDaemonService(options);
83051
+ inspection ??= inspectDaemonService(options);
83052
+ const definitionHostState = plan.platform === "win32" && inspection.state === "starting" ? "stopped" : inspection.state;
82918
83053
  const outcome = resolveEnsureDefinitionOutcome({
82919
83054
  definitionChanged,
82920
- hostState: inspection.state
83055
+ hostState: definitionHostState
82921
83056
  });
82922
83057
  if (outcome.updatePending) {
82923
83058
  return outcome;
82924
83059
  }
82925
- if (inspection.state === "running" || inspection.state === "starting") {
83060
+ if (plan.platform === "win32" && !outcome.changed) {
82926
83061
  return outcome;
82927
83062
  }
82928
- const definitionCommands = inspection.state === "stopped" ? plan.reloadDefinitionCommands : plan.loadDefinitionCommands;
83063
+ if (definitionHostState === "running" || definitionHostState === "starting") {
83064
+ return outcome;
83065
+ }
83066
+ const definitionCommands = definitionHostState === "stopped" ? plan.reloadDefinitionCommands : plan.loadDefinitionCommands;
82929
83067
  for (const command of definitionCommands) run(command);
82930
83068
  return outcome;
82931
83069
  }
@@ -82937,10 +83075,140 @@ function startDaemonService(options = {}) {
82937
83075
  const { plan, run } = createHostContext(options);
82938
83076
  for (const command of plan.startCommands) run(command);
82939
83077
  }
82940
- function uninstallDaemonService(args = [], options = {}) {
83078
+ async function stopDaemonServiceAndWait(identity, timeoutMs, options = {}) {
83079
+ const {
83080
+ plan,
83081
+ run,
83082
+ sleep: sleep5,
83083
+ nowMs,
83084
+ configPath,
83085
+ readOwner,
83086
+ isProcessAlive: processIsAlive,
83087
+ probeEndpoint,
83088
+ requestWindowsShutdown: requestShutdown
83089
+ } = createHostContext(options);
83090
+ let owner;
83091
+ try {
83092
+ owner = readOwner(configPath);
83093
+ } catch {
83094
+ return "identity_changed";
83095
+ }
83096
+ const ownerMatches = (candidate) => candidate.pid === identity.pid && candidate.daemonSessionId === identity.daemonSessionId && candidate.localApiPort === identity.port && (identity.serviceId === void 0 || candidate.localServiceId === identity.serviceId) && (identity.startedAtMs === void 0 || candidate.startedAtMs === identity.startedAtMs);
83097
+ if (!owner || !ownerMatches(owner)) {
83098
+ return "identity_changed";
83099
+ }
83100
+ if (plan.platform === "win32") {
83101
+ const shutdown = await requestShutdown(owner);
83102
+ if (shutdown === "rejected") return "identity_changed";
83103
+ if (shutdown === "unreachable") {
83104
+ for (const command of plan.stopCommands) run(command);
83105
+ }
83106
+ const deadline2 = nowMs() + timeoutMs;
83107
+ while (nowMs() < deadline2) {
83108
+ let currentOwner;
83109
+ try {
83110
+ currentOwner = readOwner(configPath);
83111
+ } catch {
83112
+ return "identity_changed";
83113
+ }
83114
+ if (currentOwner && !ownerMatches(currentOwner)) {
83115
+ return "identity_changed";
83116
+ }
83117
+ if (!processIsAlive(identity.pid)) {
83118
+ const endpoint = await probeEndpoint(identity.port, owner.localApiToken, 250);
83119
+ if (endpoint.state === "absent") return "exited";
83120
+ if (endpoint.state === "listening_unauthorized" || endpoint.status.daemonSessionId !== identity.daemonSessionId) {
83121
+ return "identity_changed";
83122
+ }
83123
+ }
83124
+ await sleep5(STOP_POLL_INTERVAL_MS);
83125
+ }
83126
+ return "timeout";
83127
+ }
83128
+ for (const command of plan.stopCommands) run(command);
83129
+ const deadline = nowMs() + timeoutMs;
83130
+ while (nowMs() < deadline) {
83131
+ const result = run(plan.isActiveCommand);
83132
+ const state = parseServiceHostState(plan.platform, result);
83133
+ if (state === "stopped") return "exited";
83134
+ if (state === "unknown") {
83135
+ } else if (identity.pid > 0) {
83136
+ try {
83137
+ process.kill(identity.pid, 0);
83138
+ } catch {
83139
+ return "exited";
83140
+ }
83141
+ }
83142
+ await sleep5(STOP_POLL_INTERVAL_MS);
83143
+ }
83144
+ return "timeout";
83145
+ }
83146
+ async function uninstallDaemonService(args = [], options = {}) {
82941
83147
  const ctx = createHostContext({ ...options, args: options.args ?? args });
82942
- for (const command of ctx.plan.uninstallCommands) ctx.run(command);
82943
- if (ctx.plan.manifestPath) ctx.removeManifest(ctx.plan.manifestPath);
83148
+ if (ctx.plan.platform === "win32") {
83149
+ const ownership = ctx.run(ctx.plan.statusCommand);
83150
+ let tasks;
83151
+ try {
83152
+ tasks = parseWindowsTaskInspection(ownership);
83153
+ } catch (error61) {
83154
+ throw new Error(
83155
+ `Cannot inspect Windows daemon task ownership before uninstall: ${error61 instanceof Error ? error61.message : String(error61)}`
83156
+ );
83157
+ }
83158
+ assertWindowsTaskOwnership(tasks);
83159
+ if (!tasks.currentPresent && !tasks.legacyPresent) {
83160
+ console.info("[alan-agent] Per-user daemon service removed");
83161
+ return;
83162
+ }
83163
+ const acquired = ctx.acquireMaintenance(ctx.configPath, "watchdog", 3e4);
83164
+ if (!acquired.acquired) {
83165
+ throw new Error("Cannot uninstall the Windows daemon service while maintenance is active");
83166
+ }
83167
+ try {
83168
+ const journal = ctx.inspectJournal(ctx.configPath);
83169
+ if (!journal.safeToRestart) {
83170
+ throw new Error(
83171
+ `Windows daemon service uninstall is deferred while ${journal.activeRunCount} active run${journal.activeRunCount === 1 ? "" : "s"} finish`
83172
+ );
83173
+ }
83174
+ let owner;
83175
+ try {
83176
+ owner = ctx.readOwner(ctx.configPath);
83177
+ } catch (error61) {
83178
+ throw new Error(
83179
+ `Cannot verify Windows daemon ownership before uninstall: ${error61 instanceof Error ? error61.message : String(error61)}`
83180
+ );
83181
+ }
83182
+ if (owner) {
83183
+ const stopped = await stopDaemonServiceAndWait(
83184
+ {
83185
+ pid: owner.pid,
83186
+ serviceId: owner.localServiceId,
83187
+ daemonSessionId: owner.daemonSessionId,
83188
+ port: owner.localApiPort,
83189
+ startedAtMs: owner.startedAtMs
83190
+ },
83191
+ 1e4,
83192
+ { ...options, args: ctx.args }
83193
+ );
83194
+ if (stopped !== "exited") {
83195
+ throw new Error(
83196
+ stopped === "identity_changed" ? "Windows daemon ownership changed before uninstall" : "Timed out waiting for the Windows daemon to stop before uninstall"
83197
+ );
83198
+ }
83199
+ } else {
83200
+ if (tasks.currentState !== "stopped" || tasks.legacyState !== "stopped") {
83201
+ throw new Error("Cannot uninstall a running Windows daemon without verified ownership");
83202
+ }
83203
+ }
83204
+ for (const command of ctx.plan.uninstallCommands) ctx.run(command);
83205
+ } finally {
83206
+ ctx.releaseMaintenance(ctx.configPath, acquired.lease.id);
83207
+ }
83208
+ } else {
83209
+ for (const command of ctx.plan.uninstallCommands) ctx.run(command);
83210
+ if (ctx.plan.manifestPath) ctx.removeManifest(ctx.plan.manifestPath);
83211
+ }
82944
83212
  console.info("[alan-agent] Per-user daemon service removed");
82945
83213
  }
82946
83214
  function installDaemonService(args = [], runtime, options = {}) {
@@ -82968,7 +83236,7 @@ function installDaemonService(args = [], runtime, options = {}) {
82968
83236
  });
82969
83237
  return;
82970
83238
  }
82971
- if (!shouldStartOnInstall(inspection.state)) {
83239
+ if (inspection.state !== "stopped") {
82972
83240
  console.info("[alan-agent] Per-user daemon service already present", {
82973
83241
  profile,
82974
83242
  state: inspection.state
@@ -82992,14 +83260,48 @@ ${result.output}
82992
83260
  }
82993
83261
 
82994
83262
  // src/install-command.ts
82995
- async function runInstallCommand(commandArgs) {
83263
+ var WINDOWS_SETUP_READY_TIMEOUT_MS = 9e3;
83264
+ async function runInstallCommand(commandArgs, currentPlatform = process.platform) {
83265
+ if (currentPlatform === "win32" && commandArgs.includes("--headless")) {
83266
+ throw new Error("--headless is only supported on Linux systemd user services");
83267
+ }
82996
83268
  bindConfigPath(commandArgs);
82997
83269
  const configured = readConfig();
82998
83270
  const configurationMode = resolveInstallConfigurationMode(commandArgs, configured);
82999
- if (configurationMode === "setup") await setupDaemon(commandArgs);
83271
+ const isWindowsSetup = currentPlatform === "win32" && configurationMode === "setup";
83272
+ const deadline = isWindowsSetup ? Date.now() + WINDOWS_SETUP_READY_TIMEOUT_MS : 0;
83273
+ const runtimeIdentityChanged = configurationMode === "setup" ? await setupDaemon(commandArgs) : false;
83000
83274
  if (configurationMode === "login") await loginWithDeviceCode(commandArgs);
83275
+ if (!isWindowsSetup) {
83276
+ installDaemonService(commandArgs);
83277
+ runMcpIntegrationCommand(["reconcile", ...commandArgs]);
83278
+ return;
83279
+ }
83280
+ const { runtimeId, localApiPort, localApiToken } = readConfig();
83281
+ if (!runtimeId || !localApiPort || !localApiToken) {
83282
+ throw new Error("Windows runtime setup completed without a local readiness endpoint");
83283
+ }
83284
+ if (runtimeIdentityChanged) {
83285
+ while (Date.now() < deadline) {
83286
+ const status = await probeLocalDaemon(localApiPort, localApiToken, 250);
83287
+ if (status?.runtimeId === runtimeId && status.connected) break;
83288
+ if (!status) {
83289
+ const service = inspectDaemonService({ args: commandArgs });
83290
+ if (service.state !== "running" && service.state !== "starting") break;
83291
+ }
83292
+ await (0, import_promises6.setTimeout)(100);
83293
+ }
83294
+ }
83001
83295
  installDaemonService(commandArgs);
83002
- runMcpIntegrationCommand(["reconcile", ...commandArgs]);
83296
+ while (Date.now() < deadline) {
83297
+ const status = await probeLocalDaemon(localApiPort, localApiToken, 250);
83298
+ if (status?.runtimeId === runtimeId && status.connected) {
83299
+ runMcpIntegrationCommand(["reconcile", ...commandArgs]);
83300
+ return;
83301
+ }
83302
+ await (0, import_promises6.setTimeout)(100);
83303
+ }
83304
+ throw new Error("Windows runtime did not become ready within 9 seconds");
83003
83305
  }
83004
83306
 
83005
83307
  // src/lifecycle-logger.ts
@@ -86376,7 +86678,7 @@ var REQUEST_STATE_ONLY_LEG_PACING_MS2 = 250;
86376
86678
  function inputRequiredRoundsExceededMessage2(method, maxRounds) {
86377
86679
  return `Multi-round-trip request '${method}' still required input after ${maxRounds} rounds (inputRequired.maxRounds)`;
86378
86680
  }
86379
- function sleep3(ms, signal) {
86681
+ function sleep4(ms, signal) {
86380
86682
  return new Promise((resolve15, reject) => {
86381
86683
  if (signal?.aborted) {
86382
86684
  reject(signal.reason instanceof SdkError2 ? signal.reason : new SdkError2(SdkErrorCode2.RequestTimeout, String(signal.reason)));
@@ -94643,7 +94945,7 @@ var LegacyInputRequiredShim = class {
94643
94945
  } finally {
94644
94946
  roundAbort.dispose();
94645
94947
  }
94646
- } else await sleep3(REQUEST_STATE_ONLY_LEG_PACING_MS2, outerSignal);
94948
+ } else await sleep4(REQUEST_STATE_ONLY_LEG_PACING_MS2, outerSignal);
94647
94949
  let ctxNext = {
94648
94950
  ...ctx,
94649
94951
  mcpReq: {
@@ -95440,7 +95742,7 @@ async function runMcpStdioProxy(env = process.env, local = new StdioServerTransp
95440
95742
  }
95441
95743
 
95442
95744
  // src/native-hook-command.ts
95443
- var import_node_crypto21 = require("crypto");
95745
+ var import_node_crypto22 = require("crypto");
95444
95746
  var import_node_fs27 = require("fs");
95445
95747
  var import_node_os17 = require("os");
95446
95748
  var import_node_path29 = require("path");
@@ -95809,7 +96111,7 @@ function parseNativeHookEnvelope(input2) {
95809
96111
  const projectPin = eventType === "session_start" && resolvedCwd ? readAlanProjectPin(resolvedCwd) : void 0;
95810
96112
  return {
95811
96113
  schemaVersion: 1,
95812
- eventId: (0, import_node_crypto21.randomUUID)(),
96114
+ eventId: (0, import_node_crypto22.randomUUID)(),
95813
96115
  provider: capability.provider,
95814
96116
  eventType,
95815
96117
  observedAt: (input2.now ?? /* @__PURE__ */ new Date()).toISOString(),
@@ -96305,7 +96607,7 @@ function delay2(ms) {
96305
96607
  }
96306
96608
 
96307
96609
  // src/web-presenter.ts
96308
- var import_node_crypto22 = require("crypto");
96610
+ var import_node_crypto23 = require("crypto");
96309
96611
  var WebPresenter = class {
96310
96612
  constructor(wsClient, conversationId, cwd) {
96311
96613
  this.conversationId = conversationId;
@@ -96491,7 +96793,7 @@ var WebPresenter = class {
96491
96793
  this.applicationProblem = createAlanProblem({
96492
96794
  domain: "application",
96493
96795
  code: input2.code,
96494
- id: (0, import_node_crypto22.randomUUID)(),
96796
+ id: (0, import_node_crypto23.randomUUID)(),
96495
96797
  title: APPLICATION_PROBLEM_TITLES[input2.code],
96496
96798
  detail: input2.message,
96497
96799
  // The schema's slot for the technical cause, so the problem carries it
@@ -96561,7 +96863,7 @@ var WebPresenter = class {
96561
96863
  };
96562
96864
 
96563
96865
  // src/ws-client.ts
96564
- var import_node_crypto24 = require("crypto");
96866
+ var import_node_crypto25 = require("crypto");
96565
96867
 
96566
96868
  // ../shared/dist/agent-liveness.js
96567
96869
  var AGENT_LIVENESS_PROTOCOL_VERSION = 1;
@@ -96589,13 +96891,13 @@ var agentProbeAckSchema = external_exports.object({
96589
96891
  });
96590
96892
 
96591
96893
  // src/sandbox-outbox.ts
96592
- var import_node_crypto23 = require("crypto");
96894
+ var import_node_crypto24 = require("crypto");
96593
96895
  var import_node_fs28 = require("fs");
96594
96896
  function deriveSandboxOutboxKey(sessionToken) {
96595
96897
  if (!sessionToken) {
96596
96898
  throw new Error("sandbox event outbox requires ALAN_SESSION_TOKEN for key derivation");
96597
96899
  }
96598
- return (0, import_node_crypto23.createHash)("sha256").update(sessionToken, "utf8").digest("base64url");
96900
+ return (0, import_node_crypto24.createHash)("sha256").update(sessionToken, "utf8").digest("base64url");
96599
96901
  }
96600
96902
  function sandboxOutboxSpoolPath(conversationId) {
96601
96903
  return `/tmp/alan-agent-outbox-${conversationId}.enc`;
@@ -96772,7 +97074,7 @@ var WSClient = class {
96772
97074
  }
96773
97075
  socket;
96774
97076
  /** Stable id for this agent process; fences stale heartbeats server-side. */
96775
- agentSessionId = (0, import_node_crypto24.randomUUID)();
97077
+ agentSessionId = (0, import_node_crypto25.randomUUID)();
96776
97078
  /** Monotonic heartbeat sequence — advances on every hello + heartbeat. */
96777
97079
  heartbeatSeq = 0;
96778
97080
  heartbeatTimer = null;
@@ -97604,7 +97906,7 @@ async function runSandbox(config2) {
97604
97906
  }
97605
97907
 
97606
97908
  // src/skills/skill-cli.ts
97607
- var import_promises6 = require("fs/promises");
97909
+ var import_promises7 = require("fs/promises");
97608
97910
  var import_node_os19 = require("os");
97609
97911
  var import_node_path30 = require("path");
97610
97912
  async function runSkillsCommand(args) {
@@ -97651,7 +97953,7 @@ async function defaultScanInput(homeDirectory2) {
97651
97953
  }
97652
97954
  async function readProtectedJson(path2) {
97653
97955
  const absolutePath = (0, import_node_path30.resolve)(path2);
97654
- const stat2 = await (0, import_promises6.lstat)(absolutePath);
97956
+ const stat2 = await (0, import_promises7.lstat)(absolutePath);
97655
97957
  if (!stat2.isFile()) throw new Error(`Skill input is not a regular file: ${absolutePath}`);
97656
97958
  if (process.platform !== "win32" && (stat2.mode & 63) !== 0) {
97657
97959
  throw new Error(`Skill input must not be accessible by group or other users: ${absolutePath}`);
@@ -97660,7 +97962,7 @@ async function readProtectedJson(path2) {
97660
97962
  throw new Error(`Skill input must be owned by the current user: ${absolutePath}`);
97661
97963
  }
97662
97964
  if (stat2.size > 2 * 1024 * 1024) throw new Error("Skill input exceeds 2 MiB");
97663
- return JSON.parse(await (0, import_promises6.readFile)(absolutePath, "utf8"));
97965
+ return JSON.parse(await (0, import_promises7.readFile)(absolutePath, "utf8"));
97664
97966
  }
97665
97967
  function option(args, name) {
97666
97968
  const index = args.indexOf(name);
@@ -97818,7 +98120,7 @@ async function main() {
97818
98120
  return;
97819
98121
  }
97820
98122
  if (command === "service" && commandArgs[0] === "uninstall") {
97821
- uninstallDaemonService(commandArgs.slice(1));
98123
+ await uninstallDaemonService(commandArgs.slice(1));
97822
98124
  return;
97823
98125
  }
97824
98126
  if (command === "service" && commandArgs[0] === "status") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alan-ai-hq/agent-manager",
3
- "version": "0.1.94",
3
+ "version": "0.1.95",
4
4
  "type": "module",
5
5
  "description": "Alan agent runtime — cloud sandbox and local daemon (alan-agent CLI)",
6
6
  "bin": {