@botiverse/raft-computer 0.0.61 → 0.0.62

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