@skrr-ai/cli 0.1.48 → 0.1.50
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/README.md +5 -4
- package/dist/base-command.js +16 -4
- package/dist/commands/agent-worker.d.ts +12 -0
- package/dist/commands/agent-worker.js +140 -0
- package/dist/commands/agents/create.js +14 -7
- package/dist/commands/agents/update.js +11 -5
- package/dist/commands/code/index.d.ts +1 -0
- package/dist/commands/code/index.js +49 -5
- package/dist/commands/code/run.js +5 -1
- package/dist/commands/commitments/cycles.d.ts +19 -0
- package/dist/commands/commitments/cycles.js +46 -0
- package/dist/commands/commitments/effective-policy.js +26 -1
- package/dist/commands/commitments/explain.d.ts +17 -0
- package/dist/commands/commitments/explain.js +40 -0
- package/dist/commands/goals/create.d.ts +1 -0
- package/dist/commands/goals/create.js +22 -0
- package/dist/commands/logout.js +12 -1
- package/dist/commands/machines/dedicated/index.js +1 -1
- package/dist/commands/machines/dedicated/sign-in.js +4 -1
- package/dist/commands/machines/dedicated/terminal/kill.d.ts +21 -0
- package/dist/commands/machines/dedicated/terminal/kill.js +55 -0
- package/dist/commands/machines/dedicated/terminal/ls.d.ts +22 -0
- package/dist/commands/machines/dedicated/terminal/ls.js +70 -0
- package/dist/commands/machines/dedicated/terminal/rename.d.ts +23 -0
- package/dist/commands/machines/dedicated/terminal/rename.js +64 -0
- package/dist/commands/machines/dedicated/terminal.d.ts +9 -0
- package/dist/commands/machines/dedicated/terminal.js +27 -2
- package/dist/commands/machines/hosted/connect.js +2 -2
- package/dist/commands/machines/hosted/destroy.js +1 -1
- package/dist/commands/machines/hosted/exec.js +1 -1
- package/dist/commands/machines/hosted/list.js +1 -1
- package/dist/commands/machines/hosted/pause.js +1 -1
- package/dist/commands/machines/hosted/pull.js +1 -1
- package/dist/commands/machines/hosted/resume.js +1 -1
- package/dist/commands/machines/hosted/start.js +2 -2
- package/dist/commands/machines/hosted/status.js +2 -1
- package/dist/commands/tasks/create.js +1 -0
- package/dist/commands/tasks/list.js +1 -0
- package/dist/commands/tasks/update.js +1 -0
- package/dist/lib/agent-config.d.ts +2 -0
- package/dist/lib/agent-config.js +3 -1
- package/dist/lib/api-fetch.js +19 -0
- package/dist/lib/auth-storage.d.ts +14 -0
- package/dist/lib/auth-storage.js +14 -0
- package/dist/lib/commitments.d.ts +17 -2
- package/dist/lib/commitments.js +114 -0
- package/dist/lib/daemon-installer.d.ts +6 -5
- package/dist/lib/daemon-installer.js +4 -17
- package/dist/lib/daemonBroker.d.ts +120 -31
- package/dist/lib/daemonBroker.js +313 -20
- package/dist/lib/dedicated-lease-command.d.ts +14 -0
- package/dist/lib/dedicated-lease-command.js +30 -1
- package/dist/lib/dedicated-machines.js +8 -25
- package/dist/lib/dedicated-service-command.d.ts +11 -3
- package/dist/lib/dedicated-service-command.js +20 -3
- package/dist/lib/dedicated-service.d.ts +46 -0
- package/dist/lib/dedicated-service.js +85 -7
- package/dist/lib/dedicated-terminal.d.ts +21 -0
- package/dist/lib/dedicated-terminal.js +104 -9
- package/dist/lib/first-party-harness-broker.d.ts +10 -0
- package/dist/lib/first-party-harness-broker.js +9 -0
- package/dist/lib/first-party-harness-doctor.js +41 -1
- package/dist/lib/first-party-harness-managed.d.ts +4 -3
- package/dist/lib/first-party-harness-project-trust.d.ts +125 -0
- package/dist/lib/first-party-harness-project-trust.js +364 -0
- package/dist/lib/first-party-harness.d.ts +9 -2
- package/dist/lib/first-party-harness.js +6 -5
- package/dist/lib/hosted-machines.d.ts +10 -1
- package/dist/lib/hosted-machines.js +36 -2
- package/dist/lib/login.js +16 -0
- package/dist/lib/machine-spend-cap.d.ts +23 -0
- package/dist/lib/machine-spend-cap.js +68 -0
- package/dist/lib/node-adapter.js +15 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +90 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +113 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.d.ts +47 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.js +69 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.d.ts +109 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +171 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +1 -1
- 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 +4 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.d.ts +8 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.js +9 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +8 -9
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refreshClassification.d.ts +6 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refreshClassification.js +32 -9
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseManifest.d.ts +38 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/releaseManifest.js +66 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/sessionPermissionAuthority.d.ts +81 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/sessionPermissionAuthority.js +87 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/types.d.ts +6 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +90 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +105 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.d.ts +47 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.js +66 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.d.ts +109 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +164 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +1 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +1 -1
- 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/messages.d.ts +8 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.js +9 -4
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +8 -9
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refreshClassification.d.ts +6 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refreshClassification.js +31 -9
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseManifest.d.ts +38 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/releaseManifest.js +64 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/sessionPermissionAuthority.d.ts +81 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/sessionPermissionAuthority.js +80 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/types.d.ts +6 -1
- package/dist/node_modules/@skrr-ai/auth-core/package.json +41 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +3413 -3343
- package/oclif.manifest.json +8930 -8466
- package/package.json +4 -1
package/dist/lib/daemonBroker.js
CHANGED
|
@@ -80,11 +80,18 @@ exports.resolveCliHandoffCandidatePaths = resolveCliHandoffCandidatePaths;
|
|
|
80
80
|
exports.findLiveDaemonBootstrap = findLiveDaemonBootstrap;
|
|
81
81
|
exports.readBootstrap = readBootstrap;
|
|
82
82
|
exports.attemptDaemonBrokerLogin = attemptDaemonBrokerLogin;
|
|
83
|
+
exports.isBrokeredHandoffToken = isBrokeredHandoffToken;
|
|
84
|
+
exports.redeemBrokeredHandoffToken = redeemBrokeredHandoffToken;
|
|
85
|
+
exports.__resetBrokeredHandoffForTest = __resetBrokeredHandoffForTest;
|
|
86
|
+
exports.findBrokeredHandoffDescriptor = findBrokeredHandoffDescriptor;
|
|
87
|
+
exports.maybeRetireStoredCliAuthFile = maybeRetireStoredCliAuthFile;
|
|
83
88
|
exports.attemptDaemonBrokerLoginAndPersist = attemptDaemonBrokerLoginAndPersist;
|
|
84
89
|
exports.maybeAutoBroker = maybeAutoBroker;
|
|
85
90
|
const fs = __importStar(require("node:fs"));
|
|
86
91
|
const path = __importStar(require("node:path"));
|
|
87
92
|
const os = __importStar(require("node:os"));
|
|
93
|
+
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
94
|
+
const cli_handoff_wire_1 = require("@skrr-ai/auth-core/cli-handoff-wire");
|
|
88
95
|
const auth_storage_1 = require("./auth-storage");
|
|
89
96
|
const config_1 = require("./config");
|
|
90
97
|
const device_id_1 = require("./device-id");
|
|
@@ -233,19 +240,25 @@ function readBootstrap(filePath) {
|
|
|
233
240
|
catch {
|
|
234
241
|
return null;
|
|
235
242
|
}
|
|
236
|
-
let
|
|
243
|
+
let parsedJson;
|
|
237
244
|
try {
|
|
238
|
-
|
|
245
|
+
parsedJson = JSON.parse(raw);
|
|
239
246
|
}
|
|
240
247
|
catch {
|
|
241
248
|
return null;
|
|
242
249
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
250
|
+
// Shared wire parser (`@skrr-ai/auth-core/cli-handoff-wire`) validates the
|
|
251
|
+
// fields a caller cannot proceed without — host, port, secret — and
|
|
252
|
+
// tolerates unknown fields and future `version` values. The CLI's own
|
|
253
|
+
// checks below stay STRICTER on purpose: the broker is loopback-only, the
|
|
254
|
+
// port must be a real TCP port, and the secret is a fixed-width key.
|
|
255
|
+
const parsed = (0, cli_handoff_wire_1.parseCliHandoffDescriptor)(parsedJson);
|
|
256
|
+
if (!parsed)
|
|
257
|
+
return null;
|
|
258
|
+
if (parsed.host !== '127.0.0.1' ||
|
|
259
|
+
typeof parsed.port !== 'number' ||
|
|
247
260
|
parsed.port > 65_535 ||
|
|
248
|
-
typeof parsed
|
|
261
|
+
typeof parsed.secret !== 'string' ||
|
|
249
262
|
Buffer.from(parsed.secret, 'base64').length !== 32) {
|
|
250
263
|
return null;
|
|
251
264
|
}
|
|
@@ -283,6 +296,8 @@ async function attemptDaemonBrokerLogin(opts) {
|
|
|
283
296
|
: resolveCliHandoffCandidatePaths(profile);
|
|
284
297
|
// Bound candidates (their serverUrl matches this CLI) in order, then unbound
|
|
285
298
|
// ones. Every usable candidate is kept, not just the first: see below.
|
|
299
|
+
// `sourcePath` rides along — the brokered-migration cleanup keys on whether
|
|
300
|
+
// the redeeming descriptor IS the Dedicated guest hand-off file.
|
|
286
301
|
const usable = [];
|
|
287
302
|
const unbound = [];
|
|
288
303
|
let mismatchedBootstrap = null;
|
|
@@ -300,10 +315,10 @@ async function attemptDaemonBrokerLogin(opts) {
|
|
|
300
315
|
}
|
|
301
316
|
if (typeof candidateBootstrap.serverUrl !== 'string' ||
|
|
302
317
|
candidateBootstrap.serverUrl.length === 0) {
|
|
303
|
-
unbound.push(candidateBootstrap);
|
|
318
|
+
unbound.push({ bootstrap: candidateBootstrap, sourcePath: candidate });
|
|
304
319
|
continue;
|
|
305
320
|
}
|
|
306
|
-
usable.push(candidateBootstrap);
|
|
321
|
+
usable.push({ bootstrap: candidateBootstrap, sourcePath: candidate });
|
|
307
322
|
}
|
|
308
323
|
usable.push(...unbound);
|
|
309
324
|
if (usable.length === 0) {
|
|
@@ -329,14 +344,153 @@ async function attemptDaemonBrokerLogin(opts) {
|
|
|
329
344
|
// in". Only a refused connection falls through: any answer, or a timeout, is a
|
|
330
345
|
// daemon that is really there, and its outcome stands.
|
|
331
346
|
let outcome = null;
|
|
332
|
-
for (const bootstrap of usable) {
|
|
333
|
-
outcome = await brokerThrough(bootstrap, opts);
|
|
347
|
+
for (const { bootstrap, sourcePath } of usable) {
|
|
348
|
+
outcome = await brokerThrough(bootstrap, opts, sourcePath);
|
|
334
349
|
if (outcome.ok || outcome.reason !== 'network')
|
|
335
350
|
return outcome;
|
|
336
351
|
}
|
|
337
352
|
return outcome;
|
|
338
353
|
}
|
|
339
|
-
async function brokerThrough(bootstrap, opts) {
|
|
354
|
+
async function brokerThrough(bootstrap, opts, sourcePath) {
|
|
355
|
+
// Mode negotiation (descriptor v2 `handoffModes`). A v1 descriptor advertises
|
|
356
|
+
// nothing and reads as `['refresh_family']` — today's behaviour, unchanged.
|
|
357
|
+
//
|
|
358
|
+
// The token route is attempted when the descriptor offers `access_token` AND
|
|
359
|
+
// either the caller prefers broker mode (auto-broker) or the durable mint is
|
|
360
|
+
// not offered at all (the Dedicated guest shape — its hand-off-only secret
|
|
361
|
+
// cannot mint a family, so the token route is the only door).
|
|
362
|
+
const modes = (0, cli_handoff_wire_1.advertisedHandoffModes)(bootstrap);
|
|
363
|
+
const offersAccessToken = modes.includes('access_token');
|
|
364
|
+
const offersRefreshFamily = modes.includes('refresh_family');
|
|
365
|
+
const preferAccessToken = opts.preferHandoffMode === 'access_token';
|
|
366
|
+
if (offersAccessToken && (preferAccessToken || !offersRefreshFamily)) {
|
|
367
|
+
const redeem = await redeemCliHandoffToken(bootstrap, {
|
|
368
|
+
forceRefresh: false,
|
|
369
|
+
timeoutMs: opts.timeoutMs,
|
|
370
|
+
});
|
|
371
|
+
if (redeem.ok) {
|
|
372
|
+
const response = redeem.body;
|
|
373
|
+
registerBrokeredHandoff(bootstrap, response.accessToken, sourcePath);
|
|
374
|
+
if (sourcePath === exports.DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR) {
|
|
375
|
+
// A guest descriptor's secret can ONLY redeem access tokens, so a
|
|
376
|
+
// stored cli-auth.json on this machine predates broker mode. Retire
|
|
377
|
+
// the old family (best-effort revoke + delete); see the helper for
|
|
378
|
+
// the provenance guard.
|
|
379
|
+
await maybeRetireStoredCliAuthFile({
|
|
380
|
+
descriptorPath: sourcePath,
|
|
381
|
+
descriptorServerUrl: bootstrap.serverUrl ?? response.serverUrl,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
ok: true,
|
|
386
|
+
brokered: true,
|
|
387
|
+
accessToken: response.accessToken,
|
|
388
|
+
...(response.accessExpiresAt !== undefined
|
|
389
|
+
? { accessExpiresAt: response.accessExpiresAt }
|
|
390
|
+
: {}),
|
|
391
|
+
...((response.serverUrl ?? bootstrap.serverUrl)
|
|
392
|
+
? { serverUrl: response.serverUrl ?? bootstrap.serverUrl }
|
|
393
|
+
: {}),
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
// The descriptor ALSO advertises the durable mint (a laptop island during
|
|
397
|
+
// transition): any redeem failure — refusal, network, outage — falls
|
|
398
|
+
// through to the legacy route below. When refresh_family is NOT offered,
|
|
399
|
+
// the redeem outcome is final: the legacy route would only answer
|
|
400
|
+
// 403 HANDOFF_BROKER_ONLY anyway.
|
|
401
|
+
if (!offersRefreshFamily)
|
|
402
|
+
return redeem;
|
|
403
|
+
}
|
|
404
|
+
return mintRefreshFamilyThrough(bootstrap, opts);
|
|
405
|
+
}
|
|
406
|
+
/**
|
|
407
|
+
* Broker-mode redeem — `POST /v1/auth/cli-handoff-token` with the
|
|
408
|
+
* descriptor's secret as bearer. Returns the parsed token response or a
|
|
409
|
+
* `DaemonBrokerFailure` the caller can return verbatim / fall through from.
|
|
410
|
+
*/
|
|
411
|
+
async function redeemCliHandoffToken(bootstrap, opts = {}) {
|
|
412
|
+
const url = `http://${bootstrap.host}:${bootstrap.port}${cli_handoff_wire_1.CLI_HANDOFF_TOKEN_PATH}`;
|
|
413
|
+
const timeoutMs = opts.timeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
414
|
+
const controller = new AbortController();
|
|
415
|
+
const abortTimer = setTimeout(() => controller.abort(), timeoutMs);
|
|
416
|
+
let response;
|
|
417
|
+
try {
|
|
418
|
+
response = await fetch(url, {
|
|
419
|
+
method: 'POST',
|
|
420
|
+
headers: {
|
|
421
|
+
Authorization: `Bearer ${bootstrap.secret}`,
|
|
422
|
+
'Content-Type': 'application/json',
|
|
423
|
+
Accept: 'application/json',
|
|
424
|
+
},
|
|
425
|
+
// No cliId — the family is daemon-owned; the caller is not enrolling a
|
|
426
|
+
// device. forceRefresh bypasses the daemon's cached access token (the
|
|
427
|
+
// 401-recovery path).
|
|
428
|
+
body: JSON.stringify({ forceRefresh: opts.forceRefresh === true }),
|
|
429
|
+
signal: controller.signal,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
catch (err) {
|
|
433
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
434
|
+
if (err.name === 'AbortError') {
|
|
435
|
+
return {
|
|
436
|
+
ok: false,
|
|
437
|
+
reason: 'timeout',
|
|
438
|
+
detail: `Handoff token redeem timed out after ${timeoutMs}ms`,
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
return { ok: false, reason: 'network', detail: msg };
|
|
442
|
+
}
|
|
443
|
+
finally {
|
|
444
|
+
clearTimeout(abortTimer);
|
|
445
|
+
}
|
|
446
|
+
let parsedBody = null;
|
|
447
|
+
try {
|
|
448
|
+
parsedBody = (await response.json());
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
parsedBody = null;
|
|
452
|
+
}
|
|
453
|
+
if (!response.ok) {
|
|
454
|
+
const code = typeof parsedBody?.code === 'string' ? parsedBody.code : undefined;
|
|
455
|
+
const message = typeof parsedBody?.message === 'string'
|
|
456
|
+
? parsedBody.message
|
|
457
|
+
: `Handoff token route returned HTTP ${response.status}`;
|
|
458
|
+
if (response.status === 401 || response.status === 403) {
|
|
459
|
+
// The daemon refused the descriptor secret outright — the descriptor is
|
|
460
|
+
// stale (secret rotated on daemon restart) or was never valid for this
|
|
461
|
+
// route.
|
|
462
|
+
return {
|
|
463
|
+
ok: false,
|
|
464
|
+
reason: 'stale_bootstrap',
|
|
465
|
+
detail: message,
|
|
466
|
+
status: response.status,
|
|
467
|
+
...(code ? { code } : {}),
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
ok: false,
|
|
472
|
+
// 503 HANDOFF_TOKEN_UNAVAILABLE lands in server_error with its code
|
|
473
|
+
// preserved — the daemon is alive but cannot mint (no daemon bearer,
|
|
474
|
+
// upstream unreachable, mint cool-down). That is a daemon outage, not a
|
|
475
|
+
// decline, and the code keeps it diagnosable.
|
|
476
|
+
reason: response.status >= 500 ? 'server_error' : 'unknown',
|
|
477
|
+
detail: message,
|
|
478
|
+
status: response.status,
|
|
479
|
+
...(code ? { code } : {}),
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
const body = (0, cli_handoff_wire_1.parseCliHandoffTokenResponse)(parsedBody);
|
|
483
|
+
if (!body) {
|
|
484
|
+
return {
|
|
485
|
+
ok: false,
|
|
486
|
+
reason: 'server_error',
|
|
487
|
+
detail: 'Handoff token route returned 200 but the response carried no access token',
|
|
488
|
+
status: response.status,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
return { ok: true, body };
|
|
492
|
+
}
|
|
493
|
+
async function mintRefreshFamilyThrough(bootstrap, opts) {
|
|
340
494
|
const url = `http://${bootstrap.host}:${bootstrap.port}/v1/auth/cli-handoff`;
|
|
341
495
|
const timeoutMs = opts.timeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
342
496
|
const controller = new AbortController();
|
|
@@ -389,6 +543,20 @@ async function brokerThrough(bootstrap, opts) {
|
|
|
389
543
|
if (response.status === 403 && code === 'CI_TOKEN_REFUSED') {
|
|
390
544
|
return { ok: false, reason: 'ci_token_refused', detail: message, status: 403, code };
|
|
391
545
|
}
|
|
546
|
+
if (response.status === 403 && code === cli_handoff_wire_1.CLI_HANDOFF_ERROR_BROKER_ONLY) {
|
|
547
|
+
// The daemon refuses to mint a refresh family for this secret — the
|
|
548
|
+
// descriptor offers `access_token`, and broker mode is the only door.
|
|
549
|
+
// Terminal (not a next-candidate fall-through): every descriptor for
|
|
550
|
+
// this daemon answers the same way, and the caller's remedy is the
|
|
551
|
+
// token route or an explicit `skrr login`, not another file.
|
|
552
|
+
return {
|
|
553
|
+
ok: false,
|
|
554
|
+
reason: 'handoff_broker_only',
|
|
555
|
+
detail: message,
|
|
556
|
+
status: 403,
|
|
557
|
+
code,
|
|
558
|
+
};
|
|
559
|
+
}
|
|
392
560
|
if (response.status === 503 && code === 'DAEMON_NOT_AUTHED') {
|
|
393
561
|
return { ok: false, reason: 'daemon_not_authed', detail: message, status: 503, code };
|
|
394
562
|
}
|
|
@@ -457,6 +625,116 @@ async function brokerThrough(bootstrap, opts) {
|
|
|
457
625
|
...(typeof parsedBody.familyId === 'string' ? { familyId: parsedBody.familyId } : {}),
|
|
458
626
|
};
|
|
459
627
|
}
|
|
628
|
+
// ---------------------------------------------------------------------
|
|
629
|
+
// Brokered (access-token) hand-off — process-local credential state
|
|
630
|
+
// ---------------------------------------------------------------------
|
|
631
|
+
/**
|
|
632
|
+
* The descriptor this process's brokered credential was redeemed from, and
|
|
633
|
+
* the token it answered. Module-scoped on purpose: broker mode persists
|
|
634
|
+
* NOTHING, so the only record of "the credential in use came from a hand-off
|
|
635
|
+
* redeem" lives here, for the 401 re-redeem path in `api-fetch`
|
|
636
|
+
* (`forceRefresh: true`, retry once). Each new `skrr` process starts empty
|
|
637
|
+
* and re-redeems on entry via `maybeAutoBroker`.
|
|
638
|
+
*/
|
|
639
|
+
let _brokeredHandoff = null;
|
|
640
|
+
function registerBrokeredHandoff(descriptor, accessToken, sourcePath) {
|
|
641
|
+
_brokeredHandoff = { descriptor, accessToken, ...(sourcePath ? { sourcePath } : {}) };
|
|
642
|
+
}
|
|
643
|
+
/**
|
|
644
|
+
* True when `token` is the access token this process redeemed through the
|
|
645
|
+
* daemon's broker-mode hand-off. `api-fetch` uses this to pick the re-redeem
|
|
646
|
+
* recovery branch instead of the stored-family refresh (which would have
|
|
647
|
+
* nothing to rotate — brokered credentials are never persisted).
|
|
648
|
+
*/
|
|
649
|
+
function isBrokeredHandoffToken(token) {
|
|
650
|
+
return Boolean(token && _brokeredHandoff && token === _brokeredHandoff.accessToken);
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Re-redeem the brokered hand-off token — the 401-recovery path. The
|
|
654
|
+
* descriptor is re-read from disk when its source path is known, so a secret
|
|
655
|
+
* rotated by a daemon restart resolves to the fresh one. Returns null when
|
|
656
|
+
* this process holds no brokered credential or the redeem fails (the caller
|
|
657
|
+
* then surfaces the original 401).
|
|
658
|
+
*/
|
|
659
|
+
async function redeemBrokeredHandoffToken(forceRefresh = true) {
|
|
660
|
+
const state = _brokeredHandoff;
|
|
661
|
+
if (!state)
|
|
662
|
+
return null;
|
|
663
|
+
const descriptor = (state.sourcePath ? readBootstrap(state.sourcePath) : null) ?? state.descriptor;
|
|
664
|
+
const result = await redeemCliHandoffToken(descriptor, { forceRefresh });
|
|
665
|
+
if (!result.ok)
|
|
666
|
+
return null;
|
|
667
|
+
_brokeredHandoff = { ...state, descriptor, accessToken: result.body.accessToken };
|
|
668
|
+
return result.body;
|
|
669
|
+
}
|
|
670
|
+
/** @internal test seam — clears the module-scoped brokered credential. */
|
|
671
|
+
function __resetBrokeredHandoffForTest() {
|
|
672
|
+
_brokeredHandoff = null;
|
|
673
|
+
}
|
|
674
|
+
/**
|
|
675
|
+
* True when a live local descriptor advertises broker-mode access — i.e.
|
|
676
|
+
* this machine's CLI authority is platform-managed and `skrr logout` has no
|
|
677
|
+
* local credential to clear. Read-only: parses candidates, never redeems.
|
|
678
|
+
*/
|
|
679
|
+
function findBrokeredHandoffDescriptor(profile = 'default') {
|
|
680
|
+
for (const candidate of resolveCliHandoffCandidatePaths(profile)) {
|
|
681
|
+
const bootstrap = readBootstrap(candidate);
|
|
682
|
+
if (bootstrap && (0, cli_handoff_wire_1.descriptorOffersAccessToken)(bootstrap))
|
|
683
|
+
return bootstrap;
|
|
684
|
+
}
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Migration cleanup: retire the refresh family a pre-broker-mode CLI
|
|
689
|
+
* generation persisted, now that this machine redeems access tokens from
|
|
690
|
+
* the daemon's in-memory broker.
|
|
691
|
+
*
|
|
692
|
+
* Provenance guard — locally we cannot tell a hand-off-minted family from a
|
|
693
|
+
* user's explicit `skrr login` family by shape (the distinguishing
|
|
694
|
+
* `daemonRefOverride` lives server-side). The safe rule, applied here:
|
|
695
|
+
* revoke + delete ONLY when all of these hold —
|
|
696
|
+
*
|
|
697
|
+
* 1. the descriptor that just redeemed IS the Dedicated Runtime guest
|
|
698
|
+
* hand-off file (`/run/skrr-dedicated-runtime/...`),
|
|
699
|
+
* 2. `~/.skrr/cli-auth.json` reads cleanly and carries a refresh token,
|
|
700
|
+
* 3. its stored `serverOrigin` matches the descriptor's `serverUrl`.
|
|
701
|
+
*
|
|
702
|
+
* When provenance can't be proven the file is KEPT: an explicit `skrr login`
|
|
703
|
+
* on a guest is rare but real, and a stale extra family is bounded by the
|
|
704
|
+
* server's session cap and expires on its own. Best-effort throughout — a
|
|
705
|
+
* failed revoke still deletes the file (the whole point is that nothing
|
|
706
|
+
* credential-shaped should remain), and any failure leaves the working
|
|
707
|
+
* brokered credential untouched.
|
|
708
|
+
*/
|
|
709
|
+
async function maybeRetireStoredCliAuthFile(opts) {
|
|
710
|
+
if (opts.descriptorPath !== exports.DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR)
|
|
711
|
+
return;
|
|
712
|
+
let stored;
|
|
713
|
+
try {
|
|
714
|
+
stored = (0, auth_storage_1.readFromFile)();
|
|
715
|
+
}
|
|
716
|
+
catch {
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (!stored?.refreshToken)
|
|
720
|
+
return;
|
|
721
|
+
const storedOrigin = stored.serverOrigin ? (0, auth_storage_1.normalizeServerOrigin)(stored.serverOrigin) : null;
|
|
722
|
+
const descriptorOrigin = opts.descriptorServerUrl
|
|
723
|
+
? (0, auth_storage_1.normalizeServerOrigin)(opts.descriptorServerUrl)
|
|
724
|
+
: null;
|
|
725
|
+
if (!storedOrigin || !descriptorOrigin || storedOrigin !== descriptorOrigin)
|
|
726
|
+
return;
|
|
727
|
+
// The revoke is authenticated by the refresh token itself — presenting it
|
|
728
|
+
// is the proof of ownership. `revokeDaemonRefreshSession` swallows its own
|
|
729
|
+
// network/HTTP failures (best-effort by contract); the delete still runs.
|
|
730
|
+
try {
|
|
731
|
+
await (0, auth_core_1.revokeDaemonRefreshSession)(descriptorOrigin, stored.refreshToken);
|
|
732
|
+
}
|
|
733
|
+
catch {
|
|
734
|
+
/* best-effort — delete proceeds regardless */
|
|
735
|
+
}
|
|
736
|
+
(0, auth_storage_1.deleteFileBackend)();
|
|
737
|
+
}
|
|
460
738
|
/** Bound the confirm so a slow server cannot stall a login that already worked. */
|
|
461
739
|
const CONFIRM_TIMEOUT_MS = 10_000;
|
|
462
740
|
/**
|
|
@@ -514,15 +792,22 @@ function expiryToEpochMs(v) {
|
|
|
514
792
|
return undefined;
|
|
515
793
|
}
|
|
516
794
|
/**
|
|
517
|
-
* Convenience wrapper: run the broker call and, on success,
|
|
518
|
-
* returned bundle through the canonical Sky CLI auth backend
|
|
519
|
-
* on macOS, `cli-auth.json` elsewhere).
|
|
520
|
-
*
|
|
795
|
+
* Convenience wrapper: run the broker call and, on a durable-mint success,
|
|
796
|
+
* persist the returned bundle through the canonical Sky CLI auth backend
|
|
797
|
+
* (Keychain on macOS, `cli-auth.json` elsewhere). A `brokered` outcome is
|
|
798
|
+
* returned WITHOUT persisting — that is the entire point of broker mode.
|
|
799
|
+
* Returns the same outcome shape so the caller can branch on `ok`/`brokered`.
|
|
521
800
|
*/
|
|
522
801
|
async function attemptDaemonBrokerLoginAndPersist(opts) {
|
|
523
802
|
const outcome = await attemptDaemonBrokerLogin(opts);
|
|
524
803
|
if (!outcome.ok)
|
|
525
804
|
return outcome;
|
|
805
|
+
// Brokered hand-off: a daemon-owned access token. The persistence policy
|
|
806
|
+
// IS the security property here — nothing credential-shaped lands on disk,
|
|
807
|
+
// so writeToBackend is never called and there is no family to confirm.
|
|
808
|
+
// Callers use the token in-memory for this process only.
|
|
809
|
+
if (outcome.brokered)
|
|
810
|
+
return outcome;
|
|
526
811
|
const serverOrigin = normalizeBaseUrl(opts.baseURL);
|
|
527
812
|
if (!serverOrigin) {
|
|
528
813
|
throw new Error('Daemon-broker credential persistence requires a valid CLI baseURL.');
|
|
@@ -586,10 +871,12 @@ const AUTH_SELF_MANAGED_COMMANDS = new Set([
|
|
|
586
871
|
* - `commandId` is in `AUTH_SELF_MANAGED_COMMANDS`
|
|
587
872
|
*
|
|
588
873
|
* Side effects:
|
|
589
|
-
* - Calls `attemptDaemonBrokerLoginAndPersist`
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
874
|
+
* - Calls `attemptDaemonBrokerLoginAndPersist` with `preferHandoffMode:
|
|
875
|
+
* 'access_token'`. On a descriptor that advertises broker mode the
|
|
876
|
+
* outcome is `brokered` — an in-memory access token, nothing written;
|
|
877
|
+
* the caller holds it for this process and the next `skrr` re-redeems.
|
|
878
|
+
* On a legacy/v1 descriptor the durable mint is persisted to the
|
|
879
|
+
* keychain / cli-auth.json exactly as before.
|
|
593
880
|
* - May generate a new `cliId` if the config doesn't have one. The
|
|
594
881
|
* caller MUST persist `updatedConfig` so the next invocation
|
|
595
882
|
* presents the same id to the server's refresh rotator.
|
|
@@ -618,6 +905,12 @@ async function maybeAutoBroker(opts) {
|
|
|
618
905
|
const outcome = await attemptDaemonBrokerLoginAndPersist({
|
|
619
906
|
cliId,
|
|
620
907
|
baseURL: opts.cliConfig.baseURL,
|
|
908
|
+
// Auto-broker prefers the brokered access-token mode when the descriptor
|
|
909
|
+
// advertises it: nothing credential-shaped is persisted, and the next
|
|
910
|
+
// `skrr` process simply re-redeems over loopback. The legacy refresh-
|
|
911
|
+
// family mint remains the fallback for v1 descriptors and the redeem
|
|
912
|
+
// path for explicit `skrr login`.
|
|
913
|
+
preferHandoffMode: 'access_token',
|
|
621
914
|
...(opts.bootstrapPathOverride ? { bootstrapPathOverride: opts.bootstrapPathOverride } : {}),
|
|
622
915
|
});
|
|
623
916
|
return {
|
|
@@ -38,10 +38,24 @@ export declare abstract class DedicatedLeaseCommand extends BaseCommand {
|
|
|
38
38
|
* machine, a daemon that is not connected, stdin that is not a terminal — so
|
|
39
39
|
* the caller gets a sentence that says what to do instead of a start that
|
|
40
40
|
* hangs and times out.
|
|
41
|
+
*
|
|
42
|
+
* Sessions are persistent by default (always-on §6.4): the daemon runs the
|
|
43
|
+
* shell inside tmux, so Ctrl-] and a lost connection only detach and the
|
|
44
|
+
* same command re-attaches the same shell. `--ephemeral` asks for the old
|
|
45
|
+
* bare-PTY semantics, and an image without `persistent_terminal_v1` gets
|
|
46
|
+
* them too — the relay forwards `persistent` as claimed and an old daemon
|
|
47
|
+
* silently ignores it, so the capability is checked here and the degraded
|
|
48
|
+
* start is announced rather than presented as resumable.
|
|
41
49
|
*/
|
|
42
50
|
protected openDedicatedTerminal(leaseId: string, flags: {
|
|
43
51
|
workspace?: string;
|
|
44
52
|
cwd?: string;
|
|
53
|
+
/** Named session to attach-or-create (default 'main'); ignored with --new. */
|
|
54
|
+
name?: string;
|
|
55
|
+
/** A fresh session the machine names (term-N), printed once the ack reports it. */
|
|
56
|
+
new?: boolean;
|
|
57
|
+
/** The pre-persistent semantics: a bare PTY that ends with the connection. */
|
|
58
|
+
ephemeral?: boolean;
|
|
45
59
|
}, initialCommand?: string): Promise<void>;
|
|
46
60
|
/**
|
|
47
61
|
* Run an API call, turning a refusal into the server's own sentence.
|
|
@@ -8,6 +8,7 @@ const base_command_1 = require("../base-command");
|
|
|
8
8
|
const api_fetch_1 = require("./api-fetch");
|
|
9
9
|
const dedicated_machines_1 = require("./dedicated-machines");
|
|
10
10
|
const dedicated_terminal_1 = require("./dedicated-terminal");
|
|
11
|
+
const dedicated_service_1 = require("./dedicated-service");
|
|
11
12
|
const daemonBroker_1 = require("./daemonBroker");
|
|
12
13
|
const harnesses_1 = require("./harnesses");
|
|
13
14
|
const dedicated_wait_1 = require("./dedicated-wait");
|
|
@@ -72,6 +73,14 @@ class DedicatedLeaseCommand extends base_command_1.BaseCommand {
|
|
|
72
73
|
* machine, a daemon that is not connected, stdin that is not a terminal — so
|
|
73
74
|
* the caller gets a sentence that says what to do instead of a start that
|
|
74
75
|
* hangs and times out.
|
|
76
|
+
*
|
|
77
|
+
* Sessions are persistent by default (always-on §6.4): the daemon runs the
|
|
78
|
+
* shell inside tmux, so Ctrl-] and a lost connection only detach and the
|
|
79
|
+
* same command re-attaches the same shell. `--ephemeral` asks for the old
|
|
80
|
+
* bare-PTY semantics, and an image without `persistent_terminal_v1` gets
|
|
81
|
+
* them too — the relay forwards `persistent` as claimed and an old daemon
|
|
82
|
+
* silently ignores it, so the capability is checked here and the degraded
|
|
83
|
+
* start is announced rather than presented as resumable.
|
|
75
84
|
*/
|
|
76
85
|
async openDedicatedTerminal(leaseId, flags, initialCommand) {
|
|
77
86
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
@@ -92,7 +101,24 @@ class DedicatedLeaseCommand extends base_command_1.BaseCommand {
|
|
|
92
101
|
if (!harness?.daemonId) {
|
|
93
102
|
this.error(`${leaseId} is ${state}, but its daemon is not connected yet. Wait a moment, or run \`${this.config.bin} machines dedicated health-check ${leaseId}\`.`, { exit: 1 });
|
|
94
103
|
}
|
|
95
|
-
|
|
104
|
+
const capabilities = Array.isArray(harness.metadata?.capabilities)
|
|
105
|
+
? harness.metadata.capabilities.filter((capability) => typeof capability === 'string')
|
|
106
|
+
: [];
|
|
107
|
+
const supportsPersistent = capabilities.includes(dedicated_service_1.DEDICATED_PERSISTENT_TERMINAL_CAPABILITY);
|
|
108
|
+
const persistent = flags.ephemeral !== true && supportsPersistent;
|
|
109
|
+
// `terminal` with no name means "the machine's terminal" — one stable
|
|
110
|
+
// session, attach-or-create, because a CLI has no picker to resume from.
|
|
111
|
+
// `--new` sends no name and the daemon allocates the next `term-N`, the
|
|
112
|
+
// same semantics the web picker's "New terminal" button has.
|
|
113
|
+
const sessionName = persistent && flags.new !== true ? (flags.name ?? 'main') : undefined;
|
|
114
|
+
if (flags.ephemeral !== true && !supportsPersistent) {
|
|
115
|
+
process.stderr.write(`${(0, dedicated_machines_1.dedicatedLeaseDisplayName)(lease)}'s image does not support persistent terminals ` +
|
|
116
|
+
`(${dedicated_service_1.DEDICATED_PERSISTENT_TERMINAL_CAPABILITY}), so this shell ends with the connection. ` +
|
|
117
|
+
`\`${this.config.bin} machines dedicated update-image ${leaseId}\` upgrades the machine.\n`);
|
|
118
|
+
}
|
|
119
|
+
process.stderr.write(persistent
|
|
120
|
+
? `Connected to ${(0, dedicated_machines_1.dedicatedLeaseDisplayName)(lease)} (${leaseId}) as skrr-workload. Ctrl-] detaches; the session keeps running on the machine.\n`
|
|
121
|
+
: `Connected to ${(0, dedicated_machines_1.dedicatedLeaseDisplayName)(lease)} (${leaseId}) as skrr-workload. Ctrl-] closes the terminal.\n`);
|
|
96
122
|
let code;
|
|
97
123
|
try {
|
|
98
124
|
code = await (0, dedicated_terminal_1.runDedicatedTerminal)({
|
|
@@ -102,6 +128,9 @@ class DedicatedLeaseCommand extends base_command_1.BaseCommand {
|
|
|
102
128
|
daemonId: harness.daemonId,
|
|
103
129
|
...(flags.cwd ? { cwd: flags.cwd } : {}),
|
|
104
130
|
...(initialCommand ? { initialCommand } : {}),
|
|
131
|
+
persistent,
|
|
132
|
+
...(sessionName ? { sessionName } : {}),
|
|
133
|
+
leaseId,
|
|
105
134
|
input: process.stdin,
|
|
106
135
|
output: process.stdout,
|
|
107
136
|
errorOutput: process.stderr,
|
|
@@ -50,6 +50,7 @@ exports.formatDedicatedRuntimeApiError = formatDedicatedRuntimeApiError;
|
|
|
50
50
|
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
51
51
|
const data_provider_1 = require("@skrr-ai/data-provider");
|
|
52
52
|
const api_fetch_1 = require("./api-fetch");
|
|
53
|
+
const machine_spend_cap_1 = require("./machine-spend-cap");
|
|
53
54
|
const edge_error_page_1 = require("./edge-error-page");
|
|
54
55
|
const harnesses_1 = require("./harnesses");
|
|
55
56
|
exports.DEDICATED_LEASE_ACTIONS = [
|
|
@@ -1185,7 +1186,7 @@ function dedicatedRuntimeApiErrorDetails(err) {
|
|
|
1185
1186
|
*/
|
|
1186
1187
|
function describeRefusalReasons(err, context = {}) {
|
|
1187
1188
|
const details = dedicatedRuntimeApiErrorDetails(err);
|
|
1188
|
-
const spend = describeSpendingCapRefusal(err.body,
|
|
1189
|
+
const spend = describeSpendingCapRefusal(err.body, context);
|
|
1189
1190
|
if (spend)
|
|
1190
1191
|
return spend;
|
|
1191
1192
|
const sentences = (0, harnesses_1.describeHealthReasonSentences)(details?.reasons, details?.state);
|
|
@@ -1197,31 +1198,13 @@ function describeRefusalReasons(err, context = {}) {
|
|
|
1197
1198
|
* the cap. Without them "would exceed the monthly cap" left a user guessing by
|
|
1198
1199
|
* how much, and which cap (OSK-9552).
|
|
1199
1200
|
*/
|
|
1200
|
-
function describeSpendingCapRefusal(body,
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
catch {
|
|
1206
|
-
return '';
|
|
1207
|
-
}
|
|
1208
|
-
if (code !== 'RUNTIME_SPENDING_LIMIT_EXCEEDED' || !details)
|
|
1209
|
-
return '';
|
|
1210
|
-
const cents = (key) => {
|
|
1211
|
-
const value = details[key];
|
|
1212
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
1213
|
-
};
|
|
1214
|
-
const limit = cents('limitCents');
|
|
1215
|
-
const spent = cents('currentSpendCents');
|
|
1216
|
-
const projected = cents('projectedCents');
|
|
1217
|
-
const parts = [
|
|
1218
|
-
limit !== null ? `Cap ${formatUsdCents(limit)}` : '',
|
|
1219
|
-
spent !== null ? `spent this month ${formatUsdCents(spent)}` : '',
|
|
1220
|
-
projected !== null ? `this would bring it to ${formatUsdCents(projected)}` : '',
|
|
1221
|
-
].filter(Boolean);
|
|
1222
|
-
if (parts.length === 0)
|
|
1201
|
+
function describeSpendingCapRefusal(body, context = {}) {
|
|
1202
|
+
// Shared with the hosted CLI (machine-spend-cap.ts), which printed the bare
|
|
1203
|
+
// sentence until OSK-11946.
|
|
1204
|
+
const figures = (0, machine_spend_cap_1.describeMonthlyCapFigures)(body);
|
|
1205
|
+
if (!figures)
|
|
1223
1206
|
return '';
|
|
1224
|
-
return ` ${
|
|
1207
|
+
return ` ${figures}. ${raiseCapHint(context)}`;
|
|
1225
1208
|
}
|
|
1226
1209
|
/**
|
|
1227
1210
|
* How to get past a cap refusal, for the command that was refused. A create or
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DedicatedLeaseCommand } from './dedicated-lease-command';
|
|
2
|
-
import { DedicatedServiceChannel, type ServiceVerb } from './dedicated-service';
|
|
2
|
+
import { DedicatedServiceChannel, type ServiceVerb, type TerminalVerb } from './dedicated-service';
|
|
3
3
|
/**
|
|
4
4
|
* The flag set `service create`/`apply` and guest `service propose` share —
|
|
5
5
|
* one definition so the three surfaces cannot drift apart on what a spec
|
|
@@ -40,6 +40,12 @@ export declare abstract class DedicatedServiceCommand extends DedicatedLeaseComm
|
|
|
40
40
|
protected serviceChannel(): Promise<DedicatedServiceChannel>;
|
|
41
41
|
/** Emit one verb and return the daemon's `result`, or throw as a CLI error. */
|
|
42
42
|
protected callService<T = unknown>(verb: ServiceVerb, payload: Record<string, unknown>, timeoutMs?: number): Promise<T | undefined>;
|
|
43
|
+
/**
|
|
44
|
+
* Emit one persistent-terminal verb (`dedicated:terminal:list|kill|rename`)
|
|
45
|
+
* on the same admitted channel the service verbs ride — the relay answers
|
|
46
|
+
* `dedicated:terminal:ack` with the same requestId discipline.
|
|
47
|
+
*/
|
|
48
|
+
protected callDedicatedTerminal<T = unknown>(verb: TerminalVerb, payload: Record<string, unknown>, timeoutMs?: number): Promise<T | undefined>;
|
|
43
49
|
/**
|
|
44
50
|
* `start|stop|restart|rm` — the shared `dedicated:service:action` round
|
|
45
51
|
* trip and its printout. `rm` maps to the `remove` action on the wire (the
|
|
@@ -54,7 +60,9 @@ export declare abstract class DedicatedServiceCommand extends DedicatedLeaseComm
|
|
|
54
60
|
/**
|
|
55
61
|
* Turn a service-lane failure into the command's exit — the refusal
|
|
56
62
|
* sentence the relay sent, or the clean capability-missing path the SSH
|
|
57
|
-
* lane established for a missing `sshBridge`.
|
|
63
|
+
* lane established for a missing `sshBridge`. `unsupportedCode` is the
|
|
64
|
+
* lane's own code a `--json` error reports; the terminal lane passes its
|
|
65
|
+
* own so a script can tell "no persistent terminals" from "no services".
|
|
58
66
|
*/
|
|
59
|
-
protected reportServiceError(err: unknown): never;
|
|
67
|
+
protected reportServiceError(err: unknown, unsupportedCode?: string): never;
|
|
60
68
|
}
|
|
@@ -94,6 +94,21 @@ class DedicatedServiceCommand extends dedicated_lease_command_1.DedicatedLeaseCo
|
|
|
94
94
|
this.reportServiceError(err);
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Emit one persistent-terminal verb (`dedicated:terminal:list|kill|rename`)
|
|
99
|
+
* on the same admitted channel the service verbs ride — the relay answers
|
|
100
|
+
* `dedicated:terminal:ack` with the same requestId discipline.
|
|
101
|
+
*/
|
|
102
|
+
async callDedicatedTerminal(verb, payload, timeoutMs) {
|
|
103
|
+
try {
|
|
104
|
+
const channel = await this.serviceChannel();
|
|
105
|
+
const { result } = await channel.callTerminal(verb, payload, timeoutMs);
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
catch (err) {
|
|
109
|
+
this.reportServiceError(err, 'DEDICATED_RUNTIME_TERMINAL_UNSUPPORTED');
|
|
110
|
+
}
|
|
111
|
+
}
|
|
97
112
|
/**
|
|
98
113
|
* `start|stop|restart|rm` — the shared `dedicated:service:action` round
|
|
99
114
|
* trip and its printout. `rm` maps to the `remove` action on the wire (the
|
|
@@ -143,14 +158,16 @@ class DedicatedServiceCommand extends dedicated_lease_command_1.DedicatedLeaseCo
|
|
|
143
158
|
/**
|
|
144
159
|
* Turn a service-lane failure into the command's exit — the refusal
|
|
145
160
|
* sentence the relay sent, or the clean capability-missing path the SSH
|
|
146
|
-
* lane established for a missing `sshBridge`.
|
|
161
|
+
* lane established for a missing `sshBridge`. `unsupportedCode` is the
|
|
162
|
+
* lane's own code a `--json` error reports; the terminal lane passes its
|
|
163
|
+
* own so a script can tell "no persistent terminals" from "no services".
|
|
147
164
|
*/
|
|
148
|
-
reportServiceError(err) {
|
|
165
|
+
reportServiceError(err, unsupportedCode = 'DEDICATED_RUNTIME_SERVICES_UNSUPPORTED') {
|
|
149
166
|
if (err instanceof dedicated_service_1.DedicatedServiceError) {
|
|
150
167
|
if ((0, dedicated_service_1.isServiceUnsupportedCode)(err.code)) {
|
|
151
168
|
this.failWithCliError({
|
|
152
169
|
message: err.message,
|
|
153
|
-
code:
|
|
170
|
+
code: unsupportedCode,
|
|
154
171
|
exit: 1,
|
|
155
172
|
retryable: false,
|
|
156
173
|
});
|