@skrr-ai/cli 0.1.89 → 0.1.91

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/commands/machines/dedicated/exec.js +60 -14
  2. package/dist/commands/machines/dedicated/index.js +2 -0
  3. package/dist/commands/machines/dedicated/power/explain.d.ts +13 -0
  4. package/dist/commands/machines/dedicated/power/explain.js +36 -0
  5. package/dist/commands/machines/dedicated/power/index.d.ts +10 -0
  6. package/dist/commands/machines/dedicated/power/index.js +24 -0
  7. package/dist/commands/machines/dedicated/power/keep-awake.d.ts +20 -0
  8. package/dist/commands/machines/dedicated/power/keep-awake.js +69 -0
  9. package/dist/commands/machines/dedicated/power/set.d.ts +23 -0
  10. package/dist/commands/machines/dedicated/power/set.js +106 -0
  11. package/dist/commands/machines/dedicated/power/show.d.ts +13 -0
  12. package/dist/commands/machines/dedicated/power/show.js +36 -0
  13. package/dist/commands/tasks/start.d.ts +10 -0
  14. package/dist/commands/tasks/start.js +59 -0
  15. package/dist/lib/commitments.d.ts +1 -1
  16. package/dist/lib/commitments.js +4 -2
  17. package/dist/lib/dedicated-keep-awake.d.ts +19 -0
  18. package/dist/lib/dedicated-keep-awake.js +64 -0
  19. package/dist/lib/dedicated-power.d.ts +61 -0
  20. package/dist/lib/dedicated-power.js +243 -0
  21. package/dist/lib/format.d.ts +13 -0
  22. package/dist/lib/format.js +51 -0
  23. package/dist/lib/keychain.js +3 -0
  24. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/computerWire.d.ts +32 -0
  25. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/computerWire.js +27 -3
  26. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dedicatedRuntimeBusyWire.d.ts +107 -0
  27. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dedicatedRuntimeBusyWire.js +151 -0
  28. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dedicatedRuntimeWakeWire.d.ts +115 -0
  29. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dedicatedRuntimeWakeWire.js +117 -0
  30. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
  31. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +8 -3
  32. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.d.ts +31 -7
  33. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +149 -9
  34. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/computerWire.d.ts +32 -0
  35. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/computerWire.js +24 -0
  36. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dedicatedRuntimeBusyWire.d.ts +107 -0
  37. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dedicatedRuntimeBusyWire.js +147 -0
  38. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dedicatedRuntimeWakeWire.d.ts +115 -0
  39. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dedicatedRuntimeWakeWire.js +113 -0
  40. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
  41. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -1
  42. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.d.ts +31 -7
  43. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +144 -8
  44. package/dist/node_modules/@skrr-ai/auth-core/package.json +21 -1
  45. package/dist/node_modules/@skrr-ai/data-provider/index.js +6393 -6278
  46. package/oclif.manifest.json +33192 -32826
  47. package/package.json +1 -1
@@ -3,8 +3,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.HOSTED_RUNTIME_MACHINE_SOURCES = void 0;
6
+ exports.MACHINE_IDENTITY_FILENAME = exports.HOSTED_RUNTIME_MACHINE_SOURCES = void 0;
7
7
  exports.hostedRuntimeIdOverride = hostedRuntimeIdOverride;
8
+ exports.getHardwareMachineUuid = getHardwareMachineUuid;
9
+ exports.qualifyMachineUuid = qualifyMachineUuid;
10
+ exports.setMachineUuidProfile = setMachineUuidProfile;
11
+ exports.getMachineUuidProfile = getMachineUuidProfile;
8
12
  exports.getMachineUuid = getMachineUuid;
9
13
  exports.__resetMachineUuidCacheForTest = __resetMachineUuidCacheForTest;
10
14
  /**
@@ -34,7 +38,9 @@ exports.__resetMachineUuidCacheForTest = __resetMachineUuidCacheForTest;
34
38
  const node_crypto_1 = __importDefault(require("node:crypto"));
35
39
  const node_fs_1 = __importDefault(require("node:fs"));
36
40
  const node_os_1 = __importDefault(require("node:os"));
41
+ const node_path_1 = __importDefault(require("node:path"));
37
42
  const node_child_process_1 = require("node:child_process");
43
+ const configRoot_js_1 = require("./configRoot.js");
38
44
  /**
39
45
  * Daemon sources whose identity is ISSUED by the control plane rather than
40
46
  * derived from the host: the provisioner mints the daemon id and machine uuid
@@ -106,20 +112,18 @@ function readMachineUuidWindows() {
106
112
  }
107
113
  }
108
114
  /**
109
- * Return a stable, machine-scoped UUID. Cached for the process lifetime.
115
+ * The HARDWARE identity of this machine (IOPlatformUUID / machine-id /
116
+ * MachineGuid, or a control-plane-issued value on a hosted runtime). Cached for
117
+ * the process lifetime. Same machine -> same value across reboots.
110
118
  *
111
- * Callers should treat this as opaque: the format differs per platform
112
- * (macOS IOPlatformUUID, Linux systemd machine-id, Windows MachineGuid).
113
- * The guarantee is only: same machine -> same value across reboots, and the
114
- * same value from every binary that calls this function.
115
- *
116
- * On a hosted runtime the control-plane-issued `OVERSKY_MACHINE_UUID` wins.
119
+ * Almost nothing should call this: `getMachineUuid()` is what goes on the wire,
120
+ * and it equals this for the default profile.
117
121
  *
118
122
  * Falls back to a deterministic hostname+username hash if the platform
119
123
  * probe fails. This preserves server-side record continuity even on
120
124
  * locked-down machines that refuse ioreg / registry access.
121
125
  */
122
- function getMachineUuid() {
126
+ function getHardwareMachineUuid() {
123
127
  const hostedMachineUuid = hostedRuntimeIdOverride('OVERSKY_MACHINE_UUID');
124
128
  if (hostedMachineUuid)
125
129
  return hostedMachineUuid;
@@ -150,7 +154,143 @@ function getMachineUuid() {
150
154
  cachedMachineUuid = fallback;
151
155
  return fallback;
152
156
  }
157
+ // ---------------------------------------------------------------------------
158
+ // Profile-qualified machine identity (OSK-13845)
159
+ //
160
+ // The server keys a Daemon row on (user, machineUuid), pins ONE device key to
161
+ // that row, and groups Harness rows into one "machine" by it. A profile is the
162
+ // documented way to run a second, isolated daemon on the same computer, and
163
+ // while the machineUuid was hardware only a second profile landed on the FIRST
164
+ // profile's row: it replaced the machine page's Daemon and working directory,
165
+ // re-registered every harness under itself, and its interactive login re-pinned
166
+ // the row's device key (OSK-12063), so the default profile's next refresh was
167
+ // refused DEVICE_KEY_MISMATCH and every `skrr` call failed until a re-login.
168
+ //
169
+ // So a profile that STARTS NEW on a machine is qualified:
170
+ // `<hardware>:p-<12 hex of sha256(profile)>`. Two rules keep everything that
171
+ // exists where it is:
172
+ //
173
+ // - the default profile is NEVER qualified: its identity is the hardware value
174
+ // it has always had, so no existing machine row changes;
175
+ // - a non-default profile that already holds credentials (an
176
+ // `auth-binding.json` or a persisted daemon identity) keeps the hardware
177
+ // value. Its refresh tokens are bound to that row and the server refuses a
178
+ // refresh whose machineUuid differs from the stored one (H7), so qualifying
179
+ // it would sign it out. It stays a second daemon on the shared row.
180
+ //
181
+ // The choice is made ONCE, from that evidence, and written to the profile's
182
+ // `machine-identity.json`; afterwards it is read, never re-derived. Deriving it
183
+ // every time would flip a profile from qualified to shared the moment its own
184
+ // first login wrote `auth-binding.json`.
185
+ // ---------------------------------------------------------------------------
186
+ /** The profile's persisted machine-identity choice, beside its other state. */
187
+ exports.MACHINE_IDENTITY_FILENAME = 'machine-identity.json';
188
+ /** Files that prove a profile already holds credentials bound to the shared row. */
189
+ const PRE_EXISTING_PROFILE_EVIDENCE = ['auth-binding.json', 'daemon-identity.json'];
190
+ const DEFAULT_PROFILE_NAME = 'default';
191
+ /** The server caps `machineUuid` at 64 characters; 36 + 15 fits, and so does a 41-character fallback. */
192
+ function qualifyMachineUuid(hardware, profile) {
193
+ const digest = node_crypto_1.default
194
+ .createHash('sha256')
195
+ .update(`profile:${profile}`)
196
+ .digest('hex')
197
+ .slice(0, 12);
198
+ return `${hardware}:p-${digest}`;
199
+ }
200
+ let activeMachineProfile = null;
201
+ const profileChoiceCache = new Map();
202
+ /**
203
+ * Tell this module which profile the process runs as. Called wherever a binary
204
+ * resolves its profile (`setActiveProfile` in the daemon and in the CLI), so the
205
+ * identity cannot depend on call order. `null`, '' and the default profile all
206
+ * mean "the hardware identity".
207
+ */
208
+ function setMachineUuidProfile(profile) {
209
+ activeMachineProfile = typeof profile === 'string' && profile.trim() ? profile.trim() : null;
210
+ }
211
+ function getMachineUuidProfile() {
212
+ return activeMachineProfile;
213
+ }
214
+ function readPersistedChoice(file, hardware, profile) {
215
+ try {
216
+ const parsed = JSON.parse(node_fs_1.default.readFileSync(file, 'utf-8'));
217
+ // A file from another machine (a restored backup, a synced home) is absent.
218
+ if (parsed?.version !== 1 || parsed.hardware !== hardware)
219
+ return null;
220
+ // Only the two values this module can produce are accepted: a hand-edited or
221
+ // foreign string must never become an identity.
222
+ if (parsed.machineUuid === hardware)
223
+ return hardware;
224
+ const qualified = qualifyMachineUuid(hardware, profile);
225
+ return parsed.machineUuid === qualified ? qualified : null;
226
+ }
227
+ catch {
228
+ return null;
229
+ }
230
+ }
231
+ function persistChoice(dir, file, hardware, machineUuid, reason) {
232
+ try {
233
+ node_fs_1.default.mkdirSync(dir, { recursive: true, mode: 0o700 });
234
+ const tmp = `${file}.${process.pid}.${Date.now()}.tmp`;
235
+ const body = { version: 1, hardware, machineUuid, reason, decidedAt: new Date().toISOString() };
236
+ node_fs_1.default.writeFileSync(tmp, `${JSON.stringify(body)}\n`, { mode: 0o600 });
237
+ node_fs_1.default.renameSync(tmp, file);
238
+ }
239
+ catch {
240
+ // Not persisted: the in-process cache keeps this process consistent, and the
241
+ // next process decides again from the same evidence.
242
+ }
243
+ }
244
+ /** The identity a NON-default profile registers under. Never throws. */
245
+ function resolveProfileMachineUuid(profile, hardware) {
246
+ const cacheKey = `${hardware}\u0000${profile}`;
247
+ const cached = profileChoiceCache.get(cacheKey);
248
+ if (cached)
249
+ return cached;
250
+ const dir = node_path_1.default.join((0, configRoot_js_1.resolveConfigRoot)(), 'profiles', profile);
251
+ const file = node_path_1.default.join(dir, exports.MACHINE_IDENTITY_FILENAME);
252
+ let chosen = readPersistedChoice(file, hardware, profile);
253
+ if (!chosen) {
254
+ const preExisting = PRE_EXISTING_PROFILE_EVIDENCE.some((name) => {
255
+ try {
256
+ return node_fs_1.default.existsSync(node_path_1.default.join(dir, name));
257
+ }
258
+ catch {
259
+ return false;
260
+ }
261
+ });
262
+ chosen = preExisting ? hardware : qualifyMachineUuid(hardware, profile);
263
+ persistChoice(dir, file, hardware, chosen, preExisting ? 'pre_existing_profile' : 'new_profile');
264
+ }
265
+ profileChoiceCache.set(cacheKey, chosen);
266
+ return chosen;
267
+ }
268
+ /**
269
+ * Return the `machineUuid` this process sends to the server.
270
+ *
271
+ * Callers should treat this as opaque: the format differs per platform
272
+ * (macOS IOPlatformUUID, Linux systemd machine-id, Windows MachineGuid), and a
273
+ * non-default profile that started new carries a `:p-<hash>` suffix. The
274
+ * guarantee is only: same machine + same profile -> same value across reboots,
275
+ * and the same value from every binary of that profile that calls this function.
276
+ *
277
+ * Equals the hardware identity for the default profile, for a hosted runtime
278
+ * (the control-plane-issued `OVERSKY_MACHINE_UUID` wins) and for a non-default
279
+ * profile that already held credentials; see the block above.
280
+ */
281
+ function getMachineUuid() {
282
+ const hardware = getHardwareMachineUuid();
283
+ // A hosted runtime's identity is ISSUED and fenced by the control plane.
284
+ if (hostedRuntimeIdOverride('OVERSKY_MACHINE_UUID'))
285
+ return hardware;
286
+ const profile = activeMachineProfile;
287
+ if (!profile || profile === DEFAULT_PROFILE_NAME)
288
+ return hardware;
289
+ return resolveProfileMachineUuid(profile, hardware);
290
+ }
153
291
  /** @internal test seam — forget the cached value so the next call re-probes. */
154
292
  function __resetMachineUuidCacheForTest() {
155
293
  cachedMachineUuid = null;
294
+ activeMachineProfile = null;
295
+ profileChoiceCache.clear();
156
296
  }
@@ -309,6 +309,28 @@ export declare const COMPUTER_CAPABILITIES: {
309
309
  * before, and never sends the input.
310
310
  */
311
311
  readonly browserViewport: "computer_browser_viewport_v1";
312
+ /**
313
+ * A browser surface whose browser is still starting says so (OSK-13824): its
314
+ * descriptor carries `starting: { since }` from the moment its Chromium
315
+ * begins to launch until that launch ends (running, or failed), and the
316
+ * window shows "Starting the browser…" instead of subscribing into a
317
+ * refusal. A cold start on a fresh guest takes 25–30 s. A daemon announcing
318
+ * this states the ABSENCE of `starting` too: a browser surface without it
319
+ * is not starting. Without the announcement a client cannot tell, and falls
320
+ * back to reading a surface it saw open moments ago as starting.
321
+ */
322
+ readonly browserStarting: "computer_browser_starting_v1";
323
+ /**
324
+ * An agent actor on the machine's activity and control leases (OSK-13827)
325
+ * names the REAL agent: `agentId` is the agent's own id for tool-call and
326
+ * browser sessions too (v1 daemons put the SESSION id there for tool calls,
327
+ * so no reader could ever name them), and may carry `engine`, the harness
328
+ * the session runs on, so one agent's sessions on different engines can be
329
+ * told apart. A daemon announcing this states what it sends; the server
330
+ * withholds `engine` from every other daemon's actors, and a reader treats
331
+ * an actor without it as it always did.
332
+ */
333
+ readonly activityActorV2: "computer_activity_actor_v2";
312
334
  };
313
335
  export type ComputerCapabilityKey = keyof typeof COMPUTER_CAPABILITIES;
314
336
  export type ComputerCapability = (typeof COMPUTER_CAPABILITIES)[ComputerCapabilityKey];
@@ -355,7 +377,17 @@ export interface ComputerWireAgentActor {
355
377
  agentId: string;
356
378
  sessionId: string;
357
379
  label: string;
380
+ /**
381
+ * The engine the session runs on (`claude-code`, `codex`, …), as the daemon
382
+ * names its backend. Present only from a daemon that announced
383
+ * `computer_activity_actor_v2` (`COMPUTER_CAPABILITIES.activityActorV2`).
384
+ * A display distinction between one agent's sessions, never an identity:
385
+ * `sameComputerActor` still compares the agent id and the session id.
386
+ */
387
+ engine?: string;
358
388
  }
389
+ /** The longest `engine` the wire carries; a longer one is not an engine name. */
390
+ export declare const COMPUTER_ACTOR_ENGINE_MAX_LENGTH = 64;
359
391
  export interface ComputerWireHumanActor {
360
392
  kind: 'human';
361
393
  userId: string;
@@ -329,6 +329,28 @@ export const COMPUTER_CAPABILITIES = {
329
329
  * before, and never sends the input.
330
330
  */
331
331
  browserViewport: 'computer_browser_viewport_v1',
332
+ /**
333
+ * A browser surface whose browser is still starting says so (OSK-13824): its
334
+ * descriptor carries `starting: { since }` from the moment its Chromium
335
+ * begins to launch until that launch ends (running, or failed), and the
336
+ * window shows "Starting the browser…" instead of subscribing into a
337
+ * refusal. A cold start on a fresh guest takes 25–30 s. A daemon announcing
338
+ * this states the ABSENCE of `starting` too: a browser surface without it
339
+ * is not starting. Without the announcement a client cannot tell, and falls
340
+ * back to reading a surface it saw open moments ago as starting.
341
+ */
342
+ browserStarting: 'computer_browser_starting_v1',
343
+ /**
344
+ * An agent actor on the machine's activity and control leases (OSK-13827)
345
+ * names the REAL agent: `agentId` is the agent's own id for tool-call and
346
+ * browser sessions too (v1 daemons put the SESSION id there for tool calls,
347
+ * so no reader could ever name them), and may carry `engine`, the harness
348
+ * the session runs on, so one agent's sessions on different engines can be
349
+ * told apart. A daemon announcing this states what it sends; the server
350
+ * withholds `engine` from every other daemon's actors, and a reader treats
351
+ * an actor without it as it always did.
352
+ */
353
+ activityActorV2: 'computer_activity_actor_v2',
332
354
  };
333
355
  export const COMPUTER_CAPABILITY_VALUES = Object.values(COMPUTER_CAPABILITIES);
334
356
  // ---------------------------------------------------------------------------
@@ -432,6 +454,8 @@ export function isComputerPlatformRouteSession(sessionId) {
432
454
  return (typeof sessionId === 'string' &&
433
455
  COMPUTER_PLATFORM_ROUTE_SESSION_PREFIXES.some((prefix) => sessionId.startsWith(prefix)));
434
456
  }
457
+ /** The longest `engine` the wire carries; a longer one is not an engine name. */
458
+ export const COMPUTER_ACTOR_ENGINE_MAX_LENGTH = 64;
435
459
  /**
436
460
  * How a person present on the machine reached it: as its owner, or through a
437
461
  * grant at one of the two tiers. Agents carry no access level — an agent
@@ -0,0 +1,107 @@
1
+ /**
2
+ * The Dedicated Runtime busy-report wire contract (power-control task K2,
3
+ * `dedicated-runtime-power-and-demand-wake-2026-10-07.md` §7.3 / §13.5).
4
+ *
5
+ * Declared ONCE, here. The daemon is published and cannot import the API, so
6
+ * the capability name, the payload shape and its bounds live in this package,
7
+ * which every end can import (`@skrr-ai/auth-core/dedicated-runtime-busy-wire`).
8
+ * `dedicatedRuntimeBusyWire.test.ts` fails on a second spelling of the
9
+ * capability anywhere else in the repo.
10
+ *
11
+ * WHAT THE REPORT SAYS
12
+ * ---------------------------------------------------------------------------
13
+ * "Does anything run in the tenant workload slice?" — read by the guest daemon
14
+ * from cgroup v2 `skrr-workload.slice`, recursively (the slice root is empty by
15
+ * contract; processes live in leaves), with the root-owned sshd listener left
16
+ * out. It is a LOCAL read summarised into counts; no per-process list ever
17
+ * crosses the wire.
18
+ *
19
+ * FAIL CLOSED. `busy` is `true | false | 'unknown'`. Any failure to read — a
20
+ * missing slice, an unreadable file, a tree too large to walk, a quiesce in
21
+ * progress, an sshd that is not where the image promises — is `'unknown'`, and
22
+ * a reader MUST treat `'unknown'` as busy. `false` is only ever the daemon
23
+ * having positively read an empty slice.
24
+ *
25
+ * THE POLICY KNOB (OPEN PRODUCT QUESTION — not decided here)
26
+ * ---------------------------------------------------------------------------
27
+ * An idle tmux server or a shell prompt keeps a leaf populated forever. The
28
+ * owner decision is that tmux/SSH shells ARE protected, but whether an IDLE
29
+ * shell should pin the machine is undecided. So the report carries both
30
+ * readings and the server chooses:
31
+ *
32
+ * - `busy` the STRICT reading (`policy: 'strict'`): every
33
+ * process in the slice counts. This is the
34
+ * default and the only one a server should act
35
+ * on until the product question is answered.
36
+ * - `busyIgnoringIdleShells` the same read with the `idleShell` bucket
37
+ * removed. Classification is by process name
38
+ * (a shell or tmux with nothing else running),
39
+ * a HEURISTIC: it cannot see a shell loop, so
40
+ * it must never be the default.
41
+ *
42
+ * The `breakdown` says which bucket made it busy, for the "why" text.
43
+ */
44
+ export declare const DEDICATED_RUNTIME_BUSY_REPORT_CAPABILITY = "dedicated_runtime_busy_report_v1";
45
+ /** The heartbeat key the report rides under. Sent only by a daemon that announced the capability. */
46
+ export declare const DEDICATED_RUNTIME_BUSY_HEARTBEAT_KEY = "dedicatedRuntimeBusy";
47
+ export declare const DEDICATED_RUNTIME_BUSY_REPORT_VERSION = 1;
48
+ /**
49
+ * `strict`: every process in the slice (less the root sshd listener) counts.
50
+ * The only value v1 sends; the field exists so a later relaxation is a new
51
+ * value rather than a silent change of meaning.
52
+ */
53
+ export declare const DEDICATED_RUNTIME_BUSY_POLICIES: readonly ["strict"];
54
+ export type DedicatedRuntimeBusyPolicy = (typeof DEDICATED_RUNTIME_BUSY_POLICIES)[number];
55
+ /**
56
+ * Why the daemon could not give a definite answer, or `ok`. Anything but `ok`
57
+ * pairs with `busy: 'unknown'`.
58
+ */
59
+ export declare const DEDICATED_RUNTIME_BUSY_SLICE_HEALTH: readonly ["ok", "not_a_guest", "slice_missing", "slice_unreadable", "ssh_not_in_slice", "tree_too_large", "quiesced", "read_error"];
60
+ export type DedicatedRuntimeBusySliceHealth = (typeof DEDICATED_RUNTIME_BUSY_SLICE_HEALTH)[number];
61
+ export declare const DEDICATED_RUNTIME_BUSY_LIMITS: {
62
+ /** Service leaf names carried for the "busy because of service X" text. */
63
+ readonly maxServiceNames: 8;
64
+ readonly maxServiceNameLength: 48;
65
+ /** Cgroup leaves the daemon will visit; more is `tree_too_large` -> unknown. */
66
+ readonly maxLeaves: 64;
67
+ readonly maxDepth: 4;
68
+ /** Processes the daemon classifies per read; the rest count as workload. */
69
+ readonly maxClassifiedPids: 256;
70
+ /** Hard ceiling on the serialized report. */
71
+ readonly maxSerializedBytes: 1024;
72
+ };
73
+ export interface DedicatedRuntimeBusyBreakdown {
74
+ /** Processes in non-service, non-ssh leaves (the `default` leaf, the slice root) that are not idle-shell class. */
75
+ workload: number;
76
+ /** Processes in `skrr-svc-*.scope` leaves: a declared service running, always-on by design. */
77
+ service: number;
78
+ /** SSH connection plumbing in `ssh.service` that is not the root listener (non-root sshd, the session helper) and other non-shell processes there. */
79
+ ssh: number;
80
+ /** Shell or tmux processes in `default` / `ssh.service` (heuristic by name; see header). */
81
+ idleShell: number;
82
+ /** Root-owned sshd processes left out of every count above. */
83
+ excludedSshd: number;
84
+ /** True when more processes existed than were classified; the rest are in `workload`. */
85
+ truncated: boolean;
86
+ /** Up to `maxServiceNames` service leaf names that have processes. */
87
+ services: string[];
88
+ }
89
+ export interface DedicatedRuntimeBusyReport {
90
+ version: typeof DEDICATED_RUNTIME_BUSY_REPORT_VERSION;
91
+ policy: DedicatedRuntimeBusyPolicy;
92
+ /** Strict reading. `'unknown'` MUST be treated as busy. */
93
+ busy: boolean | 'unknown';
94
+ /** Same read with the `idleShell` bucket removed; a heuristic, not the default. */
95
+ busyIgnoringIdleShells: boolean | 'unknown';
96
+ breakdown: DedicatedRuntimeBusyBreakdown;
97
+ /** ISO time the daemon read the slice. */
98
+ readAt: string;
99
+ sliceHealth: DedicatedRuntimeBusySliceHealth;
100
+ }
101
+ /**
102
+ * The reader's view of a daemon's heartbeat section. Anything malformed, from
103
+ * a daemon that did not announce the capability, or oversized is `null`, and
104
+ * the caller must then treat the machine as busy. Never trusts a field it did
105
+ * not validate; a value is clamped, not coerced to "idle".
106
+ */
107
+ export declare function parseDedicatedRuntimeBusyReport(raw: unknown): DedicatedRuntimeBusyReport | null;
@@ -0,0 +1,147 @@
1
+ /**
2
+ * The Dedicated Runtime busy-report wire contract (power-control task K2,
3
+ * `dedicated-runtime-power-and-demand-wake-2026-10-07.md` §7.3 / §13.5).
4
+ *
5
+ * Declared ONCE, here. The daemon is published and cannot import the API, so
6
+ * the capability name, the payload shape and its bounds live in this package,
7
+ * which every end can import (`@skrr-ai/auth-core/dedicated-runtime-busy-wire`).
8
+ * `dedicatedRuntimeBusyWire.test.ts` fails on a second spelling of the
9
+ * capability anywhere else in the repo.
10
+ *
11
+ * WHAT THE REPORT SAYS
12
+ * ---------------------------------------------------------------------------
13
+ * "Does anything run in the tenant workload slice?" — read by the guest daemon
14
+ * from cgroup v2 `skrr-workload.slice`, recursively (the slice root is empty by
15
+ * contract; processes live in leaves), with the root-owned sshd listener left
16
+ * out. It is a LOCAL read summarised into counts; no per-process list ever
17
+ * crosses the wire.
18
+ *
19
+ * FAIL CLOSED. `busy` is `true | false | 'unknown'`. Any failure to read — a
20
+ * missing slice, an unreadable file, a tree too large to walk, a quiesce in
21
+ * progress, an sshd that is not where the image promises — is `'unknown'`, and
22
+ * a reader MUST treat `'unknown'` as busy. `false` is only ever the daemon
23
+ * having positively read an empty slice.
24
+ *
25
+ * THE POLICY KNOB (OPEN PRODUCT QUESTION — not decided here)
26
+ * ---------------------------------------------------------------------------
27
+ * An idle tmux server or a shell prompt keeps a leaf populated forever. The
28
+ * owner decision is that tmux/SSH shells ARE protected, but whether an IDLE
29
+ * shell should pin the machine is undecided. So the report carries both
30
+ * readings and the server chooses:
31
+ *
32
+ * - `busy` the STRICT reading (`policy: 'strict'`): every
33
+ * process in the slice counts. This is the
34
+ * default and the only one a server should act
35
+ * on until the product question is answered.
36
+ * - `busyIgnoringIdleShells` the same read with the `idleShell` bucket
37
+ * removed. Classification is by process name
38
+ * (a shell or tmux with nothing else running),
39
+ * a HEURISTIC: it cannot see a shell loop, so
40
+ * it must never be the default.
41
+ *
42
+ * The `breakdown` says which bucket made it busy, for the "why" text.
43
+ */
44
+ export const DEDICATED_RUNTIME_BUSY_REPORT_CAPABILITY = 'dedicated_runtime_busy_report_v1';
45
+ /** The heartbeat key the report rides under. Sent only by a daemon that announced the capability. */
46
+ export const DEDICATED_RUNTIME_BUSY_HEARTBEAT_KEY = 'dedicatedRuntimeBusy';
47
+ export const DEDICATED_RUNTIME_BUSY_REPORT_VERSION = 1;
48
+ /**
49
+ * `strict`: every process in the slice (less the root sshd listener) counts.
50
+ * The only value v1 sends; the field exists so a later relaxation is a new
51
+ * value rather than a silent change of meaning.
52
+ */
53
+ export const DEDICATED_RUNTIME_BUSY_POLICIES = ['strict'];
54
+ /**
55
+ * Why the daemon could not give a definite answer, or `ok`. Anything but `ok`
56
+ * pairs with `busy: 'unknown'`.
57
+ */
58
+ export const DEDICATED_RUNTIME_BUSY_SLICE_HEALTH = [
59
+ 'ok',
60
+ 'not_a_guest',
61
+ 'slice_missing',
62
+ 'slice_unreadable',
63
+ 'ssh_not_in_slice',
64
+ 'tree_too_large',
65
+ 'quiesced',
66
+ 'read_error',
67
+ ];
68
+ export const DEDICATED_RUNTIME_BUSY_LIMITS = {
69
+ /** Service leaf names carried for the "busy because of service X" text. */
70
+ maxServiceNames: 8,
71
+ maxServiceNameLength: 48,
72
+ /** Cgroup leaves the daemon will visit; more is `tree_too_large` -> unknown. */
73
+ maxLeaves: 64,
74
+ maxDepth: 4,
75
+ /** Processes the daemon classifies per read; the rest count as workload. */
76
+ maxClassifiedPids: 256,
77
+ /** Hard ceiling on the serialized report. */
78
+ maxSerializedBytes: 1024,
79
+ };
80
+ /**
81
+ * The reader's view of a daemon's heartbeat section. Anything malformed, from
82
+ * a daemon that did not announce the capability, or oversized is `null`, and
83
+ * the caller must then treat the machine as busy. Never trusts a field it did
84
+ * not validate; a value is clamped, not coerced to "idle".
85
+ */
86
+ export function parseDedicatedRuntimeBusyReport(raw) {
87
+ if (!raw || typeof raw !== 'object')
88
+ return null;
89
+ const r = raw;
90
+ if (r.version !== DEDICATED_RUNTIME_BUSY_REPORT_VERSION)
91
+ return null;
92
+ if (!DEDICATED_RUNTIME_BUSY_POLICIES.includes(r.policy))
93
+ return null;
94
+ const tri = (v) => v === true || v === false || v === 'unknown' ? v : undefined;
95
+ const busy = tri(r.busy);
96
+ const ignoring = tri(r.busyIgnoringIdleShells);
97
+ if (busy === undefined || ignoring === undefined)
98
+ return null;
99
+ if (!DEDICATED_RUNTIME_BUSY_SLICE_HEALTH.includes(r.sliceHealth)) {
100
+ return null;
101
+ }
102
+ const readAtMs = typeof r.readAt === 'string' ? Date.parse(r.readAt) : NaN;
103
+ if (!Number.isFinite(readAtMs))
104
+ return null;
105
+ const b = r.breakdown;
106
+ if (!b || typeof b !== 'object')
107
+ return null;
108
+ const count = (v) => typeof v === 'number' && Number.isSafeInteger(v) && v >= 0 ? v : null;
109
+ const workload = count(b.workload);
110
+ const service = count(b.service);
111
+ const ssh = count(b.ssh);
112
+ const idleShell = count(b.idleShell);
113
+ const excludedSshd = count(b.excludedSshd);
114
+ if (workload === null ||
115
+ service === null ||
116
+ ssh === null ||
117
+ idleShell === null ||
118
+ excludedSshd === null ||
119
+ typeof b.truncated !== 'boolean' ||
120
+ !Array.isArray(b.services)) {
121
+ return null;
122
+ }
123
+ const services = b.services
124
+ .filter((s) => typeof s === 'string')
125
+ .slice(0, DEDICATED_RUNTIME_BUSY_LIMITS.maxServiceNames)
126
+ .map((s) => s.slice(0, DEDICATED_RUNTIME_BUSY_LIMITS.maxServiceNameLength));
127
+ const report = {
128
+ version: DEDICATED_RUNTIME_BUSY_REPORT_VERSION,
129
+ policy: r.policy,
130
+ busy,
131
+ busyIgnoringIdleShells: ignoring,
132
+ breakdown: {
133
+ workload,
134
+ service,
135
+ ssh,
136
+ idleShell,
137
+ excludedSshd,
138
+ truncated: b.truncated,
139
+ services,
140
+ },
141
+ readAt: new Date(readAtMs).toISOString(),
142
+ sliceHealth: r.sliceHealth,
143
+ };
144
+ if (JSON.stringify(report).length > DEDICATED_RUNTIME_BUSY_LIMITS.maxSerializedBytes)
145
+ return null;
146
+ return report;
147
+ }
@@ -0,0 +1,115 @@
1
+ /**
2
+ * The Dedicated Runtime wake-progress wire contract (power-control task W5,
3
+ * `dedicated-runtime-power-and-demand-wake-2026-10-07.md` §5.3, §7.2, §9).
4
+ *
5
+ * Declared ONCE, here, for the same reason `computerWire.ts` is: the browser,
6
+ * the React Native app and the CLI import it, and none of them may spell the
7
+ * event, the capability or a code a second time. `dedicatedRuntimeWakeWire.test.ts`
8
+ * fails on a second spelling in the API.
9
+ *
10
+ * WHAT IT IS
11
+ * ---------------------------------------------------------------------------
12
+ * A person opens a terminal, an SSH channel, a service call or the Computer on a
13
+ * STOPPED Dedicated Runtime. That is wake progress, not an error: the relay asks
14
+ * `requestWake` (source `person`) to start the machine, then tells THAT request,
15
+ * in stages, how far the start has got, until the machine is assignable and the
16
+ * original request is served, or a bound runs out and it says why and what to do.
17
+ *
18
+ * THE CLIENT ANNOUNCES IT
19
+ * ---------------------------------------------------------------------------
20
+ * A client that has not announced `dedicated_wake_progress_v1` in the open
21
+ * request's `capabilities` array is NEVER sent `dedicated:wake:progress` and is
22
+ * never made to wait: it gets an immediate typed refusal instead. (A client
23
+ * socket has no registration step to announce on, so the announcement rides each
24
+ * request — stateless, nothing held in the relay.)
25
+ *
26
+ * THE VOCABULARY
27
+ * ---------------------------------------------------------------------------
28
+ * `phase` uses the cold-start measurement's phase names
29
+ * (`scripts/dedicated-runtime/measure-cold-start.js` `COLD_START_PHASES`), so
30
+ * what the person sees and what F4 measures are the same words. A spec holds the
31
+ * two lists equal.
32
+ *
33
+ * NOT A SPINNER WITHOUT A BOUND
34
+ * ---------------------------------------------------------------------------
35
+ * `boundMs` is carried on every event so a surface can show elapsed-of-bound.
36
+ * The bound sits ABOVE a slow-but-working start (design §9): running out says
37
+ * "this is taking longer than expected" and the machine is left starting; it is
38
+ * never an accusation that the machine is dead.
39
+ */
40
+ /** API -> client, on the same socket as the request it belongs to. */
41
+ export declare const DEDICATED_WAKE_EVENTS: {
42
+ readonly progress: "dedicated:wake:progress";
43
+ };
44
+ /** Announced in the open request's `capabilities` array. */
45
+ export declare const DEDICATED_WAKE_CAPABILITIES: {
46
+ readonly progress: "dedicated_wake_progress_v1";
47
+ };
48
+ export type DedicatedWakeCapability = (typeof DEDICATED_WAKE_CAPABILITIES)[keyof typeof DEDICATED_WAKE_CAPABILITIES];
49
+ /** Same names, same order as `WAKE_PHASES` in api/server/services/DedicatedRuntime/wakePhases.js (pinned by a test). */
50
+ export declare const DEDICATED_WAKE_PHASES: readonly ["start_requested", "provider_start_accepted", "instance_running", "daemon_connected", "first_heartbeat", "assignable"];
51
+ export type DedicatedWakePhase = (typeof DEDICATED_WAKE_PHASES)[number];
52
+ /**
53
+ * - `waking` a start is in flight; `phase` says how far.
54
+ * - `ready` assignable; the original request is about to be served.
55
+ * - `deferred` the machine is stopping; nothing was started. Retry after `retryAfterSeconds`.
56
+ * - `refused` the start was refused, with `code`, `reason`, `detail`, `fix`.
57
+ * - `timed_out` the bound ran out while it was still starting. Not a failure of the machine.
58
+ */
59
+ export declare const DEDICATED_WAKE_STATES: readonly ["waking", "ready", "deferred", "refused", "timed_out"];
60
+ export type DedicatedWakeState = (typeof DEDICATED_WAKE_STATES)[number];
61
+ /** The surfaces a wake can be asked from. */
62
+ export declare const DEDICATED_WAKE_SURFACES: readonly ["terminal", "ssh", "services", "templates", "computer", "http"];
63
+ export type DedicatedWakeSurface = (typeof DEDICATED_WAKE_SURFACES)[number];
64
+ /**
65
+ * Typed refusals. Each is a different fact with a different next step; none of
66
+ * them is the generic "Dedicated Runtime dispatch was refused".
67
+ */
68
+ export declare const DEDICATED_WAKE_CODES: {
69
+ /** Stopped, and this request did not (or could not) wake it. */
70
+ readonly stopped: "DEDICATED_RUNTIME_STOPPED";
71
+ /** A stop is finishing; retry when it has. */
72
+ readonly stopping: "DEDICATED_RUNTIME_STOPPING";
73
+ /** A start is in flight (ours or someone's). */
74
+ readonly starting: "DEDICATED_RUNTIME_STARTING";
75
+ /** The bound ran out while it was still starting. */
76
+ readonly timeout: "DEDICATED_RUNTIME_WAKE_TIMEOUT";
77
+ /** The start was refused: the plan no longer includes the machine. */
78
+ readonly entitlement: "DEDICATED_RUNTIME_WAKE_REFUSED_ENTITLEMENT";
79
+ /** The start was refused: the spend cap was reached. */
80
+ readonly spendCap: "DEDICATED_RUNTIME_WAKE_REFUSED_SPEND_CAP";
81
+ /** Any other refusal; `reason` names it. */
82
+ readonly refused: "DEDICATED_RUNTIME_WAKE_REFUSED";
83
+ /** The start was requested and the machine fell back to stopped or failed. */
84
+ readonly failed: "DEDICATED_RUNTIME_WAKE_FAILED";
85
+ };
86
+ export type DedicatedWakeCode = (typeof DEDICATED_WAKE_CODES)[keyof typeof DEDICATED_WAKE_CODES];
87
+ /**
88
+ * How long a surface waits for a start before saying it is taking longer than
89
+ * expected. Above the slow-but-working case: a cold start on a fresh guest
90
+ * takes 25-30 s for the browser alone, and the AMI boot is longer. It is a
91
+ * bound on what we show, never a verdict on the machine.
92
+ */
93
+ export declare const DEDICATED_WAKE_BOUND_MS = 180000;
94
+ /** How often the relay re-reads the lease while waiting. */
95
+ export declare const DEDICATED_WAKE_POLL_MS = 2000;
96
+ export interface DedicatedWakeProgress {
97
+ leaseId: string;
98
+ surface: DedicatedWakeSurface;
99
+ /** The id of the request this belongs to (`requestId`, `channelId`, ...). */
100
+ ref?: string;
101
+ state: DedicatedWakeState;
102
+ /** Present while `waking`, and on `timed_out` (where the machine had got to). */
103
+ phase?: DedicatedWakePhase;
104
+ elapsedMs: number;
105
+ boundMs: number;
106
+ /** Power reason (e.g. `wake_refused_spend_cap`) or a lifecycle reason. */
107
+ reason?: string;
108
+ /** One of `DEDICATED_WAKE_CODES` on `refused`, `deferred` and `timed_out`. */
109
+ code?: DedicatedWakeCode;
110
+ detail?: string;
111
+ /** What the person can do next. */
112
+ fix?: string | null;
113
+ retryAfterSeconds?: number;
114
+ }
115
+ export declare function clientAnnouncesWakeProgress(capabilities: unknown): boolean;