@skrr-ai/cli 0.1.88 → 0.1.89

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,8 +1,9 @@
1
1
  import { BaseCommand } from '../../base-command';
2
2
  /**
3
3
  * `skrr machines computer-adoption` — the share of daemons still on the old
4
- * event families, which is the number retiring `browser:liveview:*` and
5
- * `agent:terminal:*` waits on (OSK-13578). Admin only on the server: it
4
+ * event families, the number retiring `browser:liveview:*` and
5
+ * `agent:terminal:*` waited on (OSK-13578). They are retired (OSK-13509), so it
6
+ * now counts who the retirement stranded. Admin only on the server: it
6
7
  * describes the fleet, not the caller.
7
8
  */
8
9
  export default class MachinesComputerAdoption extends BaseCommand {
@@ -5,12 +5,13 @@ const base_command_1 = require("../../base-command");
5
5
  const computer_adoption_1 = require("../../lib/computer-adoption");
6
6
  /**
7
7
  * `skrr machines computer-adoption` — the share of daemons still on the old
8
- * event families, which is the number retiring `browser:liveview:*` and
9
- * `agent:terminal:*` waits on (OSK-13578). Admin only on the server: it
8
+ * event families, the number retiring `browser:liveview:*` and
9
+ * `agent:terminal:*` waited on (OSK-13578). They are retired (OSK-13509), so it
10
+ * now counts who the retirement stranded. Admin only on the server: it
10
11
  * describes the fleet, not the caller.
11
12
  */
12
13
  class MachinesComputerAdoption extends base_command_1.BaseCommand {
13
- static description = 'Show how many daemons are still on the old browser live view / agent terminal event families instead of the computer protocol, and whether the retirement criterion is met (admin only)';
14
+ static description = 'Show how many daemons are still on the old browser live view / agent terminal event families (now retired) instead of the computer protocol, and the retirement criterion (admin only)';
14
15
  static examples = [
15
16
  '<%= config.bin %> machines computer-adoption',
16
17
  '<%= config.bin %> machines computer-adoption --json',
@@ -0,0 +1,25 @@
1
+ import { DedicatedLeaseCommand } from '../../../lib/dedicated-lease-command';
2
+ /**
3
+ * `skrr machines dedicated agent-browser` — do the agents on this machine get a
4
+ * browser? (OSK-13809)
5
+ *
6
+ * A new Dedicated Runtime gives its agents a browser by default; a machine
7
+ * created before that decision keeps what it had. This is the one switch for
8
+ * both directions, per machine: `--off` takes it away and it stays away across
9
+ * restarts, image updates and replacements; `--on` gives it to a machine that
10
+ * did not have it. Either change is written to the machine's audit record.
11
+ * With neither flag it prints the current setting.
12
+ */
13
+ export default class DedicatedMachinesAgentBrowser extends DedicatedLeaseCommand {
14
+ static description: string;
15
+ static examples: string[];
16
+ static args: {
17
+ lease: import("@oclif/core/lib/interfaces").Arg<string, Record<string, unknown>>;
18
+ };
19
+ static flags: {
20
+ on: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
21
+ off: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
22
+ json: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
23
+ };
24
+ run(): Promise<void>;
25
+ }
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const core_1 = require("@oclif/core");
4
+ const dedicated_lease_command_1 = require("../../../lib/dedicated-lease-command");
5
+ const dedicated_machines_1 = require("../../../lib/dedicated-machines");
6
+ /**
7
+ * `skrr machines dedicated agent-browser` — do the agents on this machine get a
8
+ * browser? (OSK-13809)
9
+ *
10
+ * A new Dedicated Runtime gives its agents a browser by default; a machine
11
+ * created before that decision keeps what it had. This is the one switch for
12
+ * both directions, per machine: `--off` takes it away and it stays away across
13
+ * restarts, image updates and replacements; `--on` gives it to a machine that
14
+ * did not have it. Either change is written to the machine's audit record.
15
+ * With neither flag it prints the current setting.
16
+ */
17
+ class DedicatedMachinesAgentBrowser extends dedicated_lease_command_1.DedicatedLeaseCommand {
18
+ static description = 'Show or change whether the agents on a Dedicated Runtime get a browser. On by default for new machines; --off turns it off for this machine and it stays off.';
19
+ static examples = [
20
+ '<%= config.bin %> machines dedicated agent-browser <lease-id>',
21
+ '<%= config.bin %> machines dedicated agent-browser <lease-id> --off',
22
+ '<%= config.bin %> machines dedicated agent-browser <lease-id> --on --json',
23
+ ];
24
+ static args = {
25
+ lease: core_1.Args.string({
26
+ description: dedicated_lease_command_1.DEDICATED_LEASE_ARG_DESCRIPTION,
27
+ required: true,
28
+ ignoreStdin: true,
29
+ }),
30
+ };
31
+ static flags = {
32
+ on: core_1.Flags.boolean({
33
+ description: "Give this machine's agents a browser. Recorded in the machine's audit record.",
34
+ exclusive: ['off'],
35
+ }),
36
+ off: core_1.Flags.boolean({
37
+ description: "Take the browser away from this machine's agents. Stays off until you turn it on. Recorded in the machine's audit record.",
38
+ exclusive: ['on'],
39
+ }),
40
+ json: core_1.Flags.boolean({ description: 'Output as JSON' }),
41
+ };
42
+ async run() {
43
+ this.requireAuth();
44
+ const { args, flags } = await this.parse(DedicatedMachinesAgentBrowser);
45
+ const leaseId = await this.resolveLeaseArg(args.lease);
46
+ const scope = this.scopeFrom(flags);
47
+ const wantsChange = flags.on || flags.off;
48
+ const response = await this.callDedicated(() => wantsChange
49
+ ? (0, dedicated_machines_1.setDedicatedRuntimeAgentBrowser)(leaseId, Boolean(flags.on), scope)
50
+ : (0, dedicated_machines_1.getDedicatedRuntimeAgentBrowser)(leaseId, scope));
51
+ if (flags.json) {
52
+ this.log(JSON.stringify(response, null, 2));
53
+ return;
54
+ }
55
+ this.log(`Agent browser on ${response.leaseId || leaseId}: ${(0, dedicated_machines_1.describeDedicatedAgentBrowser)(response, leaseId)}`);
56
+ if (wantsChange && response.changed === false) {
57
+ this.log(' Already set that way; nothing changed.');
58
+ }
59
+ else if (wantsChange && response.effectiveOnNextReconnect && response.offered === null) {
60
+ // The line above already names the restart when a daemon is connected.
61
+ this.log(` Applies when the machine's daemon next reconnects; \`skrr machines dedicated restart ${leaseId}\` makes that now.`);
62
+ }
63
+ }
64
+ }
65
+ exports.default = DedicatedMachinesAgentBrowser;
@@ -6,7 +6,7 @@ const daemon_installer_1 = require("../../../lib/daemon-installer");
6
6
  const dedicated_machines_1 = require("../../../lib/dedicated-machines");
7
7
  const harnesses_1 = require("../../../lib/harnesses");
8
8
  class DedicatedMachinesShow extends dedicated_lease_command_1.DedicatedLeaseCommand {
9
- static description = 'Show one Dedicated Runtime lease: state, size, workspace, spend against the cap, health, deletion clock and the engines its daemon registered';
9
+ static description = 'Show one Dedicated Runtime lease: state, size, workspace, spend against the cap, health, deletion clock, whether its agents get a browser, and the engines its daemon registered';
10
10
  static examples = [
11
11
  '<%= config.bin %> machines dedicated show <lease-id>',
12
12
  '<%= config.bin %> machines dedicated show <lease-id> --json',
@@ -47,6 +47,17 @@ class DedicatedMachinesShow extends dedicated_lease_command_1.DedicatedLeaseComm
47
47
  // which is what `skrrd update --check` on the machine reports (OSK-12153).
48
48
  // Best-effort: an unreachable feed is said, never read as "none newer".
49
49
  const latestDaemonRelease = lease.health?.image ? await readLatestDaemonRelease() : null;
50
+ // Whether the agents on this machine get a browser (OSK-13809). Its own
51
+ // route, because the setting lives on the machine's allow-list, not the
52
+ // lease. Best-effort like the reads above, and an unreadable answer is
53
+ // printed as unknown, never as off.
54
+ let agentBrowser;
55
+ try {
56
+ agentBrowser = await (0, dedicated_machines_1.getDedicatedRuntimeAgentBrowser)(leaseId, this.scopeFrom(flags));
57
+ }
58
+ catch (err) {
59
+ agentBrowser = { error: err?.message || 'unreadable' };
60
+ }
50
61
  if (flags.json) {
51
62
  const engines = (0, dedicated_machines_1.dedicatedLeaseHarnesses)(lease, harnesses).map((harness) => ({
52
63
  harnessId: harness._id,
@@ -57,11 +68,15 @@ class DedicatedMachinesShow extends dedicated_lease_command_1.DedicatedLeaseComm
57
68
  this.log(JSON.stringify({
58
69
  ...response,
59
70
  harnesses: engines,
71
+ agentBrowser,
60
72
  ...(latestDaemonRelease ? { latestDaemonRelease } : {}),
61
73
  }, null, 2));
62
74
  return;
63
75
  }
64
- for (const line of (0, dedicated_machines_1.describeDedicatedLease)(lease, harnesses, { latestDaemonRelease })) {
76
+ for (const line of (0, dedicated_machines_1.describeDedicatedLease)(lease, harnesses, {
77
+ latestDaemonRelease,
78
+ agentBrowser,
79
+ })) {
65
80
  this.log(line);
66
81
  }
67
82
  }
@@ -45,6 +45,11 @@ export interface ComputerAdoption {
45
45
  oldFamilies: string[];
46
46
  daemonHalfMet: boolean | null;
47
47
  mobileHalfMet?: boolean | null;
48
+ /** Set once the families were retired; absent from a server before OSK-13509. */
49
+ retired?: {
50
+ on: string;
51
+ reason: string;
52
+ };
48
53
  };
49
54
  }
50
55
  /**
@@ -61,7 +61,12 @@ function computerAdoptionLines(read) {
61
61
  verdict = 'not measurable: no active daemons';
62
62
  else if (criterion.daemonHalfMet)
63
63
  verdict = 'MET';
64
- lines.push('', `Retirement of ${criterion.oldFamilies.join(', ')}`, ` daemon half (< ${(criterion.maxLegacyOnlyShare * 100).toFixed(0)}% of daemons active in ${criterion.windowDays} days on the old families only): ${verdict}`, mobileHalfLine(read));
64
+ lines.push('', `Retirement of ${criterion.oldFamilies.join(', ')}`);
65
+ if (criterion.retired) {
66
+ // The families are gone: the counts above are who it stranded, not who is still served.
67
+ lines.push(` RETIRED ${criterion.retired.on}: ${criterion.retired.reason}`, ' old-families-only daemons above can reach no terminal or browser; each is told to update when it registers');
68
+ }
69
+ lines.push(` daemon half (< ${(criterion.maxLegacyOnlyShare * 100).toFixed(0)}% of daemons active in ${criterion.windowDays} days on the old families only): ${verdict}`, mobileHalfLine(read));
65
70
  lines.push(...clientLines(read));
66
71
  return lines;
67
72
  }
@@ -1,4 +1,5 @@
1
1
  import { Command } from '@oclif/core';
2
+ import { type RuntimeBinaryOutput } from './exec-runtime-binary';
2
3
  /**
3
4
  * The bridge for `skrr computer` — the machine-local consent for the
4
5
  * computer's new surfaces on a personal machine (design §18, D11;
@@ -10,4 +11,45 @@ import { Command } from '@oclif/core';
10
11
  * Keeping the CLI a thin forwarder preserves that boundary: `skrr` carries
11
12
  * the verb to the same binary `skrrd` runs it on.
12
13
  */
13
- export declare function forwardComputerConsent(command: Command, passthrough: readonly string[]): Promise<void>;
14
+ /** Bound on the `skrrd status --json` probe: it is a read, and must never stall the command. */
15
+ export declare const DAEMON_STATUS_PROBE_TIMEOUT_MS = 8000;
16
+ /** What `skrrd status --json` says about the daemon that would answer. */
17
+ export interface AnsweringDaemon {
18
+ /** The binary `skrr` forwards to. */
19
+ binary: string | null;
20
+ /** The on-disk binary's version (`statusCommandVersion`), when it said. */
21
+ binaryVersion: string | null;
22
+ /** The running daemon's version (`liveVersion`/`version`), when it said. */
23
+ runningVersion: string | null;
24
+ daemonId: string | null;
25
+ profile: string | null;
26
+ /** `update.state` — `source-build`, `update-available`, `restart-pending`, … */
27
+ updateState: string | null;
28
+ /** `update.commands`: the install-method-correct way to update THIS daemon. */
29
+ updateCommands: string[];
30
+ restartPending: boolean;
31
+ }
32
+ export interface ComputerConsentDeps {
33
+ supportsSubcommand: (subcommand: string) => Promise<boolean | null>;
34
+ exec: (args: string[]) => Promise<number>;
35
+ capture: (args: string[], opts?: {
36
+ timeoutMs?: number;
37
+ }) => Promise<RuntimeBinaryOutput>;
38
+ findBinary: () => string | null;
39
+ }
40
+ /**
41
+ * Read `skrrd status --json` for the facts a person needs to know WHICH daemon
42
+ * answered and how it updates. Null when the status cannot be read — the
43
+ * caller then says what it can without inventing the rest.
44
+ */
45
+ export declare function readAnsweringDaemon(passthrough?: readonly string[], deps?: ComputerConsentDeps): Promise<AnsweringDaemon | null>;
46
+ /** "skrrd 0.8.103 at /path (daemon_…, running 0.8.90)" — whatever of that is known. */
47
+ export declare function describeAnsweringDaemon(daemon: AnsweringDaemon | null, binary: string | null): string;
48
+ /**
49
+ * What to tell a person whose local daemon has no `computer` verb: which
50
+ * daemon answered, and the update path for THAT install (OSK-13822). A source
51
+ * build updates by rebuilding its checkout — `skrrd status --json` already
52
+ * carries that recipe under `update.commands` — not by reinstalling.
53
+ */
54
+ export declare function computerUnsupportedMessage(daemon: AnsweringDaemon | null, binary: string | null): string;
55
+ export declare function forwardComputerConsent(command: Command, passthrough: readonly string[], deps?: ComputerConsentDeps): Promise<void>;
@@ -1,5 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DAEMON_STATUS_PROBE_TIMEOUT_MS = void 0;
4
+ exports.readAnsweringDaemon = readAnsweringDaemon;
5
+ exports.describeAnsweringDaemon = describeAnsweringDaemon;
6
+ exports.computerUnsupportedMessage = computerUnsupportedMessage;
3
7
  exports.forwardComputerConsent = forwardComputerConsent;
4
8
  const exec_runtime_binary_1 = require("./exec-runtime-binary");
5
9
  /**
@@ -13,15 +17,132 @@ const exec_runtime_binary_1 = require("./exec-runtime-binary");
13
17
  * Keeping the CLI a thin forwarder preserves that boundary: `skrr` carries
14
18
  * the verb to the same binary `skrrd` runs it on.
15
19
  */
16
- async function forwardComputerConsent(command, passthrough) {
20
+ /** Bound on the `skrrd status --json` probe: it is a read, and must never stall the command. */
21
+ exports.DAEMON_STATUS_PROBE_TIMEOUT_MS = 8_000;
22
+ const defaultDeps = {
23
+ supportsSubcommand: exec_runtime_binary_1.runtimeBinarySupportsSubcommand,
24
+ exec: (args) => (0, exec_runtime_binary_1.execRuntimeBinary)(args),
25
+ capture: (args, opts) => (0, exec_runtime_binary_1.execRuntimeBinaryCapture)(args, opts),
26
+ findBinary: exec_runtime_binary_1.findRuntimeBinary,
27
+ };
28
+ function text(value) {
29
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
30
+ }
31
+ /**
32
+ * Read `skrrd status --json` for the facts a person needs to know WHICH daemon
33
+ * answered and how it updates. Null when the status cannot be read — the
34
+ * caller then says what it can without inventing the rest.
35
+ */
36
+ async function readAnsweringDaemon(passthrough = [], deps = defaultDeps) {
37
+ let output;
38
+ try {
39
+ output = await deps.capture([...profileArgs(passthrough), 'status', '--json'], {
40
+ timeoutMs: exports.DAEMON_STATUS_PROBE_TIMEOUT_MS,
41
+ });
42
+ }
43
+ catch {
44
+ return null;
45
+ }
46
+ let status;
47
+ try {
48
+ status = JSON.parse(output.stdout);
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ if (!status || typeof status !== 'object')
54
+ return null;
55
+ const update = status.update && typeof status.update === 'object'
56
+ ? status.update
57
+ : {};
58
+ return {
59
+ binary: deps.findBinary(),
60
+ binaryVersion: text(status.statusCommandVersion),
61
+ runningVersion: text(status.liveVersion) ?? text(status.version),
62
+ daemonId: text(status.daemonId),
63
+ profile: text(status.profile),
64
+ updateState: text(update.state),
65
+ updateCommands: Array.isArray(update.commands)
66
+ ? update.commands.filter((c) => typeof c === 'string' && c.trim() !== '')
67
+ : [],
68
+ restartPending: update.restartPending === true,
69
+ };
70
+ }
71
+ /** `--profile <name>` from the pass-through, so the status probe asks the same daemon. */
72
+ function profileArgs(passthrough) {
73
+ for (let i = 0; i < passthrough.length; i += 1) {
74
+ const arg = passthrough[i];
75
+ if (arg === '--profile' && passthrough[i + 1])
76
+ return ['--profile', passthrough[i + 1]];
77
+ if (arg.startsWith('--profile='))
78
+ return ['--profile', arg.slice('--profile='.length)];
79
+ }
80
+ return [];
81
+ }
82
+ /** "skrrd 0.8.103 at /path (daemon_…, running 0.8.90)" — whatever of that is known. */
83
+ function describeAnsweringDaemon(daemon, binary) {
84
+ const where = daemon?.binary ?? binary;
85
+ const parts = [];
86
+ parts.push(`${where ?? 'skrrd'}${daemon?.binaryVersion ? ` (v${daemon.binaryVersion} on disk)` : ''}`);
87
+ if (daemon?.runningVersion && daemon.runningVersion !== daemon.binaryVersion) {
88
+ parts.push(`running daemon v${daemon.runningVersion}`);
89
+ }
90
+ else if (daemon?.runningVersion && !daemon.binaryVersion) {
91
+ parts.push(`v${daemon.runningVersion}`);
92
+ }
93
+ if (daemon?.daemonId)
94
+ parts.push(daemon.daemonId);
95
+ return parts.join(', ');
96
+ }
97
+ /**
98
+ * What to tell a person whose local daemon has no `computer` verb: which
99
+ * daemon answered, and the update path for THAT install (OSK-13822). A source
100
+ * build updates by rebuilding its checkout — `skrrd status --json` already
101
+ * carries that recipe under `update.commands` — not by reinstalling.
102
+ */
103
+ function computerUnsupportedMessage(daemon, binary) {
104
+ const lines = [
105
+ `The local daemon does not support \`skrr computer\` yet: ${describeAnsweringDaemon(daemon, binary)}.`,
106
+ ];
107
+ if (daemon?.updateState === 'source-build') {
108
+ const recipe = daemon.updateCommands.length
109
+ ? daemon.updateCommands
110
+ : ['cd <daemon-dir> && git pull && npm run build'];
111
+ lines.push('It runs from a source checkout, which updates by rebuilding:');
112
+ for (const command of recipe)
113
+ lines.push(` ${command}`);
114
+ lines.push('Then restart it:', ' skrr daemon restart');
115
+ }
116
+ else if (daemon?.restartPending) {
117
+ lines.push('A newer daemon is already on disk; the running one predates it. Restart it:', ' skrr daemon restart');
118
+ }
119
+ else if (daemon && daemon.updateCommands.length > 0) {
120
+ lines.push('Update it:');
121
+ for (const command of daemon.updateCommands)
122
+ lines.push(` ${command}`);
123
+ lines.push('Then restart it:', ' skrr daemon restart');
124
+ }
125
+ else {
126
+ lines.push('Update it (`skrr daemon update`, or reinstall it), restart it (`skrr daemon restart`), then retry.');
127
+ }
128
+ return lines.join('\n');
129
+ }
130
+ async function forwardComputerConsent(command, passthrough, deps = defaultDeps) {
17
131
  try {
18
132
  // Older daemons treated unknown subcommands as a daemon startup in some
19
133
  // releases. Probe the command list before forwarding so a consent verb
20
134
  // cannot turn into a surprising service mutation.
21
- if ((await (0, exec_runtime_binary_1.runtimeBinarySupportsSubcommand)('computer')) === false) {
22
- command.error('The local `skrrd` daemon does not support `skrr computer` yet. Update or reinstall the daemon, then retry.', { exit: 2 });
135
+ if ((await deps.supportsSubcommand('computer')) === false) {
136
+ const daemon = await readAnsweringDaemon(passthrough, deps);
137
+ command.error(computerUnsupportedMessage(daemon, deps.findBinary()), { exit: 2 });
138
+ }
139
+ if (passthrough.includes('status')) {
140
+ // Which daemon answered, before its answer — on stderr, so `--json`
141
+ // output stays exactly what the daemon printed.
142
+ const daemon = await readAnsweringDaemon(passthrough, deps);
143
+ command.logToStderr(`Answered by: ${describeAnsweringDaemon(daemon, deps.findBinary())}`);
23
144
  }
24
- command.exit(await (0, exec_runtime_binary_1.execRuntimeBinary)(['computer', ...passthrough]));
145
+ command.exit(await deps.exec(['computer', ...passthrough]));
25
146
  }
26
147
  catch (error) {
27
148
  if (error.message === 'runtime-binary-not-installed') {
@@ -358,6 +358,34 @@ export declare function restoreDeletedDedicatedRuntime(leaseId: string, options?
358
358
  * on the next reconciler pass.
359
359
  */
360
360
  export declare function setDedicatedRuntimeSpendingLimit(leaseId: string, spendingLimitCents: number | null, scope?: DedicatedRuntimeScope): Promise<DedicatedRuntimeResponse>;
361
+ /**
362
+ * Whether the agents working on a Dedicated Runtime get a browser (OSK-13809).
363
+ *
364
+ * `state` is the machine's setting; `offered` is whether its CONNECTED daemon
365
+ * offers the browser right now (null: nothing connected, or unreadable). The
366
+ * two differ after a change until the daemon reconnects, and on an image
367
+ * without Chrome, which never offers it.
368
+ */
369
+ export type DedicatedAgentBrowserState = {
370
+ leaseId: string;
371
+ state: 'on' | 'off' | 'pending_first_start';
372
+ enabled: boolean;
373
+ offered: boolean | null;
374
+ capability: string;
375
+ changed?: boolean;
376
+ effectiveOnNextReconnect?: boolean;
377
+ };
378
+ export declare function getDedicatedRuntimeAgentBrowser(leaseId: string, scope?: DedicatedRuntimeScope): Promise<DedicatedAgentBrowserState>;
379
+ /** Turn the agents' browser on or off. An actual change is in the machine's audit record. */
380
+ export declare function setDedicatedRuntimeAgentBrowser(leaseId: string, enabled: boolean, scope?: DedicatedRuntimeScope): Promise<DedicatedAgentBrowserState>;
381
+ /**
382
+ * One line for a person. Says the bad cases out loud: a setting that is on
383
+ * while the connected daemon does not offer it yet, and a setting we could
384
+ * not read at all.
385
+ */
386
+ export declare function describeDedicatedAgentBrowser(value: DedicatedAgentBrowserState | {
387
+ error: string;
388
+ } | null | undefined, leaseId: string): string;
361
389
  export declare function closeDedicatedRuntimeIncidentHold(leaseId: string, options?: {
362
390
  requestId?: string;
363
391
  } & DedicatedRuntimeScope): Promise<DedicatedRuntimeResponse>;
@@ -627,6 +655,9 @@ export declare function dedicatedSpendSummary(lease: DedicatedLease): string;
627
655
  export declare function describeDedicatedDeletion(deletion: DedicatedLeaseDeletion, leaseId?: string): string;
628
656
  export declare function describeDedicatedLease(lease: DedicatedLease, harnesses?: readonly Harness[], opts?: {
629
657
  latestDaemonRelease?: LatestDaemonRelease | null;
658
+ agentBrowser?: DedicatedAgentBrowserState | {
659
+ error: string;
660
+ } | null;
630
661
  }): string[];
631
662
  /** "2026-09-30 14:00 UTC (in 17 days)" — a deadline readable without converting it. */
632
663
  export declare function formatDedicatedDeadline(value: string | null | undefined, now?: Date): string;
@@ -17,6 +17,9 @@ exports.performDedicatedLeaseAction = performDedicatedLeaseAction;
17
17
  exports.growDedicatedRuntime = growDedicatedRuntime;
18
18
  exports.restoreDeletedDedicatedRuntime = restoreDeletedDedicatedRuntime;
19
19
  exports.setDedicatedRuntimeSpendingLimit = setDedicatedRuntimeSpendingLimit;
20
+ exports.getDedicatedRuntimeAgentBrowser = getDedicatedRuntimeAgentBrowser;
21
+ exports.setDedicatedRuntimeAgentBrowser = setDedicatedRuntimeAgentBrowser;
22
+ exports.describeDedicatedAgentBrowser = describeDedicatedAgentBrowser;
20
23
  exports.closeDedicatedRuntimeIncidentHold = closeDedicatedRuntimeIncidentHold;
21
24
  exports.destroyDedicatedRuntime = destroyDedicatedRuntime;
22
25
  exports.execDedicatedRuntime = execDedicatedRuntime;
@@ -368,6 +371,40 @@ async function restoreDeletedDedicatedRuntime(leaseId, options = {}) {
368
371
  async function setDedicatedRuntimeSpendingLimit(leaseId, spendingLimitCents, scope = {}) {
369
372
  return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/leases/${encodeURIComponent(leaseId)}/billing`, scope), { method: 'PATCH', body: { spendingLimitCents } });
370
373
  }
374
+ async function getDedicatedRuntimeAgentBrowser(leaseId, scope = {}) {
375
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/agent-browser`, scope));
376
+ }
377
+ /** Turn the agents' browser on or off. An actual change is in the machine's audit record. */
378
+ async function setDedicatedRuntimeAgentBrowser(leaseId, enabled, scope = {}) {
379
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/agent-browser`, scope), { method: 'PUT', body: { enabled } });
380
+ }
381
+ /**
382
+ * One line for a person. Says the bad cases out loud: a setting that is on
383
+ * while the connected daemon does not offer it yet, and a setting we could
384
+ * not read at all.
385
+ */
386
+ function describeDedicatedAgentBrowser(value, leaseId) {
387
+ if (!value)
388
+ return 'unknown';
389
+ if ('error' in value)
390
+ return `unknown (could not read: ${value.error})`;
391
+ if (value.state === 'pending_first_start') {
392
+ return value.enabled
393
+ ? 'on when the machine first starts (the default for new machines)'
394
+ : 'off when the machine first starts';
395
+ }
396
+ if (value.state === 'off') {
397
+ return value.offered
398
+ ? `off — sessions already running keep theirs until the daemon reconnects (\`skrr machines dedicated restart ${leaseId}\`)`
399
+ : `off — turn on with \`skrr machines dedicated agent-browser ${leaseId} --on\``;
400
+ }
401
+ if (value.offered === true)
402
+ return 'on — every agent session can open one; you can watch and take it over';
403
+ if (value.offered === false) {
404
+ return `on, but the connected daemon does not offer it yet: it applies when the daemon reconnects (\`skrr machines dedicated restart ${leaseId}\`), and an image without Chrome never offers it`;
405
+ }
406
+ return 'on (not connected now, so whether the machine offers it is unknown)';
407
+ }
371
408
  async function closeDedicatedRuntimeIncidentHold(leaseId, options = {}) {
372
409
  return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/incident-hold/close`, options), {
373
410
  method: 'POST',
@@ -813,6 +850,9 @@ function describeDedicatedLease(lease, harnesses = [], opts = {}) {
813
850
  if (checkpoint)
814
851
  lines.push(` ${''.padEnd(13)}${checkpoint.sentence}`);
815
852
  }
853
+ if (opts.agentBrowser !== undefined) {
854
+ row('agent browser', describeDedicatedAgentBrowser(opts.agentBrowser, dedicatedLeaseId(lease)));
855
+ }
816
856
  const engines = dedicatedLeaseHarnesses(lease, harnesses);
817
857
  if (engines.length > 0) {
818
858
  row('engines', engines.map((harness) => `${harness.provider} (${harness.status || 'unknown'})`).join(', '));
@@ -86,7 +86,9 @@ export type RuntimeBinaryOutput = {
86
86
  * post-condition probes, where output is evidence rather than user-facing
87
87
  * command output.
88
88
  */
89
- export declare function execRuntimeBinaryCapture(args: string[]): Promise<RuntimeBinaryOutput>;
89
+ export declare function execRuntimeBinaryCapture(args: string[], opts?: {
90
+ timeoutMs?: number;
91
+ }): Promise<RuntimeBinaryOutput>;
90
92
  /**
91
93
  * Check an installed daemon's top-level help before forwarding a command that
92
94
  * was added after older daemon releases shipped. A false result is useful to
@@ -251,7 +251,7 @@ function execRuntimeBinary(args, opts = {}) {
251
251
  * post-condition probes, where output is evidence rather than user-facing
252
252
  * command output.
253
253
  */
254
- function execRuntimeBinaryCapture(args) {
254
+ function execRuntimeBinaryCapture(args, opts = {}) {
255
255
  return new Promise((resolve, reject) => {
256
256
  const bin = findRuntimeBinary();
257
257
  if (!bin) {
@@ -279,8 +279,22 @@ function execRuntimeBinaryCapture(args) {
279
279
  child.stderr?.on('data', (chunk) => {
280
280
  stderr = append(stderr, chunk);
281
281
  });
282
- child.on('error', reject);
282
+ // A probe is a read: past its bound it is abandoned (and the child
283
+ // stopped) rather than allowed to stall the command that asked.
284
+ const timer = opts.timeoutMs && opts.timeoutMs > 0
285
+ ? setTimeout(() => {
286
+ child.kill('SIGTERM');
287
+ reject(new Error('runtime-binary-probe-timeout'));
288
+ }, opts.timeoutMs)
289
+ : null;
290
+ child.on('error', (error) => {
291
+ if (timer)
292
+ clearTimeout(timer);
293
+ reject(error);
294
+ });
283
295
  child.on('exit', (code, signal) => {
296
+ if (timer)
297
+ clearTimeout(timer);
284
298
  resolve({
285
299
  code: signal ? 128 + signalNumber(signal) : (code ?? 0),
286
300
  stdout,