@skrr-ai/cli 0.1.28 → 0.1.29
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/base-command.d.ts +15 -0
- package/dist/base-command.js +49 -0
- package/dist/commands/agents/chat.d.ts +2 -0
- package/dist/commands/agents/chat.js +40 -2
- package/dist/commands/balance/show.d.ts +2 -3
- package/dist/commands/balance/show.js +2 -3
- package/dist/commands/balance/usage.d.ts +3 -11
- package/dist/commands/balance/usage.js +19 -72
- package/dist/commands/code/index.d.ts +4 -0
- package/dist/commands/code/index.js +9 -0
- package/dist/commands/code/install.d.ts +10 -2
- package/dist/commands/code/install.js +38 -28
- package/dist/commands/harnesses/leases/show.js +7 -4
- package/dist/commands/inbox/index.d.ts +15 -0
- package/dist/commands/inbox/index.js +52 -20
- package/dist/commands/instructions/install.d.ts +12 -0
- package/dist/commands/instructions/install.js +59 -14
- package/dist/commands/login.js +6 -0
- package/dist/commands/machines/dedicated/attach.js +1 -1
- package/dist/commands/machines/dedicated/cp.d.ts +1 -0
- package/dist/commands/machines/dedicated/cp.js +63 -9
- package/dist/commands/machines/dedicated/create.d.ts +1 -1
- package/dist/commands/machines/dedicated/create.js +7 -2
- package/dist/commands/machines/dedicated/exec.d.ts +23 -1
- package/dist/commands/machines/dedicated/exec.js +67 -7
- package/dist/commands/machines/dedicated/index.js +2 -0
- package/dist/commands/machines/dedicated/restore.d.ts +6 -0
- package/dist/commands/machines/dedicated/restore.js +7 -1
- package/dist/commands/machines/dedicated/sign-in.d.ts +4 -3
- package/dist/commands/machines/dedicated/sign-in.js +4 -3
- package/dist/commands/machines/dedicated/terminal.js +1 -1
- package/dist/commands/machines/dedicated/update-image.d.ts +15 -0
- package/dist/commands/machines/dedicated/update-image.js +38 -0
- package/dist/lib/balance.d.ts +2 -2
- package/dist/lib/balance.js +6 -4
- package/dist/lib/daemon-target.d.ts +103 -0
- package/dist/lib/daemon-target.js +110 -0
- package/dist/lib/dedicated-copy.d.ts +92 -2
- package/dist/lib/dedicated-copy.js +223 -18
- package/dist/lib/dedicated-lease-command.d.ts +7 -1
- package/dist/lib/dedicated-lease-command.js +16 -3
- package/dist/lib/dedicated-machines.d.ts +130 -8
- package/dist/lib/dedicated-machines.js +274 -15
- package/dist/lib/dedicated-terminal.d.ts +5 -25
- package/dist/lib/dedicated-terminal.js +45 -73
- package/dist/lib/dedicated-wait.d.ts +10 -0
- package/dist/lib/dedicated-wait.js +52 -0
- package/dist/lib/device-code.d.ts +12 -1
- package/dist/lib/device-code.js +44 -9
- package/dist/lib/harnesses.d.ts +13 -0
- package/dist/lib/harnesses.js +24 -0
- package/dist/lib/login.js +8 -7
- package/dist/lib/sky-code-broker.d.ts +46 -5
- package/dist/lib/sky-code-broker.js +96 -26
- package/dist/lib/sky-code.d.ts +33 -0
- package/dist/lib/sky-code.js +45 -7
- package/dist/lib/task-instruction-offer.js +12 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +67 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +124 -12
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +67 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +123 -11
- package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +3061 -2876
- package/dist/node_modules/@skrr-ai/data-provider/package.json +1 -1
- package/oclif.manifest.json +2930 -2828
- package/package.json +1 -1
|
@@ -32,8 +32,26 @@ function progressOf(lease) {
|
|
|
32
32
|
* reading that cannot turn into the goal without someone acting again; anything
|
|
33
33
|
* that might still settle is `pending`, and the caller's timeout decides how long
|
|
34
34
|
* that is worth watching.
|
|
35
|
+
*
|
|
36
|
+
* Reaching the goal is not the end of the wait while the lease's operation is
|
|
37
|
+
* still in progress: `--wait` promises the operation has FINISHED, and the
|
|
38
|
+
* server refuses the next one until it has. A create read `ready` the moment its
|
|
39
|
+
* guest registered and returned, and the restart that followed was refused for
|
|
40
|
+
* minutes (OSK-8775). A server that does not report the operation is judged on
|
|
41
|
+
* the goal alone, as before.
|
|
35
42
|
*/
|
|
36
43
|
function evaluateDedicatedWait(goal, lease) {
|
|
44
|
+
const verdict = evaluateDedicatedWaitGoal(goal, lease);
|
|
45
|
+
if (verdict.status !== 'done' || lease?.operation?.status !== 'in_progress') {
|
|
46
|
+
return verdict;
|
|
47
|
+
}
|
|
48
|
+
const action = lease.operation.action;
|
|
49
|
+
return {
|
|
50
|
+
status: 'pending',
|
|
51
|
+
progress: `${progressOf(lease)} · ${action ? `${action} ` : 'operation '}still finishing`,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function evaluateDedicatedWaitGoal(goal, lease) {
|
|
37
55
|
if (!lease) {
|
|
38
56
|
return { status: 'failed', reason: 'the lease is no longer visible' };
|
|
39
57
|
}
|
|
@@ -89,6 +107,40 @@ function evaluateDedicatedWait(goal, lease) {
|
|
|
89
107
|
reason: `the snapshot ended (${state}) without recording a new recovery point`,
|
|
90
108
|
};
|
|
91
109
|
}
|
|
110
|
+
case 'image_updated': {
|
|
111
|
+
// Running says nothing: a machine returned to its previous image runs too.
|
|
112
|
+
// The update's own record does, once the platform has judged it — and only
|
|
113
|
+
// an update that started after the request (the 202 carries the one before).
|
|
114
|
+
if (DEAD_STATES.has(state) || state === 'archived' || state === 'terminating') {
|
|
115
|
+
return { status: 'failed', reason: `the machine is ${state}` };
|
|
116
|
+
}
|
|
117
|
+
const last = lease.health?.image?.lastUpdate;
|
|
118
|
+
const fresh = Boolean(last?.startedAt) && last?.startedAt !== goal.previousUpdateStartedAt;
|
|
119
|
+
if (fresh && last?.status === 'succeeded' && RUNNING_STATES.has(state)) {
|
|
120
|
+
return { status: 'done' };
|
|
121
|
+
}
|
|
122
|
+
// Nothing was left to move by the time the update ran (the image it was
|
|
123
|
+
// asked for stopped being newer): the machine is back, as it was.
|
|
124
|
+
if (!fresh && RUNNING_STATES.has(state) && lease.health?.image?.updateAvailable === false) {
|
|
125
|
+
return { status: 'done' };
|
|
126
|
+
}
|
|
127
|
+
if (fresh && (last?.status === 'rolled_back' || last?.status === 'failed')) {
|
|
128
|
+
return {
|
|
129
|
+
status: 'failed',
|
|
130
|
+
reason: last.status === 'rolled_back'
|
|
131
|
+
? 'the current image did not come back healthy; the machine is back on its previous image, files kept'
|
|
132
|
+
: 'the update did not complete on either image; files are kept',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
let moving = 'update requested';
|
|
136
|
+
if (fresh) {
|
|
137
|
+
moving =
|
|
138
|
+
last?.status === 'rolling_back'
|
|
139
|
+
? 'returning to the previous image'
|
|
140
|
+
: 'moving onto the current image';
|
|
141
|
+
}
|
|
142
|
+
return { status: 'pending', progress: `${progressOf(lease)} · ${moving}` };
|
|
143
|
+
}
|
|
92
144
|
case 'grown': {
|
|
93
145
|
const size = lease.resources?.storageGb;
|
|
94
146
|
if (typeof size === 'number' && size >= goal.storageGb)
|
|
@@ -38,9 +38,20 @@ export interface DeviceCodePollerOptions {
|
|
|
38
38
|
onStart?: (start: DeviceCodeStart) => void;
|
|
39
39
|
/**
|
|
40
40
|
* Called on every poll tick before the HTTP call. Returns false to
|
|
41
|
-
* cancel
|
|
41
|
+
* cancel.
|
|
42
42
|
*/
|
|
43
43
|
shouldContinue?: () => boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Cancels the poll at once — the request in flight and the wait between
|
|
46
|
+
* ticks — with DeviceCodeLoginCancelledError. What Ctrl-C uses.
|
|
47
|
+
*/
|
|
48
|
+
signal?: AbortSignal;
|
|
49
|
+
}
|
|
50
|
+
/** The person stopped waiting for approval. Exits 130, as the Ctrl-C that caused it would. */
|
|
51
|
+
export declare class DeviceCodeLoginCancelledError extends Error {
|
|
52
|
+
readonly code = "LOGIN_CANCELLED";
|
|
53
|
+
readonly exitCode = 130;
|
|
54
|
+
constructor();
|
|
44
55
|
}
|
|
45
56
|
/**
|
|
46
57
|
* Issue a device code from the server.
|
package/dist/lib/device-code.js
CHANGED
|
@@ -21,12 +21,23 @@
|
|
|
21
21
|
* → 404 { error: 'Device code expired or not found' }
|
|
22
22
|
*/
|
|
23
23
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.DeviceCodeLoginCancelledError = void 0;
|
|
24
25
|
exports.startDeviceCode = startDeviceCode;
|
|
25
26
|
exports.pollDeviceCode = pollDeviceCode;
|
|
26
27
|
exports.revokeDeviceRefreshToken = revokeDeviceRefreshToken;
|
|
27
28
|
const DEFAULT_POLL_INTERVAL_MS = 2_000;
|
|
28
29
|
/** Server-side TTL is 300s; we add a small safety margin for clock drift. */
|
|
29
30
|
const DEFAULT_OVERALL_TIMEOUT_MS = 320_000;
|
|
31
|
+
/** The person stopped waiting for approval. Exits 130, as the Ctrl-C that caused it would. */
|
|
32
|
+
class DeviceCodeLoginCancelledError extends Error {
|
|
33
|
+
code = 'LOGIN_CANCELLED';
|
|
34
|
+
exitCode = 130;
|
|
35
|
+
constructor() {
|
|
36
|
+
super('Login cancelled before the device code was approved; nothing was saved.');
|
|
37
|
+
this.name = 'DeviceCodeLoginCancelledError';
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
exports.DeviceCodeLoginCancelledError = DeviceCodeLoginCancelledError;
|
|
30
41
|
/**
|
|
31
42
|
* Issue a device code from the server.
|
|
32
43
|
*
|
|
@@ -69,18 +80,21 @@ async function pollDeviceCode(baseURL, code, opts = {}) {
|
|
|
69
80
|
const interval = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
70
81
|
const timeout = opts.overallTimeoutMs ?? DEFAULT_OVERALL_TIMEOUT_MS;
|
|
71
82
|
const shouldContinue = opts.shouldContinue ?? (() => true);
|
|
83
|
+
const { signal } = opts;
|
|
72
84
|
const deadline = Date.now() + timeout;
|
|
73
85
|
while (Date.now() < deadline) {
|
|
74
|
-
if (!shouldContinue()) {
|
|
75
|
-
throw new
|
|
86
|
+
if (signal?.aborted || !shouldContinue()) {
|
|
87
|
+
throw new DeviceCodeLoginCancelledError();
|
|
76
88
|
}
|
|
77
89
|
let res;
|
|
78
90
|
try {
|
|
79
|
-
res = await fetch(`${baseURL.replace(/\/$/, '')}/api/daemons/device-code/${encodeURIComponent(code)}/status`, { method: 'GET', headers: { Accept: 'application/json' } });
|
|
91
|
+
res = await fetch(`${baseURL.replace(/\/$/, '')}/api/daemons/device-code/${encodeURIComponent(code)}/status`, { method: 'GET', headers: { Accept: 'application/json' }, ...(signal ? { signal } : {}) });
|
|
80
92
|
}
|
|
81
93
|
catch {
|
|
94
|
+
if (signal?.aborted)
|
|
95
|
+
throw new DeviceCodeLoginCancelledError();
|
|
82
96
|
// Transient network error — wait and retry.
|
|
83
|
-
await sleep(interval);
|
|
97
|
+
await sleep(interval, signal);
|
|
84
98
|
continue;
|
|
85
99
|
}
|
|
86
100
|
if (res.status === 404) {
|
|
@@ -88,21 +102,29 @@ async function pollDeviceCode(baseURL, code, opts = {}) {
|
|
|
88
102
|
}
|
|
89
103
|
if (res.status === 429) {
|
|
90
104
|
// Server is asking us to slow down. Double the wait this round.
|
|
91
|
-
await sleep(interval * 2);
|
|
105
|
+
await sleep(interval * 2, signal);
|
|
92
106
|
continue;
|
|
93
107
|
}
|
|
94
108
|
if (!res.ok) {
|
|
95
109
|
const body = await res.text().catch(() => '');
|
|
96
110
|
throw new Error(`Poll failed: HTTP ${res.status} ${body.slice(0, 200)}`);
|
|
97
111
|
}
|
|
98
|
-
|
|
112
|
+
let data;
|
|
113
|
+
try {
|
|
114
|
+
data = (await res.json());
|
|
115
|
+
}
|
|
116
|
+
catch (err) {
|
|
117
|
+
if (signal?.aborted)
|
|
118
|
+
throw new DeviceCodeLoginCancelledError();
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
99
121
|
if (data.status === 'approved') {
|
|
100
122
|
if (!('token' in data) || !data.token) {
|
|
101
123
|
throw new Error('Server reported approval without a token');
|
|
102
124
|
}
|
|
103
125
|
return data;
|
|
104
126
|
}
|
|
105
|
-
await sleep(interval);
|
|
127
|
+
await sleep(interval, signal);
|
|
106
128
|
}
|
|
107
129
|
throw new Error('Device-code login timed out waiting for approval');
|
|
108
130
|
}
|
|
@@ -133,13 +155,26 @@ async function revokeDeviceRefreshToken(baseURL, refreshToken) {
|
|
|
133
155
|
clearTimeout(timer);
|
|
134
156
|
}
|
|
135
157
|
}
|
|
136
|
-
function sleep(ms) {
|
|
158
|
+
function sleep(ms, signal) {
|
|
137
159
|
// Do NOT unref — pollDeviceCode is the only thing keeping the event
|
|
138
160
|
// loop alive during device-code login, so an unref'd timer lets Node
|
|
139
161
|
// exit cleanly mid-poll (process exits with code 0 after the first
|
|
140
162
|
// sleep, never reaching approval). Mirrors the same intentional
|
|
141
163
|
// non-unref in loginLocalhost.ts:419.
|
|
142
164
|
return new Promise((resolve) => {
|
|
143
|
-
|
|
165
|
+
if (signal?.aborted) {
|
|
166
|
+
resolve();
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const timer = setTimeout(() => {
|
|
170
|
+
signal?.removeEventListener('abort', wake);
|
|
171
|
+
resolve();
|
|
172
|
+
}, ms);
|
|
173
|
+
// A cancelled login wakes now; the loop's next check throws.
|
|
174
|
+
const wake = () => {
|
|
175
|
+
clearTimeout(timer);
|
|
176
|
+
resolve();
|
|
177
|
+
};
|
|
178
|
+
signal?.addEventListener('abort', wake, { once: true });
|
|
144
179
|
});
|
|
145
180
|
}
|
package/dist/lib/harnesses.d.ts
CHANGED
|
@@ -57,6 +57,19 @@ export interface Harness {
|
|
|
57
57
|
*/
|
|
58
58
|
metadata?: Record<string, unknown>;
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* A machine's health reason codes as the sentences a person reads, from the
|
|
62
|
+
* copy map the web renders too (`describeMachineHealthReasons`,
|
|
63
|
+
* `@skrr-ai/data-provider`). Every CLI surface that explains why a lease cannot
|
|
64
|
+
* take work goes through this: `machines dedicated` and `harnesses leases show`.
|
|
65
|
+
*
|
|
66
|
+
* With the lease `state`, reasons that only follow from a lease at rest or in
|
|
67
|
+
* transition are left out: an archived machine is archived, and that it has not
|
|
68
|
+
* checked in is the same fact. A code this build has no words for keeps its code
|
|
69
|
+
* in parentheses, so the fallback sentence can still be traced. Empty when there
|
|
70
|
+
* is nothing to say. Scripts read the codes from `--json`, never from this.
|
|
71
|
+
*/
|
|
72
|
+
export declare function describeHealthReasonSentences(reasons: unknown, state?: unknown): string;
|
|
60
73
|
/**
|
|
61
74
|
* One open lease, as `/api/machines/leases` actually returns it.
|
|
62
75
|
*
|
package/dist/lib/harnesses.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.HARNESS_LEASE_ACTIONS = void 0;
|
|
4
|
+
exports.describeHealthReasonSentences = describeHealthReasonSentences;
|
|
4
5
|
exports.listHarnesses = listHarnesses;
|
|
5
6
|
exports.getHarness = getHarness;
|
|
6
7
|
exports.getHarnessModels = getHarnessModels;
|
|
@@ -22,7 +23,30 @@ exports.getHarnessLease = getHarnessLease;
|
|
|
22
23
|
exports.releaseHarnessLease = releaseHarnessLease;
|
|
23
24
|
exports.performHarnessLeaseAction = performHarnessLeaseAction;
|
|
24
25
|
exports.setHarnessLeaseBilling = setHarnessLeaseBilling;
|
|
26
|
+
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
25
27
|
const api_fetch_1 = require("./api-fetch");
|
|
28
|
+
/**
|
|
29
|
+
* A machine's health reason codes as the sentences a person reads, from the
|
|
30
|
+
* copy map the web renders too (`describeMachineHealthReasons`,
|
|
31
|
+
* `@skrr-ai/data-provider`). Every CLI surface that explains why a lease cannot
|
|
32
|
+
* take work goes through this: `machines dedicated` and `harnesses leases show`.
|
|
33
|
+
*
|
|
34
|
+
* With the lease `state`, reasons that only follow from a lease at rest or in
|
|
35
|
+
* transition are left out: an archived machine is archived, and that it has not
|
|
36
|
+
* checked in is the same fact. A code this build has no words for keeps its code
|
|
37
|
+
* in parentheses, so the fallback sentence can still be traced. Empty when there
|
|
38
|
+
* is nothing to say. Scripts read the codes from `--json`, never from this.
|
|
39
|
+
*/
|
|
40
|
+
function describeHealthReasonSentences(reasons, state) {
|
|
41
|
+
const { lines } = (0, data_provider_1.describeMachineHealthReasons)(reasons, {
|
|
42
|
+
state: typeof state === 'string' ? state : undefined,
|
|
43
|
+
});
|
|
44
|
+
return lines
|
|
45
|
+
.map((line) => line.known || line.codes.length === 0
|
|
46
|
+
? line.sentence
|
|
47
|
+
: `${line.sentence.replace(/\.$/, '')} (${line.codes.join(', ')}).`)
|
|
48
|
+
.join(' ');
|
|
49
|
+
}
|
|
26
50
|
/**
|
|
27
51
|
* Every action `POST /api/machines/leases/:id/actions/:action` accepts —
|
|
28
52
|
* `MACHINE_LEASE_ACTIONS` in MachineLeaseService. Which of them a given lease
|
package/dist/lib/login.js
CHANGED
|
@@ -402,16 +402,17 @@ async function runDeviceCode(serverUrl, cliId, reason, ssh, errorMessage) {
|
|
|
402
402
|
openBrowserPlatform(start.verificationUrl);
|
|
403
403
|
}
|
|
404
404
|
console.log(' Waiting for approval — this command will continue automatically.');
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
405
|
+
// Ctrl-C stops the wait at once — the status request in flight and the pause
|
|
406
|
+
// between polls — and the command exits 130 (DeviceCodeLoginCancelledError).
|
|
407
|
+
// This listener only started running with OSK-8708: auth-core's lock handler
|
|
408
|
+
// used to exit the process first. A flag checked on the next tick would now
|
|
409
|
+
// leave Ctrl-C waiting out a poll and ending as an ordinary error.
|
|
410
|
+
const cancel = new AbortController();
|
|
411
|
+
const onSigint = () => cancel.abort();
|
|
409
412
|
process.on('SIGINT', onSigint);
|
|
410
413
|
let approved;
|
|
411
414
|
try {
|
|
412
|
-
approved = await (0, device_code_1.pollDeviceCode)(serverUrl, start.code, {
|
|
413
|
-
shouldContinue: () => !cancelled,
|
|
414
|
-
});
|
|
415
|
+
approved = await (0, device_code_1.pollDeviceCode)(serverUrl, start.code, { signal: cancel.signal });
|
|
415
416
|
}
|
|
416
417
|
finally {
|
|
417
418
|
process.off('SIGINT', onSigint);
|
|
@@ -50,6 +50,12 @@
|
|
|
50
50
|
* refusals already carry codes (`model-denied`, `budget-exhausted`,
|
|
51
51
|
* `policy-unavailable`, `session-lease-exists`, …); this module forwards them
|
|
52
52
|
* verbatim and adds its own for the local preconditions.
|
|
53
|
+
*
|
|
54
|
+
* The code is also what decides whether there is a fallback at all. A refusal
|
|
55
|
+
* that is a DECISION about the account (`plan-allowance-exhausted`,
|
|
56
|
+
* `balance-exhausted`, …) stops the run instead (OSK-8822): switching to the
|
|
57
|
+
* user's own key because their plan said no is the silent mode switch I4
|
|
58
|
+
* forbids, and without a key it only starts an engine that cannot run.
|
|
53
59
|
*/
|
|
54
60
|
import { buildManagedInferenceRelayCredential, startManagedInferenceBroker, type RenewalStopReason } from '@skrr-ai/inference-broker';
|
|
55
61
|
import { type ApiFetchOptions } from './api-fetch';
|
|
@@ -237,10 +243,17 @@ export interface ManagedEngineHooksInput {
|
|
|
237
243
|
* engine binary FIRST and refuses a missing one before `prepare` runs. Acquiring
|
|
238
244
|
* earlier would mint a lease and open a socket for an engine that never starts.
|
|
239
245
|
*
|
|
240
|
-
* `prepare`
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
246
|
+
* `prepare` rejects for exactly one reason. Any rejection aborts the run, so a
|
|
247
|
+
* failure that merely means "we could not get you a managed session" must not
|
|
248
|
+
* reject — that would be the hard failure invariant I4 forbids. Those are caught,
|
|
249
|
+
* said once, and answered with `{}` (BYOK).
|
|
250
|
+
*
|
|
251
|
+
* The one reason is a refusal that is a DECISION about the account (OSK-8822).
|
|
252
|
+
* There, starting the engine is the wrong answer whether or not a provider key
|
|
253
|
+
* exists — with one it silently moves the spend to a different billing boundary
|
|
254
|
+
* because the plan said no, and without one it starts an engine whose first
|
|
255
|
+
* request fails — so it is said once and rejected with `EngineStartRefusedError`,
|
|
256
|
+
* which `execEngine` answers by not spawning at all.
|
|
244
257
|
*/
|
|
245
258
|
export declare function createManagedEngineHooks(input: ManagedEngineHooksInput): ManagedEngineHooks;
|
|
246
259
|
/**
|
|
@@ -269,5 +282,33 @@ export declare function createManagedEngineHooks(input: ManagedEngineHooksInput)
|
|
|
269
282
|
* Returns null when there is nothing worth saying.
|
|
270
283
|
*/
|
|
271
284
|
export declare function managedRenewalStoppedLine(reason: RenewalStopReason, detail?: string): string | null;
|
|
272
|
-
export
|
|
285
|
+
export type ManagedAcquisitionFailure =
|
|
286
|
+
/** The server decided about the account. The engine must not start. */
|
|
287
|
+
{
|
|
288
|
+
action: 'refuse';
|
|
289
|
+
code: string;
|
|
290
|
+
line: string;
|
|
291
|
+
}
|
|
292
|
+
/** Nothing was decided about the account. The run continues without a broker. */
|
|
293
|
+
| {
|
|
294
|
+
action: 'fallback';
|
|
295
|
+
code?: string;
|
|
296
|
+
line: string;
|
|
297
|
+
};
|
|
298
|
+
/**
|
|
299
|
+
* What a failed acquisition does next, and the ONE line that says so.
|
|
300
|
+
*
|
|
301
|
+
* One function for both because they must never disagree: a line promising
|
|
302
|
+
* "continuing with your own provider credentials" over a run that was stopped,
|
|
303
|
+
* or "did not start the engine" over one that started, is the confusion between
|
|
304
|
+
* managed, BYOK and offline that I4 forbids.
|
|
305
|
+
*
|
|
306
|
+
* The line names the machine-readable code as well as the sentence: the codes
|
|
307
|
+
* are the server's typed refusals (`model-denied`, `budget-exhausted`,
|
|
308
|
+
* `policy-unavailable` …) and "which of those happened" is the first thing
|
|
309
|
+
* support asks. It ends by saying what is about to happen instead, reading the
|
|
310
|
+
* environment for whether "your own key" is a thing that exists here, because
|
|
311
|
+
* naming the wrong one of managed / BYOK / offline is the same failure.
|
|
312
|
+
*/
|
|
313
|
+
export declare function managedAcquisitionFailure(err: unknown, env?: NodeJS.ProcessEnv): ManagedAcquisitionFailure;
|
|
273
314
|
export {};
|
|
@@ -51,6 +51,12 @@
|
|
|
51
51
|
* refusals already carry codes (`model-denied`, `budget-exhausted`,
|
|
52
52
|
* `policy-unavailable`, `session-lease-exists`, …); this module forwards them
|
|
53
53
|
* verbatim and adds its own for the local preconditions.
|
|
54
|
+
*
|
|
55
|
+
* The code is also what decides whether there is a fallback at all. A refusal
|
|
56
|
+
* that is a DECISION about the account (`plan-allowance-exhausted`,
|
|
57
|
+
* `balance-exhausted`, …) stops the run instead (OSK-8822): switching to the
|
|
58
|
+
* user's own key because their plan said no is the silent mode switch I4
|
|
59
|
+
* forbids, and without a key it only starts an engine that cannot run.
|
|
54
60
|
*/
|
|
55
61
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
56
62
|
exports.ManagedAcquisitionError = void 0;
|
|
@@ -59,7 +65,7 @@ exports.hasLocalCredential = hasLocalCredential;
|
|
|
59
65
|
exports.managedLeaseClockLine = managedLeaseClockLine;
|
|
60
66
|
exports.createManagedEngineHooks = createManagedEngineHooks;
|
|
61
67
|
exports.managedRenewalStoppedLine = managedRenewalStoppedLine;
|
|
62
|
-
exports.
|
|
68
|
+
exports.managedAcquisitionFailure = managedAcquisitionFailure;
|
|
63
69
|
const node_crypto_1 = require("node:crypto");
|
|
64
70
|
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
65
71
|
const inference_broker_1 = require("@skrr-ai/inference-broker");
|
|
@@ -970,10 +976,17 @@ function fallbackTail(env) {
|
|
|
970
976
|
* engine binary FIRST and refuses a missing one before `prepare` runs. Acquiring
|
|
971
977
|
* earlier would mint a lease and open a socket for an engine that never starts.
|
|
972
978
|
*
|
|
973
|
-
* `prepare`
|
|
974
|
-
*
|
|
975
|
-
*
|
|
976
|
-
*
|
|
979
|
+
* `prepare` rejects for exactly one reason. Any rejection aborts the run, so a
|
|
980
|
+
* failure that merely means "we could not get you a managed session" must not
|
|
981
|
+
* reject — that would be the hard failure invariant I4 forbids. Those are caught,
|
|
982
|
+
* said once, and answered with `{}` (BYOK).
|
|
983
|
+
*
|
|
984
|
+
* The one reason is a refusal that is a DECISION about the account (OSK-8822).
|
|
985
|
+
* There, starting the engine is the wrong answer whether or not a provider key
|
|
986
|
+
* exists — with one it silently moves the spend to a different billing boundary
|
|
987
|
+
* because the plan said no, and without one it starts an engine whose first
|
|
988
|
+
* request fails — so it is said once and rejected with `EngineStartRefusedError`,
|
|
989
|
+
* which `execEngine` answers by not spawning at all.
|
|
977
990
|
*/
|
|
978
991
|
function createManagedEngineHooks(input) {
|
|
979
992
|
const env = input.env ?? process.env;
|
|
@@ -1043,7 +1056,13 @@ function createManagedEngineHooks(input) {
|
|
|
1043
1056
|
return handle.env;
|
|
1044
1057
|
}
|
|
1045
1058
|
catch (err) {
|
|
1046
|
-
|
|
1059
|
+
// ONE classification decides both the sentence and the action, so the line
|
|
1060
|
+
// cannot promise a fallback the run does not take, or the reverse.
|
|
1061
|
+
const failure = managedAcquisitionFailure(err, env);
|
|
1062
|
+
log(failure.line);
|
|
1063
|
+
if (failure.action === 'refuse') {
|
|
1064
|
+
throw new sky_code_1.EngineStartRefusedError(failure.code, failure.line);
|
|
1065
|
+
}
|
|
1047
1066
|
return {};
|
|
1048
1067
|
}
|
|
1049
1068
|
},
|
|
@@ -1170,29 +1189,33 @@ function managedRenewalStoppedLine(reason, detail) {
|
|
|
1170
1189
|
'your own key.');
|
|
1171
1190
|
}
|
|
1172
1191
|
}
|
|
1173
|
-
/**
|
|
1174
|
-
* The ONE line a failed acquisition prints.
|
|
1175
|
-
*
|
|
1176
|
-
* Names the machine-readable code as well as the sentence: the codes are the
|
|
1177
|
-
* server's typed refusals (`model-denied`, `budget-exhausted`, `policy-unavailable`
|
|
1178
|
-
* …) and "which of those happened" is the first thing support asks. Ends by saying
|
|
1179
|
-
* what is about to happen instead, because a user who does not know they switched
|
|
1180
|
-
* to their own key is the failure mode I4 exists to prevent — and by reading the
|
|
1181
|
-
* environment for whether "your own key" is a thing that exists here, because
|
|
1182
|
-
* naming the wrong one of managed / BYOK / offline is the same failure.
|
|
1183
|
-
*/
|
|
1184
1192
|
/**
|
|
1185
1193
|
* Refusals that are a DECISION about the account, not the service being down.
|
|
1186
1194
|
*
|
|
1187
|
-
* The server answers these with
|
|
1188
|
-
*
|
|
1195
|
+
* The server answers these with a status that is not a 5xx (402 where money or
|
|
1196
|
+
* an allowance is the remedy, 403 for a model the plan excludes, 429 for the
|
|
1197
|
+
* plan's concurrent-session ceiling) and a sentence that already says what is
|
|
1198
|
+
* wrong and what to do — 1408f64ab4 and OSK-8780 moved them off `503
|
|
1189
1199
|
* admission-unavailable` precisely so a short account would stop reading as an
|
|
1190
1200
|
* outage. Prefixing "Managed inference unavailable" put the outage back on:
|
|
1191
1201
|
* the reader was told the platform was broken and, underneath, that their
|
|
1192
1202
|
* balance was empty, and the first sentence is the one people act on.
|
|
1193
1203
|
*
|
|
1194
|
-
*
|
|
1195
|
-
*
|
|
1204
|
+
* Mirrored from `sessionInferenceRelay.js`, which declares them inline: no
|
|
1205
|
+
* package owns this vocabulary, so there is nothing to import it from. A new
|
|
1206
|
+
* account-decision code the server adds and this set lacks degrades to the
|
|
1207
|
+
* cautious wording AND the fallback — exactly what OSK-8822 reported for the four
|
|
1208
|
+
* plan codes — so it belongs here in the same change.
|
|
1209
|
+
*
|
|
1210
|
+
* Membership STOPS the run (OSK-8822), not only words it: a decision about the
|
|
1211
|
+
* account is not answered by starting the engine on some other credential.
|
|
1212
|
+
* Everything else keeps the cautious wording and the fallback, which is right
|
|
1213
|
+
* for something this CLI does not recognise.
|
|
1214
|
+
*
|
|
1215
|
+
* `plan-concurrency-exhausted` clears on its own and is here anyway: the server
|
|
1216
|
+
* READ the plan and named the remedy (end another session), and falling back
|
|
1217
|
+
* would move this session's spend onto the user's own key because a different
|
|
1218
|
+
* session holds the slot.
|
|
1196
1219
|
*
|
|
1197
1220
|
* `budget-exhausted` is deliberately NOT here. It reports a lease's budget or
|
|
1198
1221
|
* concurrency slot being held — including the case where a user who quit and
|
|
@@ -1201,14 +1224,61 @@ function managedRenewalStoppedLine(reason, detail) {
|
|
|
1201
1224
|
* the answer. Widening this set to a code whose semantics are inferred rather
|
|
1202
1225
|
* than read is how honest copy drifts back into vague copy.
|
|
1203
1226
|
*/
|
|
1204
|
-
const ACCOUNT_DECISION_CODES = new Set([
|
|
1205
|
-
|
|
1227
|
+
const ACCOUNT_DECISION_CODES = new Set([
|
|
1228
|
+
'balance-exhausted',
|
|
1229
|
+
'billing-account-missing',
|
|
1230
|
+
'plan-allowance-exhausted',
|
|
1231
|
+
'plan-model-denied',
|
|
1232
|
+
'plan-window-exhausted',
|
|
1233
|
+
'plan-concurrency-exhausted',
|
|
1234
|
+
]);
|
|
1235
|
+
/**
|
|
1236
|
+
* What a failed acquisition does next, and the ONE line that says so.
|
|
1237
|
+
*
|
|
1238
|
+
* One function for both because they must never disagree: a line promising
|
|
1239
|
+
* "continuing with your own provider credentials" over a run that was stopped,
|
|
1240
|
+
* or "did not start the engine" over one that started, is the confusion between
|
|
1241
|
+
* managed, BYOK and offline that I4 forbids.
|
|
1242
|
+
*
|
|
1243
|
+
* The line names the machine-readable code as well as the sentence: the codes
|
|
1244
|
+
* are the server's typed refusals (`model-denied`, `budget-exhausted`,
|
|
1245
|
+
* `policy-unavailable` …) and "which of those happened" is the first thing
|
|
1246
|
+
* support asks. It ends by saying what is about to happen instead, reading the
|
|
1247
|
+
* environment for whether "your own key" is a thing that exists here, because
|
|
1248
|
+
* naming the wrong one of managed / BYOK / offline is the same failure.
|
|
1249
|
+
*/
|
|
1250
|
+
function managedAcquisitionFailure(err, env = process.env) {
|
|
1206
1251
|
const code = errorCodeOf(err);
|
|
1207
1252
|
if (code && ACCOUNT_DECISION_CODES.has(code)) {
|
|
1208
1253
|
// No "unavailable": nothing is down. The server's sentence names the pot
|
|
1209
1254
|
// and the remedy, so it leads.
|
|
1210
|
-
return
|
|
1255
|
+
return {
|
|
1256
|
+
action: 'refuse',
|
|
1257
|
+
code,
|
|
1258
|
+
line: `Managed inference was refused (${code}): ${describe(err)}. ${refusalTail(env)}`,
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
return {
|
|
1262
|
+
action: 'fallback',
|
|
1263
|
+
...(code ? { code } : {}),
|
|
1264
|
+
line: `Managed inference unavailable${code ? ` (${code})` : ''}: ${describe(err)}. ` +
|
|
1265
|
+
fallbackTail(env),
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
/**
|
|
1269
|
+
* What a refused run says where a fallback would have said where it was going.
|
|
1270
|
+
*
|
|
1271
|
+
* The remedy is the server's sentence, which comes first; this adds only what the
|
|
1272
|
+
* server cannot know. That the engine did not start, so a reader who next sees a
|
|
1273
|
+
* shell prompt does not wonder whether the task ran. And, when this machine HAS a
|
|
1274
|
+
* provider key, how to spend it deliberately — the managed opt-out, which makes
|
|
1275
|
+
* BYOK a declared mode instead of something a plan refusal switched on. Never
|
|
1276
|
+
* offered where there is no key to spend.
|
|
1277
|
+
*/
|
|
1278
|
+
function refusalTail(env) {
|
|
1279
|
+
if ((0, sky_code_1.hasProviderCredential)(env)) {
|
|
1280
|
+
return ('`skrr code` did not start the engine; to run on your own provider key instead, ' +
|
|
1281
|
+
`set ${sky_code_managed_1.MANAGED_ENV_FLAG}=0.`);
|
|
1211
1282
|
}
|
|
1212
|
-
return
|
|
1213
|
-
fallbackTail(env));
|
|
1283
|
+
return '`skrr code` did not start the engine.';
|
|
1214
1284
|
}
|
package/dist/lib/sky-code.d.ts
CHANGED
|
@@ -169,6 +169,36 @@ export declare function notInstalledMessage(env?: NodeJS.ProcessEnv): string;
|
|
|
169
169
|
export interface EngineExitOutcome {
|
|
170
170
|
interrupted: boolean;
|
|
171
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* OSK-8822 — the run was REFUSED before the engine started.
|
|
174
|
+
*
|
|
175
|
+
* `prepare` used to have exactly one way to answer: an env map, where `{}` means
|
|
176
|
+
* "run on the operator's own credentials". So when the server refused the
|
|
177
|
+
* ACCOUNT — a plan allowance used up, a model the plan does not include — the
|
|
178
|
+
* refusal could only become a BYOK run, and the engine started with no
|
|
179
|
+
* credential and failed a second time with a generic error, exiting with the
|
|
180
|
+
* same `1` as a task that ran and failed.
|
|
181
|
+
*
|
|
182
|
+
* A distinct type rather than a sentinel env, so the one outcome that must stop
|
|
183
|
+
* the run cannot be produced by accident and cannot be mistaken for an ordinary
|
|
184
|
+
* `prepare` failure. Whoever throws it has already told the user why: it is a
|
|
185
|
+
* control signal, and its message is for logs, not for printing a second time.
|
|
186
|
+
*/
|
|
187
|
+
export declare class EngineStartRefusedError extends Error {
|
|
188
|
+
/** The refusal's machine-readable code, e.g. `plan-allowance-exhausted`. */
|
|
189
|
+
readonly code: string;
|
|
190
|
+
constructor(code: string, message: string);
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* The exit code of a refused run: sysexits' `EX_NOPERM`, the one that means
|
|
194
|
+
* "not permitted" rather than "not available".
|
|
195
|
+
*
|
|
196
|
+
* CLI-owned, like `127` for a missing engine, because nothing ran to report one.
|
|
197
|
+
* It has to differ from `1`, which is what an engine that started and failed
|
|
198
|
+
* reports — the production case this exists for exited `1`, and CI could not tell
|
|
199
|
+
* "your plan refused this" from "the task failed".
|
|
200
|
+
*/
|
|
201
|
+
export declare const ENGINE_START_REFUSED_EXIT_CODE = 77;
|
|
172
202
|
export interface EngineSpawnOptions {
|
|
173
203
|
/**
|
|
174
204
|
* OSK-3892 — run before the engine is spawned, to acquire whatever the child
|
|
@@ -177,6 +207,9 @@ export interface EngineSpawnOptions {
|
|
|
177
207
|
* Async, which is why this function is no longer a bare `new Promise` executor:
|
|
178
208
|
* that shape cannot `await`, and a managed session has to reach the API before
|
|
179
209
|
* the engine exists.
|
|
210
|
+
*
|
|
211
|
+
* Rejecting with {@link EngineStartRefusedError} refuses the run: the engine is
|
|
212
|
+
* never spawned, `cleanup` still runs, and the error reaches the caller.
|
|
180
213
|
*/
|
|
181
214
|
prepare?: () => Promise<Record<string, string>>;
|
|
182
215
|
/** Always run, on every exit path. Tear down anything `prepare` created. */
|