@skrr-ai/cli 0.1.46 → 0.1.48

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 (305) hide show
  1. package/README.md +1 -1
  2. package/dist/base-command.d.ts +18 -0
  3. package/dist/base-command.js +61 -1
  4. package/dist/commands/agents/chat.d.ts +21 -0
  5. package/dist/commands/agents/chat.js +26 -9
  6. package/dist/commands/balance/usage/breakdown.d.ts +14 -0
  7. package/dist/commands/balance/usage/breakdown.js +68 -0
  8. package/dist/commands/balance/usage.js +24 -0
  9. package/dist/commands/code/handover.d.ts +2 -2
  10. package/dist/commands/code/handover.js +12 -13
  11. package/dist/commands/code/install.js +31 -0
  12. package/dist/commands/code/update.d.ts +46 -0
  13. package/dist/commands/code/update.js +242 -0
  14. package/dist/commands/commerce/entitlements/explain.d.ts +22 -0
  15. package/dist/commands/commerce/entitlements/explain.js +40 -0
  16. package/dist/commands/commerce/incidents/audit.d.ts +20 -0
  17. package/dist/commands/commerce/incidents/audit.js +37 -0
  18. package/dist/commands/commerce/incidents/list.d.ts +22 -0
  19. package/dist/commands/commerce/incidents/list.js +49 -0
  20. package/dist/commands/commerce/incidents/resolve.d.ts +23 -0
  21. package/dist/commands/commerce/incidents/resolve.js +45 -0
  22. package/dist/commands/commerce/incidents/retry.d.ts +22 -0
  23. package/dist/commands/commerce/incidents/retry.js +45 -0
  24. package/dist/commands/commerce/incidents/show.d.ts +13 -0
  25. package/dist/commands/commerce/incidents/show.js +30 -0
  26. package/dist/commands/commerce/offers/approve.d.ts +17 -0
  27. package/dist/commands/commerce/offers/approve.js +33 -0
  28. package/dist/commands/commerce/offers/create.d.ts +18 -0
  29. package/dist/commands/commerce/offers/create.js +34 -0
  30. package/dist/commands/commerce/offers/publish.d.ts +13 -0
  31. package/dist/commands/commerce/offers/publish.js +28 -0
  32. package/dist/commands/commerce/offers/retire.d.ts +17 -0
  33. package/dist/commands/commerce/offers/retire.js +32 -0
  34. package/dist/commands/commerce/offers/validate.d.ts +18 -0
  35. package/dist/commands/commerce/offers/validate.js +33 -0
  36. package/dist/commands/commerce/products/create.d.ts +10 -0
  37. package/dist/commands/commerce/products/create.js +25 -0
  38. package/dist/commands/commerce/promotions/redeem.d.ts +13 -0
  39. package/dist/commands/commerce/promotions/redeem.js +32 -0
  40. package/dist/commands/commerce/terms/accept.d.ts +18 -0
  41. package/dist/commands/commerce/terms/accept.js +39 -0
  42. package/dist/commands/commerce/terms/show.d.ts +10 -0
  43. package/dist/commands/commerce/terms/show.js +34 -0
  44. package/dist/commands/commerce/trials/conversion-preview.d.ts +18 -0
  45. package/dist/commands/commerce/trials/conversion-preview.js +39 -0
  46. package/dist/commands/commerce/trials/start.d.ts +23 -0
  47. package/dist/commands/commerce/trials/start.js +58 -0
  48. package/dist/commands/commitments/accounting.d.ts +39 -0
  49. package/dist/commands/commitments/accounting.js +116 -0
  50. package/dist/commands/commitments/action-history.d.ts +1 -0
  51. package/dist/commands/commitments/action-history.js +8 -1
  52. package/dist/commands/commitments/case.d.ts +1 -0
  53. package/dist/commands/commitments/case.js +19 -3
  54. package/dist/commands/commitments/cases.d.ts +1 -0
  55. package/dist/commands/commitments/cases.js +11 -1
  56. package/dist/commands/commitments/checks.d.ts +1 -0
  57. package/dist/commands/commitments/checks.js +5 -1
  58. package/dist/commands/commitments/cost.d.ts +62 -0
  59. package/dist/commands/commitments/cost.js +130 -0
  60. package/dist/commands/commitments/create.js +1 -1
  61. package/dist/commands/commitments/delete.d.ts +1 -0
  62. package/dist/commands/commitments/delete.js +56 -6
  63. package/dist/commands/commitments/feedback.js +9 -1
  64. package/dist/commands/commitments/handover.js +16 -0
  65. package/dist/commands/commitments/hypotheses.d.ts +1 -0
  66. package/dist/commands/commitments/hypotheses.js +17 -2
  67. package/dist/commands/commitments/links/list.js +4 -1
  68. package/dist/commands/commitments/liveness.d.ts +18 -0
  69. package/dist/commands/commitments/liveness.js +80 -0
  70. package/dist/commands/commitments/metrics.d.ts +1 -0
  71. package/dist/commands/commitments/metrics.js +8 -1
  72. package/dist/commands/commitments/next-tick.d.ts +39 -0
  73. package/dist/commands/commitments/next-tick.js +122 -0
  74. package/dist/commands/commitments/observations.d.ts +1 -0
  75. package/dist/commands/commitments/observations.js +19 -2
  76. package/dist/commands/commitments/pause.js +12 -0
  77. package/dist/commands/commitments/receipt.d.ts +2 -0
  78. package/dist/commands/commitments/receipt.js +16 -2
  79. package/dist/commands/commitments/remediation-proposal.js +6 -2
  80. package/dist/commands/commitments/remediation-proposals.js +5 -1
  81. package/dist/commands/commitments/restore.d.ts +25 -0
  82. package/dist/commands/commitments/restore.js +46 -0
  83. package/dist/commands/commitments/show.js +32 -0
  84. package/dist/commands/commitments/trigger-status.js +33 -0
  85. package/dist/commands/commitments/update.d.ts +1 -0
  86. package/dist/commands/commitments/update.js +31 -5
  87. package/dist/commands/commitments/wake-capabilities.d.ts +0 -1
  88. package/dist/commands/commitments/wake-capabilities.js +8 -4
  89. package/dist/commands/convos/handover.d.ts +1 -0
  90. package/dist/commands/convos/handover.js +4 -0
  91. package/dist/commands/daemon/index.js +2 -0
  92. package/dist/commands/daemon/update.d.ts +19 -0
  93. package/dist/commands/daemon/update.js +48 -0
  94. package/dist/commands/enterprise/budgets/create.d.ts +11 -0
  95. package/dist/commands/enterprise/budgets/create.js +27 -0
  96. package/dist/commands/enterprise/budgets/evaluate.d.ts +17 -0
  97. package/dist/commands/enterprise/budgets/evaluate.js +32 -0
  98. package/dist/commands/enterprise/budgets/publish.d.ts +14 -0
  99. package/dist/commands/enterprise/budgets/publish.js +30 -0
  100. package/dist/commands/enterprise/contracts/activate.d.ts +14 -0
  101. package/dist/commands/enterprise/contracts/activate.js +30 -0
  102. package/dist/commands/enterprise/contracts/create.d.ts +14 -0
  103. package/dist/commands/enterprise/contracts/create.js +32 -0
  104. package/dist/commands/enterprise/contracts/issue.d.ts +18 -0
  105. package/dist/commands/enterprise/contracts/issue.js +34 -0
  106. package/dist/commands/enterprise/domains/begin.d.ts +20 -0
  107. package/dist/commands/enterprise/domains/begin.js +39 -0
  108. package/dist/commands/enterprise/domains/verify.d.ts +14 -0
  109. package/dist/commands/enterprise/domains/verify.js +32 -0
  110. package/dist/commands/enterprise/external-config/activate.d.ts +22 -0
  111. package/dist/commands/enterprise/external-config/activate.js +49 -0
  112. package/dist/commands/enterprise/external-config/set.d.ts +25 -0
  113. package/dist/commands/enterprise/external-config/set.js +123 -0
  114. package/dist/commands/enterprise/external-config/show.d.ts +12 -0
  115. package/dist/commands/enterprise/external-config/show.js +39 -0
  116. package/dist/commands/enterprise/external-config/suspend.d.ts +13 -0
  117. package/dist/commands/enterprise/external-config/suspend.js +41 -0
  118. package/dist/commands/enterprise/finance/close-snapshot.d.ts +11 -0
  119. package/dist/commands/enterprise/finance/close-snapshot.js +28 -0
  120. package/dist/commands/enterprise/finance/record-document.d.ts +18 -0
  121. package/dist/commands/enterprise/finance/record-document.js +35 -0
  122. package/dist/commands/enterprise/profile.d.ts +20 -0
  123. package/dist/commands/enterprise/profile.js +38 -0
  124. package/dist/commands/login.d.ts +13 -0
  125. package/dist/commands/login.js +77 -0
  126. package/dist/commands/machines/dedicated/index.js +3 -0
  127. package/dist/commands/machines/dedicated/service/apply.d.ts +23 -0
  128. package/dist/commands/machines/dedicated/service/apply.js +99 -0
  129. package/dist/commands/machines/dedicated/service/approve.d.ts +23 -0
  130. package/dist/commands/machines/dedicated/service/approve.js +52 -0
  131. package/dist/commands/machines/dedicated/service/create.d.ts +55 -0
  132. package/dist/commands/machines/dedicated/service/create.js +222 -0
  133. package/dist/commands/machines/dedicated/service/edit.d.ts +30 -0
  134. package/dist/commands/machines/dedicated/service/edit.js +146 -0
  135. package/dist/commands/machines/dedicated/service/exec.d.ts +26 -0
  136. package/dist/commands/machines/dedicated/service/exec.js +92 -0
  137. package/dist/commands/machines/dedicated/service/export.d.ts +29 -0
  138. package/dist/commands/machines/dedicated/service/export.js +106 -0
  139. package/dist/commands/machines/dedicated/service/index.d.ts +14 -0
  140. package/dist/commands/machines/dedicated/service/index.js +49 -0
  141. package/dist/commands/machines/dedicated/service/logs.d.ts +33 -0
  142. package/dist/commands/machines/dedicated/service/logs.js +148 -0
  143. package/dist/commands/machines/dedicated/service/ls.d.ts +20 -0
  144. package/dist/commands/machines/dedicated/service/ls.js +80 -0
  145. package/dist/commands/machines/dedicated/service/restart.d.ts +14 -0
  146. package/dist/commands/machines/dedicated/service/restart.js +29 -0
  147. package/dist/commands/machines/dedicated/service/rm.d.ts +20 -0
  148. package/dist/commands/machines/dedicated/service/rm.js +33 -0
  149. package/dist/commands/machines/dedicated/service/secret/index.d.ts +14 -0
  150. package/dist/commands/machines/dedicated/service/secret/index.js +25 -0
  151. package/dist/commands/machines/dedicated/service/secret/rm.d.ts +22 -0
  152. package/dist/commands/machines/dedicated/service/secret/rm.js +54 -0
  153. package/dist/commands/machines/dedicated/service/secret/set.d.ts +22 -0
  154. package/dist/commands/machines/dedicated/service/secret/set.js +93 -0
  155. package/dist/commands/machines/dedicated/service/start.d.ts +14 -0
  156. package/dist/commands/machines/dedicated/service/start.js +29 -0
  157. package/dist/commands/machines/dedicated/service/status.d.ts +21 -0
  158. package/dist/commands/machines/dedicated/service/status.js +85 -0
  159. package/dist/commands/machines/dedicated/service/stop.d.ts +20 -0
  160. package/dist/commands/machines/dedicated/service/stop.js +35 -0
  161. package/dist/commands/machines/dedicated/service/top.d.ts +22 -0
  162. package/dist/commands/machines/dedicated/service/top.js +120 -0
  163. package/dist/commands/machines/dedicated/sign-in-status.d.ts +22 -0
  164. package/dist/commands/machines/dedicated/sign-in-status.js +79 -0
  165. package/dist/commands/machines/dedicated/ssh-config.d.ts +28 -0
  166. package/dist/commands/machines/dedicated/ssh-config.js +66 -0
  167. package/dist/commands/machines/dedicated/ssh-proxy.d.ts +22 -0
  168. package/dist/commands/machines/dedicated/ssh-proxy.js +62 -0
  169. package/dist/commands/machines/dedicated/ssh.d.ts +29 -0
  170. package/dist/commands/machines/dedicated/ssh.js +121 -0
  171. package/dist/commands/native/allow-all.d.ts +14 -0
  172. package/dist/commands/native/allow-all.js +25 -0
  173. package/dist/commands/native/allow.d.ts +24 -0
  174. package/dist/commands/native/allow.js +45 -0
  175. package/dist/commands/native/deny.d.ts +24 -0
  176. package/dist/commands/native/deny.js +43 -0
  177. package/dist/commands/native/disable.d.ts +17 -0
  178. package/dist/commands/native/disable.js +28 -0
  179. package/dist/commands/native/enable.d.ts +12 -0
  180. package/dist/commands/native/enable.js +29 -0
  181. package/dist/commands/native/list-apps.d.ts +19 -0
  182. package/dist/commands/native/list-apps.js +30 -0
  183. package/dist/commands/native/restrict-to.d.ts +22 -0
  184. package/dist/commands/native/restrict-to.js +41 -0
  185. package/dist/commands/native/status.d.ts +19 -0
  186. package/dist/commands/native/status.js +33 -0
  187. package/dist/commands/native.d.ts +39 -0
  188. package/dist/commands/native.js +76 -0
  189. package/dist/commands/service/index.d.ts +14 -0
  190. package/dist/commands/service/index.js +31 -0
  191. package/dist/commands/service/logs.d.ts +18 -0
  192. package/dist/commands/service/logs.js +53 -0
  193. package/dist/commands/service/ls.d.ts +13 -0
  194. package/dist/commands/service/ls.js +48 -0
  195. package/dist/commands/service/propose.d.ts +34 -0
  196. package/dist/commands/service/propose.js +113 -0
  197. package/dist/commands/service/status.d.ts +17 -0
  198. package/dist/commands/service/status.js +56 -0
  199. package/dist/commands/spaces/auto-executor/runs.d.ts +13 -0
  200. package/dist/commands/spaces/auto-executor/runs.js +56 -0
  201. package/dist/commands/spaces/briefs/get.js +8 -0
  202. package/dist/commands/spaces/briefs/list.js +2 -0
  203. package/dist/commands/spaces/briefs/set.d.ts +3 -0
  204. package/dist/commands/spaces/briefs/set.js +78 -13
  205. package/dist/commands/spaces/expertise-suggestions.d.ts +12 -0
  206. package/dist/commands/spaces/expertise-suggestions.js +71 -0
  207. package/dist/commands/spaces/members/add.d.ts +1 -0
  208. package/dist/commands/spaces/members/add.js +11 -3
  209. package/dist/commands/spaces/members/list.js +11 -0
  210. package/dist/commands/store/governance/artifacts.d.ts +25 -0
  211. package/dist/commands/store/governance/artifacts.js +55 -0
  212. package/dist/commands/store/governance/assign.d.ts +16 -0
  213. package/dist/commands/store/governance/assign.js +54 -0
  214. package/dist/commands/store/governance/create-license.d.ts +18 -0
  215. package/dist/commands/store/governance/create-license.js +35 -0
  216. package/dist/commands/store/governance/install-plan.d.ts +25 -0
  217. package/dist/commands/store/governance/install-plan.js +56 -0
  218. package/dist/commands/store/governance/license-policy.d.ts +30 -0
  219. package/dist/commands/store/governance/license-policy.js +69 -0
  220. package/dist/commands/store/governance/policy.d.ts +25 -0
  221. package/dist/commands/store/governance/policy.js +65 -0
  222. package/dist/commands/store/governance/publish-release.d.ts +17 -0
  223. package/dist/commands/store/governance/publish-release.js +34 -0
  224. package/dist/commands/tasks/assign.d.ts +9 -0
  225. package/dist/commands/tasks/assign.js +108 -1
  226. package/dist/commands/tasks/deliverable/add.d.ts +15 -0
  227. package/dist/commands/tasks/deliverable/add.js +174 -0
  228. package/dist/commands/tasks/deliverable/archive.d.ts +35 -0
  229. package/dist/commands/tasks/deliverable/archive.js +205 -0
  230. package/dist/commands/tasks/labels/detach.js +3 -0
  231. package/dist/commands/tasks/labels/suggest.d.ts +24 -0
  232. package/dist/commands/tasks/labels/suggest.js +69 -0
  233. package/dist/commands/tasks/list.d.ts +16 -2
  234. package/dist/commands/tasks/list.js +100 -24
  235. package/dist/commands/tasks/show.d.ts +23 -0
  236. package/dist/commands/tasks/show.js +51 -1
  237. package/dist/commands/wiki/rm.d.ts +22 -1
  238. package/dist/commands/wiki/rm.js +52 -3
  239. package/dist/lib/agentic-stream.js +10 -2
  240. package/dist/lib/assignee-resolver.d.ts +22 -0
  241. package/dist/lib/assignee-resolver.js +26 -0
  242. package/dist/lib/balance.d.ts +6 -1
  243. package/dist/lib/balance.js +15 -0
  244. package/dist/lib/code-handover.d.ts +0 -10
  245. package/dist/lib/code-handover.js +20 -5
  246. package/dist/lib/commerce-command.d.ts +48 -0
  247. package/dist/lib/commerce-command.js +70 -0
  248. package/dist/lib/commerce.d.ts +93 -0
  249. package/dist/lib/commerce.js +129 -0
  250. package/dist/lib/commitment-product.js +1 -1
  251. package/dist/lib/commitments.d.ts +202 -29
  252. package/dist/lib/commitments.js +300 -33
  253. package/dist/lib/dedicated-lease-command.js +10 -0
  254. package/dist/lib/dedicated-machines.d.ts +37 -0
  255. package/dist/lib/dedicated-machines.js +47 -0
  256. package/dist/lib/dedicated-service-command.d.ts +60 -0
  257. package/dist/lib/dedicated-service-command.js +182 -0
  258. package/dist/lib/dedicated-service.d.ts +342 -0
  259. package/dist/lib/dedicated-service.js +741 -0
  260. package/dist/lib/dedicated-ssh.d.ts +136 -0
  261. package/dist/lib/dedicated-ssh.js +387 -0
  262. package/dist/lib/delegated-cli.js +51 -13
  263. package/dist/lib/enterprise-commerce.d.ts +67 -0
  264. package/dist/lib/enterprise-commerce.js +112 -0
  265. package/dist/lib/enterprise-external-configuration.d.ts +60 -0
  266. package/dist/lib/enterprise-external-configuration.js +58 -0
  267. package/dist/lib/first-party-harness.js +17 -3
  268. package/dist/lib/governed-store.d.ts +110 -0
  269. package/dist/lib/governed-store.js +79 -0
  270. package/dist/lib/guest-service-command.d.ts +16 -0
  271. package/dist/lib/guest-service-command.js +32 -0
  272. package/dist/lib/guest-service.d.ts +67 -0
  273. package/dist/lib/guest-service.js +112 -0
  274. package/dist/lib/prompt.d.ts +1 -0
  275. package/dist/lib/prompt.js +1 -0
  276. package/dist/lib/task-asset-upload.d.ts +20 -0
  277. package/dist/lib/task-asset-upload.js +30 -8
  278. package/dist/lib/task-extras.d.ts +23 -0
  279. package/dist/lib/task-extras.js +6 -0
  280. package/dist/lib/update-check.d.ts +80 -7
  281. package/dist/lib/update-check.js +365 -33
  282. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonScopes.d.ts +9 -3
  283. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonScopes.js +10 -3
  284. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.d.ts +9 -1
  285. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/firstPartyHarness.js +8 -0
  286. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
  287. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +6 -5
  288. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.d.ts +16 -0
  289. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.js +21 -4
  290. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +1 -1
  291. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +24 -2
  292. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonScopes.d.ts +9 -3
  293. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonScopes.js +10 -3
  294. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.d.ts +9 -1
  295. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/firstPartyHarness.js +8 -0
  296. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
  297. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -1
  298. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.d.ts +16 -0
  299. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.js +20 -4
  300. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +1 -1
  301. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +24 -2
  302. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  303. package/dist/node_modules/@skrr-ai/data-provider/index.js +5067 -4821
  304. package/oclif.manifest.json +34877 -28659
  305. package/package.json +43 -1
@@ -0,0 +1,136 @@
1
+ import { io, type Socket } from 'socket.io-client';
2
+ /**
3
+ * Owner SSH to a Dedicated Runtime — SSH protocol over the admitted relay.
4
+ *
5
+ * Design: docs/architecture/dedicated-runtime-ssh-access-2026-09-21.md.
6
+ *
7
+ * This module is the ProxyCommand end of the arrangement. `ssh` speaks its
8
+ * binary protocol to a ProxyCommand's stdio; `runSshProxy` connects the CLI's
9
+ * authenticated socket, asks the API for a channel on the lease
10
+ * (`dedicated:ssh:start`, carrying an ephemeral public key the daemon admits
11
+ * for this one channel), and then pipes bytes. Nothing here authenticates the
12
+ * MACHINE — the API admission does — and nothing here authenticates the
13
+ * CLIENT to sshd — the daemon's admission of the public key does, for exactly
14
+ * as long as the channel lives.
15
+ *
16
+ * Per-device files under ~/.skrr/ssh/ (one identity, one pin file, every
17
+ * lease — the config block is a wildcard `Host rtl_*.skrr`, so setup is
18
+ * once-ever, not once-per-lease):
19
+ *
20
+ * id_ed25519(.pub) the identity this device offers. It is NOT "the key to
21
+ * the machine" — it opens nothing unless the daemon
22
+ * admitted it for a live channel, and the daemon revokes
23
+ * it the moment the channel ends. Losing it loses nothing
24
+ * that was not already a logged-in CLI.
25
+ * known_hosts the host's line is rewritten on every connect from the
26
+ * host keys the daemon just read off THIS instance's
27
+ * /etc/ssh — carried inside the admitted, generation-
28
+ * fenced channel, so the usual first-connect TOFU
29
+ * question never exists, and a replaced machine (new host
30
+ * keys, same workspace) is a silent update rather than a
31
+ * MITM warning.
32
+ */
33
+ export declare const SSH_HOST_SUFFIX = ".skrr";
34
+ export declare const SSH_USER = "skrr-workload";
35
+ export declare const SSH_START_TIMEOUT_MS = 30000;
36
+ /** The managed block's markers in ~/.ssh/config — one bounded, owned region. */
37
+ export declare const SSH_CONFIG_BEGIN = "# >>> skrr dedicated runtime >>>";
38
+ export declare const SSH_CONFIG_END = "# <<< skrr dedicated runtime <<<";
39
+ export declare class DedicatedSshError extends Error {
40
+ readonly code: string;
41
+ constructor(message: string, code: string);
42
+ }
43
+ /** The lease a ProxyCommand host names: `rtl_x.skrr` or a bare `rtl_x`. */
44
+ export declare function leaseIdForSshHost(host: string): string | null;
45
+ export declare function sshDir(home?: string): string;
46
+ /**
47
+ * The identity key this device offers to any lease, created on first use.
48
+ * 0600 in a 0700 directory; `ssh-keygen` is on every machine that has `ssh`,
49
+ * which is every machine this feature runs on. Per-device by design (§8): the
50
+ * grant is the channel admission, not the key — one keypair for the device is
51
+ * the whole surface, so there is nothing per-lease to rotate or lose.
52
+ */
53
+ export declare function ensureSshIdentity(home?: string): {
54
+ privateKey: string;
55
+ publicKey: string;
56
+ keyType: string;
57
+ keyBlob: string;
58
+ };
59
+ /**
60
+ * Rewrite `host`'s lines in the shared known_hosts from keys carried by the
61
+ * admitted channel. Other leases' pins are kept — each connect replaces only
62
+ * its own host's rows, atomically. Returns false when the daemon reported no
63
+ * keys: the caller proceeds, and StrictHostKeyChecking=yes surfaces the gap
64
+ * honestly rather than silently accepting a key nobody attested.
65
+ */
66
+ export declare function writeKnownHosts(host: string, hostKeys: {
67
+ type: string;
68
+ key: string;
69
+ }[] | undefined, home?: string): boolean;
70
+ export declare function knownHostsPath(home?: string): string;
71
+ /**
72
+ * The ProxyCommand string OpenSSH runs, quoted for the shell OpenSSH applies
73
+ * to it: `/bin/sh -c` on POSIX (single quotes), `cmd.exe /c` on Windows
74
+ * (double quotes). `argv[1]` is the CLI's entry script (bin/run.js), so this
75
+ * re-invokes whatever binary the user called — installed or in dev.
76
+ */
77
+ export declare function sshProxyCommand(platform?: NodeJS.Platform): string;
78
+ /**
79
+ * The managed `Host` block — one wildcard stanza covers every lease, so the
80
+ * user installs it once and `ssh rtl_<id>.skrr` (and scp, rsync, Remote-SSH)
81
+ * works for every machine they ever lease.
82
+ *
83
+ * `StrictHostKeyChecking yes`, deliberately not accept-new: the host key is
84
+ * pinned by the proxy from the admitted channel BEFORE the handshake reads
85
+ * the file, so "yes" costs nothing and an unattested key can never be
86
+ * silently accepted. `IdentitiesOnly` keeps the user's real agent keys from
87
+ * being offered — they are never admitted, and offering them would just
88
+ * spend `MaxAuthTries`. Paths are absolute so the block stays correct even
89
+ * if the user's HOME moves.
90
+ */
91
+ export declare function sshConfigBlock(home?: string): string;
92
+ /**
93
+ * Install the managed block into ~/.ssh/config between the skrr markers — a
94
+ * bounded, owned region of a file the user owns (the control-surface rule).
95
+ *
96
+ * Refuses — rather than edits — when the markers exist but cannot bound a
97
+ * region: one marker alone, out of order, or duplicated (a hand-edited file,
98
+ * or another tool already using these strings). Guessing at a corrupted
99
+ * region risks writing over the user's own Host blocks; a refusal names
100
+ * exactly what to fix.
101
+ */
102
+ export declare function installSshConfigBlock(home?: string): {
103
+ configPath: string;
104
+ changed: boolean;
105
+ };
106
+ export interface SshStartAck {
107
+ channelId?: string;
108
+ ok?: boolean;
109
+ error?: string;
110
+ code?: string;
111
+ hostKeys?: {
112
+ type: string;
113
+ key: string;
114
+ }[];
115
+ }
116
+ export interface SshProxyOptions {
117
+ baseURL: string;
118
+ token: string;
119
+ leaseId: string;
120
+ input: NodeJS.ReadStream;
121
+ output: NodeJS.WriteStream;
122
+ /** Diagnostics; never stdout — ssh reads stdout as protocol bytes. */
123
+ errorOutput: NodeJS.WritableStream;
124
+ ttlSeconds?: number;
125
+ /** Injected for tests. */
126
+ connect?: (url: string, opts: Parameters<typeof io>[1]) => Socket;
127
+ /** Injected for tests. */
128
+ writeKnownHosts?: typeof writeKnownHosts;
129
+ }
130
+ export declare function describeSshStartRefusal(ack: SshStartAck): string;
131
+ /**
132
+ * Bridge stdio to one SSH channel for as long as it lives. Used as
133
+ * `ProxyCommand`: ssh writes the protocol to our stdin and reads it from our
134
+ * stdout. Resolves when the channel ends.
135
+ */
136
+ export declare function runSshProxy(options: SshProxyOptions): Promise<number>;
@@ -0,0 +1,387 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.DedicatedSshError = exports.SSH_CONFIG_END = exports.SSH_CONFIG_BEGIN = exports.SSH_START_TIMEOUT_MS = exports.SSH_USER = exports.SSH_HOST_SUFFIX = void 0;
7
+ exports.leaseIdForSshHost = leaseIdForSshHost;
8
+ exports.sshDir = sshDir;
9
+ exports.ensureSshIdentity = ensureSshIdentity;
10
+ exports.writeKnownHosts = writeKnownHosts;
11
+ exports.knownHostsPath = knownHostsPath;
12
+ exports.sshProxyCommand = sshProxyCommand;
13
+ exports.sshConfigBlock = sshConfigBlock;
14
+ exports.installSshConfigBlock = installSshConfigBlock;
15
+ exports.describeSshStartRefusal = describeSshStartRefusal;
16
+ exports.runSshProxy = runSshProxy;
17
+ const node_crypto_1 = require("node:crypto");
18
+ const node_child_process_1 = require("node:child_process");
19
+ const node_fs_1 = require("node:fs");
20
+ const node_os_1 = require("node:os");
21
+ const node_path_1 = __importDefault(require("node:path"));
22
+ const socket_io_client_1 = require("socket.io-client");
23
+ const data_provider_1 = require("@skrr-ai/data-provider");
24
+ const agentic_stream_1 = require("./agentic-stream");
25
+ /**
26
+ * Owner SSH to a Dedicated Runtime — SSH protocol over the admitted relay.
27
+ *
28
+ * Design: docs/architecture/dedicated-runtime-ssh-access-2026-09-21.md.
29
+ *
30
+ * This module is the ProxyCommand end of the arrangement. `ssh` speaks its
31
+ * binary protocol to a ProxyCommand's stdio; `runSshProxy` connects the CLI's
32
+ * authenticated socket, asks the API for a channel on the lease
33
+ * (`dedicated:ssh:start`, carrying an ephemeral public key the daemon admits
34
+ * for this one channel), and then pipes bytes. Nothing here authenticates the
35
+ * MACHINE — the API admission does — and nothing here authenticates the
36
+ * CLIENT to sshd — the daemon's admission of the public key does, for exactly
37
+ * as long as the channel lives.
38
+ *
39
+ * Per-device files under ~/.skrr/ssh/ (one identity, one pin file, every
40
+ * lease — the config block is a wildcard `Host rtl_*.skrr`, so setup is
41
+ * once-ever, not once-per-lease):
42
+ *
43
+ * id_ed25519(.pub) the identity this device offers. It is NOT "the key to
44
+ * the machine" — it opens nothing unless the daemon
45
+ * admitted it for a live channel, and the daemon revokes
46
+ * it the moment the channel ends. Losing it loses nothing
47
+ * that was not already a logged-in CLI.
48
+ * known_hosts the host's line is rewritten on every connect from the
49
+ * host keys the daemon just read off THIS instance's
50
+ * /etc/ssh — carried inside the admitted, generation-
51
+ * fenced channel, so the usual first-connect TOFU
52
+ * question never exists, and a replaced machine (new host
53
+ * keys, same workspace) is a silent update rather than a
54
+ * MITM warning.
55
+ */
56
+ exports.SSH_HOST_SUFFIX = '.skrr';
57
+ exports.SSH_USER = 'skrr-workload';
58
+ exports.SSH_START_TIMEOUT_MS = 30_000;
59
+ /** The managed block's markers in ~/.ssh/config — one bounded, owned region. */
60
+ exports.SSH_CONFIG_BEGIN = '# >>> skrr dedicated runtime >>>';
61
+ exports.SSH_CONFIG_END = '# <<< skrr dedicated runtime <<<';
62
+ const LEASE_ID_PATTERN = /^rtl_[a-z0-9]{8,64}$/;
63
+ const HOSTKEY_TYPES = new Set([
64
+ 'ssh-ed25519',
65
+ 'ssh-rsa',
66
+ 'ecdsa-sha2-nistp256',
67
+ 'ecdsa-sha2-nistp384',
68
+ 'ecdsa-sha2-nistp521',
69
+ ]);
70
+ class DedicatedSshError extends Error {
71
+ code;
72
+ constructor(message, code) {
73
+ super(message);
74
+ this.name = 'DedicatedSshError';
75
+ this.code = code;
76
+ }
77
+ }
78
+ exports.DedicatedSshError = DedicatedSshError;
79
+ /** The lease a ProxyCommand host names: `rtl_x.skrr` or a bare `rtl_x`. */
80
+ function leaseIdForSshHost(host) {
81
+ const value = String(host || '').trim();
82
+ const bare = value.endsWith(exports.SSH_HOST_SUFFIX) ? value.slice(0, -exports.SSH_HOST_SUFFIX.length) : value;
83
+ return LEASE_ID_PATTERN.test(bare) ? bare : null;
84
+ }
85
+ function sshDir(home = (0, node_os_1.homedir)()) {
86
+ return node_path_1.default.join(home, '.skrr', 'ssh');
87
+ }
88
+ /**
89
+ * The identity key this device offers to any lease, created on first use.
90
+ * 0600 in a 0700 directory; `ssh-keygen` is on every machine that has `ssh`,
91
+ * which is every machine this feature runs on. Per-device by design (§8): the
92
+ * grant is the channel admission, not the key — one keypair for the device is
93
+ * the whole surface, so there is nothing per-lease to rotate or lose.
94
+ */
95
+ function ensureSshIdentity(home = (0, node_os_1.homedir)()) {
96
+ const dir = sshDir(home);
97
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true, mode: 0o700 });
98
+ (0, node_fs_1.chmodSync)(dir, 0o700);
99
+ const privateKey = node_path_1.default.join(dir, 'id_ed25519');
100
+ const publicKeyPath = `${privateKey}.pub`;
101
+ if (!(0, node_fs_1.existsSync)(privateKey) || !(0, node_fs_1.existsSync)(publicKeyPath)) {
102
+ try {
103
+ (0, node_child_process_1.execFileSync)('ssh-keygen', [
104
+ '-q',
105
+ '-t',
106
+ 'ed25519',
107
+ '-N',
108
+ '',
109
+ '-C',
110
+ 'skrr-ssh-device',
111
+ '-f',
112
+ privateKey,
113
+ ]);
114
+ }
115
+ catch (error) {
116
+ throw new DedicatedSshError(`could not create the SSH identity — is \`ssh-keygen\` installed? (${error instanceof Error ? error.message : error})`, 'SSH_KEYGEN_FAILED');
117
+ }
118
+ (0, node_fs_1.chmodSync)(privateKey, 0o600);
119
+ (0, node_fs_1.chmodSync)(publicKeyPath, 0o644);
120
+ }
121
+ const line = (0, node_fs_1.readFileSync)(publicKeyPath, 'utf8').trim();
122
+ const [keyType, keyBlob] = line.split(/\s+/);
123
+ if (!keyType || !keyBlob) {
124
+ throw new DedicatedSshError(`could not read ${publicKeyPath}`, 'SSH_KEY_UNREADABLE');
125
+ }
126
+ return { privateKey, publicKey: publicKeyPath, keyType, keyBlob };
127
+ }
128
+ /**
129
+ * Rewrite `host`'s lines in the shared known_hosts from keys carried by the
130
+ * admitted channel. Other leases' pins are kept — each connect replaces only
131
+ * its own host's rows, atomically. Returns false when the daemon reported no
132
+ * keys: the caller proceeds, and StrictHostKeyChecking=yes surfaces the gap
133
+ * honestly rather than silently accepting a key nobody attested.
134
+ */
135
+ function writeKnownHosts(host, hostKeys, home = (0, node_os_1.homedir)()) {
136
+ if (!Array.isArray(hostKeys) || hostKeys.length === 0)
137
+ return false;
138
+ const dir = sshDir(home);
139
+ (0, node_fs_1.mkdirSync)(dir, { recursive: true, mode: 0o700 });
140
+ (0, node_fs_1.chmodSync)(dir, 0o700);
141
+ const lines = hostKeys
142
+ .filter((k) => k && HOSTKEY_TYPES.has(k.type) && /^[A-Za-z0-9+/]+=*$/.test(k.key))
143
+ .map((k) => `${host} ${k.type} ${k.key}`);
144
+ if (lines.length === 0)
145
+ return false;
146
+ const target = node_path_1.default.join(dir, 'known_hosts');
147
+ let existing = [];
148
+ try {
149
+ existing = (0, node_fs_1.readFileSync)(target, 'utf8')
150
+ .split('\n')
151
+ .filter((l) => l.trim().length > 0);
152
+ }
153
+ catch {
154
+ /* first connect */
155
+ }
156
+ const kept = existing.filter((l) => l.split(/\s+/)[0] !== host);
157
+ const tmp = `${target}.${process.pid}.${(0, node_crypto_1.randomUUID)()}.tmp`;
158
+ (0, node_fs_1.writeFileSync)(tmp, `${[...kept, ...lines].join('\n')}\n`, { mode: 0o600 });
159
+ (0, node_fs_1.renameSync)(tmp, target);
160
+ return true;
161
+ }
162
+ function knownHostsPath(home = (0, node_os_1.homedir)()) {
163
+ return node_path_1.default.join(sshDir(home), 'known_hosts');
164
+ }
165
+ /**
166
+ * The ProxyCommand string OpenSSH runs, quoted for the shell OpenSSH applies
167
+ * to it: `/bin/sh -c` on POSIX (single quotes), `cmd.exe /c` on Windows
168
+ * (double quotes). `argv[1]` is the CLI's entry script (bin/run.js), so this
169
+ * re-invokes whatever binary the user called — installed or in dev.
170
+ */
171
+ function sshProxyCommand(platform = process.platform) {
172
+ // POSIX: sh single-quotes — literal except ', which ends the quote and is
173
+ // re-entered as '\''. Windows: OpenSSH there hands the command to
174
+ // `cmd.exe /c`, where ' is not a quote at all — double-quote each word.
175
+ // Backslashes stay literal (cmd does not treat \ as an escape inside
176
+ // quotes), and " cannot appear — Windows forbids it in paths.
177
+ const quote = platform === 'win32'
178
+ ? (w) => `"${w}"`
179
+ : (w) => `'${w.split("'").join(`'\\''`)}'`;
180
+ const words = [process.execPath, process.argv[1]]
181
+ .filter((w) => typeof w === 'string' && w.length > 0)
182
+ .map(quote);
183
+ return `${words.join(' ')} machines dedicated ssh-proxy %h`;
184
+ }
185
+ /**
186
+ * The managed `Host` block — one wildcard stanza covers every lease, so the
187
+ * user installs it once and `ssh rtl_<id>.skrr` (and scp, rsync, Remote-SSH)
188
+ * works for every machine they ever lease.
189
+ *
190
+ * `StrictHostKeyChecking yes`, deliberately not accept-new: the host key is
191
+ * pinned by the proxy from the admitted channel BEFORE the handshake reads
192
+ * the file, so "yes" costs nothing and an unattested key can never be
193
+ * silently accepted. `IdentitiesOnly` keeps the user's real agent keys from
194
+ * being offered — they are never admitted, and offering them would just
195
+ * spend `MaxAuthTries`. Paths are absolute so the block stays correct even
196
+ * if the user's HOME moves.
197
+ */
198
+ function sshConfigBlock(home = (0, node_os_1.homedir)()) {
199
+ const dir = sshDir(home);
200
+ return [
201
+ `Host rtl_*${exports.SSH_HOST_SUFFIX}`,
202
+ ` User ${exports.SSH_USER}`,
203
+ ` IdentityFile ${node_path_1.default.join(dir, 'id_ed25519')}`,
204
+ ` IdentitiesOnly yes`,
205
+ ` ProxyCommand ${sshProxyCommand()}`,
206
+ ` UserKnownHostsFile ${node_path_1.default.join(dir, 'known_hosts')}`,
207
+ ` StrictHostKeyChecking yes`,
208
+ ` ServerAliveInterval 30`,
209
+ ].join('\n');
210
+ }
211
+ /**
212
+ * Install the managed block into ~/.ssh/config between the skrr markers — a
213
+ * bounded, owned region of a file the user owns (the control-surface rule).
214
+ *
215
+ * Refuses — rather than edits — when the markers exist but cannot bound a
216
+ * region: one marker alone, out of order, or duplicated (a hand-edited file,
217
+ * or another tool already using these strings). Guessing at a corrupted
218
+ * region risks writing over the user's own Host blocks; a refusal names
219
+ * exactly what to fix.
220
+ */
221
+ function installSshConfigBlock(home = (0, node_os_1.homedir)()) {
222
+ const configPath = node_path_1.default.join(home, '.ssh', 'config');
223
+ (0, node_fs_1.mkdirSync)(node_path_1.default.dirname(configPath), { recursive: true, mode: 0o700 });
224
+ (0, node_fs_1.chmodSync)(node_path_1.default.dirname(configPath), 0o700);
225
+ const block = `${exports.SSH_CONFIG_BEGIN}\n${sshConfigBlock(home)}\n${exports.SSH_CONFIG_END}`;
226
+ let text = '';
227
+ try {
228
+ text = (0, node_fs_1.readFileSync)(configPath, 'utf8');
229
+ }
230
+ catch (error) {
231
+ if (error.code !== 'ENOENT')
232
+ throw error;
233
+ }
234
+ const firstBegin = text.indexOf(exports.SSH_CONFIG_BEGIN);
235
+ const firstEnd = text.indexOf(exports.SSH_CONFIG_END);
236
+ const malformed = firstBegin === -1 ||
237
+ firstEnd === -1 ||
238
+ firstEnd < firstBegin ||
239
+ text.indexOf(exports.SSH_CONFIG_BEGIN, firstBegin + 1) !== -1 ||
240
+ text.indexOf(exports.SSH_CONFIG_END, firstEnd + 1) !== -1;
241
+ if (text.includes(exports.SSH_CONFIG_BEGIN) || text.includes(exports.SSH_CONFIG_END)) {
242
+ if (malformed) {
243
+ throw new DedicatedSshError(`${configPath} has a partial or duplicated skrr block and is not safe to edit. Fix it by hand, or remove the lines containing 'skrr dedicated runtime' and run ssh-config again.`, 'SSH_CONFIG_COLLISION');
244
+ }
245
+ const next = text.slice(0, firstBegin) + block + text.slice(firstEnd + exports.SSH_CONFIG_END.length);
246
+ (0, node_fs_1.writeFileSync)(configPath, next, { mode: 0o600 });
247
+ return { configPath, changed: next !== text };
248
+ }
249
+ let separator = '';
250
+ if (text.length > 0)
251
+ separator = text.endsWith('\n\n') ? '' : '\n\n';
252
+ const next = `${text}${separator}${block}\n`;
253
+ (0, node_fs_1.writeFileSync)(configPath, next, { mode: 0o600 });
254
+ return { configPath, changed: true };
255
+ }
256
+ function describeSshStartRefusal(ack) {
257
+ const code = ack.code || '';
258
+ if (code === 'DEDICATED_RUNTIME_SSH_UNSUPPORTED') {
259
+ return (ack.error ||
260
+ 'This machine’s image does not carry SSH support yet. Update it with `skrr machines dedicated update-image <lease-id> --wait`.');
261
+ }
262
+ if (code === 'dedicated_runtime_quiesced') {
263
+ return 'The machine is checkpointing its workspace right now; try again in a moment.';
264
+ }
265
+ if (code === 'DEDICATED_RUNTIME_GENERATION_STALE') {
266
+ return 'The machine was replaced since its daemon registered. Wait a minute and try again.';
267
+ }
268
+ if (code === 'DEDICATED_RUNTIME_NOT_ASSIGNABLE' || code === 'not_found' || code === 'offline') {
269
+ return 'The machine is not running or its daemon is not connected. Check it with `skrr machines dedicated show <lease-id>`.';
270
+ }
271
+ return ack.error ? `SSH did not start: ${ack.error}` : 'SSH did not start.';
272
+ }
273
+ /**
274
+ * Bridge stdio to one SSH channel for as long as it lives. Used as
275
+ * `ProxyCommand`: ssh writes the protocol to our stdin and reads it from our
276
+ * stdout. Resolves when the channel ends.
277
+ */
278
+ function runSshProxy(options) {
279
+ const channelId = `ssh-${(0, node_crypto_1.randomUUID)()}`;
280
+ const connect = options.connect ?? socket_io_client_1.io;
281
+ const { input, output, errorOutput } = options;
282
+ const identity = ensureSshIdentity();
283
+ const writeHosts = options.writeKnownHosts ?? writeKnownHosts;
284
+ return new Promise((resolve) => {
285
+ let started = false;
286
+ let finished = false;
287
+ const socket = connect((0, agentic_stream_1.toWebSocketOrigin)(options.baseURL), {
288
+ path: '/ws/agentic',
289
+ auth: {
290
+ token: options.token,
291
+ ...(0, data_provider_1.getClientIdentityAuth)(),
292
+ clientPlatform: 'cli',
293
+ clientOs: process.platform === 'darwin' ? 'macos' : process.platform,
294
+ },
295
+ transports: ['websocket'],
296
+ // A reconnect cannot resume a channel whose admission line the daemon
297
+ // revoked on socket death — say it ended and let ssh retry fresh.
298
+ reconnection: false,
299
+ });
300
+ const startTimer = setTimeout(() => {
301
+ finish(1, 'The machine did not answer the SSH channel in time. Its daemon may be busy or offline.');
302
+ }, exports.SSH_START_TIMEOUT_MS);
303
+ const onInput = (chunk) => {
304
+ const bytes = typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk;
305
+ socket.emit('dedicated:ssh:data', { channelId, data: bytes.toString('base64') });
306
+ };
307
+ function cleanup() {
308
+ clearTimeout(startTimer);
309
+ input.off('data', onInput);
310
+ input.off('end', onInputEnded);
311
+ input.off('close', onInputEnded);
312
+ output.off('error', onOutputError);
313
+ input.pause();
314
+ }
315
+ // The ssh client exiting is visible ONLY as stdin EOF and an EPIPE on
316
+ // stdout — sshd cannot tell the ProxyCommand died until its socket sees
317
+ // the stop or the transport drops. Listen for both so the channel, and
318
+ // the daemon's key admission, end when ssh does instead of at idle
319
+ // timeout.
320
+ const onInputEnded = () => finish(0);
321
+ const onOutputError = () => finish(1);
322
+ input.once('end', onInputEnded);
323
+ input.once('close', onInputEnded);
324
+ output.once('error', onOutputError);
325
+ function finish(code, notice) {
326
+ if (finished)
327
+ return;
328
+ finished = true;
329
+ cleanup();
330
+ if (notice)
331
+ errorOutput.write(`${notice}\n`);
332
+ if (started)
333
+ socket.emit('dedicated:ssh:stop', { channelId });
334
+ socket.disconnect();
335
+ resolve(code);
336
+ }
337
+ socket.on('connect_error', (err) => {
338
+ finish(1, `Could not connect to skrr: ${err.message}`);
339
+ });
340
+ socket.on('connect', () => {
341
+ socket.emit('dedicated:ssh:start', {
342
+ leaseId: options.leaseId,
343
+ channelId,
344
+ keyType: identity.keyType,
345
+ keyBlob: identity.keyBlob,
346
+ });
347
+ });
348
+ socket.on('dedicated:ssh:start:ack', (ack) => {
349
+ if (!ack || ack.channelId !== channelId || started || finished)
350
+ return;
351
+ if (!ack.ok) {
352
+ finish(1, describeSshStartRefusal(ack));
353
+ return;
354
+ }
355
+ started = true;
356
+ clearTimeout(startTimer);
357
+ // The instance's keys, attested by the channel — write them before the
358
+ // first protocol byte flows so ssh's host-key check reads the new file.
359
+ // `host` is what ssh asks about (`%h`), so the pin goes under the alias;
360
+ // a daemon that reported no keys becomes an honest StrictHostKeyChecking
361
+ // refusal, never a silent TOFU.
362
+ const host = `${options.leaseId}${exports.SSH_HOST_SUFFIX}`;
363
+ if (!writeHosts(host, ack.hostKeys)) {
364
+ errorOutput.write('[skrr ssh] warning: the machine reported no host keys; the connection will be refused if none are pinned\n');
365
+ }
366
+ input.resume();
367
+ input.on('data', onInput);
368
+ });
369
+ socket.on('dedicated:ssh:data', (frame) => {
370
+ if (!frame || frame.channelId !== channelId || finished)
371
+ return;
372
+ if (typeof frame.data === 'string' && frame.data.length > 0) {
373
+ output.write(Buffer.from(frame.data, 'base64'));
374
+ }
375
+ });
376
+ socket.on('dedicated:ssh:closed', (frame) => {
377
+ if (!frame || frame.channelId !== channelId)
378
+ return;
379
+ finish(0, frame.reason ? `[ssh channel closed: ${frame.reason}]` : undefined);
380
+ });
381
+ socket.on('disconnect', () => {
382
+ if (finished)
383
+ return;
384
+ finish(started ? 1 : 1, '[lost the connection to skrr; the SSH channel has ended]');
385
+ });
386
+ });
387
+ }
@@ -77,12 +77,12 @@ function resetDelegatedCliCredentialCache() {
77
77
  _cached = null;
78
78
  }
79
79
  /**
80
- * Walk `err.cause` / `err.errors[]` looking for a `code: 'ECONNREFUSED'`.
80
+ * Walk `err.cause` / `err.errors[]` looking for an errno-style `code`.
81
81
  * Node's `fetch` throws a `TypeError('fetch failed')` whose `cause` is either
82
82
  * a plain `Error` with `code` set, or an `AggregateError` whose `errors[]`
83
83
  * carry it (dual-stack retries). Neither shape is visible at the top level.
84
84
  */
85
- function isConnectionRefusedError(err) {
85
+ function fetchTransportCode(err) {
86
86
  const seen = new Set();
87
87
  const stack = [err];
88
88
  while (stack.length) {
@@ -91,15 +91,18 @@ function isConnectionRefusedError(err) {
91
91
  continue;
92
92
  seen.add(cur);
93
93
  const anyCur = cur;
94
- if (anyCur.code === 'ECONNREFUSED')
95
- return true;
94
+ if (typeof anyCur.code === 'string' && anyCur.code.length > 0)
95
+ return anyCur.code;
96
96
  if (anyCur.cause !== undefined)
97
97
  stack.push(anyCur.cause);
98
98
  if (Array.isArray(anyCur.errors))
99
99
  for (const e of anyCur.errors)
100
100
  stack.push(e);
101
101
  }
102
- return false;
102
+ return null;
103
+ }
104
+ function isConnectionRefusedError(err) {
105
+ return fetchTransportCode(err) === 'ECONNREFUSED';
103
106
  }
104
107
  /**
105
108
  * Best-effort probe of the daemon's bootstrap file — the CLI's
@@ -164,6 +167,35 @@ function composeStaleBrokerMessage(brokerUrl) {
164
167
  ` unset OVERSKY_DELEGATED_CLI_BROKER_URL OVERSKY_DELEGATED_CLI_CAPABILITY OVERSKY_AUTONOMOUS_SESSION\n` +
165
168
  `Subsequent \`skrr\` calls will then auto-broker a fresh credential via the live daemon.`);
166
169
  }
170
+ /**
171
+ * Rewrite a transport-level failure (anything that is not ECONNREFUSED — that
172
+ * one means a dead daemon, handled above) into a message that names the real
173
+ * cause and the recovery, instead of the bare `fetch failed` Node surfaces.
174
+ */
175
+ function composeUnreachableBrokerMessage(brokerUrl, err) {
176
+ const code = fetchTransportCode(err);
177
+ if (code === 'EPERM') {
178
+ // Observed concretely: a Codex-managed session under a seatbelt
179
+ // workspace-write/read-only profile gets EPERM connecting to
180
+ // 127.0.0.1 — seatbelt's `network-outbound` denial covers loopback.
181
+ return (`the OS denied the connection to the delegated CLI broker at ${brokerUrl} (EPERM). ` +
182
+ `This process is inside a sandbox that blocks outbound network — including ` +
183
+ `127.0.0.1 (Codex's seatbelt workspace-write/read-only profiles do this). ` +
184
+ `Re-run with the sandbox's network access enabled, or outside the sandbox. ` +
185
+ `Unsetting the OVERSKY_DELEGATED_CLI_* vars does not help: the same sandbox ` +
186
+ `would block the upstream API as well.`);
187
+ }
188
+ // undici rejects an aborted fetch with a DOMException named 'AbortError',
189
+ // which is NOT an `instanceof Error` in Node — check the name structurally.
190
+ if (err?.name === 'AbortError') {
191
+ return (`the delegated CLI broker at ${brokerUrl} did not answer within 5s. ` +
192
+ `The daemon listener is accepting connections but not responding — it may be ` +
193
+ `wedged; check the daemon and retry.`);
194
+ }
195
+ const detail = err instanceof Error ? err.message : String(err);
196
+ return (`the delegated CLI broker at ${brokerUrl} is unreachable: ${detail}` +
197
+ (code ? ` (${code})` : ''));
198
+ }
167
199
  /** Fetch a full-permission CLI access token from the local daemon. */
168
200
  async function fetchDelegatedCliCredential(env = process.env, options = {}) {
169
201
  if (!isDelegatedCliSession(env))
@@ -197,12 +229,14 @@ async function fetchDelegatedCliCredential(env = process.env, options = {}) {
197
229
  // from a live daemon: it means the whole loopback listener is gone, so
198
230
  // the capability is unrecoverable rather than transiently unavailable.
199
231
  // Rewrite the message so `base-command` surfaces recovery text instead
200
- // of a bare `fetch failed`. Any other network error (DNS, TLS,
201
- // AbortError, etc.) propagates unchanged.
232
+ // of a bare `fetch failed`. Other transport errors keep the original as
233
+ // `cause` but still get a rewritten headline — EPERM (a sandbox denying
234
+ // loopback), a timeout, and a bare `fetch failed` are different problems
235
+ // with different remedies.
202
236
  if (isConnectionRefusedError(err)) {
203
- throw new Error(composeStaleBrokerMessage(brokerUrl));
237
+ throw new Error(composeStaleBrokerMessage(brokerUrl), { cause: err });
204
238
  }
205
- throw err;
239
+ throw new Error(composeUnreachableBrokerMessage(brokerUrl, err), { cause: err });
206
240
  }
207
241
  if (!res.ok) {
208
242
  // A DELIBERATE decline is not a broker failure — WL-5 Step 1.
@@ -220,10 +254,14 @@ async function fetchDelegatedCliCredential(env = process.env, options = {}) {
220
254
  // Agent whose daemon cannot rotate its family must surface that rather
221
255
  // than quietly degrade, because a degraded credential path is exactly
222
256
  // where a human's stored family gets adopted by accident.
223
- if (res.status === 403) {
224
- const body = (await res.json().catch(() => null));
225
- if (body?.code === 'DELEGATED_CLI_CAPABILITY_REJECTED')
226
- return null;
257
+ const body = (await res.json().catch(() => null));
258
+ if (res.status === 403 && body?.code === 'DELEGATED_CLI_CAPABILITY_REJECTED')
259
+ return null;
260
+ if (res.status === 503 || body?.code === 'DELEGATED_CLI_CREDENTIAL_UNAVAILABLE') {
261
+ throw new Error(`the daemon's delegated CLI broker is alive but cannot mint a credential ` +
262
+ `(HTTP ${res.status}). The daemon cannot reach the auth upstream, holds ` +
263
+ `no daemon bearer, or is inside its mint cool-down — check the daemon ` +
264
+ `log for [delegated-cli]/[task-token] lines and retry.`);
227
265
  }
228
266
  throw new Error(`Delegated skrr CLI credential broker returned HTTP ${res.status}.`);
229
267
  }