@slock-ai/computer 0.0.16-alpha.0 → 0.0.17

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.
@@ -11,7 +11,7 @@ interface ServerStatusRow {
11
11
  serverMachineId: string;
12
12
  serverUrl: string;
13
13
  attachedAt: string | null;
14
- daemonLogPath: string;
14
+ serverRunnerLogPath: string;
15
15
  daemon: DaemonState;
16
16
  /** v6 §11 health enum surfaced in `status` table (PR-H §3.3). */
17
17
  health: ServerHealth;
@@ -28,6 +28,33 @@ interface ComputerStatusReport {
28
28
  servers: ServerStatusRow[];
29
29
  }
30
30
 
31
+ interface LegacyMachineRosterEntry {
32
+ daemonId: string;
33
+ apiKeyFingerprint: string;
34
+ machineName: string;
35
+ hostname: string | null;
36
+ lastSeenAt: string | null;
37
+ }
38
+ type LegacyMachineRosterResult = {
39
+ status: "success";
40
+ entries: LegacyMachineRosterEntry[];
41
+ } | {
42
+ status: "auth_required";
43
+ } | {
44
+ status: "not_authorized";
45
+ } | {
46
+ status: "disabled";
47
+ } | {
48
+ status: "error";
49
+ code: string;
50
+ };
51
+ declare class LegacyMachinesClient {
52
+ private readonly baseUrl;
53
+ private readonly accessToken;
54
+ constructor(baseUrl: string, accessToken: string);
55
+ private url;
56
+ list(serverSlug: string): Promise<LegacyMachineRosterResult>;
57
+ }
31
58
  interface RunnerListItem {
32
59
  agentId: string;
33
60
  name: string;
@@ -354,55 +381,66 @@ interface UpgradeStartResult {
354
381
  targetVersion: string;
355
382
  }
356
383
  /**
357
- * Identity record for a legacy `@slock-ai/daemon` machine candidate that
358
- * `detectLegacyMigration` discovered locally on this Computer install
359
- * root. The shape is identity-only — it never carries credentials.
384
+ * Identity record for a legacy `@slock-ai/daemon` machine candidate
385
+ * surfaced by `detectLegacyMigration`. Each candidate is the result of
386
+ * intersecting (a) local `<installRoot>/machines/machine-<fp>/owner.json`
387
+ * evidence with (b) the server's `GET /api/computer/legacy-machines`
388
+ * roster on `apiKeyFingerprint` for the logged-in user + target server.
389
+ * The shape is identity-only — it never carries credentials.
390
+ *
391
+ * `apiKeyFingerprint` is the v9.9 intersection key —
392
+ * `sha256(apiKey).slice(0,16)`. The same value is computed locally by
393
+ * the legacy daemon (`packages/daemon/src/machineLock.ts`) and stored
394
+ * server-side on `daemons.api_key_fingerprint` (handshake-backfilled).
395
+ * Stable as long as the legacy api key is unchanged.
360
396
  *
361
- * `machineId` is the legacy daemon's lock-id (`machine-<sha256(apiKey)
362
- * [0..15]>`). It is stable across legacy daemon restarts but changes if
363
- * the user rotates the api key. The id is what the user would see in
364
- * `<installRoot>/machines/` directory listings, and is what
365
- * `AdoptLegacyService.migrate()` uses to find the lock owner file
366
- * during migration.
397
+ * `daemonId` is the server's `daemons.id` UUID (display + post-migration
398
+ * identity). It is NOT a join key fingerprint is. Cody redline
399
+ * msg=ec68c27f: server daemonId / machineName are display-only.
367
400
  *
368
- * `machineName` is the host display name observed in the legacy
369
- * daemon's `daemon.lock/owner.json` (typically `os.hostname()` at
370
- * legacy daemon spawn time). Best-effort, may be undefined for stale
371
- * lock files.
401
+ * `localPath` is the absolute path of the local owner.json that
402
+ * supplied the fingerprint side of the intersection. Surfaced so the
403
+ * picker can show the operator which on-disk legacy install they are
404
+ * about to adopt, and so the manual `--migrate-from` flag's three-gate
405
+ * validator (path-readable / owner.json-parseable / fingerprint-in-roster)
406
+ * can return a candidate that points at the same path the operator
407
+ * passed.
372
408
  *
373
- * `serverUrl` is the API server the legacy daemon was attached to.
374
- * Used by the migrate prompt to surface "you are about to migrate
375
- * machine X attached to <server>" before the user accepts.
409
+ * `machineName` / `hostname` / `lastSeenAt` come from the server roster
410
+ * row display-only, used for picker presentation. May be empty for
411
+ * stale rows; the picker tolerates blanks.
376
412
  */
377
413
  interface LegacyMachineCandidate {
378
- machineId: string;
379
- machineName?: string;
380
- serverUrl?: string;
414
+ apiKeyFingerprint: string;
415
+ daemonId: string;
416
+ localPath: string;
417
+ machineName: string;
418
+ hostname?: string;
419
+ lastSeenAt?: string;
381
420
  }
382
421
  /**
383
- * §X migration detection result — the discriminated union returned by
384
- * `detectLegacyMigration(installRoot, loggedInUserId)`.
422
+ * §X.2 migration detection result — the discriminated union returned by
423
+ * `detectLegacyMigration(installRoot, target)`.
385
424
  *
386
- * Per RFC §X.2:
387
- * - `match`: exactly one local legacy daemon machine. The setup flow
388
- * SHOULD prompt the user to migrate before falling back to fresh
389
- * setup. Migration itself is delegated to `AdoptLegacyService.
390
- * migrate()` (preserved as internal helper per §X.6 — 11 typed
391
- * errors / 4 events / Result discriminator byte-identical).
392
- * - `no-match`: zero local legacy daemon machines. Fresh setup
393
- * proceeds without any migrate prompt.
394
- * - `ambiguous`: two or more local legacy daemon machines. Per §X.2
395
- * "no auto-listed-pick fallback" — fresh setup proceeds without
396
- * any migrate prompt; the user is expected to start the legacy
397
- * daemon they want migrated first to disambiguate.
425
+ * Per RFC v9.9 §X.2:
426
+ * - `candidates`: zero-or-more legacy daemons survived the
427
+ * local-owner-json server-roster intersection. Empty intersection
428
+ * is a valid `candidates` result with `candidates.length === 0` —
429
+ * the setup flow's fresh-attach trigger #1 (§X.4). One-or-more
430
+ * candidates setup picker prompts the operator (TTY) or falls
431
+ * through to fresh attach (non-TTY).
432
+ * - `server-unavailable`: roster fetch failed (network / auth /
433
+ * server gate off / 5xx). Setup falls through to fresh attach
434
+ * under the documented "best-effort detection" discipline a
435
+ * transiently unreachable server must NOT block fresh setup.
398
436
  *
399
- * Fingerprint discipline (best-effort non-authoritative — Hao
400
- * `msg=4d170194`): the candidate set is computed from local
401
- * `<installRoot>/machines/machine-*` directory state. It identifies
402
- * "this host has legacy daemon installs" it does NOT prove ownership.
403
- * Final adoption still walks the existing
404
- * `AdoptLegacyService.migrate()` credential validation path; a match
405
- * is a prompt-gate, not a trust-gate.
437
+ * Fingerprint discipline (Cody msg=ec68c27f redline):
438
+ * - `apiKeyFingerprint` is treated as a sensitive identity. It is
439
+ * scoped to this caller's authenticated request and never logged.
440
+ * - The intersection is the trust-gate: a candidate proves the
441
+ * server has a roster row keyed by the same fingerprint that
442
+ * appears in the local owner.json. Final adoption still walks
443
+ * `AdoptLegacyService.migrate()` credential validation.
406
444
  */
407
445
  /**
408
446
  * Closed set of `MigrationDetection.kind` discriminator values. Mirrors
@@ -412,48 +450,110 @@ interface LegacyMachineCandidate {
412
450
  * `as` cast (state-machine-modeling discipline — correlated/mutex
413
451
  * properties land as a closed-set state-enum).
414
452
  */
415
- declare const MIGRATION_DETECTION_KINDS: readonly ["match", "no-match", "ambiguous"];
453
+ declare const MIGRATION_DETECTION_KINDS: readonly ["candidates", "server-unavailable"];
416
454
  type MigrationDetectionKind = (typeof MIGRATION_DETECTION_KINDS)[number];
417
455
  /** Runtime narrow guard for `MigrationDetection.kind`. */
418
456
  declare function isMigrationDetectionKind(value: unknown): value is MigrationDetectionKind;
419
457
  type MigrationDetection = {
420
- kind: "match";
421
- machineId: string;
422
- machineName?: string;
423
- serverUrl?: string;
424
- } | {
425
- kind: "no-match";
426
- } | {
427
- kind: "ambiguous";
458
+ kind: "candidates";
428
459
  candidates: LegacyMachineCandidate[];
460
+ } | {
461
+ kind: "server-unavailable";
429
462
  };
430
463
 
431
464
  /**
432
- * Detect whether `installRoot` already contains legacy
433
- * `@slock-ai/daemon` machine state that could be migrated to a
434
- * Computer attachment under the logged-in user.
465
+ * Roster fetcher contract — `LegacyMachinesClient.list(serverSlug)`
466
+ * satisfies this structurally. Defining it as an interface here lets
467
+ * tests stub the network call without instantiating undici.
468
+ */
469
+ interface LegacyMachineRosterClient {
470
+ list(serverSlug: string): Promise<LegacyMachineRosterResult>;
471
+ }
472
+ /**
473
+ * Detect whether the logged-in user has a legacy `@slock-ai/daemon`
474
+ * machine on the target server that matches a local install on this
475
+ * host (intersection on `apiKeyFingerprint`).
476
+ *
477
+ * Returns:
478
+ * - `{ kind: "candidates"; candidates: LegacyMachineCandidate[] }` —
479
+ * zero-or-more mutually-known legacy daemons. Empty list is the
480
+ * §X.4 fresh-attach trigger #1; non-empty drives the picker.
481
+ * - `{ kind: "server-unavailable" }` — roster fetch failed
482
+ * transiently. Caller falls through to fresh attach.
435
483
  *
436
- * Returns a discriminated `MigrationDetection`:
437
- * - `match` (exactly one candidate) caller SHOULD prompt the user
438
- * to migrate before fresh setup (§X.2 step 3).
439
- * - `no-match` (zero candidates) — caller proceeds with fresh
440
- * setup, NO prompt (§X.2 step 5).
441
- * - `ambiguous` (two-or-more candidates) — caller proceeds with
442
- * fresh setup, NO prompt (§X.2 step 5; "no auto-listed-pick
443
- * fallback").
484
+ * Lib-pure: no env reads, no terminal IO, no `process.exit`. Local
485
+ * filesystem errors are swallowed; only the roster-fetch outcome can
486
+ * change the discriminator.
487
+ */
488
+ declare function detectLegacyMigration(installRoot: string, serverSlug: string, client: LegacyMachineRosterClient): Promise<MigrationDetection>;
489
+ /**
490
+ * Manual `--migrate-from <path>` validator the three-gate chain that
491
+ * RFC v9.9 §X.4 specifies for the manual escape hatch when the picker's
492
+ * intersection set is non-empty but the operator wants to migrate a
493
+ * different on-disk install.
444
494
  *
445
- * Lib-pure: no env reads, no terminal IO, no process.exit. Filesystem
446
- * errors are swallowed and treated as "no evidence" (per the doc-
447
- * block above).
495
+ * Gates (executed in order, hard-fail on first failure):
496
+ * 1. `MIGRATE_FROM_NOT_FOUND` `path` does not exist or is not a
497
+ * readable directory / file. Accepts either the `machines/
498
+ * machine-<fp>/` directory or its `daemon.lock/owner.json`
499
+ * directly; the latter is what doctor / forensic flows surface.
500
+ * 2. `MIGRATE_FROM_INVALID` — owner.json is unreadable, malformed,
501
+ * or missing a 16-hex-char `apiKeyFingerprint` field.
502
+ * 3. `MIGRATE_FROM_NOT_OWNED` — owner.json's fingerprint is not in
503
+ * the supplied roster (the logged-in user does not have a
504
+ * legacy daemon row with this fingerprint on the target server,
505
+ * or the row is already migrated / NULL-fingerprinted).
448
506
  *
449
- * @param installRoot The Slock home (`SLOCK_HOME`) to scan. Resolved
450
- * by the CLI wrapper via `resolveSlockHome`.
451
- * @param loggedInUserId Reserved for the server-roster intersection
452
- * (forward-compat per §X.2 step 1). v0 does not consume this value;
453
- * accept the typed argument so consumers of this function can
454
- * already pass it.
455
- */
456
- declare function detectLegacyMigration(installRoot: string, loggedInUserId: string): Promise<MigrationDetection>;
507
+ * On success returns the same `LegacyMachineCandidate` shape the
508
+ * picker emits, so the setup driver's adoption call is the same code
509
+ * path on both branches.
510
+ */
511
+ type ManualPathValidation = {
512
+ ok: true;
513
+ candidate: LegacyMachineCandidate;
514
+ } | {
515
+ ok: false;
516
+ code: "MIGRATE_FROM_NOT_FOUND" | "MIGRATE_FROM_INVALID" | "MIGRATE_FROM_NOT_OWNED";
517
+ };
518
+ declare function validateManualMigratePath(inputPath: string, roster: LegacyMachineRosterEntry[]): Promise<ManualPathValidation>;
519
+
520
+ type PickerSelection = {
521
+ kind: "candidate";
522
+ index: number;
523
+ } | {
524
+ kind: "fresh";
525
+ } | {
526
+ kind: "manual";
527
+ path: string;
528
+ };
529
+ /**
530
+ * Closed-set enumeration of the §X.4 fresh-attach triggers. Used in
531
+ * setup info() lines so QA can grep for "fresh trigger: <name>" and
532
+ * test mocks can assert that ONLY these inputs route to fresh attach
533
+ * (Cody NIT `msg=9102a817`). The picker function itself only emits
534
+ * `explicit-zero` / `eof`; the other three are decided upstream in
535
+ * `runSetup` before the picker ever fires.
536
+ */
537
+ declare const MIGRATION_FRESH_TRIGGERS: readonly ["empty-intersection", "explicit-zero", "eof", "non-tty", "server-unavailable"];
538
+ type MigrationFreshTrigger = (typeof MIGRATION_FRESH_TRIGGERS)[number];
539
+ /**
540
+ * Pure picker grammar — extracted so it can be exercised by unit tests
541
+ * without stubbing global stdin/stdout. RFC v9.9 §X.4 (post-Jianwei FAIL
542
+ * `msg=ecc2e57e` / Cody NIT `msg=9102a817`):
543
+ * - `1`..`N` / `<Enter>` → adopt that candidate (1-indexed; Enter picks
544
+ * the first candidate, since the operator already saw the
545
+ * intersection list).
546
+ * - `0` → fresh attach (Trigger #2: explicit-zero).
547
+ * - EOF (Ctrl-D / closed stdin) → fresh attach (Trigger #2: eof).
548
+ * - `m` / `M` → emits `manual` and prompts for path on the next line.
549
+ * - Anything else → reprompt. The picker NEVER silently falls through
550
+ * to fresh attach on unrecognized input — that was the B1/B4 contract
551
+ * drift that QA rejected.
552
+ */
553
+ declare function pickMigrationCandidateFromInput(candidates: LegacyMachineCandidate[], read: () => Promise<{
554
+ line: string;
555
+ eof: boolean;
556
+ }>, write: (s: string) => void): Promise<PickerSelection>;
457
557
 
458
558
  /**
459
559
  * Read the Computer-level aggregate status from `installRoot`. Identical
@@ -551,4 +651,4 @@ interface UpgradeLogEntryErr {
551
651
  */
552
652
  declare function assertUpgradeLogEntry(entry: UpgradeLogEntry): void;
553
653
 
554
- export { type ComputerStatusReport, type ConnectService, type ConnectServiceOptions, type DaemonState, IPC_ERROR_CODES, type IpcErrorCode, type LegacyMachineCandidate, type ListRunnersResult, MIGRATION_DETECTION_KINDS, type MigrationDetection, type MigrationDetectionKind, 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, 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, assertUpgradeLogEntry, detectLegacyMigration, isIpcErrorCode, isMigrationDetectionKind, isRunnerState, isServiceState, listRunners, readRunnerStatus, readServiceStatus };
654
+ 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, 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, assertUpgradeLogEntry, detectLegacyMigration, isIpcErrorCode, isMigrationDetectionKind, isRunnerState, isServiceState, listRunners, pickMigrationCandidateFromInput, readRunnerStatus, readServiceStatus, validateManualMigratePath };