@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.
Files changed (180) hide show
  1. package/README.md +5 -4
  2. package/dist/base-command.js +75 -6
  3. package/dist/commands/agents/chat.d.ts +14 -0
  4. package/dist/commands/agents/chat.js +37 -1
  5. package/dist/commands/agents/create.js +14 -7
  6. package/dist/commands/agents/show.d.ts +9 -0
  7. package/dist/commands/agents/show.js +53 -0
  8. package/dist/commands/agents/update.d.ts +9 -0
  9. package/dist/commands/agents/update.js +38 -5
  10. package/dist/commands/balance/show.d.ts +34 -0
  11. package/dist/commands/balance/show.js +92 -2
  12. package/dist/commands/code/index.d.ts +1 -0
  13. package/dist/commands/code/index.js +49 -5
  14. package/dist/commands/code/run.js +5 -1
  15. package/dist/commands/commitments/cycles.d.ts +19 -0
  16. package/dist/commands/commitments/cycles.js +46 -0
  17. package/dist/commands/commitments/doctor.d.ts +1 -0
  18. package/dist/commands/commitments/doctor.js +19 -4
  19. package/dist/commands/commitments/effective-policy.js +40 -1
  20. package/dist/commands/commitments/explain.d.ts +17 -0
  21. package/dist/commands/commitments/explain.js +40 -0
  22. package/dist/commands/commitments/next-tick.js +3 -1
  23. package/dist/commands/commitments/preflight.js +10 -0
  24. package/dist/commands/commitments/show.js +11 -1
  25. package/dist/commands/daemon/config.d.ts +15 -0
  26. package/dist/commands/daemon/config.js +38 -0
  27. package/dist/commands/daemon/index.js +1 -0
  28. package/dist/commands/daemon/status.js +15 -1
  29. package/dist/commands/goals/create.d.ts +1 -0
  30. package/dist/commands/goals/create.js +22 -0
  31. package/dist/commands/harnesses/models.d.ts +54 -0
  32. package/dist/commands/harnesses/models.js +100 -7
  33. package/dist/commands/harnesses/show.d.ts +11 -0
  34. package/dist/commands/harnesses/show.js +35 -0
  35. package/dist/commands/harnesses/update.d.ts +28 -1
  36. package/dist/commands/harnesses/update.js +73 -10
  37. package/dist/commands/logout.js +12 -1
  38. package/dist/commands/machines/dedicated/index.js +1 -1
  39. package/dist/commands/machines/dedicated/sign-in.js +4 -1
  40. package/dist/commands/machines/dedicated/terminal/kill.d.ts +21 -0
  41. package/dist/commands/machines/dedicated/terminal/kill.js +55 -0
  42. package/dist/commands/machines/dedicated/terminal/ls.d.ts +22 -0
  43. package/dist/commands/machines/dedicated/terminal/ls.js +70 -0
  44. package/dist/commands/machines/dedicated/terminal/rename.d.ts +23 -0
  45. package/dist/commands/machines/dedicated/terminal/rename.js +64 -0
  46. package/dist/commands/machines/dedicated/terminal.d.ts +9 -0
  47. package/dist/commands/machines/dedicated/terminal.js +27 -2
  48. package/dist/commands/machines/hosted/connect.js +2 -2
  49. package/dist/commands/machines/hosted/destroy.js +1 -1
  50. package/dist/commands/machines/hosted/exec.js +1 -1
  51. package/dist/commands/machines/hosted/list.js +1 -1
  52. package/dist/commands/machines/hosted/pause.js +1 -1
  53. package/dist/commands/machines/hosted/pull.js +1 -1
  54. package/dist/commands/machines/hosted/resume.js +1 -1
  55. package/dist/commands/machines/hosted/start.js +2 -2
  56. package/dist/commands/machines/hosted/status.js +2 -1
  57. package/dist/commands/outbox/list.d.ts +12 -1
  58. package/dist/commands/outbox/list.js +31 -2
  59. package/dist/commands/revoke-daemon.js +9 -0
  60. package/dist/commands/spaces/create.d.ts +1 -0
  61. package/dist/commands/spaces/create.js +17 -0
  62. package/dist/commands/spaces/show.js +5 -0
  63. package/dist/commands/spaces/update.d.ts +1 -0
  64. package/dist/commands/spaces/update.js +16 -0
  65. package/dist/commands/tasks/create.d.ts +1 -0
  66. package/dist/commands/tasks/create.js +6 -0
  67. package/dist/commands/tasks/list.js +1 -0
  68. package/dist/commands/tasks/show.js +45 -0
  69. package/dist/commands/tasks/update.d.ts +1 -0
  70. package/dist/commands/tasks/update.js +22 -0
  71. package/dist/commands/tasks/upsert.d.ts +1 -0
  72. package/dist/commands/tasks/upsert.js +16 -0
  73. package/dist/lib/agent-config.d.ts +2 -0
  74. package/dist/lib/agent-config.js +3 -1
  75. package/dist/lib/agentic-stream.d.ts +68 -1
  76. package/dist/lib/agentic-stream.js +294 -7
  77. package/dist/lib/api-fetch.js +19 -0
  78. package/dist/lib/auth-core-init.d.ts +2 -10
  79. package/dist/lib/auth-core-init.js +20 -67
  80. package/dist/lib/auth-failure.d.ts +33 -0
  81. package/dist/lib/auth-failure.js +65 -0
  82. package/dist/lib/auth-storage.d.ts +14 -0
  83. package/dist/lib/auth-storage.js +14 -0
  84. package/dist/lib/commitments.d.ts +37 -2
  85. package/dist/lib/commitments.js +148 -0
  86. package/dist/lib/daemonBroker.d.ts +134 -31
  87. package/dist/lib/daemonBroker.js +376 -31
  88. package/dist/lib/dedicated-lease-command.d.ts +14 -0
  89. package/dist/lib/dedicated-lease-command.js +30 -1
  90. package/dist/lib/dedicated-machines.js +8 -25
  91. package/dist/lib/dedicated-service-command.d.ts +11 -3
  92. package/dist/lib/dedicated-service-command.js +20 -3
  93. package/dist/lib/dedicated-service.d.ts +46 -0
  94. package/dist/lib/dedicated-service.js +85 -7
  95. package/dist/lib/dedicated-terminal.d.ts +21 -0
  96. package/dist/lib/dedicated-terminal.js +104 -9
  97. package/dist/lib/delegated-cli.js +4 -1
  98. package/dist/lib/exec-runtime-binary.d.ts +3 -1
  99. package/dist/lib/exec-runtime-binary.js +4 -2
  100. package/dist/lib/first-party-harness-broker.d.ts +10 -0
  101. package/dist/lib/first-party-harness-broker.js +13 -1
  102. package/dist/lib/first-party-harness-doctor.js +41 -1
  103. package/dist/lib/first-party-harness-managed.d.ts +4 -3
  104. package/dist/lib/first-party-harness-project-trust.d.ts +125 -0
  105. package/dist/lib/first-party-harness-project-trust.js +364 -0
  106. package/dist/lib/first-party-harness.d.ts +9 -2
  107. package/dist/lib/first-party-harness.js +6 -5
  108. package/dist/lib/harnesses.d.ts +10 -0
  109. package/dist/lib/hosted-machines.d.ts +10 -1
  110. package/dist/lib/hosted-machines.js +36 -2
  111. package/dist/lib/live-runtime-binary.d.ts +40 -0
  112. package/dist/lib/live-runtime-binary.js +193 -0
  113. package/dist/lib/login.d.ts +23 -0
  114. package/dist/lib/login.js +60 -10
  115. package/dist/lib/machine-spend-cap.d.ts +23 -0
  116. package/dist/lib/machine-spend-cap.js +68 -0
  117. package/dist/lib/message-intent.js +3 -1
  118. package/dist/lib/node-adapter.js +15 -3
  119. package/dist/lib/tasks.d.ts +20 -0
  120. package/dist/lib/tasks.js +54 -0
  121. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.d.ts +90 -0
  122. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/cliHandoffWire.js +113 -0
  123. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.d.ts +2 -0
  124. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialEnvelopeBridge.js +26 -1
  125. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.d.ts +47 -0
  126. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/credentialSession.js +69 -0
  127. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.d.ts +133 -0
  128. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonToolApproval.js +189 -0
  129. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/deviceIdentityBridge.js +41 -3
  130. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +3 -1
  131. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +25 -3
  132. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.d.ts +32 -0
  133. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/loopbackHttp.js +268 -0
  134. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.d.ts +2 -2
  135. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineId.js +11 -10
  136. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.d.ts +36 -0
  137. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/machineUuid.js +156 -0
  138. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.d.ts +8 -3
  139. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.js +9 -4
  140. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +8 -0
  141. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +16 -10
  142. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refreshClassification.d.ts +6 -0
  143. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refreshClassification.js +32 -9
  144. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/sessionPermissionAuthority.d.ts +81 -0
  145. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/sessionPermissionAuthority.js +87 -0
  146. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +22 -0
  147. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +61 -2
  148. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/types.d.ts +6 -1
  149. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.d.ts +90 -0
  150. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/cliHandoffWire.js +105 -0
  151. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.d.ts +2 -0
  152. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialEnvelopeBridge.js +25 -1
  153. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.d.ts +47 -0
  154. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/credentialSession.js +66 -0
  155. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.d.ts +133 -0
  156. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonToolApproval.js +182 -0
  157. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/deviceIdentityBridge.js +41 -3
  158. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +3 -1
  159. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +10 -1
  160. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.d.ts +32 -0
  161. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/loopbackHttp.js +257 -0
  162. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.d.ts +2 -2
  163. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineId.js +11 -10
  164. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.d.ts +36 -0
  165. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/machineUuid.js +147 -0
  166. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.d.ts +8 -3
  167. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.js +9 -4
  168. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +8 -0
  169. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +16 -10
  170. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refreshClassification.d.ts +6 -0
  171. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refreshClassification.js +31 -9
  172. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/sessionPermissionAuthority.d.ts +81 -0
  173. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/sessionPermissionAuthority.js +80 -0
  174. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +22 -0
  175. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +58 -1
  176. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/types.d.ts +6 -1
  177. package/dist/node_modules/@skrr-ai/auth-core/package.json +61 -1
  178. package/dist/node_modules/@skrr-ai/data-provider/index.js +3418 -3344
  179. package/oclif.manifest.json +24813 -24242
  180. package/package.json +4 -1
@@ -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
- return [primary];
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 dir = path.dirname(primary);
151
- const entryNames = fs
152
- .readdirSync(dir, { withFileTypes: true })
153
- .filter((entry) => entry.isFile())
154
- .map((entry) => entry.name);
155
- return resolveProfileBootstrapPaths(primary, entryNames);
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 parsed;
284
+ let parsedJson;
237
285
  try {
238
- parsed = JSON.parse(raw);
286
+ parsedJson = JSON.parse(raw);
239
287
  }
240
288
  catch {
241
289
  return null;
242
290
  }
243
- if (parsed?.host !== '127.0.0.1' ||
244
- typeof parsed?.port !== 'number' ||
245
- !Number.isInteger(parsed.port) ||
246
- parsed.port < 1 ||
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?.secret !== 'string' ||
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 ??= candidateBootstrap;
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
- detail: `The skrr background service on this computer is not logged in to ${opts.baseURL}; it is logged in to ${mismatchedBootstrap.serverUrl}.`,
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
- response = await fetch(url, {
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, persist the
518
- * returned bundle through the canonical Sky CLI auth backend (Keychain
519
- * on macOS, `cli-auth.json` elsewhere). Returns the same outcome shape
520
- * so the caller can branch on `ok`.
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` which writes to the
590
- * keychain / cli-auth.json on success (this is the whole point —
591
- * subsequent commands read the persisted token via the normal
592
- * resolver chain, no broker round-trip per command).
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
- process.stderr.write(`Connected to ${(0, dedicated_machines_1.dedicatedLeaseDisplayName)(lease)} (${leaseId}) as skrr-workload. Ctrl-] closes the terminal.\n`);
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,