@skrr-ai/cli 0.1.49 → 0.1.51
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 +75 -6
- package/dist/commands/agents/chat.d.ts +14 -0
- package/dist/commands/agents/chat.js +37 -1
- package/dist/commands/agents/create.js +14 -7
- package/dist/commands/agents/show.d.ts +9 -0
- package/dist/commands/agents/show.js +53 -0
- package/dist/commands/agents/update.d.ts +9 -0
- package/dist/commands/agents/update.js +38 -5
- package/dist/commands/balance/show.d.ts +34 -0
- package/dist/commands/balance/show.js +92 -2
- 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/doctor.d.ts +1 -0
- package/dist/commands/commitments/doctor.js +19 -4
- package/dist/commands/commitments/effective-policy.js +40 -1
- package/dist/commands/commitments/explain.d.ts +17 -0
- package/dist/commands/commitments/explain.js +40 -0
- package/dist/commands/commitments/next-tick.js +3 -1
- package/dist/commands/commitments/preflight.js +10 -0
- package/dist/commands/commitments/show.js +11 -1
- package/dist/commands/daemon/config.d.ts +15 -0
- package/dist/commands/daemon/config.js +38 -0
- package/dist/commands/daemon/index.js +1 -0
- package/dist/commands/daemon/status.js +15 -1
- package/dist/commands/goals/create.d.ts +1 -0
- package/dist/commands/goals/create.js +22 -0
- package/dist/commands/harnesses/models.d.ts +54 -0
- package/dist/commands/harnesses/models.js +100 -7
- package/dist/commands/harnesses/show.d.ts +11 -0
- package/dist/commands/harnesses/show.js +35 -0
- package/dist/commands/harnesses/update.d.ts +28 -1
- package/dist/commands/harnesses/update.js +73 -10
- 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/outbox/list.d.ts +12 -1
- package/dist/commands/outbox/list.js +31 -2
- package/dist/commands/revoke-daemon.js +9 -0
- package/dist/commands/spaces/create.d.ts +1 -0
- package/dist/commands/spaces/create.js +17 -0
- package/dist/commands/spaces/show.js +5 -0
- package/dist/commands/spaces/update.d.ts +1 -0
- package/dist/commands/spaces/update.js +16 -0
- package/dist/commands/tasks/create.d.ts +1 -0
- package/dist/commands/tasks/create.js +6 -0
- package/dist/commands/tasks/list.js +1 -0
- package/dist/commands/tasks/show.js +45 -0
- package/dist/commands/tasks/update.d.ts +1 -0
- package/dist/commands/tasks/update.js +22 -0
- package/dist/commands/tasks/upsert.d.ts +1 -0
- package/dist/commands/tasks/upsert.js +16 -0
- package/dist/lib/agent-config.d.ts +2 -0
- package/dist/lib/agent-config.js +3 -1
- package/dist/lib/agentic-stream.d.ts +68 -1
- package/dist/lib/agentic-stream.js +294 -7
- package/dist/lib/api-fetch.js +19 -0
- package/dist/lib/auth-core-init.d.ts +2 -10
- package/dist/lib/auth-core-init.js +20 -67
- package/dist/lib/auth-failure.d.ts +33 -0
- package/dist/lib/auth-failure.js +65 -0
- package/dist/lib/auth-storage.d.ts +14 -0
- package/dist/lib/auth-storage.js +14 -0
- package/dist/lib/commitments.d.ts +37 -2
- package/dist/lib/commitments.js +148 -0
- package/dist/lib/daemonBroker.d.ts +134 -31
- package/dist/lib/daemonBroker.js +376 -31
- 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/delegated-cli.js +4 -1
- package/dist/lib/exec-runtime-binary.d.ts +3 -1
- package/dist/lib/exec-runtime-binary.js +4 -2
- package/dist/lib/first-party-harness-broker.d.ts +10 -0
- package/dist/lib/first-party-harness-broker.js +13 -1
- 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/harnesses.d.ts +10 -0
- package/dist/lib/hosted-machines.d.ts +10 -1
- package/dist/lib/hosted-machines.js +36 -2
- package/dist/lib/live-runtime-binary.d.ts +40 -0
- package/dist/lib/live-runtime-binary.js +193 -0
- package/dist/lib/login.d.ts +23 -0
- package/dist/lib/login.js +60 -10
- package/dist/lib/machine-spend-cap.d.ts +23 -0
- package/dist/lib/machine-spend-cap.js +68 -0
- package/dist/lib/message-intent.js +3 -1
- package/dist/lib/node-adapter.js +15 -3
- package/dist/lib/tasks.d.ts +20 -0
- package/dist/lib/tasks.js +54 -0
- 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/credentialEnvelopeBridge.d.ts +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +26 -1
- 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 +133 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +189 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +41 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +25 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.d.ts +32 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.js +268 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.d.ts +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +11 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.d.ts +36 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +156 -0
- 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.d.ts +8 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +16 -10
- 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/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/spawnEnv.d.ts +22 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +61 -2
- 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/credentialEnvelopeBridge.d.ts +2 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +25 -1
- 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 +133 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +182 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +41 -3
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.d.ts +32 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.js +257 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.d.ts +2 -2
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +11 -10
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.d.ts +36 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +147 -0
- 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.d.ts +8 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +16 -10
- 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/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/spawnEnv.d.ts +22 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +58 -1
- 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 +61 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +3418 -3344
- package/oclif.manifest.json +24813 -24242
- package/package.json +4 -1
package/dist/lib/daemonBroker.js
CHANGED
|
@@ -77,14 +77,23 @@ exports.resolveBootstrapPath = resolveBootstrapPath;
|
|
|
77
77
|
exports.resolveProfileBootstrapPaths = resolveProfileBootstrapPaths;
|
|
78
78
|
exports.resolveBootstrapCandidatePaths = resolveBootstrapCandidatePaths;
|
|
79
79
|
exports.resolveCliHandoffCandidatePaths = resolveCliHandoffCandidatePaths;
|
|
80
|
+
exports.resolveExactProfileBootstrapPaths = resolveExactProfileBootstrapPaths;
|
|
80
81
|
exports.findLiveDaemonBootstrap = findLiveDaemonBootstrap;
|
|
81
82
|
exports.readBootstrap = readBootstrap;
|
|
82
83
|
exports.attemptDaemonBrokerLogin = attemptDaemonBrokerLogin;
|
|
84
|
+
exports.isBrokeredHandoffToken = isBrokeredHandoffToken;
|
|
85
|
+
exports.redeemBrokeredHandoffToken = redeemBrokeredHandoffToken;
|
|
86
|
+
exports.__resetBrokeredHandoffForTest = __resetBrokeredHandoffForTest;
|
|
87
|
+
exports.findBrokeredHandoffDescriptor = findBrokeredHandoffDescriptor;
|
|
88
|
+
exports.maybeRetireStoredCliAuthFile = maybeRetireStoredCliAuthFile;
|
|
83
89
|
exports.attemptDaemonBrokerLoginAndPersist = attemptDaemonBrokerLoginAndPersist;
|
|
84
90
|
exports.maybeAutoBroker = maybeAutoBroker;
|
|
85
91
|
const fs = __importStar(require("node:fs"));
|
|
86
92
|
const path = __importStar(require("node:path"));
|
|
87
93
|
const os = __importStar(require("node:os"));
|
|
94
|
+
const auth_core_1 = require("@skrr-ai/auth-core");
|
|
95
|
+
const loopback_http_1 = require("@skrr-ai/auth-core/loopback-http");
|
|
96
|
+
const cli_handoff_wire_1 = require("@skrr-ai/auth-core/cli-handoff-wire");
|
|
88
97
|
const auth_storage_1 = require("./auth-storage");
|
|
89
98
|
const config_1 = require("./config");
|
|
90
99
|
const device_id_1 = require("./device-id");
|
|
@@ -132,6 +141,16 @@ function resolveProfileBootstrapPaths(primary, entryNames) {
|
|
|
132
141
|
.map((name) => path.join(path.dirname(primary), name));
|
|
133
142
|
return [primary, ...extras];
|
|
134
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Legacy bootstrap directory (darwin): pre-rename daemons wrote under
|
|
146
|
+
* `~/Library/Application Support/OverSky/` while this reader looked under
|
|
147
|
+
* `skrr/` — a drift that silently killed island bootstrapping on macOS
|
|
148
|
+
* (OSK-12057). New daemons dual-write both; old daemons keep writing only
|
|
149
|
+
* `OverSky/`, so these stay consulted as fallbacks.
|
|
150
|
+
*/
|
|
151
|
+
function legacyDarwinBootstrapDir() {
|
|
152
|
+
return path.join(os.homedir(), 'Library', 'Application Support', 'OverSky');
|
|
153
|
+
}
|
|
135
154
|
/**
|
|
136
155
|
* List the bootstrap files that a Sky CLI may use for a local daemon
|
|
137
156
|
* handoff. `default` stays first for backwards compatibility; any other
|
|
@@ -143,16 +162,31 @@ function resolveProfileBootstrapPaths(primary, entryNames) {
|
|
|
143
162
|
*/
|
|
144
163
|
function resolveBootstrapCandidatePaths(profile = 'default') {
|
|
145
164
|
const primary = resolveBootstrapPath(profile);
|
|
146
|
-
if (profile !== 'default')
|
|
147
|
-
|
|
165
|
+
if (profile !== 'default') {
|
|
166
|
+
const legacy = process.platform === 'darwin'
|
|
167
|
+
? [path.join(legacyDarwinBootstrapDir(), `daemon-island.${profile}.json`)]
|
|
168
|
+
: [];
|
|
169
|
+
return [primary, ...legacy];
|
|
170
|
+
}
|
|
148
171
|
try {
|
|
149
172
|
if (process.platform === 'darwin') {
|
|
150
|
-
const
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
173
|
+
const candidates = [primary];
|
|
174
|
+
for (const dir of [path.dirname(primary), legacyDarwinBootstrapDir()]) {
|
|
175
|
+
let entryNames = [];
|
|
176
|
+
try {
|
|
177
|
+
entryNames = fs
|
|
178
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
179
|
+
.filter((entry) => entry.isFile())
|
|
180
|
+
.map((entry) => entry.name);
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
for (const p of resolveProfileBootstrapPaths(path.join(dir, 'daemon-island.default.json'), entryNames)) {
|
|
186
|
+
candidates.push(p);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
return [...new Set(candidates)];
|
|
156
190
|
}
|
|
157
191
|
const profilesDir = path.dirname(path.dirname(primary));
|
|
158
192
|
const extras = fs
|
|
@@ -201,6 +235,20 @@ function resolveCliHandoffCandidatePaths(profile = 'default') {
|
|
|
201
235
|
function legacyBootstrapPath() {
|
|
202
236
|
return path.join(os.homedir(), '.skrr', 'daemon-island.json');
|
|
203
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* The island files that belong to exactly `profile` — its canonical path and
|
|
240
|
+
* the pre-rename darwin location — and never another profile's. The broker's
|
|
241
|
+
* candidate list for `default` deliberately falls through to any running
|
|
242
|
+
* profile, which is right for borrowing a credential and wrong for naming
|
|
243
|
+
* which daemon a profile's status describes (OSK-12102).
|
|
244
|
+
*/
|
|
245
|
+
function resolveExactProfileBootstrapPaths(profile = 'default') {
|
|
246
|
+
const primary = resolveBootstrapPath(profile);
|
|
247
|
+
if (process.platform === 'darwin') {
|
|
248
|
+
return [primary, path.join(legacyDarwinBootstrapDir(), `daemon-island.${profile}.json`)];
|
|
249
|
+
}
|
|
250
|
+
return profile === 'default' ? [primary, legacyBootstrapPath()] : [primary];
|
|
251
|
+
}
|
|
204
252
|
/**
|
|
205
253
|
* Resolve the first live daemon bootstrap for a profile. Message-intent
|
|
206
254
|
* submission reuses the same authenticated loopback discovery as the login
|
|
@@ -233,19 +281,25 @@ function readBootstrap(filePath) {
|
|
|
233
281
|
catch {
|
|
234
282
|
return null;
|
|
235
283
|
}
|
|
236
|
-
let
|
|
284
|
+
let parsedJson;
|
|
237
285
|
try {
|
|
238
|
-
|
|
286
|
+
parsedJson = JSON.parse(raw);
|
|
239
287
|
}
|
|
240
288
|
catch {
|
|
241
289
|
return null;
|
|
242
290
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
291
|
+
// Shared wire parser (`@skrr-ai/auth-core/cli-handoff-wire`) validates the
|
|
292
|
+
// fields a caller cannot proceed without — host, port, secret — and
|
|
293
|
+
// tolerates unknown fields and future `version` values. The CLI's own
|
|
294
|
+
// checks below stay STRICTER on purpose: the broker is loopback-only, the
|
|
295
|
+
// port must be a real TCP port, and the secret is a fixed-width key.
|
|
296
|
+
const parsed = (0, cli_handoff_wire_1.parseCliHandoffDescriptor)(parsedJson);
|
|
297
|
+
if (!parsed)
|
|
298
|
+
return null;
|
|
299
|
+
if (parsed.host !== '127.0.0.1' ||
|
|
300
|
+
typeof parsed.port !== 'number' ||
|
|
247
301
|
parsed.port > 65_535 ||
|
|
248
|
-
typeof parsed
|
|
302
|
+
typeof parsed.secret !== 'string' ||
|
|
249
303
|
Buffer.from(parsed.secret, 'base64').length !== 32) {
|
|
250
304
|
return null;
|
|
251
305
|
}
|
|
@@ -283,9 +337,12 @@ async function attemptDaemonBrokerLogin(opts) {
|
|
|
283
337
|
: resolveCliHandoffCandidatePaths(profile);
|
|
284
338
|
// Bound candidates (their serverUrl matches this CLI) in order, then unbound
|
|
285
339
|
// ones. Every usable candidate is kept, not just the first: see below.
|
|
340
|
+
// `sourcePath` rides along — the brokered-migration cleanup keys on whether
|
|
341
|
+
// the redeeming descriptor IS the Dedicated guest hand-off file.
|
|
286
342
|
const usable = [];
|
|
287
343
|
const unbound = [];
|
|
288
344
|
let mismatchedBootstrap = null;
|
|
345
|
+
let mismatchedPath = null;
|
|
289
346
|
for (const candidate of candidates) {
|
|
290
347
|
const candidateBootstrap = readBootstrap(candidate);
|
|
291
348
|
if (!candidateBootstrap)
|
|
@@ -295,15 +352,18 @@ async function attemptDaemonBrokerLogin(opts) {
|
|
|
295
352
|
typeof opts.baseURL === 'string' &&
|
|
296
353
|
opts.baseURL.length > 0 &&
|
|
297
354
|
normalizeBaseUrl(opts.baseURL) !== normalizeBaseUrl(candidateBootstrap.serverUrl)) {
|
|
298
|
-
mismatchedBootstrap
|
|
355
|
+
if (!mismatchedBootstrap) {
|
|
356
|
+
mismatchedBootstrap = candidateBootstrap;
|
|
357
|
+
mismatchedPath = candidate;
|
|
358
|
+
}
|
|
299
359
|
continue;
|
|
300
360
|
}
|
|
301
361
|
if (typeof candidateBootstrap.serverUrl !== 'string' ||
|
|
302
362
|
candidateBootstrap.serverUrl.length === 0) {
|
|
303
|
-
unbound.push(candidateBootstrap);
|
|
363
|
+
unbound.push({ bootstrap: candidateBootstrap, sourcePath: candidate });
|
|
304
364
|
continue;
|
|
305
365
|
}
|
|
306
|
-
usable.push(candidateBootstrap);
|
|
366
|
+
usable.push({ bootstrap: candidateBootstrap, sourcePath: candidate });
|
|
307
367
|
}
|
|
308
368
|
usable.push(...unbound);
|
|
309
369
|
if (usable.length === 0) {
|
|
@@ -311,8 +371,11 @@ async function attemptDaemonBrokerLogin(opts) {
|
|
|
311
371
|
return {
|
|
312
372
|
ok: false,
|
|
313
373
|
reason: 'base_url_mismatch',
|
|
314
|
-
|
|
374
|
+
// What was READ, not a claim about the daemon: the descriptor may be
|
|
375
|
+
// another profile's (OSK-12117).
|
|
376
|
+
detail: `The daemon descriptor ${mismatchedPath ?? ''} is for ${mismatchedBootstrap.serverUrl}, not ${opts.baseURL}.`,
|
|
315
377
|
daemonServerUrl: mismatchedBootstrap.serverUrl,
|
|
378
|
+
...(mismatchedPath ? { descriptorPath: mismatchedPath } : {}),
|
|
316
379
|
};
|
|
317
380
|
}
|
|
318
381
|
return {
|
|
@@ -329,21 +392,164 @@ async function attemptDaemonBrokerLogin(opts) {
|
|
|
329
392
|
// in". Only a refused connection falls through: any answer, or a timeout, is a
|
|
330
393
|
// daemon that is really there, and its outcome stands.
|
|
331
394
|
let outcome = null;
|
|
332
|
-
for (const bootstrap of usable) {
|
|
333
|
-
outcome = await brokerThrough(bootstrap, opts);
|
|
395
|
+
for (const { bootstrap, sourcePath } of usable) {
|
|
396
|
+
outcome = await brokerThrough(bootstrap, opts, sourcePath);
|
|
334
397
|
if (outcome.ok || outcome.reason !== 'network')
|
|
335
398
|
return outcome;
|
|
336
399
|
}
|
|
337
400
|
return outcome;
|
|
338
401
|
}
|
|
339
|
-
async function brokerThrough(bootstrap, opts) {
|
|
402
|
+
async function brokerThrough(bootstrap, opts, sourcePath) {
|
|
403
|
+
// Mode negotiation (descriptor v2 `handoffModes`). A v1 descriptor advertises
|
|
404
|
+
// nothing and reads as `['refresh_family']` — today's behaviour, unchanged.
|
|
405
|
+
//
|
|
406
|
+
// The token route is attempted when the descriptor offers `access_token` AND
|
|
407
|
+
// either the caller prefers broker mode (auto-broker) or the durable mint is
|
|
408
|
+
// not offered at all (the Dedicated guest shape — its hand-off-only secret
|
|
409
|
+
// cannot mint a family, so the token route is the only door).
|
|
410
|
+
const modes = (0, cli_handoff_wire_1.advertisedHandoffModes)(bootstrap);
|
|
411
|
+
const offersAccessToken = modes.includes('access_token');
|
|
412
|
+
const offersRefreshFamily = modes.includes('refresh_family');
|
|
413
|
+
const preferAccessToken = opts.preferHandoffMode === 'access_token';
|
|
414
|
+
if (offersAccessToken && (preferAccessToken || !offersRefreshFamily)) {
|
|
415
|
+
const redeem = await redeemCliHandoffToken(bootstrap, {
|
|
416
|
+
forceRefresh: false,
|
|
417
|
+
timeoutMs: opts.timeoutMs,
|
|
418
|
+
});
|
|
419
|
+
if (redeem.ok) {
|
|
420
|
+
const response = redeem.body;
|
|
421
|
+
registerBrokeredHandoff(bootstrap, response.accessToken, sourcePath);
|
|
422
|
+
if (sourcePath === exports.DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR) {
|
|
423
|
+
// A guest descriptor's secret can ONLY redeem access tokens, so a
|
|
424
|
+
// stored cli-auth.json on this machine predates broker mode. Retire
|
|
425
|
+
// the old family (best-effort revoke + delete); see the helper for
|
|
426
|
+
// the provenance guard.
|
|
427
|
+
await maybeRetireStoredCliAuthFile({
|
|
428
|
+
descriptorPath: sourcePath,
|
|
429
|
+
descriptorServerUrl: bootstrap.serverUrl ?? response.serverUrl,
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
ok: true,
|
|
434
|
+
brokered: true,
|
|
435
|
+
accessToken: response.accessToken,
|
|
436
|
+
...(response.accessExpiresAt !== undefined
|
|
437
|
+
? { accessExpiresAt: response.accessExpiresAt }
|
|
438
|
+
: {}),
|
|
439
|
+
...((response.serverUrl ?? bootstrap.serverUrl)
|
|
440
|
+
? { serverUrl: response.serverUrl ?? bootstrap.serverUrl }
|
|
441
|
+
: {}),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
// The descriptor ALSO advertises the durable mint (a laptop island during
|
|
445
|
+
// transition): any redeem failure — refusal, network, outage — falls
|
|
446
|
+
// through to the legacy route below. When refresh_family is NOT offered,
|
|
447
|
+
// the redeem outcome is final: the legacy route would only answer
|
|
448
|
+
// 403 HANDOFF_BROKER_ONLY anyway.
|
|
449
|
+
if (!offersRefreshFamily)
|
|
450
|
+
return redeem;
|
|
451
|
+
}
|
|
452
|
+
return mintRefreshFamilyThrough(bootstrap, opts);
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Broker-mode redeem — `POST /v1/auth/cli-handoff-token` with the
|
|
456
|
+
* descriptor's secret as bearer. Returns the parsed token response or a
|
|
457
|
+
* `DaemonBrokerFailure` the caller can return verbatim / fall through from.
|
|
458
|
+
*/
|
|
459
|
+
async function redeemCliHandoffToken(bootstrap, opts = {}) {
|
|
460
|
+
const url = `http://${bootstrap.host}:${bootstrap.port}${cli_handoff_wire_1.CLI_HANDOFF_TOKEN_PATH}`;
|
|
461
|
+
const timeoutMs = opts.timeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
462
|
+
const controller = new AbortController();
|
|
463
|
+
const abortTimer = setTimeout(() => controller.abort(), timeoutMs);
|
|
464
|
+
let response;
|
|
465
|
+
try {
|
|
466
|
+
// The descriptor secret is a loopback-only credential: never `fetch`, which
|
|
467
|
+
// sends 127.0.0.1 to HTTP_PROXY under Bun and NODE_USE_ENV_PROXY (OSK-12134).
|
|
468
|
+
response = await (0, loopback_http_1.loopbackFetch)(url, {
|
|
469
|
+
method: 'POST',
|
|
470
|
+
headers: {
|
|
471
|
+
Authorization: `Bearer ${bootstrap.secret}`,
|
|
472
|
+
'Content-Type': 'application/json',
|
|
473
|
+
Accept: 'application/json',
|
|
474
|
+
},
|
|
475
|
+
// No cliId — the family is daemon-owned; the caller is not enrolling a
|
|
476
|
+
// device. forceRefresh bypasses the daemon's cached access token (the
|
|
477
|
+
// 401-recovery path).
|
|
478
|
+
body: JSON.stringify({ forceRefresh: opts.forceRefresh === true }),
|
|
479
|
+
signal: controller.signal,
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
catch (err) {
|
|
483
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
484
|
+
if (err.name === 'AbortError') {
|
|
485
|
+
return {
|
|
486
|
+
ok: false,
|
|
487
|
+
reason: 'timeout',
|
|
488
|
+
detail: `Handoff token redeem timed out after ${timeoutMs}ms`,
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
return { ok: false, reason: 'network', detail: msg };
|
|
492
|
+
}
|
|
493
|
+
finally {
|
|
494
|
+
clearTimeout(abortTimer);
|
|
495
|
+
}
|
|
496
|
+
let parsedBody = null;
|
|
497
|
+
try {
|
|
498
|
+
parsedBody = (await response.json());
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
parsedBody = null;
|
|
502
|
+
}
|
|
503
|
+
if (!response.ok) {
|
|
504
|
+
const code = typeof parsedBody?.code === 'string' ? parsedBody.code : undefined;
|
|
505
|
+
const message = typeof parsedBody?.message === 'string'
|
|
506
|
+
? parsedBody.message
|
|
507
|
+
: `Handoff token route returned HTTP ${response.status}`;
|
|
508
|
+
if (response.status === 401 || response.status === 403) {
|
|
509
|
+
// The daemon refused the descriptor secret outright — the descriptor is
|
|
510
|
+
// stale (secret rotated on daemon restart) or was never valid for this
|
|
511
|
+
// route.
|
|
512
|
+
return {
|
|
513
|
+
ok: false,
|
|
514
|
+
reason: 'stale_bootstrap',
|
|
515
|
+
detail: message,
|
|
516
|
+
status: response.status,
|
|
517
|
+
...(code ? { code } : {}),
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
return {
|
|
521
|
+
ok: false,
|
|
522
|
+
// 503 HANDOFF_TOKEN_UNAVAILABLE lands in server_error with its code
|
|
523
|
+
// preserved — the daemon is alive but cannot mint (no daemon bearer,
|
|
524
|
+
// upstream unreachable, mint cool-down). That is a daemon outage, not a
|
|
525
|
+
// decline, and the code keeps it diagnosable.
|
|
526
|
+
reason: response.status >= 500 ? 'server_error' : 'unknown',
|
|
527
|
+
detail: message,
|
|
528
|
+
status: response.status,
|
|
529
|
+
...(code ? { code } : {}),
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
const body = (0, cli_handoff_wire_1.parseCliHandoffTokenResponse)(parsedBody);
|
|
533
|
+
if (!body) {
|
|
534
|
+
return {
|
|
535
|
+
ok: false,
|
|
536
|
+
reason: 'server_error',
|
|
537
|
+
detail: 'Handoff token route returned 200 but the response carried no access token',
|
|
538
|
+
status: response.status,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
return { ok: true, body };
|
|
542
|
+
}
|
|
543
|
+
async function mintRefreshFamilyThrough(bootstrap, opts) {
|
|
340
544
|
const url = `http://${bootstrap.host}:${bootstrap.port}/v1/auth/cli-handoff`;
|
|
341
545
|
const timeoutMs = opts.timeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
342
546
|
const controller = new AbortController();
|
|
343
547
|
const abortTimer = setTimeout(() => controller.abort(), timeoutMs);
|
|
344
548
|
let response;
|
|
345
549
|
try {
|
|
346
|
-
|
|
550
|
+
// The descriptor secret is a loopback-only credential: never `fetch`, which
|
|
551
|
+
// sends 127.0.0.1 to HTTP_PROXY under Bun and NODE_USE_ENV_PROXY (OSK-12134).
|
|
552
|
+
response = await (0, loopback_http_1.loopbackFetch)(url, {
|
|
347
553
|
method: 'POST',
|
|
348
554
|
headers: {
|
|
349
555
|
Authorization: `Bearer ${bootstrap.secret}`,
|
|
@@ -389,6 +595,20 @@ async function brokerThrough(bootstrap, opts) {
|
|
|
389
595
|
if (response.status === 403 && code === 'CI_TOKEN_REFUSED') {
|
|
390
596
|
return { ok: false, reason: 'ci_token_refused', detail: message, status: 403, code };
|
|
391
597
|
}
|
|
598
|
+
if (response.status === 403 && code === cli_handoff_wire_1.CLI_HANDOFF_ERROR_BROKER_ONLY) {
|
|
599
|
+
// The daemon refuses to mint a refresh family for this secret — the
|
|
600
|
+
// descriptor offers `access_token`, and broker mode is the only door.
|
|
601
|
+
// Terminal (not a next-candidate fall-through): every descriptor for
|
|
602
|
+
// this daemon answers the same way, and the caller's remedy is the
|
|
603
|
+
// token route or an explicit `skrr login`, not another file.
|
|
604
|
+
return {
|
|
605
|
+
ok: false,
|
|
606
|
+
reason: 'handoff_broker_only',
|
|
607
|
+
detail: message,
|
|
608
|
+
status: 403,
|
|
609
|
+
code,
|
|
610
|
+
};
|
|
611
|
+
}
|
|
392
612
|
if (response.status === 503 && code === 'DAEMON_NOT_AUTHED') {
|
|
393
613
|
return { ok: false, reason: 'daemon_not_authed', detail: message, status: 503, code };
|
|
394
614
|
}
|
|
@@ -457,6 +677,116 @@ async function brokerThrough(bootstrap, opts) {
|
|
|
457
677
|
...(typeof parsedBody.familyId === 'string' ? { familyId: parsedBody.familyId } : {}),
|
|
458
678
|
};
|
|
459
679
|
}
|
|
680
|
+
// ---------------------------------------------------------------------
|
|
681
|
+
// Brokered (access-token) hand-off — process-local credential state
|
|
682
|
+
// ---------------------------------------------------------------------
|
|
683
|
+
/**
|
|
684
|
+
* The descriptor this process's brokered credential was redeemed from, and
|
|
685
|
+
* the token it answered. Module-scoped on purpose: broker mode persists
|
|
686
|
+
* NOTHING, so the only record of "the credential in use came from a hand-off
|
|
687
|
+
* redeem" lives here, for the 401 re-redeem path in `api-fetch`
|
|
688
|
+
* (`forceRefresh: true`, retry once). Each new `skrr` process starts empty
|
|
689
|
+
* and re-redeems on entry via `maybeAutoBroker`.
|
|
690
|
+
*/
|
|
691
|
+
let _brokeredHandoff = null;
|
|
692
|
+
function registerBrokeredHandoff(descriptor, accessToken, sourcePath) {
|
|
693
|
+
_brokeredHandoff = { descriptor, accessToken, ...(sourcePath ? { sourcePath } : {}) };
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* True when `token` is the access token this process redeemed through the
|
|
697
|
+
* daemon's broker-mode hand-off. `api-fetch` uses this to pick the re-redeem
|
|
698
|
+
* recovery branch instead of the stored-family refresh (which would have
|
|
699
|
+
* nothing to rotate — brokered credentials are never persisted).
|
|
700
|
+
*/
|
|
701
|
+
function isBrokeredHandoffToken(token) {
|
|
702
|
+
return Boolean(token && _brokeredHandoff && token === _brokeredHandoff.accessToken);
|
|
703
|
+
}
|
|
704
|
+
/**
|
|
705
|
+
* Re-redeem the brokered hand-off token — the 401-recovery path. The
|
|
706
|
+
* descriptor is re-read from disk when its source path is known, so a secret
|
|
707
|
+
* rotated by a daemon restart resolves to the fresh one. Returns null when
|
|
708
|
+
* this process holds no brokered credential or the redeem fails (the caller
|
|
709
|
+
* then surfaces the original 401).
|
|
710
|
+
*/
|
|
711
|
+
async function redeemBrokeredHandoffToken(forceRefresh = true) {
|
|
712
|
+
const state = _brokeredHandoff;
|
|
713
|
+
if (!state)
|
|
714
|
+
return null;
|
|
715
|
+
const descriptor = (state.sourcePath ? readBootstrap(state.sourcePath) : null) ?? state.descriptor;
|
|
716
|
+
const result = await redeemCliHandoffToken(descriptor, { forceRefresh });
|
|
717
|
+
if (!result.ok)
|
|
718
|
+
return null;
|
|
719
|
+
_brokeredHandoff = { ...state, descriptor, accessToken: result.body.accessToken };
|
|
720
|
+
return result.body;
|
|
721
|
+
}
|
|
722
|
+
/** @internal test seam — clears the module-scoped brokered credential. */
|
|
723
|
+
function __resetBrokeredHandoffForTest() {
|
|
724
|
+
_brokeredHandoff = null;
|
|
725
|
+
}
|
|
726
|
+
/**
|
|
727
|
+
* True when a live local descriptor advertises broker-mode access — i.e.
|
|
728
|
+
* this machine's CLI authority is platform-managed and `skrr logout` has no
|
|
729
|
+
* local credential to clear. Read-only: parses candidates, never redeems.
|
|
730
|
+
*/
|
|
731
|
+
function findBrokeredHandoffDescriptor(profile = 'default') {
|
|
732
|
+
for (const candidate of resolveCliHandoffCandidatePaths(profile)) {
|
|
733
|
+
const bootstrap = readBootstrap(candidate);
|
|
734
|
+
if (bootstrap && (0, cli_handoff_wire_1.descriptorOffersAccessToken)(bootstrap))
|
|
735
|
+
return bootstrap;
|
|
736
|
+
}
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
/**
|
|
740
|
+
* Migration cleanup: retire the refresh family a pre-broker-mode CLI
|
|
741
|
+
* generation persisted, now that this machine redeems access tokens from
|
|
742
|
+
* the daemon's in-memory broker.
|
|
743
|
+
*
|
|
744
|
+
* Provenance guard — locally we cannot tell a hand-off-minted family from a
|
|
745
|
+
* user's explicit `skrr login` family by shape (the distinguishing
|
|
746
|
+
* `daemonRefOverride` lives server-side). The safe rule, applied here:
|
|
747
|
+
* revoke + delete ONLY when all of these hold —
|
|
748
|
+
*
|
|
749
|
+
* 1. the descriptor that just redeemed IS the Dedicated Runtime guest
|
|
750
|
+
* hand-off file (`/run/skrr-dedicated-runtime/...`),
|
|
751
|
+
* 2. `~/.skrr/cli-auth.json` reads cleanly and carries a refresh token,
|
|
752
|
+
* 3. its stored `serverOrigin` matches the descriptor's `serverUrl`.
|
|
753
|
+
*
|
|
754
|
+
* When provenance can't be proven the file is KEPT: an explicit `skrr login`
|
|
755
|
+
* on a guest is rare but real, and a stale extra family is bounded by the
|
|
756
|
+
* server's session cap and expires on its own. Best-effort throughout — a
|
|
757
|
+
* failed revoke still deletes the file (the whole point is that nothing
|
|
758
|
+
* credential-shaped should remain), and any failure leaves the working
|
|
759
|
+
* brokered credential untouched.
|
|
760
|
+
*/
|
|
761
|
+
async function maybeRetireStoredCliAuthFile(opts) {
|
|
762
|
+
if (opts.descriptorPath !== exports.DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR)
|
|
763
|
+
return;
|
|
764
|
+
let stored;
|
|
765
|
+
try {
|
|
766
|
+
stored = (0, auth_storage_1.readFromFile)();
|
|
767
|
+
}
|
|
768
|
+
catch {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
if (!stored?.refreshToken)
|
|
772
|
+
return;
|
|
773
|
+
const storedOrigin = stored.serverOrigin ? (0, auth_storage_1.normalizeServerOrigin)(stored.serverOrigin) : null;
|
|
774
|
+
const descriptorOrigin = opts.descriptorServerUrl
|
|
775
|
+
? (0, auth_storage_1.normalizeServerOrigin)(opts.descriptorServerUrl)
|
|
776
|
+
: null;
|
|
777
|
+
if (!storedOrigin || !descriptorOrigin || storedOrigin !== descriptorOrigin)
|
|
778
|
+
return;
|
|
779
|
+
// The revoke is authenticated by the refresh token itself — presenting it
|
|
780
|
+
// is the proof of ownership. `revokeDaemonRefreshSession` swallows its own
|
|
781
|
+
// network/HTTP failures (best-effort by contract); the delete still runs.
|
|
782
|
+
try {
|
|
783
|
+
await (0, auth_core_1.revokeDaemonRefreshSession)(descriptorOrigin, stored.refreshToken);
|
|
784
|
+
}
|
|
785
|
+
catch {
|
|
786
|
+
/* best-effort — delete proceeds regardless */
|
|
787
|
+
}
|
|
788
|
+
(0, auth_storage_1.deleteFileBackend)();
|
|
789
|
+
}
|
|
460
790
|
/** Bound the confirm so a slow server cannot stall a login that already worked. */
|
|
461
791
|
const CONFIRM_TIMEOUT_MS = 10_000;
|
|
462
792
|
/**
|
|
@@ -514,15 +844,22 @@ function expiryToEpochMs(v) {
|
|
|
514
844
|
return undefined;
|
|
515
845
|
}
|
|
516
846
|
/**
|
|
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
|
-
*
|
|
847
|
+
* Convenience wrapper: run the broker call and, on a durable-mint success,
|
|
848
|
+
* persist the returned bundle through the canonical Sky CLI auth backend
|
|
849
|
+
* (Keychain on macOS, `cli-auth.json` elsewhere). A `brokered` outcome is
|
|
850
|
+
* returned WITHOUT persisting — that is the entire point of broker mode.
|
|
851
|
+
* Returns the same outcome shape so the caller can branch on `ok`/`brokered`.
|
|
521
852
|
*/
|
|
522
853
|
async function attemptDaemonBrokerLoginAndPersist(opts) {
|
|
523
854
|
const outcome = await attemptDaemonBrokerLogin(opts);
|
|
524
855
|
if (!outcome.ok)
|
|
525
856
|
return outcome;
|
|
857
|
+
// Brokered hand-off: a daemon-owned access token. The persistence policy
|
|
858
|
+
// IS the security property here — nothing credential-shaped lands on disk,
|
|
859
|
+
// so writeToBackend is never called and there is no family to confirm.
|
|
860
|
+
// Callers use the token in-memory for this process only.
|
|
861
|
+
if (outcome.brokered)
|
|
862
|
+
return outcome;
|
|
526
863
|
const serverOrigin = normalizeBaseUrl(opts.baseURL);
|
|
527
864
|
if (!serverOrigin) {
|
|
528
865
|
throw new Error('Daemon-broker credential persistence requires a valid CLI baseURL.');
|
|
@@ -586,10 +923,12 @@ const AUTH_SELF_MANAGED_COMMANDS = new Set([
|
|
|
586
923
|
* - `commandId` is in `AUTH_SELF_MANAGED_COMMANDS`
|
|
587
924
|
*
|
|
588
925
|
* Side effects:
|
|
589
|
-
* - Calls `attemptDaemonBrokerLoginAndPersist`
|
|
590
|
-
*
|
|
591
|
-
*
|
|
592
|
-
*
|
|
926
|
+
* - Calls `attemptDaemonBrokerLoginAndPersist` with `preferHandoffMode:
|
|
927
|
+
* 'access_token'`. On a descriptor that advertises broker mode the
|
|
928
|
+
* outcome is `brokered` — an in-memory access token, nothing written;
|
|
929
|
+
* the caller holds it for this process and the next `skrr` re-redeems.
|
|
930
|
+
* On a legacy/v1 descriptor the durable mint is persisted to the
|
|
931
|
+
* keychain / cli-auth.json exactly as before.
|
|
593
932
|
* - May generate a new `cliId` if the config doesn't have one. The
|
|
594
933
|
* caller MUST persist `updatedConfig` so the next invocation
|
|
595
934
|
* presents the same id to the server's refresh rotator.
|
|
@@ -618,6 +957,12 @@ async function maybeAutoBroker(opts) {
|
|
|
618
957
|
const outcome = await attemptDaemonBrokerLoginAndPersist({
|
|
619
958
|
cliId,
|
|
620
959
|
baseURL: opts.cliConfig.baseURL,
|
|
960
|
+
// Auto-broker prefers the brokered access-token mode when the descriptor
|
|
961
|
+
// advertises it: nothing credential-shaped is persisted, and the next
|
|
962
|
+
// `skrr` process simply re-redeems over loopback. The legacy refresh-
|
|
963
|
+
// family mint remains the fallback for v1 descriptors and the redeem
|
|
964
|
+
// path for explicit `skrr login`.
|
|
965
|
+
preferHandoffMode: 'access_token',
|
|
621
966
|
...(opts.bootstrapPathOverride ? { bootstrapPathOverride: opts.bootstrapPathOverride } : {}),
|
|
622
967
|
});
|
|
623
968
|
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,
|