@botiverse/raft-computer 0.0.61 → 0.0.63

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.
@@ -1,3 +1,13 @@
1
+ import { Tracer } from '@slock-ai/shared';
2
+ import { TraceClientSource } from '@slock-ai/trace-client';
3
+
4
+ declare class ComputerError extends Error {
5
+ readonly code: string;
6
+ readonly exitCode: number;
7
+ constructor(code: string, message: string, exitCode?: number);
8
+ }
9
+ declare function isComputerError(err: unknown): err is ComputerError;
10
+
1
11
  type DaemonState = {
2
12
  running: true;
3
13
  pid: number;
@@ -486,6 +496,314 @@ type MigrationDetection = {
486
496
  kind: "server-unavailable";
487
497
  };
488
498
 
499
+ type ComputerApiEvent = {
500
+ kind: "login.device-code";
501
+ verifyUrl: string;
502
+ userCode: string;
503
+ expiresAt: string;
504
+ expiresInSeconds: number;
505
+ } | {
506
+ kind: "login.polling";
507
+ } | {
508
+ kind: "login.approved";
509
+ userId: string;
510
+ sessionPath: string;
511
+ } | {
512
+ kind: "attach.attaching";
513
+ serverSlug: string;
514
+ } | {
515
+ kind: "attach.preflight";
516
+ resumed: boolean;
517
+ } | {
518
+ kind: "attach.attached";
519
+ serverId: string;
520
+ serverMachineId: string;
521
+ serverSlug: string;
522
+ attachmentPath: string;
523
+ resumed: boolean;
524
+ apiKeyRedactedPrefix: string;
525
+ } | {
526
+ kind: "detach.detaching";
527
+ serverId: string;
528
+ serverLabel: string;
529
+ } | {
530
+ kind: "detach.revoke_succeeded";
531
+ serverId: string;
532
+ serverLabel: string;
533
+ } | {
534
+ kind: "detach.revoke_failed";
535
+ serverId: string;
536
+ serverLabel: string;
537
+ reason: "http_error" | "network_error";
538
+ httpStatus?: number;
539
+ message: string;
540
+ } | {
541
+ kind: "detach.subtree_cleared";
542
+ serverId: string;
543
+ serverLabel: string;
544
+ } | {
545
+ kind: "detach.detached";
546
+ serverId: string;
547
+ serverLabel: string;
548
+ } | {
549
+ kind: "start.starting";
550
+ managedTargets: string[];
551
+ attachedCount: number;
552
+ foreground: boolean;
553
+ } | {
554
+ kind: "start.already_running";
555
+ servicePid: number;
556
+ managedTargets: string[];
557
+ attachedCount: number;
558
+ } | {
559
+ kind: "start.running";
560
+ managedTargets: string[];
561
+ attachedCount: number;
562
+ } | {
563
+ kind: "start.spawned";
564
+ servicePid: number;
565
+ managedTargets: string[];
566
+ attachedCount: number;
567
+ } | {
568
+ kind: "start.ready";
569
+ ready: Map<string, number>;
570
+ managedTargets: string[];
571
+ } | {
572
+ kind: "start.aborted";
573
+ servicePid: number;
574
+ managedTargets: string[];
575
+ ready: Map<string, number>;
576
+ } | {
577
+ kind: "stop.stopping";
578
+ pid: number | null;
579
+ } | {
580
+ kind: "stop.not_running";
581
+ } | {
582
+ kind: "stop.stale_pidfile_cleared";
583
+ pid: number;
584
+ } | {
585
+ kind: "stop.signaled";
586
+ pid: number;
587
+ } | {
588
+ kind: "stop.stopped";
589
+ pid: number;
590
+ } | {
591
+ kind: "log.line";
592
+ line: string;
593
+ };
594
+
595
+ interface LoginInput {
596
+ serverUrl?: string;
597
+ }
598
+ interface LoginResult {
599
+ userId: string;
600
+ sessionPath: string;
601
+ serverUrl: string;
602
+ }
603
+
604
+ interface AttachInput {
605
+ serverSlug: string;
606
+ serverUrl?: string;
607
+ name?: string;
608
+ }
609
+ interface AttachResult {
610
+ serverId: string;
611
+ serverMachineId: string;
612
+ serverSlug: string;
613
+ serverUrl: string;
614
+ attachmentPath: string;
615
+ resumed: boolean;
616
+ /** First 8 characters of the freshly-issued sk_computer_*; mirrors
617
+ * adoption.log redacted_prefix convention. The raw key lives only in
618
+ * runner.state.json (mode 0o600) — NEVER on this event/result. */
619
+ apiKeyRedactedPrefix: string;
620
+ }
621
+
622
+ /** Generic pidfile reader by absolute path. Returns null on missing / junk. */
623
+ declare function readPidfileAt(pidfilePath: string): Promise<number | null>;
624
+ /** Liveness via signal 0. */
625
+ declare function isProcessAlive(pid: number): boolean;
626
+
627
+ /**
628
+ * Spawn a detached `slock-computer __service` child. The detached child
629
+ * survives the parent process exit, writes its stdio to the service
630
+ * log, and registers its pid in `service.pid`. Returns the pid.
631
+ *
632
+ * Used by `runStart` (foreground CLI → background service) AND by the
633
+ * §2.5 upgrade flow's restart phase: after `swap` renames the new bits
634
+ * into `currentBinaryDir`, this helper re-execs the freshly-installed
635
+ * binary as the new service. After a §2.3 rollback (`.prev` restored)
636
+ * it re-execs the old bits the same way — same code path, same log sink,
637
+ * same pidfile invariant. Dayu blocker (msg=911eb84e): without this
638
+ * shared helper, `runUpgradeCli` had no spawn callback wired into
639
+ * `runUpgrade`, so the production CLI's restart phase never spawned a
640
+ * fresh service — it would time out on health and then fail rollback
641
+ * respawn for the same reason.
642
+ *
643
+ * Both current callsites (`runStart`, `runUpgradeCli`) invoke this while
644
+ * holding `withMutationLock`, so the spawn always sets
645
+ * `PARENT_LOCK_HELD_ENV_VAR=1` to suppress the child service's
646
+ * unconditional `forceReleaseLock` startup step. See env-var docstring
647
+ * above.
648
+ *
649
+ * NOT for use inside `__service` itself (the service is already the
650
+ * detached process). NOT for use inside `restartPhase` defaults: the
651
+ * orchestrator's `deps.spawnFreshService` callback is the integration
652
+ * point so unit tests can stub it without spawning real children.
653
+ *
654
+ * Throws if the OS refuses the spawn (no pid available).
655
+ */
656
+ declare function spawnDetachedService(slockHome: string): Promise<number>;
657
+ interface ResidentCore {
658
+ start: () => void | Promise<void>;
659
+ stop: () => void | Promise<void>;
660
+ }
661
+ type ResidentCoreFactory = (creds: {
662
+ serverId: string;
663
+ serverMachineId: string;
664
+ apiKey: string;
665
+ serverUrl: string;
666
+ }) => ResidentCore | Promise<ResidentCore>;
667
+ type RunStartDeps = {
668
+ coreFactory?: ResidentCoreFactory;
669
+ spawnDetachedService?: typeof spawnDetachedService;
670
+ readPidfile?: typeof readPidfileAt;
671
+ isProcessAlive?: typeof isProcessAlive;
672
+ sleep?: (ms: number) => Promise<void>;
673
+ ensureTimeoutMs?: number;
674
+ ensurePollIntervalMs?: number;
675
+ };
676
+ declare function runService(): Promise<void>;
677
+ /**
678
+ * `slock-computer start [serverId]` CLI adapter. Thin wrapper over the
679
+ * StartService (`./services/start.ts`); see service file header for the
680
+ * locked shape (RFC v0.8 contract v4 §6 line 80 — typed events +
681
+ * AbortSignal pre/post-spawn boundary + closed-set § codes byte-identical).
682
+ *
683
+ * This adapter's responsibilities:
684
+ * 1. Map start.* events → info() lines byte-identical to the
685
+ * pre-extraction implementation.
686
+ * 2. Map ComputerServiceError → fail(code, message), preserving
687
+ * CliExit semantics for the test harness.
688
+ * 3. Render `formatReadySummary` (kept in adapter on purpose:
689
+ * user-visible text formatter, not service axis).
690
+ *
691
+ * The pre-extraction `RunStartDeps` shape is preserved so existing
692
+ * service.test.ts test cases keep working — we forward the deps
693
+ * straight into the service.
694
+ */
695
+ declare function runStart(opts?: {
696
+ foreground?: boolean;
697
+ serverId?: string | null;
698
+ serverLabel?: string | null;
699
+ }, deps?: RunStartDeps): Promise<void>;
700
+
701
+ type StartStatus = "running" | "already_running" | "spawned" | "aborted";
702
+ interface StartResult {
703
+ status: StartStatus;
704
+ /** Managed set for this start invocation (single id when serverId
705
+ * was set; full attached set otherwise). */
706
+ managedTargets: string[];
707
+ /** Total number of attached servers at the moment of start (includes
708
+ * managedTargets when start was global). */
709
+ attachedCount: number;
710
+ /** Map of serverId → daemon pid for daemons confirmed ready before
711
+ * return. Empty when status === "running" (foreground); on
712
+ * status === "aborted" reflects whatever was ready at the moment
713
+ * the abort was observed. */
714
+ ready: Map<string, number>;
715
+ /** Pid of the (newly spawned OR existing) service when known.
716
+ * null for status === "running" (the calling process IS the
717
+ * service in foreground mode). */
718
+ servicePid: number | null;
719
+ /** Path of the service's combined log (for adapter "Logs: …" line). */
720
+ serviceLogPath: string;
721
+ }
722
+ interface StartDeps {
723
+ /** Test seams (default to module-level real impls). Mirrors the
724
+ * pre-extraction `RunStartDeps` shape so existing service.test.ts
725
+ * test cases keep working through the adapter. */
726
+ spawnDetachedService?: typeof spawnDetachedService;
727
+ readPidfile?: typeof readPidfileAt;
728
+ isProcessAlive?: typeof isProcessAlive;
729
+ sleep?: (ms: number) => Promise<void>;
730
+ ensureTimeoutMs?: number;
731
+ ensurePollIntervalMs?: number;
732
+ /** Foreground service loop. Default: real `runService`. */
733
+ runService?: typeof runService;
734
+ }
735
+
736
+ type StopStatus = "not_running" | "stale_pidfile_cleared" | "stopped";
737
+ interface StopResult {
738
+ status: StopStatus;
739
+ /** Pid the service observed in the pidfile when status !== "not_running".
740
+ * null on the not_running path (no pid was read). */
741
+ pid: number | null;
742
+ /** Path of the service pidfile (so adapters can render hints
743
+ * without re-resolving paths). */
744
+ pidfilePath: string;
745
+ }
746
+ interface StopDeps {
747
+ /** Test seams — pre-extraction `RunStopDeps` shape preserved so
748
+ * existing service.test.ts cases keep byte-identical imports
749
+ * through the adapter. */
750
+ readPidfile?: typeof readPidfileAt;
751
+ isProcessAlive?: typeof isProcessAlive;
752
+ killService?: (pid: number) => void;
753
+ sleep?: (ms: number) => Promise<void>;
754
+ pollIntervalMs?: number;
755
+ timeoutMs?: number;
756
+ }
757
+
758
+ type DetachStatus = "detached";
759
+ type RevokeOutcome = "success" | "http_error" | "network_error";
760
+ interface DetachResult {
761
+ status: DetachStatus;
762
+ serverId: string;
763
+ serverLabel: string;
764
+ revokeOutcome: RevokeOutcome;
765
+ /** HTTP status code on http_error path; undefined otherwise. */
766
+ revokeHttpStatus?: number;
767
+ }
768
+
769
+ interface DoctorCheck {
770
+ name: string;
771
+ ok: boolean;
772
+ detail: string;
773
+ }
774
+
775
+ interface CleanupReport {
776
+ stalePidfiles: string[];
777
+ orphanProcesses: number[];
778
+ powerLossRecovered: string[];
779
+ tmpFilesCleared: string[];
780
+ staleLocks: string[];
781
+ /** Was any cleanup actually performed? false = clean baseline. */
782
+ anyAction: boolean;
783
+ }
784
+
785
+ interface CrashEntry {
786
+ at: string;
787
+ exitCode: number | null;
788
+ signal: string | null;
789
+ }
790
+ /**
791
+ * Read the recent crash history (within the crash-window). Returns []
792
+ * if file missing/corrupt.
793
+ */
794
+ declare function readCrashHistory(slockHome: string, serverId: string, nowMs?: number): Promise<CrashEntry[]>;
795
+
796
+ type Channel = "latest" | "alpha" | `pinned:${string}`;
797
+
798
+ declare function runAttach(opts: {
799
+ serverSlug: string;
800
+ serverUrl?: string;
801
+ name?: string;
802
+ start?: boolean;
803
+ foreground?: boolean;
804
+ orchestrated?: boolean;
805
+ }): Promise<void>;
806
+
489
807
  /**
490
808
  * Roster fetcher contract — `LegacyMachinesClient.list(serverSlug)`
491
809
  * satisfies this structurally. Defining it as an interface here lets
@@ -542,6 +860,117 @@ type ManualPathValidation = {
542
860
  };
543
861
  declare function validateManualMigratePath(inputPath: string, roster: LegacyMachineRosterEntry[]): Promise<ManualPathValidation>;
544
862
 
863
+ declare function runLogin(opts: {
864
+ serverUrl?: string;
865
+ orchestrated?: boolean;
866
+ }): Promise<void>;
867
+
868
+ /** Credential bridge mode — §5.11.4 closed enum, snake_case byte-exact. */
869
+ type CredentialBridgeMode = "legacy_key_argv" | "legacy_key_file" | "legacy_key_stdin" | "legacy_key_env" | "legacy_fingerprint_roster";
870
+ interface AdoptLegacyByFingerprintInput {
871
+ serverSlug: string;
872
+ serverUrl?: string;
873
+ name?: string;
874
+ /** Server roster identity selected after user login. */
875
+ legacyMachineId: string;
876
+ /** sha256(legacy api key).slice(0,16), intersected with local owner.json. */
877
+ apiKeyFingerprint: string;
878
+ /** Absolute local owner.json path from detection/validation. */
879
+ legacyOwnerPath: string;
880
+ }
881
+ interface LegacyStopResult {
882
+ attempted: boolean;
883
+ pid?: number;
884
+ outcome: "absent" | "already_dead" | "stopped" | "timed_out" | "denied" | "error";
885
+ reason?: string;
886
+ }
887
+ interface AdoptLegacyResult {
888
+ serverId: string;
889
+ serverMachineId: string;
890
+ /** Legacy machine row id — emitted by the server on success so the
891
+ * attachment.json can carry `adoptedFromLegacy: true` / `legacyMachineId`. */
892
+ legacyMachineId: string;
893
+ serverSlug: string;
894
+ serverUrl: string;
895
+ attachmentPath: string;
896
+ resumed: boolean;
897
+ /** First 8 characters of the FRESHLY-ISSUED sk_computer_*; mirrors
898
+ * attachment.json/adoption.log convention. The raw fresh key lives ONLY
899
+ * in attachment.json (mode 0o600) — NEVER on this event/result. */
900
+ apiKeyRedactedPrefix: string;
901
+ legacyStop: LegacyStopResult;
902
+ }
903
+ type AdoptLegacyEvent = {
904
+ type: "adopting";
905
+ serverSlug: string;
906
+ mode: CredentialBridgeMode;
907
+ } | {
908
+ type: "preflight";
909
+ resumed: boolean;
910
+ } | {
911
+ type: "adopted";
912
+ serverId: string;
913
+ serverMachineId: string;
914
+ legacyMachineId: string;
915
+ serverSlug: string;
916
+ attachmentPath: string;
917
+ resumed: boolean;
918
+ apiKeyRedactedPrefix: string;
919
+ legacyStop: LegacyStopResult;
920
+ };
921
+ interface AdoptLegacyOptions {
922
+ signal?: AbortSignal;
923
+ onEvent?: (event: AdoptLegacyEvent) => void;
924
+ }
925
+ declare function adoptLegacyByFingerprint(input: AdoptLegacyByFingerprintInput, options?: AdoptLegacyOptions): Promise<AdoptLegacyResult>;
926
+
927
+ interface SetupOptions {
928
+ serverSlug: string;
929
+ serverUrl?: string;
930
+ name?: string;
931
+ start?: boolean;
932
+ foreground?: boolean;
933
+ yes?: boolean;
934
+ /**
935
+ * RFC v9.9 §X.4 manual escape hatch. When set, bypasses the picker
936
+ * and runs the three-gate validator
937
+ * (`MIGRATE_FROM_NOT_FOUND` / `MIGRATE_FROM_INVALID` /
938
+ * `MIGRATE_FROM_NOT_OWNED`) against the supplied path. Hard-fails on
939
+ * any failure — the operator chose this path explicitly.
940
+ */
941
+ migrateFrom?: string;
942
+ }
943
+ interface SetupDeps {
944
+ isTty?: boolean;
945
+ runLogin?: typeof runLogin;
946
+ runAttach?: typeof runAttach;
947
+ runStart?: typeof runStart;
948
+ refreshUserSession?: typeof refreshUserSession;
949
+ detectLegacyMigration?: typeof detectLegacyMigration;
950
+ validateManualMigratePath?: typeof validateManualMigratePath;
951
+ /**
952
+ * Roster client factory — defaults to `new LegacyMachinesClient(...)`.
953
+ * Tests stub this to feed a fake roster without booting undici.
954
+ */
955
+ buildRosterClient?: (baseUrl: string, accessToken: string) => {
956
+ list: LegacyMachinesClient["list"];
957
+ };
958
+ /**
959
+ * Picker prompt — returns the operator's selection. Defaults to a
960
+ * stdin reader. Each return value drives a distinct branch:
961
+ * - `{ kind: "candidate"; index: number }` — adopt picker entry 1..N
962
+ * - `{ kind: "fresh" }` — `0` / `<Enter>` / EOF / unrecognized
963
+ * - `{ kind: "manual"; path: string }` — `m`, then path on next line
964
+ */
965
+ pickMigrationCandidate?: (candidates: LegacyMachineCandidate[]) => Promise<PickerSelection>;
966
+ /**
967
+ * Roster-fingerprint adoption service injection — defaults to
968
+ * `adoptLegacyByFingerprintService`. Tests stub this to verify picker
969
+ * wiring without hitting the network. Normal setup never reads a raw
970
+ * legacy key; the selected candidate's roster identity is the authority.
971
+ */
972
+ adoptLegacyByFingerprint?: typeof adoptLegacyByFingerprint;
973
+ }
545
974
  type PickerSelection = {
546
975
  kind: "candidate";
547
976
  index: number;
@@ -561,6 +990,7 @@ type PickerSelection = {
561
990
  */
562
991
  declare const MIGRATION_FRESH_TRIGGERS: readonly ["empty-intersection", "explicit-zero", "eof", "non-tty", "server-unavailable"];
563
992
  type MigrationFreshTrigger = (typeof MIGRATION_FRESH_TRIGGERS)[number];
993
+ declare function refreshUserSession(slockHome: string, serverUrl?: string): Promise<boolean>;
564
994
  /**
565
995
  * Pure picker grammar — extracted so it can be exercised by unit tests
566
996
  * without stubbing global stdin/stdout. RFC v9.9 §X.4 (post-Jianwei FAIL
@@ -580,31 +1010,132 @@ declare function pickMigrationCandidateFromInput(candidates: LegacyMachineCandid
580
1010
  eof: boolean;
581
1011
  }>, write: (s: string) => void): Promise<PickerSelection>;
582
1012
 
1013
+ /** Public CDN root for published Computer SEA binaries (prod default). */
1014
+ declare const DEFAULT_UPGRADE_BASE_URL = "https://cdn.slock.ai/computer";
1015
+ interface SeaStageResult {
1016
+ /** Absolute path to the downloaded + sha256-verified staged binary. */
1017
+ readonly stagedBinaryPath: string;
1018
+ readonly version: string;
1019
+ /** Verified sha256 (hex) of the staged binary — matches the manifest. */
1020
+ readonly sha256: string;
1021
+ }
1022
+ interface SeaStageDeps {
1023
+ readonly fetchFn?: typeof fetch;
1024
+ readonly baseUrl?: string;
1025
+ /** The `computer:upgrade` requestId, echoed onto emitted download-progress frames. */
1026
+ readonly requestId?: string;
1027
+ /** Optional sink for throttled byte-% progress during the `downloading` phase. */
1028
+ readonly onProgress?: (e: SeaUpgradeProgressEvent) => void;
1029
+ }
583
1030
  /**
584
- * Read the Computer-level aggregate status from `installRoot`. Identical
585
- * shape to `buildStatusReport(installRoot)` exposed under the
586
- * wire-aligned name `readServiceStatus` for IPC `service-status` parity.
1031
+ * SEA Phase 1 — download the target-version binary for this platform into
1032
+ * `stagingDir` and verify its sha256 against the published manifest. Throws on
1033
+ * any failure (no manifest, no target for this platform, download error, sha
1034
+ * mismatch) so the caller aborts BEFORE the swap — nothing on disk is touched
1035
+ * outside `stagingDir`.
587
1036
  */
588
- declare function readServiceStatus(installRoot: string): Promise<ServiceStatusResult>;
1037
+ declare function stageSeaPhase(stagingDir: string, version: string, platform: NodeJS.Platform, arch: string, deps?: SeaStageDeps): Promise<SeaStageResult>;
1038
+ interface SeaSwapResult {
1039
+ /** Where the old binary was moved to (rollback restores from here). */
1040
+ readonly prevBinaryPath: string;
1041
+ /** The live executable path (unchanged identity; new bytes). */
1042
+ readonly currentBinaryPath: string;
1043
+ }
589
1044
  /**
590
- * Read a single attached server's daemon state row plus the runners
591
- * currently running on it. Throws `StateReaderError("NOT_ATTACHED")` if
592
- * `serverId` is not in the attached set, or `"INVALID_ATTACHMENT"` if
593
- * the runner.state.json under that id is unreadable.
1045
+ * SEA Phase 4 atomic single-file swap. Moves the current executable to
1046
+ * `<exe>.prev` (kept until the new service passes health-check), then renames
1047
+ * the staged binary into the executable's path. A stale `.prev` from a prior
1048
+ * failed upgrade is cleared first. On failure mid-swap the caller rolls back
1049
+ * via {@link rollbackSeaSwap}.
594
1050
  */
595
- declare function readRunnerStatus(installRoot: string, serverId: string): Promise<RunnerStatusResult>;
1051
+ declare function swapSeaPhase(currentBinaryPath: string, stagedBinaryPath: string): Promise<SeaSwapResult>;
1052
+ interface ManualRollbackResult {
1053
+ /** The `<exe>.prev` that was restored over the live executable. */
1054
+ readonly restoredFrom: string;
1055
+ /** The live executable path (unchanged identity; restored bytes). */
1056
+ readonly currentBinaryPath: string;
1057
+ }
596
1058
  /**
597
- * List runners across attached servers. With no `serverId` filter,
598
- * returns one block per attached server (zero if no attachments). With
599
- * a `serverId` filter, returns exactly that server's block or throws
600
- * `StateReaderError("NOT_ATTACHED")`.
1059
+ * Operator-initiated `upgrade --rollback`: restore the `<exe>.prev` backup the
1060
+ * last successful swap left behind, putting the previous binary back at the
1061
+ * live executable path. Reuses {@link rollbackSeaSwap} (the same primitive the
1062
+ * supervisor's bad-binary watchdog uses) once the `.prev` is confirmed present.
601
1063
  *
602
- * Per-server discriminator preserves `unauthorized` / `error` outcomes
603
- * so consumers can pattern-match without losing context.
1064
+ * Throws `Error("UPGRADE_NO_ROLLBACK")` if no `.prev` exists (nothing to roll
1065
+ * back to) so the CLI can map it to the closed-set token. The supervisor
1066
+ * restarts the service on the restored binary (same as a normal swap).
604
1067
  */
605
- declare function listRunners(installRoot: string, opts?: {
606
- serverId?: string;
607
- }): Promise<ListRunnersResult>;
1068
+ declare function rollbackToPrev(currentBinaryPath: string): Promise<ManualRollbackResult>;
1069
+ type SeaUpgradePhase = "downloading" | "verifying" | "applying" | "restarting";
1070
+ interface SeaUpgradeProgressEvent {
1071
+ readonly requestId: string;
1072
+ readonly phase: SeaUpgradePhase;
1073
+ readonly message?: string;
1074
+ /** 0-100 byte-progress, present ONLY on `downloading`-phase progress frames. */
1075
+ readonly percent?: number;
1076
+ }
1077
+ interface RunSeaUpgradeOpts {
1078
+ readonly slockHome: string;
1079
+ readonly requestId: string;
1080
+ readonly fromVersion: string;
1081
+ readonly targetVersion: string;
1082
+ /** The live executable to swap — normally process.execPath. */
1083
+ readonly currentBinaryPath: string;
1084
+ /** ISO timestamp for the marker (injected for deterministic tests). */
1085
+ readonly nowIso: string;
1086
+ readonly platform?: NodeJS.Platform;
1087
+ readonly arch?: string;
1088
+ readonly deps?: {
1089
+ readonly baseUrl?: string;
1090
+ readonly fetchFn?: typeof fetch;
1091
+ readonly onProgress?: (e: SeaUpgradeProgressEvent) => void;
1092
+ readonly stageFn?: typeof stageSeaPhase;
1093
+ readonly swapFn?: typeof swapSeaPhase;
1094
+ readonly stagingDir?: string;
1095
+ };
1096
+ }
1097
+ interface RunSeaUpgradeResult {
1098
+ readonly ok: boolean;
1099
+ /** Which phase failed (absent on success). */
1100
+ readonly failedPhase?: "stage" | "swap";
1101
+ readonly reason?: string;
1102
+ /** Present on success — the swap, so the caller / watchdog can roll back via `.prev`. */
1103
+ readonly swap?: SeaSwapResult;
1104
+ }
1105
+ declare function runSeaUpgrade(opts: RunSeaUpgradeOpts): Promise<RunSeaUpgradeResult>;
1106
+ /** Resolve the CDN's always-latest version from the top-level manifest
1107
+ * (`<base>/manifest.json` → `{ version }`). This is the SAME pointer
1108
+ * `install.sh` consumes, so `install` and `upgrade` share one source of
1109
+ * "latest" — no npm registry round-trip (npm dist-tags went stale at the SEA
1110
+ * cutover; the binary store is the CDN). Returns null on any failure (caller
1111
+ * maps to a resolve error). */
1112
+ declare function fetchCdnLatestVersion(baseUrl: string, fetchFn?: typeof fetch): Promise<string | null>;
1113
+ interface ResolveAndRunSeaUpgradeOpts {
1114
+ readonly slockHome: string;
1115
+ readonly requestId: string;
1116
+ readonly fromVersion: string;
1117
+ readonly currentBinaryPath: string;
1118
+ readonly nowIso: string;
1119
+ readonly platform?: NodeJS.Platform;
1120
+ readonly arch?: string;
1121
+ readonly deps?: RunSeaUpgradeOpts["deps"] & {
1122
+ /** Override channel resolution (default: readChannel(slockHome)). */
1123
+ readonly readChannelFn?: (slockHome: string) => Promise<Channel>;
1124
+ /** Override the CDN latest-version fetch (default: read `<base>/manifest.json`). */
1125
+ readonly fetchLatestVersion?: (baseUrl: string) => Promise<string | null>;
1126
+ /** Override the full target-version resolver (bypasses channel resolution). */
1127
+ readonly resolveTargetVersion?: () => Promise<string | null>;
1128
+ readonly runSeaUpgradeFn?: typeof runSeaUpgrade;
1129
+ };
1130
+ }
1131
+ interface ResolveAndRunSeaUpgradeResult extends Omit<RunSeaUpgradeResult, "failedPhase"> {
1132
+ readonly targetVersion?: string;
1133
+ /** True when already at the resolved target (no-op, no swap, no restart). */
1134
+ readonly alreadyCurrent?: boolean;
1135
+ /** Extends RunSeaUpgradeResult's failedPhase with the resolution step. */
1136
+ readonly failedPhase?: "resolve" | "stage" | "swap";
1137
+ }
1138
+ declare function resolveAndRunSeaUpgrade(opts: ResolveAndRunSeaUpgradeOpts): Promise<ResolveAndRunSeaUpgradeResult>;
608
1139
 
609
1140
  type UpgradeTrigger = "cli" | "web" | "tray";
610
1141
  /**
@@ -681,4 +1212,281 @@ interface UpgradeLogEntryErr {
681
1212
  */
682
1213
  declare function assertUpgradeLogEntry(entry: UpgradeLogEntry): void;
683
1214
 
684
- export { type ComputerStatusReport, type ConnectService, type ConnectServiceOptions, type DaemonState, IPC_ERROR_CODES, type IpcErrorCode, type LegacyMachineCandidate, type LegacyMachineRosterClient, type LegacyMachineRosterEntry, type LegacyMachineRosterResult, LegacyMachinesClient, type ListRunnersResult, MIGRATION_DETECTION_KINDS, MIGRATION_FRESH_TRIGGERS, type ManualPathValidation, type MigrationDetection, type MigrationDetectionKind, type MigrationFreshTrigger, type PickerSelection, RUNNER_STATE_VALUES, type RequestMethodMap, type RequestOptions, type ResetRunnerResult, type ResetServiceResult, type RunnerListItem as RunnerInfo, type RunnerListPerServer, type RunnerState, type RunnerStatusResult, SERVICE_STATE_VALUES, STATE_READER_ERROR_CODES, type ServerHealth, type ServerStatusRow, ServersClient, type ServiceClient, ServiceClientError, type ServiceEvent, type ServiceState, type ServiceStatusResult, StateReaderError, type StateReaderErrorCode, UPGRADE_ERROR_CODES, type UpgradeBundle, type UpgradeErrorCode, type UpgradeLogEntry, type UpgradeLogEntryErr, type UpgradeLogEntryOk, type UpgradeStartResult, type UpgradeTrigger, type UserServerEntry, type UserServersResult, assertUpgradeLogEntry, detectLegacyMigration, isIpcErrorCode, isMigrationDetectionKind, isRunnerState, isServiceState, listRunners, pickMigrationCandidateFromInput, readRunnerStatus, readServiceStatus, validateManualMigratePath };
1215
+ interface UpgradeCliOptions {
1216
+ dryRun?: boolean;
1217
+ channel?: string;
1218
+ /**
1219
+ * Explicit target version (bypasses channel resolution). Surfaced on
1220
+ * the CLI as `--target-version`, NOT `--version` — Commander treats
1221
+ * `--version` as the root program version flag and silently shadows
1222
+ * any subcommand option of the same name.
1223
+ */
1224
+ targetVersion?: string;
1225
+ /**
1226
+ * Restore the `<exe>.prev` backup the last successful swap left behind,
1227
+ * putting the previous binary back at the live executable path. Mutually
1228
+ * exclusive with --target-version / --channel / --dry-run (it resolves /
1229
+ * downloads nothing). Fails with `UPGRADE_NO_ROLLBACK` when no `.prev`
1230
+ * exists. SEA-only (same gate as a normal upgrade).
1231
+ */
1232
+ rollback?: boolean;
1233
+ /**
1234
+ * §4.4 (4) trigger source attribution for upgrade.log audit trail.
1235
+ * Defaults to "cli" when invoked through the bare CLI entry point.
1236
+ */
1237
+ trigger?: UpgradeTrigger;
1238
+ }
1239
+ /**
1240
+ * Pure upgrade core (CLI-over-lib convergence). Validates inputs, resolves
1241
+ * channel + target version, and dispatches to the SEA in-process upgrade. It
1242
+ * NEVER writes to stdout directly: human lines go through the `emit` sink the
1243
+ * presenter supplies, and the UPGRADE / CHANNEL_INVALID / UPGRADE_SEA_ONLY fail
1244
+ * points throw `ComputerError` (the presenter maps them to the
1245
+ * `{ok:false,code,message}` fail-shape). The dry-run / channel / target-version
1246
+ * / rollback flag handling + the SEA-only gate are byte-identical to the
1247
+ * pre-convergence `runUpgradeCli` body. (The IPC-routing + the
1248
+ * UPGRADE_ALREADY_RUNNING mutation-lock remap stay in the index.ts presenter.)
1249
+ */
1250
+ declare function upgradeCliCore(slockHome: string, opts: UpgradeCliOptions, onEvent: (event: ComputerApiEvent) => void, deps?: {
1251
+ /** Override the CDN latest-version fetch (default: read `<base>/manifest.json`). */
1252
+ fetchLatestVersion?: (baseUrl: string) => Promise<string | null>;
1253
+ /** Override the CDN base-url resolver (default: `resolveUpgradeBaseUrl()`). */
1254
+ resolveUpgradeBaseUrlFn?: () => string;
1255
+ currentVersion?: () => Promise<string>;
1256
+ /** The live executable to swap (default: `process.execPath`). */
1257
+ currentBinaryPath?: () => string;
1258
+ /** Override the SEA upgrade runner (tests stub this). */
1259
+ resolveAndRunSeaUpgradeFn?: typeof resolveAndRunSeaUpgrade;
1260
+ /** Override the SEA stage primitive (dry-run download+verify). */
1261
+ stageSeaPhaseFn?: typeof stageSeaPhase;
1262
+ /** Override the rollback primitive (restore `<exe>.prev`). Tests stub this. */
1263
+ rollbackToPrevFn?: typeof rollbackToPrev;
1264
+ /**
1265
+ * Test seam for the SEA-only gate. Production uses `isSeaBinary()`.
1266
+ * Tests stub it true so the SEA flow is exercised without packaging a
1267
+ * real single-binary, or false to exercise the migration refusal.
1268
+ */
1269
+ isSeaBinary?: () => boolean;
1270
+ }): Promise<void>;
1271
+
1272
+ /**
1273
+ * Pure result of `doctor` (`slock-computer doctor [serverSlug] [--fix]`). The
1274
+ * presenter formats it (with `redactSecrets` on every emitted line) and sets
1275
+ * the process exit code from `allOk`.
1276
+ */
1277
+ interface DoctorReport {
1278
+ checks: DoctorCheck[];
1279
+ allOk: boolean;
1280
+ cleanup: CleanupReport | null;
1281
+ crashes: Awaited<ReturnType<typeof readCrashHistory>>;
1282
+ serverId?: string;
1283
+ serverLabel?: string;
1284
+ }
1285
+ interface RunnersListResult {
1286
+ serverSlug: string | null;
1287
+ serverId: string;
1288
+ runners: RunnerListItem[];
1289
+ }
1290
+ interface RunnersStopResult {
1291
+ serverSlug: string | null;
1292
+ serverId: string;
1293
+ agentId: string;
1294
+ }
1295
+ interface LogoutResult {
1296
+ status: "logged-out" | "already-logged-out";
1297
+ sessionPath: string;
1298
+ }
1299
+ /**
1300
+ * The typed Computer library API. Each method is PURE (result | throw
1301
+ * `ComputerError`). `createComputerApi(slockHome)` binds an install root so the
1302
+ * methods take no ambient env reads of their own.
1303
+ */
1304
+ interface ComputerApi {
1305
+ /**
1306
+ * Build the Computer-level aggregate status report. Read-only and
1307
+ * secret-free (status.ts §3.3.1 redline).
1308
+ */
1309
+ getStatus(): Promise<ComputerStatusReport>;
1310
+ /**
1311
+ * Clear the service-level crash history and transition service state
1312
+ * `degraded → running` (§1.3). Routes through the running service via IPC
1313
+ * when one is up (single-writer), else applies the disk-only handler.
1314
+ */
1315
+ resetService(): Promise<ResetServiceResult>;
1316
+ /**
1317
+ * Clear a per-runner crash history and transition runner state
1318
+ * `degraded → running` (§2.4). Same single-writer routing as `resetService`.
1319
+ * Returns `{status:"not-found"}` (a RESULT, not a throw) when `serverId` is
1320
+ * not an attached server.
1321
+ */
1322
+ resetRunner(serverId: string): Promise<ResetRunnerResult>;
1323
+ /**
1324
+ * Device-code login (one user identity per Computer / SLOCK_HOME). Drives
1325
+ * the device-code flow; the presenter-supplied `onEvent` sink prints the
1326
+ * verification URL/code + waiting lines. The api itself never prints.
1327
+ */
1328
+ login(opts: {
1329
+ serverUrl?: string;
1330
+ }, onEvent?: (event: ComputerApiEvent) => void): Promise<LoginResult>;
1331
+ /** Clear the saved user session (idempotent). Pure: returns a result. */
1332
+ logout(): Promise<LogoutResult>;
1333
+ /**
1334
+ * Attach this Computer to one server (add-not-replace). `onEvent` prints
1335
+ * the attach progress lines presenter-side.
1336
+ */
1337
+ attach(opts: {
1338
+ serverSlug: string;
1339
+ serverUrl?: string;
1340
+ name?: string;
1341
+ }, onEvent?: (event: ComputerApiEvent) => void): Promise<AttachResult>;
1342
+ /** Remove ONE server's local attachment (per-server isolated). */
1343
+ detach(serverId: string, serverLabel: string, onEvent?: (event: ComputerApiEvent) => void): Promise<DetachResult>;
1344
+ /** Start/ensure the Computer service. `onEvent` formats the start lines. */
1345
+ start(opts: {
1346
+ foreground?: boolean;
1347
+ serverId?: string | null;
1348
+ serverLabel?: string | null;
1349
+ }, onEvent?: (event: ComputerApiEvent) => void, deps?: StartDeps): Promise<StartResult>;
1350
+ /** Stop the Computer service (idempotent). `onEvent` formats the stop lines. */
1351
+ stop(onEvent?: (event: ComputerApiEvent) => void, deps?: StopDeps): Promise<StopResult>;
1352
+ /**
1353
+ * Diagnose login + per-server attachments + preflight + service. Returns the
1354
+ * report (runs the cleanup pass when `cleanup` is true). Pure — the presenter
1355
+ * formats (with `redactSecrets`) and sets the exit code from `allOk`.
1356
+ */
1357
+ doctor(opts: {
1358
+ cleanup?: boolean;
1359
+ serverId?: string;
1360
+ serverLabel?: string;
1361
+ }): Promise<DoctorReport>;
1362
+ /**
1363
+ * Tail one server's runner log (or the service log). Returns the trailing
1364
+ * (already secret-redacted) lines; the presenter prints them. Throws
1365
+ * `NO_DAEMON_LOG` when the target log is absent.
1366
+ */
1367
+ logs(opts: {
1368
+ lines?: number;
1369
+ serverId?: string;
1370
+ service?: boolean;
1371
+ }): Promise<string[]>;
1372
+ /** List runners on one attached server. Throws RUNNERS_* on failure. */
1373
+ runnersList(serverId: string): Promise<RunnersListResult>;
1374
+ /** Stop a runner on one attached server. Throws the RUNNER/RUNNERS errors on failure. */
1375
+ runnersStop(agentId: string, attachment: {
1376
+ serverUrl: string;
1377
+ apiKey: string;
1378
+ serverSlug: string | null;
1379
+ serverId: string;
1380
+ }): Promise<RunnersStopResult>;
1381
+ /** Read the current release channel (default `latest` when unset). */
1382
+ channelShow(): Promise<Channel>;
1383
+ /** Validate + persist the release channel. Throws `CHANNEL_INVALID`. */
1384
+ channelSet(raw: string): Promise<Channel>;
1385
+ /**
1386
+ * Orchestrate `setup <serverSlug>`: login → (detect + picker / --migrate-from)
1387
+ * → attach → start. INTERACTIVE: `onEvent` is the event sink (the presenter
1388
+ * unwraps `log.line` events to `info`); the interactive picker is the
1389
+ * dep-injected `pickMigrationCandidate` prompt callback. The api drives the
1390
+ * flow and throws `ComputerError` for its closed-set fail points; it never
1391
+ * prints.
1392
+ */
1393
+ setup(opts: SetupOptions, deps: SetupDeps, onEvent: (event: ComputerApiEvent) => void): Promise<void>;
1394
+ /**
1395
+ * SEA self-upgrade (or `--rollback`). Validates inputs, resolves channel +
1396
+ * target version, runs the SEA download+verify+swap (or the dry-run / rollback
1397
+ * primitive). INTERACTIVE-ish: `onEvent` is the event sink (the presenter
1398
+ * unwraps `log.line` events to `info`); the SEA-only gate +
1399
+ * dry-run/channel/target-version/rollback flag handling live here, throwing
1400
+ * `ComputerError` for the UPGRADE_* closed set. This is the STANDALONE swap
1401
+ * primitive (cold-boot path); the live-service IPC route is
1402
+ * `tryUpgradeViaService`. The in-process mutation-lock + the
1403
+ * UPGRADE_ALREADY_RUNNING remap stay in the index.ts presenter (the lock
1404
+ * owner) — they wrap this standalone call.
1405
+ */
1406
+ upgrade(opts: UpgradeCliOptions, deps: Parameters<typeof upgradeCliCore>[3], onEvent: (event: ComputerApiEvent) => void): Promise<void>;
1407
+ /**
1408
+ * Route a PLAIN FORWARD upgrade through a live service via IPC
1409
+ * (`upgrade-start`): the service drives download/verify/swap + self-restart
1410
+ * in-process (single-writer). Returns `{routed:true}` after kicking it off
1411
+ * (the started / already-running line is emitted via `onEvent` as a
1412
+ * `log.line`); returns `{routed:false}` when NO service is live (cold boot)
1413
+ * so the caller runs the standalone `upgrade()` under its own mutation lock.
1414
+ * The cross-process upgrade race is guarded by the service's own
1415
+ * `inFlightUpgrade` + the `upgrade-start` already-running response — NOT a
1416
+ * caller-side lock. Only for plain forward upgrades; the presenter gates out
1417
+ * dry-run / channel-override / rollback (those are always standalone).
1418
+ */
1419
+ tryUpgradeViaService(targetVersion: string | undefined, onEvent?: (event: ComputerApiEvent) => void): Promise<{
1420
+ routed: boolean;
1421
+ }>;
1422
+ }
1423
+ /**
1424
+ * Construct a ComputerApi bound to an explicit install root. The single-writer
1425
+ * reset routing (`resetViaServiceOrDisk`) lives here so the api owns the
1426
+ * IPC-vs-disk decision: when a service is running, mutations route through it
1427
+ * via IPC so the supervisor's in-memory runner state stays consistent; when no
1428
+ * service is up (cold boot) the disk-only lib-pure handlers apply directly.
1429
+ *
1430
+ * The single-writer ops (reset / upgrade routing) are spanned through the
1431
+ * caller-injected `opts.tracer` (default `noopTracer`, so untraced callers /
1432
+ * tests are zero-side-effect). The CALLER owns `source` attribution — the CLI
1433
+ * injects a `computer.cli` trace-client, the menu-bar a `computer.menu-bar`
1434
+ * one — so the same lib code attributes correctly across both surfaces.
1435
+ */
1436
+ declare function createComputerApi(slockHome: string, opts?: {
1437
+ tracer?: Tracer;
1438
+ }): ComputerApi;
1439
+
1440
+ /**
1441
+ * Build the env-gated Computer trace client for a given emitting `source`
1442
+ * (CLI vs menu-bar). Both surfaces share this so their spans land in the same
1443
+ * `<computerDir>/traces/` sink with consistent gating. `SLOCK_COMPUTER_LOCAL_TRACE=0`
1444
+ * disables (default on, mirrors the daemon); any setup failure falls back to noop.
1445
+ */
1446
+ declare function createComputerTracer(slockHome: string, source: TraceClientSource): Tracer;
1447
+
1448
+ /**
1449
+ * Read the Computer-level aggregate status from `installRoot`. Identical
1450
+ * shape to `buildStatusReport(installRoot)` — exposed under the
1451
+ * wire-aligned name `readServiceStatus` for IPC `service-status` parity.
1452
+ */
1453
+ declare function readServiceStatus(installRoot: string): Promise<ServiceStatusResult>;
1454
+ /**
1455
+ * Read a single attached server's daemon state row plus the runners
1456
+ * currently running on it. Throws `StateReaderError("NOT_ATTACHED")` if
1457
+ * `serverId` is not in the attached set, or `"INVALID_ATTACHMENT"` if
1458
+ * the runner.state.json under that id is unreadable.
1459
+ */
1460
+ declare function readRunnerStatus(installRoot: string, serverId: string): Promise<RunnerStatusResult>;
1461
+ /**
1462
+ * List runners across attached servers. With no `serverId` filter,
1463
+ * returns one block per attached server (zero if no attachments). With
1464
+ * a `serverId` filter, returns exactly that server's block or throws
1465
+ * `StateReaderError("NOT_ATTACHED")`.
1466
+ *
1467
+ * Per-server discriminator preserves `unauthorized` / `error` outcomes
1468
+ * so consumers can pattern-match without losing context.
1469
+ */
1470
+ declare function listRunners(installRoot: string, opts?: {
1471
+ serverId?: string;
1472
+ }): Promise<ListRunnersResult>;
1473
+
1474
+ /**
1475
+ * Connect to the Computer service over the §4 IPC transport, perform the
1476
+ * versioned handshake (§4.3), and return a typed `ServiceClient`. Throws
1477
+ * `ServiceClientError` (member of `IPC_ERROR_CODES`) on any handshake or
1478
+ * connect-time failure.
1479
+ */
1480
+ declare function connectService(installRoot: string, options?: ConnectServiceOptions): Promise<ServiceClient>;
1481
+
1482
+ declare class ComputerServiceError extends Error {
1483
+ readonly code: string;
1484
+ readonly cause?: unknown;
1485
+ constructor(code: string, message: string, cause?: unknown);
1486
+ }
1487
+
1488
+ declare function userSessionPath(slockHome: string): string;
1489
+
1490
+ declare const COMPUTER_VERSION: string;
1491
+
1492
+ export { type AttachInput, type AttachResult, COMPUTER_VERSION, type ComputerApi, type ComputerApiEvent, ComputerError, ComputerServiceError, type ComputerStatusReport, type ConnectService, type ConnectServiceOptions, DEFAULT_UPGRADE_BASE_URL, type DaemonState, IPC_ERROR_CODES, type IpcErrorCode, type LegacyMachineCandidate, type LegacyMachineRosterClient, type LegacyMachineRosterEntry, type LegacyMachineRosterResult, LegacyMachinesClient, type ListRunnersResult, type LoginInput, type LoginResult, MIGRATION_DETECTION_KINDS, MIGRATION_FRESH_TRIGGERS, type ManualPathValidation, type MigrationDetection, type MigrationDetectionKind, type MigrationFreshTrigger, type PickerSelection, RUNNER_STATE_VALUES, type RequestMethodMap, type RequestOptions, type ResetRunnerResult, type ResetServiceResult, type RunnerListItem as RunnerInfo, type RunnerListPerServer, type RunnerState, type RunnerStatusResult, SERVICE_STATE_VALUES, STATE_READER_ERROR_CODES, type ServerHealth, type ServerStatusRow, ServersClient, type ServiceClient, ServiceClientError, type ServiceEvent, type ServiceState, type ServiceStatusResult, StateReaderError, type StateReaderErrorCode, UPGRADE_ERROR_CODES, type UpgradeBundle, type UpgradeErrorCode, type UpgradeLogEntry, type UpgradeLogEntryErr, type UpgradeLogEntryOk, type UpgradeStartResult, type UpgradeTrigger, type UserServerEntry, type UserServersResult, assertUpgradeLogEntry, connectService, createComputerApi, createComputerTracer, detectLegacyMigration, fetchCdnLatestVersion, isComputerError, isIpcErrorCode, isMigrationDetectionKind, isRunnerState, isServiceState, listRunners, pickMigrationCandidateFromInput, readRunnerStatus, readServiceStatus, userSessionPath, validateManualMigratePath };