@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
@@ -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, details, context);
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, details, context = {}) {
1201
- let code;
1202
- try {
1203
- code = JSON.parse(body || '{}').code;
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 ` ${parts.join('; ')}. ${raiseCapHint(context)}`;
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: 'DEDICATED_RUNTIME_SERVICES_UNSUPPORTED',
170
+ code: unsupportedCode,
154
171
  exit: 1,
155
172
  retryable: false,
156
173
  });
@@ -43,6 +43,31 @@ export declare const DEDICATED_SERVICE_EVENTS: {
43
43
  readonly logClosed: "dedicated:service:log:closed";
44
44
  readonly proposed: "dedicated:service:proposed";
45
45
  };
46
+ /**
47
+ * Persistent-terminal management (always-on §6.4) — the sibling lane on the
48
+ * same admitted socket. A session is a tmux `skrr-term-<name>` the machine
49
+ * keeps alive across attaches; `terminal` opens one over the ordinary
50
+ * terminal start lane, and these verbs enumerate, end and rename it.
51
+ */
52
+ export declare const DEDICATED_TERMINAL_EVENTS: {
53
+ readonly list: "dedicated:terminal:list";
54
+ readonly kill: "dedicated:terminal:kill";
55
+ readonly rename: "dedicated:terminal:rename";
56
+ readonly ack: "dedicated:terminal:ack";
57
+ };
58
+ export type TerminalVerb = 'list' | 'kill' | 'rename';
59
+ /** `skrr-term-<name>`'s suffix — the pattern the relay and daemon both enforce. */
60
+ export declare const DEDICATED_TERMINAL_NAME_PATTERN: RegExp;
61
+ /** The wire capability a live daemon must announce for persistent terminals. */
62
+ export declare const DEDICATED_PERSISTENT_TERMINAL_CAPABILITY = "persistent_terminal_v1";
63
+ /** One `dedicated:terminal:list` row — the tmux session under its suffix. */
64
+ export interface DedicatedTerminalSessionInfo {
65
+ name: string;
66
+ /** tmux `session_created` — SECONDS when numeric, not milliseconds. */
67
+ createdAt?: string | number;
68
+ attached?: boolean;
69
+ windows?: number;
70
+ }
46
71
  /** How long a verb has to answer before the machine is declared unreachable. */
47
72
  export declare const SERVICE_CALL_TIMEOUT_MS = 30000;
48
73
  /** §5.3 — the scope name `skrr-svc-<name>` derives from it. */
@@ -167,6 +192,17 @@ export declare function describeServiceRefusal(ack: Pick<DedicatedServiceAck, 'c
167
192
  verb?: string;
168
193
  name?: string;
169
194
  }): string;
195
+ /**
196
+ * A persistent-terminal refusal in the same register `describeServiceRefusal`
197
+ * gives the service lane — a named condition and the next step, never a bare
198
+ * code. `dedicated:terminal:*` acks share this channel's requestId shape, so
199
+ * the same refusal codes arrive; the words name terminal sessions, not specs.
200
+ */
201
+ export declare function describeTerminalRefusal(ack: Pick<DedicatedServiceAck, 'code' | 'error'>, context?: {
202
+ leaseId?: string;
203
+ verb?: string;
204
+ name?: string;
205
+ }): string;
170
206
  /** Validation that costs the daemon nothing to re-check — §5.3's table, enforced client-side first. */
171
207
  export declare function validateServiceSpec(doc: unknown, options?: {
172
208
  requireName?: boolean;
@@ -251,6 +287,16 @@ export declare class DedicatedServiceChannel {
251
287
  result: T | undefined;
252
288
  ack: DedicatedServiceAck;
253
289
  }>;
290
+ /**
291
+ * A persistent-terminal verb — `dedicated:terminal:list|kill|rename`,
292
+ * answered on `dedicated:terminal:ack`. Same requestId/ack discipline as
293
+ * the service verbs; the event family and the refusal's wording are the
294
+ * only differences.
295
+ */
296
+ callTerminal<T = unknown>(verb: TerminalVerb, payload: Record<string, unknown>, timeoutMs?: number): Promise<{
297
+ result: T | undefined;
298
+ ack: DedicatedServiceAck;
299
+ }>;
254
300
  /** The shared request/ack round-trip. */
255
301
  private callEvent;
256
302
  /** Subscribe to a server-pushed frame (`state`, `log`, `log:closed`, `proposed`). */
@@ -3,9 +3,10 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.GUEST_SERVICE_SOCKET_PATH = exports.DedicatedServiceChannel = exports.DedicatedServiceError = exports.SERVICE_ACTIONS = exports.SERVICE_RESTART_POLICIES = exports.SERVICE_RESERVED_NAMES = exports.SERVICE_SECRET_NAME_PATTERN = exports.SERVICE_NAME_PATTERN = exports.SERVICE_CALL_TIMEOUT_MS = exports.DEDICATED_SERVICE_EVENTS = void 0;
6
+ exports.GUEST_SERVICE_SOCKET_PATH = exports.DedicatedServiceChannel = exports.DedicatedServiceError = exports.SERVICE_ACTIONS = exports.SERVICE_RESTART_POLICIES = exports.SERVICE_RESERVED_NAMES = exports.SERVICE_SECRET_NAME_PATTERN = exports.SERVICE_NAME_PATTERN = exports.SERVICE_CALL_TIMEOUT_MS = exports.DEDICATED_PERSISTENT_TERMINAL_CAPABILITY = exports.DEDICATED_TERMINAL_NAME_PATTERN = exports.DEDICATED_TERMINAL_EVENTS = exports.DEDICATED_SERVICE_EVENTS = void 0;
7
7
  exports.isServiceUnsupportedCode = isServiceUnsupportedCode;
8
8
  exports.describeServiceRefusal = describeServiceRefusal;
9
+ exports.describeTerminalRefusal = describeTerminalRefusal;
9
10
  exports.validateServiceSpec = validateServiceSpec;
10
11
  exports.parseServiceSpecDocument = parseServiceSpecDocument;
11
12
  exports.specPayload = specPayload;
@@ -72,6 +73,22 @@ exports.DEDICATED_SERVICE_EVENTS = {
72
73
  logClosed: 'dedicated:service:log:closed',
73
74
  proposed: 'dedicated:service:proposed',
74
75
  };
76
+ /**
77
+ * Persistent-terminal management (always-on §6.4) — the sibling lane on the
78
+ * same admitted socket. A session is a tmux `skrr-term-<name>` the machine
79
+ * keeps alive across attaches; `terminal` opens one over the ordinary
80
+ * terminal start lane, and these verbs enumerate, end and rename it.
81
+ */
82
+ exports.DEDICATED_TERMINAL_EVENTS = {
83
+ list: 'dedicated:terminal:list',
84
+ kill: 'dedicated:terminal:kill',
85
+ rename: 'dedicated:terminal:rename',
86
+ ack: 'dedicated:terminal:ack',
87
+ };
88
+ /** `skrr-term-<name>`'s suffix — the pattern the relay and daemon both enforce. */
89
+ exports.DEDICATED_TERMINAL_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,31}$/;
90
+ /** The wire capability a live daemon must announce for persistent terminals. */
91
+ exports.DEDICATED_PERSISTENT_TERMINAL_CAPABILITY = 'persistent_terminal_v1';
75
92
  /** How long a verb has to answer before the machine is declared unreachable. */
76
93
  exports.SERVICE_CALL_TIMEOUT_MS = 30_000;
77
94
  /** §5.3 — the scope name `skrr-svc-<name>` derives from it. */
@@ -106,6 +123,7 @@ exports.DedicatedServiceError = DedicatedServiceError;
106
123
  function isServiceUnsupportedCode(code) {
107
124
  const c = (code || '').toLowerCase();
108
125
  return (c === 'dedicated_runtime_services_unsupported' ||
126
+ c === 'dedicated_runtime_terminal_unsupported' ||
109
127
  c === 'services_unsupported' ||
110
128
  c === 'unsupported' ||
111
129
  c === 'not_implemented' ||
@@ -152,6 +170,48 @@ function describeServiceRefusal(ack, context = {}) {
152
170
  }
153
171
  return ack.error ? `The service request failed: ${ack.error}` : 'The service request failed.';
154
172
  }
173
+ /**
174
+ * A persistent-terminal refusal in the same register `describeServiceRefusal`
175
+ * gives the service lane — a named condition and the next step, never a bare
176
+ * code. `dedicated:terminal:*` acks share this channel's requestId shape, so
177
+ * the same refusal codes arrive; the words name terminal sessions, not specs.
178
+ */
179
+ function describeTerminalRefusal(ack, context = {}) {
180
+ const code = ack.code || '';
181
+ if (isServiceUnsupportedCode(code)) {
182
+ return (ack.error ||
183
+ `This machine's image does not support persistent terminals (persistent_terminal_v1). ` +
184
+ `Update it with \`skrr machines dedicated update-image ${context.leaseId || '<lease-id>'} --wait\`; \`skrr machines dedicated terminal\` opens a plain shell in the meantime.`);
185
+ }
186
+ if (code === 'DEDICATED_RUNTIME_NOT_ASSIGNABLE') {
187
+ return ('The machine is not ready to take terminal commands (it must be running with a healthy workspace). ' +
188
+ `Check it with \`skrr machines dedicated show ${context.leaseId || '<lease-id>'}\`.`);
189
+ }
190
+ if (code === 'DEDICATED_RUNTIME_GENERATION_STALE') {
191
+ return 'The machine was replaced since its daemon registered. Wait a minute and try again.';
192
+ }
193
+ if (code === 'dedicated_runtime_quiesced') {
194
+ return 'The machine is checkpointing its workspace right now; try again in a moment.';
195
+ }
196
+ if (code === 'offline' ||
197
+ code === 'daemon_offline' ||
198
+ code === 'daemon-gone' ||
199
+ code === 'daemon-timeout') {
200
+ return 'The machine’s daemon is not answering. Wait for it to come online and try again.';
201
+ }
202
+ if (code === 'not_found') {
203
+ return context.name
204
+ ? `No terminal session named ${context.name} on this machine — \`skrr machines dedicated terminal ls ${context.leaseId || '<lease-id>'}\` lists them.`
205
+ : 'The machine or the named session was not found.';
206
+ }
207
+ if (code === 'invalid' || code === 'validation') {
208
+ return `The terminal request was refused: ${ack.error || 'invalid request'}`;
209
+ }
210
+ if (code === 'rate-limited' || code === 'cap') {
211
+ return 'Too many terminal requests in flight — wait a moment and retry.';
212
+ }
213
+ return ack.error ? `The terminal request failed: ${ack.error}` : 'The terminal request failed.';
214
+ }
155
215
  /* ---------------------------------------------------------------- spec --- */
156
216
  /** Validation that costs the daemon nothing to re-check — §5.3's table, enforced client-side first. */
157
217
  function validateServiceSpec(doc, options = {}) {
@@ -383,18 +443,22 @@ class DedicatedServiceChannel {
383
443
  this.options = options;
384
444
  const connect = options.connect ?? socket_io_client_1.io;
385
445
  this.socket = connect((0, agentic_stream_1.toWebSocketOrigin)(options.baseURL), socketOptions(options.token));
386
- const onAck = (ack) => {
446
+ // An ack resolves a request only when it arrives on THAT request's own
447
+ // ack event — a requestId echoed back on the wrong family is not an
448
+ // answer, however well-formed it looks.
449
+ const onAck = (ackEvent) => (ack) => {
387
450
  if (!ack?.requestId)
388
451
  return;
389
452
  const waiter = this.pending.get(ack.requestId);
390
- if (!waiter)
453
+ if (!waiter || waiter.ackEvent !== ackEvent)
391
454
  return;
392
455
  this.pending.delete(ack.requestId);
393
456
  clearTimeout(waiter.timer);
394
457
  waiter.resolve(ack);
395
458
  };
396
- this.socket.on(exports.DEDICATED_SERVICE_EVENTS.ack, onAck);
397
- this.socket.on(exports.DEDICATED_SERVICE_EVENTS.templatesAck, onAck);
459
+ this.socket.on(exports.DEDICATED_SERVICE_EVENTS.ack, onAck(exports.DEDICATED_SERVICE_EVENTS.ack));
460
+ this.socket.on(exports.DEDICATED_SERVICE_EVENTS.templatesAck, onAck(exports.DEDICATED_SERVICE_EVENTS.templatesAck));
461
+ this.socket.on(exports.DEDICATED_TERMINAL_EVENTS.ack, onAck(exports.DEDICATED_TERMINAL_EVENTS.ack));
398
462
  }
399
463
  /** Resolves once the transport is up; rejects on connect failure. */
400
464
  open() {
@@ -455,8 +519,21 @@ class DedicatedServiceChannel {
455
519
  verb: `templates:${verb}`,
456
520
  }, timeoutMs);
457
521
  }
522
+ /**
523
+ * A persistent-terminal verb — `dedicated:terminal:list|kill|rename`,
524
+ * answered on `dedicated:terminal:ack`. Same requestId/ack discipline as
525
+ * the service verbs; the event family and the refusal's wording are the
526
+ * only differences.
527
+ */
528
+ callTerminal(verb, payload, timeoutMs = exports.SERVICE_CALL_TIMEOUT_MS) {
529
+ return this.callEvent(`dedicated:terminal:${verb}`, exports.DEDICATED_TERMINAL_EVENTS.ack, payload, {
530
+ leaseId: typeof payload.leaseId === 'string' ? payload.leaseId : undefined,
531
+ verb: `terminal:${verb}`,
532
+ name: typeof payload.name === 'string' ? payload.name : undefined,
533
+ }, timeoutMs, describeTerminalRefusal);
534
+ }
458
535
  /** The shared request/ack round-trip. */
459
- callEvent(event, ackEvent, payload, context, timeoutMs) {
536
+ callEvent(event, ackEvent, payload, context, timeoutMs, describe = describeServiceRefusal) {
460
537
  const requestId = `svc-${(0, node_crypto_1.randomUUID)()}`;
461
538
  return new Promise((resolve, reject) => {
462
539
  if (this.dead) {
@@ -469,9 +546,10 @@ class DedicatedServiceChannel {
469
546
  }, timeoutMs);
470
547
  this.pending.set(requestId, {
471
548
  timer,
549
+ ackEvent,
472
550
  resolve: (ack) => {
473
551
  if (!ack.ok) {
474
- reject(new DedicatedServiceError(describeServiceRefusal(ack, context), ack.code || 'SERVICE_REFUSED'));
552
+ reject(new DedicatedServiceError(describe(ack, context), ack.code || 'SERVICE_REFUSED'));
475
553
  return;
476
554
  }
477
555
  resolve({ result: ack.result, ack });
@@ -34,6 +34,8 @@ export interface TerminalStartAck {
34
34
  ok?: boolean;
35
35
  error?: string;
36
36
  code?: string;
37
+ /** The tmux session a persistent start attached (`skrr-term-<name>`'s suffix). */
38
+ persistentName?: string;
37
39
  }
38
40
  /** Bytes of one output frame, without splitting a UTF-8 or ANSI sequence. */
39
41
  export declare function decodeTerminalFrame(frame: TerminalOutputFrame): Buffer;
@@ -56,6 +58,25 @@ export interface DedicatedTerminalOptions {
56
58
  daemonId: string;
57
59
  cwd?: string;
58
60
  initialCommand?: string;
61
+ /**
62
+ * A tmux-backed session (always-on §6.4): the machine keeps it alive across
63
+ * attaches, disconnects and daemon restarts, and a reconnect re-attaches it
64
+ * by name instead of opening a new shell. A daemon that predates
65
+ * `persistent_terminal_v1` silently ignores it — callers gate on the
66
+ * capability before asking.
67
+ */
68
+ persistent?: boolean;
69
+ /**
70
+ * The session suffix (`skrr-term-<name>`). The same name re-attaches the
71
+ * same session; omitted under `persistent`, the machine names the session
72
+ * `term-N` and the start ack reports it as `persistentName`.
73
+ */
74
+ sessionName?: string;
75
+ /**
76
+ * The lease this terminal claims — the relay refuses a start that names a
77
+ * lease but resolves to another lease's daemon.
78
+ */
79
+ leaseId?: string;
59
80
  input: NodeJS.ReadStream;
60
81
  output: NodeJS.WriteStream;
61
82
  errorOutput: NodeJS.WritableStream;
@@ -78,13 +78,22 @@ function describeTerminalStartRefusal(ack) {
78
78
  */
79
79
  function runDedicatedTerminal(options) {
80
80
  const events = data_provider_1.HARNESS_TERMINAL_EVENTS;
81
- const sessionId = (0, node_crypto_1.randomUUID)();
81
+ const persistent = options.persistent === true;
82
82
  const connect = options.connect ?? socket_io_client_1.io;
83
83
  const { input, output, errorOutput } = options;
84
84
  return new Promise((resolve, reject) => {
85
+ // The daemon keys a terminal by the sessionId in its start — one attach.
86
+ // A persistent session re-attaches across socket reconnects, and each
87
+ // attach is a fresh id over the same tmux name.
88
+ let sessionId = '';
85
89
  let started = false;
86
90
  let finished = false;
87
91
  let rawMode = false;
92
+ // First attach armed the tty; a re-attach needs only the ack, not the
93
+ // setup again.
94
+ let sessionReady = false;
95
+ let reattaching = false;
96
+ let persistentName = options.sessionName;
88
97
  const socket = connect((0, agentic_stream_1.toWebSocketOrigin)(options.baseURL), {
89
98
  path: '/ws/agentic',
90
99
  auth: {
@@ -94,10 +103,15 @@ function runDedicatedTerminal(options) {
94
103
  clientOs: process.platform === 'darwin' ? 'macos' : process.platform,
95
104
  },
96
105
  transports: ['websocket'],
97
- // A reconnect cannot resume a PTY: the relay forgets the session with the
106
+ // A bare PTY cannot be resumed: the relay forgets the session with the
98
107
  // socket, and silently opening a NEW shell would drop the user into a
99
- // different process tree mid-command. Say it ended instead.
100
- reconnection: false,
108
+ // different process tree mid-command — say it ended instead. A
109
+ // persistent session is the opposite by design: tmux keeps it, and the
110
+ // reconnect re-attaches it BY NAME, which is the same shell.
111
+ reconnection: persistent,
112
+ reconnectionAttempts: 10,
113
+ reconnectionDelay: 1_000,
114
+ reconnectionDelayMax: 5_000,
101
115
  });
102
116
  // Typed only once the shell's line editor says it is reading a command line
103
117
  // (`createPendingShellCommand`, the rule the web terminal reads too). A
@@ -106,7 +120,10 @@ function runDedicatedTerminal(options) {
106
120
  const pending = initialCommand
107
121
  ? (0, data_provider_1.createPendingShellCommand)({
108
122
  command: initialCommand,
109
- send: (data) => socket.emit(events.input, { sessionId, data }),
123
+ send: (data) => {
124
+ if (started)
125
+ socket.emit(events.input, { sessionId, data });
126
+ },
110
127
  onStatusChange: (status) => {
111
128
  const notice = describePendingCommand(status, initialCommand);
112
129
  if (notice && !finished)
@@ -129,13 +146,20 @@ function runDedicatedTerminal(options) {
129
146
  if (detachAt > 0) {
130
147
  const typed = buffer.subarray(0, detachAt);
131
148
  pending?.observeUserInput(typed);
132
- socket.emit(events.input, { sessionId, data: typed.toString('utf8') });
149
+ if (started)
150
+ socket.emit(events.input, { sessionId, data: typed.toString('utf8') });
133
151
  }
134
- errorOutput.write('\r\n[closed the terminal]\r\n');
152
+ errorOutput.write(persistent && persistentName
153
+ ? `\r\n[detached — session '${persistentName}' keeps running on the machine]\r\n`
154
+ : '\r\n[closed the terminal]\r\n');
135
155
  finish(0, true);
136
156
  return;
137
157
  }
138
158
  pending?.observeUserInput(buffer);
159
+ // While a reconnect is in flight the attach is gone: the frame would be
160
+ // dropped daemon-side anyway, so it is not sent.
161
+ if (!started)
162
+ return;
139
163
  socket.emit(events.input, { sessionId, data: buffer.toString('utf8') });
140
164
  };
141
165
  // A terminal that closes (SIGHUP) or a supervisor that stops us (SIGTERM):
@@ -145,9 +169,11 @@ function runDedicatedTerminal(options) {
145
169
  // SIGINT's status, for both.
146
170
  const onSigterm = () => finish(143, true);
147
171
  const onSighup = () => finish(129, true);
148
- const startTimer = setTimeout(() => {
172
+ const onStartTimeout = () => {
149
173
  fail(new DedicatedTerminalError('The machine did not answer the terminal start in time. Its daemon may be busy or offline.', 'TERMINAL_START_TIMEOUT'));
150
- }, exports.TERMINAL_START_TIMEOUT_MS);
174
+ };
175
+ let startTimer = setTimeout(onStartTimeout, exports.TERMINAL_START_TIMEOUT_MS);
176
+ startTimer.unref();
151
177
  function restoreTty() {
152
178
  input.off('data', onInput);
153
179
  output.off('resize', onResize);
@@ -166,6 +192,7 @@ function runDedicatedTerminal(options) {
166
192
  clearTimeout(startTimer);
167
193
  pending?.close();
168
194
  restoreTty();
195
+ detachReconnectFailed();
169
196
  if (stopRemote && started)
170
197
  socket.emit(events.stop, { sessionId });
171
198
  socket.disconnect();
@@ -178,17 +205,34 @@ function runDedicatedTerminal(options) {
178
205
  clearTimeout(startTimer);
179
206
  pending?.close();
180
207
  restoreTty();
208
+ detachReconnectFailed();
181
209
  socket.disconnect();
182
210
  reject(error);
183
211
  }
184
212
  socket.on('connect_error', (err) => {
213
+ // A failed retry while reattaching is not fatal — the attempt countdown
214
+ // keeps running and `reconnect_failed` ends it if they all miss.
215
+ if (persistent && reattaching)
216
+ return;
185
217
  fail(new DedicatedTerminalError(`Could not connect to skrr: ${err.message}`, err.data?.code || 'TERMINAL_CONNECT_FAILED'));
186
218
  });
187
219
  socket.on('connect', () => {
220
+ if (finished)
221
+ return;
222
+ sessionId = (0, node_crypto_1.randomUUID)();
223
+ clearTimeout(startTimer);
224
+ startTimer = setTimeout(onStartTimeout, exports.TERMINAL_START_TIMEOUT_MS);
225
+ startTimer.unref();
188
226
  socket.emit(events.start, {
189
227
  runtimeId: options.runtimeId,
190
228
  sessionId,
191
229
  ...(options.cwd ? { cwd: options.cwd } : {}),
230
+ ...(persistent ? { persistent: true } : {}),
231
+ // The RESOLVED name, not just the requested one: a `--new` session the
232
+ // machine named itself comes back in the ack, and a re-attach must
233
+ // name it — `tmux -A` without `-s` would open a fresh session instead.
234
+ ...(persistentName ? { sessionName: persistentName } : {}),
235
+ ...(options.leaseId ? { leaseId: options.leaseId } : {}),
192
236
  ...size(),
193
237
  });
194
238
  });
@@ -201,8 +245,20 @@ function runDedicatedTerminal(options) {
201
245
  fail(new DedicatedTerminalError(describeTerminalStartRefusal(ack), ack.code || 'TERMINAL_START_REFUSED'));
202
246
  return;
203
247
  }
248
+ const reattached = reattaching;
204
249
  started = true;
250
+ reattaching = false;
205
251
  clearTimeout(startTimer);
252
+ if (typeof ack.persistentName === 'string' && ack.persistentName) {
253
+ persistentName = ack.persistentName;
254
+ }
255
+ if (sessionReady) {
256
+ if (reattached) {
257
+ errorOutput.write(`\r\n[reattached to session '${persistentName}']\r\n`);
258
+ }
259
+ return;
260
+ }
261
+ sessionReady = true;
206
262
  if (typeof input.setRawMode === 'function') {
207
263
  input.setRawMode(true);
208
264
  rawMode = true;
@@ -213,6 +269,9 @@ function runDedicatedTerminal(options) {
213
269
  process.on('SIGTERM', onSigterm);
214
270
  process.on('SIGHUP', onSighup);
215
271
  pending?.beginSession();
272
+ if (persistent && persistentName) {
273
+ errorOutput.write(`\r\n[session '${persistentName}' is kept alive on the machine — Ctrl-] detaches, the same command re-attaches]\r\n`);
274
+ }
216
275
  });
217
276
  socket.on(events.output, (frame) => {
218
277
  if (!frame || frame.sessionId !== sessionId || finished)
@@ -233,9 +292,45 @@ function runDedicatedTerminal(options) {
233
292
  output.write(bytes);
234
293
  pending?.observeOutput(bytes);
235
294
  });
295
+ // socket.io reports a given-up reconnect on the manager; a bare EventEmitter
296
+ // test socket has none, so the socket itself carries the listener then.
297
+ const onReconnectFailed = () => {
298
+ if (finished || !persistent)
299
+ return;
300
+ const named = persistentName ? ` '${persistentName}'` : '';
301
+ errorOutput.write(`\r\n[could not reconnect to skrr — session${named} still runs on the machine; rerun the same command to reattach]\r\n`);
302
+ finish(1, false);
303
+ };
304
+ const manager = socket.io;
305
+ const onManager = typeof manager?.on === 'function';
306
+ if (onManager)
307
+ manager.on('reconnect_failed', onReconnectFailed);
308
+ else
309
+ socket.on('reconnect_failed', onReconnectFailed);
310
+ // The manager is SHARED per url+path (socket.io multiplexing), so the
311
+ // listener outlives this terminal unless it is removed on every exit.
312
+ function detachReconnectFailed() {
313
+ if (onManager && typeof manager?.off === 'function') {
314
+ manager.off('reconnect_failed', onReconnectFailed);
315
+ }
316
+ else if (!onManager) {
317
+ socket.off('reconnect_failed', onReconnectFailed);
318
+ }
319
+ }
236
320
  socket.on('disconnect', () => {
237
321
  if (finished)
238
322
  return;
323
+ if (persistent) {
324
+ // The tmux session outlives the link — the tmux client's death only
325
+ // detaches. socket.io retries and the next start re-attaches by name.
326
+ started = false;
327
+ if (!reattaching) {
328
+ reattaching = true;
329
+ const named = persistentName ? ` '${persistentName}'` : '';
330
+ errorOutput.write(`\r\n[lost the connection to skrr — reattaching to session${named}…]\r\n`);
331
+ }
332
+ return;
333
+ }
239
334
  if (!started) {
240
335
  fail(new DedicatedTerminalError('The connection to skrr closed before the terminal started.', 'TERMINAL_DISCONNECTED'));
241
336
  return;
@@ -49,6 +49,7 @@ const fs = __importStar(require("node:fs"));
49
49
  const os = __importStar(require("node:os"));
50
50
  const path = __importStar(require("node:path"));
51
51
  const auth_core_1 = require("@skrr-ai/auth-core");
52
+ const loopback_http_1 = require("@skrr-ai/auth-core/loopback-http");
52
53
  function isDelegatedCliSession(env = process.env) {
53
54
  return Boolean(env.OVERSKY_AUTONOMOUS_SESSION === '1' &&
54
55
  env.OVERSKY_DELEGATED_CLI_BROKER_URL?.trim() &&
@@ -213,7 +214,9 @@ async function fetchDelegatedCliCredential(env = process.env, options = {}) {
213
214
  try {
214
215
  let res;
215
216
  try {
216
- res = await fetch(brokerUrl, {
217
+ // Proxy-free and loopback-only (OSK-12134): the capability and the
218
+ // credential it returns must never reach HTTP_PROXY.
219
+ res = await (0, loopback_http_1.loopbackFetch)(brokerUrl, {
217
220
  method: 'POST',
218
221
  headers: { Accept: 'application/json', 'Content-Type': 'application/json' },
219
222
  body: JSON.stringify({
@@ -71,7 +71,9 @@ export declare function notInstalledMessage(): string;
71
71
  * preserving "exit code = the daemon's exit code" semantics that scripts
72
72
  * may depend on.
73
73
  */
74
- export declare function execRuntimeBinary(args: string[]): Promise<number>;
74
+ export declare function execRuntimeBinary(args: string[], opts?: {
75
+ binary?: string;
76
+ }): Promise<number>;
75
77
  /** Output from a short-lived runtime probe such as `skrrd status --json`. */
76
78
  export type RuntimeBinaryOutput = {
77
79
  code: number;
@@ -212,9 +212,11 @@ function notInstalledMessage() {
212
212
  * preserving "exit code = the daemon's exit code" semantics that scripts
213
213
  * may depend on.
214
214
  */
215
- function execRuntimeBinary(args) {
215
+ function execRuntimeBinary(args, opts = {}) {
216
216
  return new Promise((resolve, reject) => {
217
- const bin = findRuntimeBinary();
217
+ // `binary` lets `status` render from the LIVE daemon's program rather than
218
+ // the first one on PATH (OSK-12102); every other verb forwards to PATH.
219
+ const bin = opts.binary ?? findRuntimeBinary();
218
220
  if (!bin) {
219
221
  reject(new Error('runtime-binary-not-installed'));
220
222
  return;
@@ -223,6 +223,16 @@ export interface ManagedEngineHooks {
223
223
  * never from the env this hook returned.
224
224
  */
225
225
  credentialMode: () => 'managed' | 'byok';
226
+ /**
227
+ * `prepare` fell back to BYOK AND no provider credential will reach the
228
+ * engine — the exact case where the fallback line already warned that the
229
+ * first request will fail. The caller uses this AFTER a non-zero engine
230
+ * exit to tie an opaque engine failure (an `Unexpected server error` that
231
+ * says nothing about auth) back to the missing credential, without
232
+ * refusing the launch up front: the engine can carry a stored credential
233
+ * `hasProviderCredential` cannot see (OSK-11949).
234
+ */
235
+ unprovisionedByok: () => boolean;
226
236
  }
227
237
  export interface ManagedEngineHooksInput {
228
238
  subcommand: string | null;