@skrr-ai/cli 0.1.88 → 0.1.90
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.
- package/dist/commands/machines/computer-adoption.d.ts +3 -2
- package/dist/commands/machines/computer-adoption.js +4 -3
- package/dist/commands/machines/dedicated/agent-browser.d.ts +25 -0
- package/dist/commands/machines/dedicated/agent-browser.js +65 -0
- package/dist/commands/machines/dedicated/exec.js +60 -14
- package/dist/commands/machines/dedicated/index.js +2 -0
- package/dist/commands/machines/dedicated/power/explain.d.ts +13 -0
- package/dist/commands/machines/dedicated/power/explain.js +36 -0
- package/dist/commands/machines/dedicated/power/index.d.ts +10 -0
- package/dist/commands/machines/dedicated/power/index.js +24 -0
- package/dist/commands/machines/dedicated/power/keep-awake.d.ts +20 -0
- package/dist/commands/machines/dedicated/power/keep-awake.js +69 -0
- package/dist/commands/machines/dedicated/power/set.d.ts +23 -0
- package/dist/commands/machines/dedicated/power/set.js +106 -0
- package/dist/commands/machines/dedicated/power/show.d.ts +13 -0
- package/dist/commands/machines/dedicated/power/show.js +36 -0
- package/dist/commands/machines/dedicated/show.js +17 -2
- package/dist/lib/commitments.d.ts +1 -1
- package/dist/lib/commitments.js +4 -2
- package/dist/lib/computer-adoption.d.ts +5 -0
- package/dist/lib/computer-adoption.js +6 -1
- package/dist/lib/computer-consent.d.ts +43 -1
- package/dist/lib/computer-consent.js +125 -4
- package/dist/lib/dedicated-keep-awake.d.ts +19 -0
- package/dist/lib/dedicated-keep-awake.js +64 -0
- package/dist/lib/dedicated-machines.d.ts +31 -0
- package/dist/lib/dedicated-machines.js +40 -0
- package/dist/lib/dedicated-power.d.ts +61 -0
- package/dist/lib/dedicated-power.js +243 -0
- package/dist/lib/exec-runtime-binary.d.ts +3 -1
- package/dist/lib/exec-runtime-binary.js +16 -2
- package/dist/lib/format.d.ts +13 -0
- package/dist/lib/format.js +51 -0
- package/dist/lib/keychain.js +3 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/computerWire.d.ts +135 -8
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/computerWire.js +166 -16
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dedicatedRuntimeBusyWire.d.ts +107 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dedicatedRuntimeBusyWire.js +151 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dedicatedRuntimeWakeWire.d.ts +115 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/dedicatedRuntimeWakeWire.js +117 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +8 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.d.ts +31 -7
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +149 -9
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/computerWire.d.ts +135 -8
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/computerWire.js +161 -13
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dedicatedRuntimeBusyWire.d.ts +107 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dedicatedRuntimeBusyWire.js +147 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dedicatedRuntimeWakeWire.d.ts +115 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/dedicatedRuntimeWakeWire.js +113 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.d.ts +31 -7
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +144 -8
- package/dist/node_modules/@skrr-ai/auth-core/package.json +21 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +6730 -6366
- package/dist/node_modules/@skrr-ai/data-provider/package.json +1 -1
- package/oclif.manifest.json +6303 -5852
- package/package.json +2 -2
|
@@ -36,6 +36,22 @@ export const COMPUTER_EVENTS = {
|
|
|
36
36
|
attach: 'computer:attach',
|
|
37
37
|
/** client → API: leave. */
|
|
38
38
|
detach: 'computer:detach',
|
|
39
|
+
/**
|
|
40
|
+
* client → API: read the whole manifest again ON THE SAME ATTACHMENT, after
|
|
41
|
+
* a missed delta or a `stale_epoch` (OSK-13815). The ack carries what an
|
|
42
|
+
* attach's does — the attachment id (unchanged), the manifest and the
|
|
43
|
+
* operations — or a typed refusal; `not_found` means the attachment is gone
|
|
44
|
+
* and only then does the client attach afresh. Re-reading state must never
|
|
45
|
+
* change who the reader is: a control lease is bound to the attachment
|
|
46
|
+
* (`holderAttachmentId`), and a resync that detached and attached again
|
|
47
|
+
* orphaned the hold the person had just taken. Answered by the API, which
|
|
48
|
+
* renews the binding with the daemon exactly as its periodic re-check does
|
|
49
|
+
* (a repeated `daemon:computer:attach` with the same id), so it has no
|
|
50
|
+
* daemon mirror and needs no daemon capability. An API that predates it
|
|
51
|
+
* never answers; the client bounds the wait and falls back to attaching
|
|
52
|
+
* afresh.
|
|
53
|
+
*/
|
|
54
|
+
resync: 'computer:resync',
|
|
39
55
|
/** API → client: this attachment was detached, with the reason (revoked, stale generation, …). */
|
|
40
56
|
detached: 'computer:detached',
|
|
41
57
|
/** API → client: a whole manifest, or a revisioned delta. */
|
|
@@ -177,6 +193,24 @@ export const COMPUTER_SERVER_EVENT_KEYS = [
|
|
|
177
193
|
'activity',
|
|
178
194
|
'filesChanged',
|
|
179
195
|
];
|
|
196
|
+
/**
|
|
197
|
+
* Client → API requests the API answers itself, with no `daemon:computer:*`
|
|
198
|
+
* mirror: every other client request is relayed to the daemon under the same
|
|
199
|
+
* key. `resync` renews the binding through the daemon's existing attach
|
|
200
|
+
* (OSK-13815).
|
|
201
|
+
*/
|
|
202
|
+
export const COMPUTER_API_ANSWERED_EVENT_KEYS = [
|
|
203
|
+
'resync',
|
|
204
|
+
];
|
|
205
|
+
/** How long the client waits for a `computer:resync` answer before attaching afresh (an API that predates it never answers). */
|
|
206
|
+
export const COMPUTER_RESYNC_TIMEOUT_MS = 12_000;
|
|
207
|
+
/**
|
|
208
|
+
* How long a client holds a manifest delta that arrived AHEAD of its
|
|
209
|
+
* predecessor before reading the hole as a loss (OSK-13815). Deltas fan out
|
|
210
|
+
* through a relay of several replicas; a reorder is milliseconds, a loss is
|
|
211
|
+
* forever, and only a loss needs the whole manifest again.
|
|
212
|
+
*/
|
|
213
|
+
export const COMPUTER_MANIFEST_REORDER_WAIT_MS = 300;
|
|
180
214
|
// ---------------------------------------------------------------------------
|
|
181
215
|
// Capabilities (§7.1)
|
|
182
216
|
// ---------------------------------------------------------------------------
|
|
@@ -281,6 +315,42 @@ export const COMPUTER_CAPABILITIES = {
|
|
|
281
315
|
* browser; nothing is proxied to it. Announced by the relay that serves it.
|
|
282
316
|
*/
|
|
283
317
|
declaredServices: 'computer_declared_services_v1',
|
|
318
|
+
/**
|
|
319
|
+
* The page follows the controlling window (§6.2 "a stable viewport", §7.1,
|
|
320
|
+
* OSK-13808): a browser `viewport` input, sent only by the attachment that
|
|
321
|
+
* holds the browser's lease at the current epoch, sizes the streamed page
|
|
322
|
+
* to that window's content box (`COMPUTER_BROWSER_VIEWPORT_LIMITS`). Only
|
|
323
|
+
* while a person holds control: handing back — or the hold lapsing, being
|
|
324
|
+
* revoked or moving to the agent any other way — restores the browser's own
|
|
325
|
+
* viewport before the agent can act, so an agent never inherits a person's
|
|
326
|
+
* size. Watchers keep seeing the page at whatever size it is, scaled to
|
|
327
|
+
* fit. Announced only where the browser driver can override and restore
|
|
328
|
+
* the page's metrics; without it the window scales the fixed page, as
|
|
329
|
+
* before, and never sends the input.
|
|
330
|
+
*/
|
|
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',
|
|
284
354
|
};
|
|
285
355
|
export const COMPUTER_CAPABILITY_VALUES = Object.values(COMPUTER_CAPABILITIES);
|
|
286
356
|
// ---------------------------------------------------------------------------
|
|
@@ -322,6 +392,12 @@ export const COMPUTER_REFUSAL_CODES = [
|
|
|
322
392
|
'machine_unavailable',
|
|
323
393
|
/** The machine's daemon is not answering. */
|
|
324
394
|
'daemon_unreachable',
|
|
395
|
+
/**
|
|
396
|
+
* The relay failed while answering — an exception, not a fact about the
|
|
397
|
+
* machine. Retryable; the API log carries the same `correlationId`
|
|
398
|
+
* (OSK-13823). Never `machine_unavailable`, which says the machine is down.
|
|
399
|
+
*/
|
|
400
|
+
'relay_error',
|
|
325
401
|
/** A bounded wait ran out (§7.2 R4). */
|
|
326
402
|
'timeout',
|
|
327
403
|
'invalid_request',
|
|
@@ -335,22 +411,16 @@ export const COMPUTER_REFUSAL_CODES = [
|
|
|
335
411
|
'service_limit',
|
|
336
412
|
];
|
|
337
413
|
/**
|
|
338
|
-
*
|
|
339
|
-
*
|
|
414
|
+
* The code when it is a computer refusal, else `null`. There is one spelling
|
|
415
|
+
* per code: the browser's older spelling of `human_control` was accepted for
|
|
416
|
+
* one release and retired with the old browser events (§5.2, OSK-13509).
|
|
340
417
|
*/
|
|
341
|
-
export const COMPUTER_LEGACY_REFUSAL_SPELLINGS = Object.freeze({
|
|
342
|
-
live_view_active: 'human_control',
|
|
343
|
-
});
|
|
344
|
-
/** The canonical code for a received spelling, or `null` when it is not a computer refusal. */
|
|
345
418
|
export function normalizeComputerRefusalCode(code) {
|
|
346
419
|
if (typeof code !== 'string') {
|
|
347
420
|
return null;
|
|
348
421
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
}
|
|
352
|
-
return Object.prototype.hasOwnProperty.call(COMPUTER_LEGACY_REFUSAL_SPELLINGS, code)
|
|
353
|
-
? COMPUTER_LEGACY_REFUSAL_SPELLINGS[code]
|
|
422
|
+
return COMPUTER_REFUSAL_CODES.includes(code)
|
|
423
|
+
? code
|
|
354
424
|
: null;
|
|
355
425
|
}
|
|
356
426
|
// ---------------------------------------------------------------------------
|
|
@@ -365,6 +435,27 @@ export const COMPUTER_ACTOR_KINDS = ['agent', 'human'];
|
|
|
365
435
|
* API and a client saw a `seq` hole no fill could close.
|
|
366
436
|
*/
|
|
367
437
|
export const COMPUTER_ACTIVITY_ACTOR_KINDS = [...COMPUTER_ACTOR_KINDS, 'system'];
|
|
438
|
+
/**
|
|
439
|
+
* The session ids the PLATFORM's own routes run daemon tools under — the web
|
|
440
|
+
* Files surface (`computer-files:` on a personal machine, `dedicated-runtime:`
|
|
441
|
+
* on a Dedicated Runtime) and the owner's `skrr machines dedicated` commands.
|
|
442
|
+
* No agent and no model is behind such a session: the person who asked is
|
|
443
|
+
* known to the API (which audits it), not to the daemon. So an activity event
|
|
444
|
+
* produced under one names no agent (OSK-13790: a person's upload used to read
|
|
445
|
+
* "agent created …"). Built by `ComputerFileService.runPersonalMachineTool`
|
|
446
|
+
* and `DedicatedRuntimeMachineAccess.runDedicatedRuntimeTool`.
|
|
447
|
+
*/
|
|
448
|
+
export const COMPUTER_PLATFORM_ROUTE_SESSION_PREFIXES = [
|
|
449
|
+
'dedicated-runtime:',
|
|
450
|
+
'computer-files:',
|
|
451
|
+
];
|
|
452
|
+
/** Whether a tool call's session is one of the platform's own routes, not an agent's. */
|
|
453
|
+
export function isComputerPlatformRouteSession(sessionId) {
|
|
454
|
+
return (typeof sessionId === 'string' &&
|
|
455
|
+
COMPUTER_PLATFORM_ROUTE_SESSION_PREFIXES.some((prefix) => sessionId.startsWith(prefix)));
|
|
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;
|
|
368
459
|
/**
|
|
369
460
|
* How a person present on the machine reached it: as its owner, or through a
|
|
370
461
|
* grant at one of the two tiers. Agents carry no access level — an agent
|
|
@@ -533,7 +624,11 @@ export const COMPUTER_SURFACE_END_REASONS = [
|
|
|
533
624
|
/** Someone signed the computer's browser out everywhere, which ends every browser window. */
|
|
534
625
|
'browser_reset',
|
|
535
626
|
];
|
|
536
|
-
/**
|
|
627
|
+
/**
|
|
628
|
+
* The browser's logical viewport whoever is looking (§6.2): what the agent
|
|
629
|
+
* always works at. A person holding control may size the page to their window
|
|
630
|
+
* (`computer_browser_viewport_v1`); handing back restores this.
|
|
631
|
+
*/
|
|
537
632
|
export const COMPUTER_BROWSER_VIEWPORT = Object.freeze({ width: 1280, height: 800 });
|
|
538
633
|
/** One frame never exceeds this, so it fits the SSE transport every guest uses (§7.2 R7). */
|
|
539
634
|
export const COMPUTER_FRAME_MAX_BYTES = 1024 * 1024;
|
|
@@ -607,7 +702,10 @@ export const COMPUTER_TERMINAL_HISTORY_CAUSES = [
|
|
|
607
702
|
* verified (OSK-13535).
|
|
608
703
|
*/
|
|
609
704
|
export const COMPUTER_TERMINAL_CAVEATS = ['windows_pty_unverified'];
|
|
610
|
-
/**
|
|
705
|
+
/**
|
|
706
|
+
* Input a browser surface takes. `tab` needs `computer_browser_tabs_v1`;
|
|
707
|
+
* `viewport` needs `computer_browser_viewport_v1`.
|
|
708
|
+
*/
|
|
611
709
|
export const COMPUTER_BROWSER_INPUT_TYPES = [
|
|
612
710
|
'pointer',
|
|
613
711
|
'key',
|
|
@@ -615,7 +713,57 @@ export const COMPUTER_BROWSER_INPUT_TYPES = [
|
|
|
615
713
|
'navigate',
|
|
616
714
|
'history',
|
|
617
715
|
'tab',
|
|
716
|
+
'viewport',
|
|
618
717
|
];
|
|
718
|
+
/**
|
|
719
|
+
* What a `viewport` input may ask of the page (OSK-13808), in CSS pixels: the
|
|
720
|
+
* controlling window's content box, bounded so no window can make the page
|
|
721
|
+
* absurd for the agent that gets it back, and aspect-clamped (`minAspect` and
|
|
722
|
+
* `maxAspect` are width ÷ height) so a sliver of a window cannot either.
|
|
723
|
+
* Device pixel ratio is not part of it: the page keeps the browser's own, so a
|
|
724
|
+
* high-density screen changes no layout and no picture size.
|
|
725
|
+
*/
|
|
726
|
+
export const COMPUTER_BROWSER_VIEWPORT_LIMITS = Object.freeze({
|
|
727
|
+
minWidth: 320,
|
|
728
|
+
maxWidth: 2560,
|
|
729
|
+
minHeight: 240,
|
|
730
|
+
maxHeight: 1600,
|
|
731
|
+
minAspect: 0.4,
|
|
732
|
+
maxAspect: 3.2,
|
|
733
|
+
});
|
|
734
|
+
/**
|
|
735
|
+
* The size a `viewport` input asks for, made legal: whole CSS pixels inside
|
|
736
|
+
* `COMPUTER_BROWSER_VIEWPORT_LIMITS`, its aspect inside the clamp. Total and
|
|
737
|
+
* idempotent: the window sends what this returns and the daemon applies what
|
|
738
|
+
* this returns, so both ends agree on the page a size produces. Null for a size
|
|
739
|
+
* with no area (a hidden or collapsed window asks for nothing).
|
|
740
|
+
*/
|
|
741
|
+
export function clampComputerBrowserViewport(size) {
|
|
742
|
+
const { width: rawWidth, height: rawHeight } = size ?? {};
|
|
743
|
+
if (!Number.isFinite(rawWidth) || !Number.isFinite(rawHeight))
|
|
744
|
+
return null;
|
|
745
|
+
if (!(rawWidth > 0) || !(rawHeight > 0))
|
|
746
|
+
return null;
|
|
747
|
+
const limits = COMPUTER_BROWSER_VIEWPORT_LIMITS;
|
|
748
|
+
const bound = (value, low, high) => Math.max(low, Math.min(high, Math.round(value)));
|
|
749
|
+
let width = bound(rawWidth, limits.minWidth, limits.maxWidth);
|
|
750
|
+
let height = bound(rawHeight, limits.minHeight, limits.maxHeight);
|
|
751
|
+
if (width / height > limits.maxAspect) {
|
|
752
|
+
// Too wide: narrow it first; a height already at its floor cannot grow the other way.
|
|
753
|
+
width = Math.max(limits.minWidth, Math.floor(height * limits.maxAspect));
|
|
754
|
+
if (width / height > limits.maxAspect) {
|
|
755
|
+
height = Math.min(limits.maxHeight, Math.ceil(width / limits.maxAspect));
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
else if (width / height < limits.minAspect) {
|
|
759
|
+
// Too tall: shorten it first, then widen if the width is at its floor.
|
|
760
|
+
height = Math.max(limits.minHeight, Math.floor(width / limits.minAspect));
|
|
761
|
+
if (width / height < limits.minAspect) {
|
|
762
|
+
width = Math.min(limits.maxWidth, Math.ceil(height * limits.minAspect));
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
return { width, height };
|
|
766
|
+
}
|
|
619
767
|
/**
|
|
620
768
|
* The client OS a browser `key` input comes from. A person's shortcuts are
|
|
621
769
|
* mapped onto the Linux page's by the daemon: on `mac`, Command is Control,
|
|
@@ -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;
|