@skrr-ai/cli 0.1.47 → 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 (274) 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/commerce/entitlements/explain.d.ts +22 -0
  10. package/dist/commands/commerce/entitlements/explain.js +40 -0
  11. package/dist/commands/commerce/incidents/audit.d.ts +20 -0
  12. package/dist/commands/commerce/incidents/audit.js +37 -0
  13. package/dist/commands/commerce/incidents/list.d.ts +22 -0
  14. package/dist/commands/commerce/incidents/list.js +49 -0
  15. package/dist/commands/commerce/incidents/resolve.d.ts +23 -0
  16. package/dist/commands/commerce/incidents/resolve.js +45 -0
  17. package/dist/commands/commerce/incidents/retry.d.ts +22 -0
  18. package/dist/commands/commerce/incidents/retry.js +45 -0
  19. package/dist/commands/commerce/incidents/show.d.ts +13 -0
  20. package/dist/commands/commerce/incidents/show.js +30 -0
  21. package/dist/commands/commerce/offers/approve.d.ts +17 -0
  22. package/dist/commands/commerce/offers/approve.js +33 -0
  23. package/dist/commands/commerce/offers/create.d.ts +18 -0
  24. package/dist/commands/commerce/offers/create.js +34 -0
  25. package/dist/commands/commerce/offers/publish.d.ts +13 -0
  26. package/dist/commands/commerce/offers/publish.js +28 -0
  27. package/dist/commands/commerce/offers/retire.d.ts +17 -0
  28. package/dist/commands/commerce/offers/retire.js +32 -0
  29. package/dist/commands/commerce/offers/validate.d.ts +18 -0
  30. package/dist/commands/commerce/offers/validate.js +33 -0
  31. package/dist/commands/commerce/products/create.d.ts +10 -0
  32. package/dist/commands/commerce/products/create.js +25 -0
  33. package/dist/commands/commerce/promotions/redeem.d.ts +13 -0
  34. package/dist/commands/commerce/promotions/redeem.js +32 -0
  35. package/dist/commands/commerce/terms/accept.d.ts +18 -0
  36. package/dist/commands/commerce/terms/accept.js +39 -0
  37. package/dist/commands/commerce/terms/show.d.ts +10 -0
  38. package/dist/commands/commerce/terms/show.js +34 -0
  39. package/dist/commands/commerce/trials/conversion-preview.d.ts +18 -0
  40. package/dist/commands/commerce/trials/conversion-preview.js +39 -0
  41. package/dist/commands/commerce/trials/start.d.ts +23 -0
  42. package/dist/commands/commerce/trials/start.js +58 -0
  43. package/dist/commands/commitments/accounting.d.ts +39 -0
  44. package/dist/commands/commitments/accounting.js +116 -0
  45. package/dist/commands/commitments/action-history.d.ts +1 -0
  46. package/dist/commands/commitments/action-history.js +8 -1
  47. package/dist/commands/commitments/case.d.ts +1 -0
  48. package/dist/commands/commitments/case.js +19 -3
  49. package/dist/commands/commitments/cases.d.ts +1 -0
  50. package/dist/commands/commitments/cases.js +11 -1
  51. package/dist/commands/commitments/checks.d.ts +1 -0
  52. package/dist/commands/commitments/checks.js +5 -1
  53. package/dist/commands/commitments/cost.d.ts +62 -0
  54. package/dist/commands/commitments/cost.js +130 -0
  55. package/dist/commands/commitments/create.js +1 -1
  56. package/dist/commands/commitments/delete.d.ts +1 -0
  57. package/dist/commands/commitments/delete.js +56 -6
  58. package/dist/commands/commitments/feedback.js +9 -1
  59. package/dist/commands/commitments/handover.js +16 -0
  60. package/dist/commands/commitments/hypotheses.d.ts +1 -0
  61. package/dist/commands/commitments/hypotheses.js +17 -2
  62. package/dist/commands/commitments/links/list.js +4 -1
  63. package/dist/commands/commitments/liveness.d.ts +18 -0
  64. package/dist/commands/commitments/liveness.js +80 -0
  65. package/dist/commands/commitments/metrics.d.ts +1 -0
  66. package/dist/commands/commitments/metrics.js +8 -1
  67. package/dist/commands/commitments/next-tick.d.ts +39 -0
  68. package/dist/commands/commitments/next-tick.js +122 -0
  69. package/dist/commands/commitments/observations.d.ts +1 -0
  70. package/dist/commands/commitments/observations.js +19 -2
  71. package/dist/commands/commitments/pause.js +12 -0
  72. package/dist/commands/commitments/receipt.d.ts +2 -0
  73. package/dist/commands/commitments/receipt.js +16 -2
  74. package/dist/commands/commitments/remediation-proposal.js +6 -2
  75. package/dist/commands/commitments/remediation-proposals.js +5 -1
  76. package/dist/commands/commitments/restore.d.ts +25 -0
  77. package/dist/commands/commitments/restore.js +46 -0
  78. package/dist/commands/commitments/show.js +32 -0
  79. package/dist/commands/commitments/trigger-status.js +33 -0
  80. package/dist/commands/commitments/update.d.ts +1 -0
  81. package/dist/commands/commitments/update.js +31 -5
  82. package/dist/commands/commitments/wake-capabilities.d.ts +0 -1
  83. package/dist/commands/commitments/wake-capabilities.js +8 -4
  84. package/dist/commands/enterprise/budgets/create.d.ts +11 -0
  85. package/dist/commands/enterprise/budgets/create.js +27 -0
  86. package/dist/commands/enterprise/budgets/evaluate.d.ts +17 -0
  87. package/dist/commands/enterprise/budgets/evaluate.js +32 -0
  88. package/dist/commands/enterprise/budgets/publish.d.ts +14 -0
  89. package/dist/commands/enterprise/budgets/publish.js +30 -0
  90. package/dist/commands/enterprise/contracts/activate.d.ts +14 -0
  91. package/dist/commands/enterprise/contracts/activate.js +30 -0
  92. package/dist/commands/enterprise/contracts/create.d.ts +14 -0
  93. package/dist/commands/enterprise/contracts/create.js +32 -0
  94. package/dist/commands/enterprise/contracts/issue.d.ts +18 -0
  95. package/dist/commands/enterprise/contracts/issue.js +34 -0
  96. package/dist/commands/enterprise/domains/begin.d.ts +20 -0
  97. package/dist/commands/enterprise/domains/begin.js +39 -0
  98. package/dist/commands/enterprise/domains/verify.d.ts +14 -0
  99. package/dist/commands/enterprise/domains/verify.js +32 -0
  100. package/dist/commands/enterprise/external-config/activate.d.ts +22 -0
  101. package/dist/commands/enterprise/external-config/activate.js +49 -0
  102. package/dist/commands/enterprise/external-config/set.d.ts +25 -0
  103. package/dist/commands/enterprise/external-config/set.js +123 -0
  104. package/dist/commands/enterprise/external-config/show.d.ts +12 -0
  105. package/dist/commands/enterprise/external-config/show.js +39 -0
  106. package/dist/commands/enterprise/external-config/suspend.d.ts +13 -0
  107. package/dist/commands/enterprise/external-config/suspend.js +41 -0
  108. package/dist/commands/enterprise/finance/close-snapshot.d.ts +11 -0
  109. package/dist/commands/enterprise/finance/close-snapshot.js +28 -0
  110. package/dist/commands/enterprise/finance/record-document.d.ts +18 -0
  111. package/dist/commands/enterprise/finance/record-document.js +35 -0
  112. package/dist/commands/enterprise/profile.d.ts +20 -0
  113. package/dist/commands/enterprise/profile.js +38 -0
  114. package/dist/commands/login.d.ts +13 -0
  115. package/dist/commands/login.js +77 -0
  116. package/dist/commands/machines/dedicated/index.js +3 -0
  117. package/dist/commands/machines/dedicated/service/apply.d.ts +23 -0
  118. package/dist/commands/machines/dedicated/service/apply.js +99 -0
  119. package/dist/commands/machines/dedicated/service/approve.d.ts +23 -0
  120. package/dist/commands/machines/dedicated/service/approve.js +52 -0
  121. package/dist/commands/machines/dedicated/service/create.d.ts +55 -0
  122. package/dist/commands/machines/dedicated/service/create.js +222 -0
  123. package/dist/commands/machines/dedicated/service/edit.d.ts +30 -0
  124. package/dist/commands/machines/dedicated/service/edit.js +146 -0
  125. package/dist/commands/machines/dedicated/service/exec.d.ts +26 -0
  126. package/dist/commands/machines/dedicated/service/exec.js +92 -0
  127. package/dist/commands/machines/dedicated/service/export.d.ts +29 -0
  128. package/dist/commands/machines/dedicated/service/export.js +106 -0
  129. package/dist/commands/machines/dedicated/service/index.d.ts +14 -0
  130. package/dist/commands/machines/dedicated/service/index.js +49 -0
  131. package/dist/commands/machines/dedicated/service/logs.d.ts +33 -0
  132. package/dist/commands/machines/dedicated/service/logs.js +148 -0
  133. package/dist/commands/machines/dedicated/service/ls.d.ts +20 -0
  134. package/dist/commands/machines/dedicated/service/ls.js +80 -0
  135. package/dist/commands/machines/dedicated/service/restart.d.ts +14 -0
  136. package/dist/commands/machines/dedicated/service/restart.js +29 -0
  137. package/dist/commands/machines/dedicated/service/rm.d.ts +20 -0
  138. package/dist/commands/machines/dedicated/service/rm.js +33 -0
  139. package/dist/commands/machines/dedicated/service/secret/index.d.ts +14 -0
  140. package/dist/commands/machines/dedicated/service/secret/index.js +25 -0
  141. package/dist/commands/machines/dedicated/service/secret/rm.d.ts +22 -0
  142. package/dist/commands/machines/dedicated/service/secret/rm.js +54 -0
  143. package/dist/commands/machines/dedicated/service/secret/set.d.ts +22 -0
  144. package/dist/commands/machines/dedicated/service/secret/set.js +93 -0
  145. package/dist/commands/machines/dedicated/service/start.d.ts +14 -0
  146. package/dist/commands/machines/dedicated/service/start.js +29 -0
  147. package/dist/commands/machines/dedicated/service/status.d.ts +21 -0
  148. package/dist/commands/machines/dedicated/service/status.js +85 -0
  149. package/dist/commands/machines/dedicated/service/stop.d.ts +20 -0
  150. package/dist/commands/machines/dedicated/service/stop.js +35 -0
  151. package/dist/commands/machines/dedicated/service/top.d.ts +22 -0
  152. package/dist/commands/machines/dedicated/service/top.js +120 -0
  153. package/dist/commands/machines/dedicated/sign-in-status.d.ts +22 -0
  154. package/dist/commands/machines/dedicated/sign-in-status.js +79 -0
  155. package/dist/commands/machines/dedicated/ssh-config.d.ts +28 -0
  156. package/dist/commands/machines/dedicated/ssh-config.js +66 -0
  157. package/dist/commands/machines/dedicated/ssh-proxy.d.ts +22 -0
  158. package/dist/commands/machines/dedicated/ssh-proxy.js +62 -0
  159. package/dist/commands/machines/dedicated/ssh.d.ts +29 -0
  160. package/dist/commands/machines/dedicated/ssh.js +121 -0
  161. package/dist/commands/native/allow-all.d.ts +14 -0
  162. package/dist/commands/native/allow-all.js +25 -0
  163. package/dist/commands/native/allow.d.ts +24 -0
  164. package/dist/commands/native/allow.js +45 -0
  165. package/dist/commands/native/deny.d.ts +24 -0
  166. package/dist/commands/native/deny.js +43 -0
  167. package/dist/commands/native/disable.d.ts +17 -0
  168. package/dist/commands/native/disable.js +28 -0
  169. package/dist/commands/native/enable.d.ts +12 -0
  170. package/dist/commands/native/enable.js +29 -0
  171. package/dist/commands/native/list-apps.d.ts +19 -0
  172. package/dist/commands/native/list-apps.js +30 -0
  173. package/dist/commands/native/restrict-to.d.ts +22 -0
  174. package/dist/commands/native/restrict-to.js +41 -0
  175. package/dist/commands/native/status.d.ts +19 -0
  176. package/dist/commands/native/status.js +33 -0
  177. package/dist/commands/native.d.ts +39 -0
  178. package/dist/commands/native.js +76 -0
  179. package/dist/commands/service/index.d.ts +14 -0
  180. package/dist/commands/service/index.js +31 -0
  181. package/dist/commands/service/logs.d.ts +18 -0
  182. package/dist/commands/service/logs.js +53 -0
  183. package/dist/commands/service/ls.d.ts +13 -0
  184. package/dist/commands/service/ls.js +48 -0
  185. package/dist/commands/service/propose.d.ts +34 -0
  186. package/dist/commands/service/propose.js +113 -0
  187. package/dist/commands/service/status.d.ts +17 -0
  188. package/dist/commands/service/status.js +56 -0
  189. package/dist/commands/spaces/auto-executor/runs.d.ts +13 -0
  190. package/dist/commands/spaces/auto-executor/runs.js +56 -0
  191. package/dist/commands/store/governance/artifacts.d.ts +25 -0
  192. package/dist/commands/store/governance/artifacts.js +55 -0
  193. package/dist/commands/store/governance/assign.d.ts +16 -0
  194. package/dist/commands/store/governance/assign.js +54 -0
  195. package/dist/commands/store/governance/create-license.d.ts +18 -0
  196. package/dist/commands/store/governance/create-license.js +35 -0
  197. package/dist/commands/store/governance/install-plan.d.ts +25 -0
  198. package/dist/commands/store/governance/install-plan.js +56 -0
  199. package/dist/commands/store/governance/license-policy.d.ts +30 -0
  200. package/dist/commands/store/governance/license-policy.js +69 -0
  201. package/dist/commands/store/governance/policy.d.ts +25 -0
  202. package/dist/commands/store/governance/policy.js +65 -0
  203. package/dist/commands/store/governance/publish-release.d.ts +17 -0
  204. package/dist/commands/store/governance/publish-release.js +34 -0
  205. package/dist/commands/tasks/deliverable/add.d.ts +15 -0
  206. package/dist/commands/tasks/deliverable/add.js +174 -0
  207. package/dist/commands/tasks/deliverable/archive.d.ts +35 -0
  208. package/dist/commands/tasks/deliverable/archive.js +205 -0
  209. package/dist/commands/tasks/labels/detach.js +3 -0
  210. package/dist/commands/tasks/labels/suggest.d.ts +24 -0
  211. package/dist/commands/tasks/labels/suggest.js +69 -0
  212. package/dist/commands/tasks/list.d.ts +16 -2
  213. package/dist/commands/tasks/list.js +100 -24
  214. package/dist/commands/tasks/show.d.ts +23 -0
  215. package/dist/commands/tasks/show.js +51 -1
  216. package/dist/commands/wiki/rm.d.ts +22 -1
  217. package/dist/commands/wiki/rm.js +52 -3
  218. package/dist/lib/agentic-stream.js +10 -2
  219. package/dist/lib/balance.d.ts +6 -1
  220. package/dist/lib/balance.js +15 -0
  221. package/dist/lib/commerce-command.d.ts +48 -0
  222. package/dist/lib/commerce-command.js +70 -0
  223. package/dist/lib/commerce.d.ts +93 -0
  224. package/dist/lib/commerce.js +129 -0
  225. package/dist/lib/commitment-product.js +1 -1
  226. package/dist/lib/commitments.d.ts +202 -29
  227. package/dist/lib/commitments.js +300 -33
  228. package/dist/lib/dedicated-lease-command.js +10 -0
  229. package/dist/lib/dedicated-machines.d.ts +37 -0
  230. package/dist/lib/dedicated-machines.js +47 -0
  231. package/dist/lib/dedicated-service-command.d.ts +60 -0
  232. package/dist/lib/dedicated-service-command.js +182 -0
  233. package/dist/lib/dedicated-service.d.ts +342 -0
  234. package/dist/lib/dedicated-service.js +741 -0
  235. package/dist/lib/dedicated-ssh.d.ts +136 -0
  236. package/dist/lib/dedicated-ssh.js +387 -0
  237. package/dist/lib/delegated-cli.js +51 -13
  238. package/dist/lib/enterprise-commerce.d.ts +67 -0
  239. package/dist/lib/enterprise-commerce.js +112 -0
  240. package/dist/lib/enterprise-external-configuration.d.ts +60 -0
  241. package/dist/lib/enterprise-external-configuration.js +58 -0
  242. package/dist/lib/first-party-harness.js +17 -3
  243. package/dist/lib/governed-store.d.ts +110 -0
  244. package/dist/lib/governed-store.js +79 -0
  245. package/dist/lib/guest-service-command.d.ts +16 -0
  246. package/dist/lib/guest-service-command.js +32 -0
  247. package/dist/lib/guest-service.d.ts +67 -0
  248. package/dist/lib/guest-service.js +112 -0
  249. package/dist/lib/prompt.d.ts +1 -0
  250. package/dist/lib/prompt.js +1 -0
  251. package/dist/lib/task-asset-upload.d.ts +20 -0
  252. package/dist/lib/task-asset-upload.js +30 -8
  253. package/dist/lib/task-extras.d.ts +23 -0
  254. package/dist/lib/task-extras.js +6 -0
  255. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonScopes.d.ts +9 -3
  256. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/daemonScopes.js +10 -3
  257. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
  258. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +6 -5
  259. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.d.ts +16 -0
  260. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/messages.js +21 -4
  261. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +1 -1
  262. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +24 -2
  263. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonScopes.d.ts +9 -3
  264. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/daemonScopes.js +10 -3
  265. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
  266. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -1
  267. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.d.ts +16 -0
  268. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/messages.js +20 -4
  269. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +1 -1
  270. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +24 -2
  271. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  272. package/dist/node_modules/@skrr-ai/data-provider/index.js +5060 -4827
  273. package/oclif.manifest.json +37416 -31416
  274. package/package.json +43 -1
@@ -0,0 +1,741 @@
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.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;
7
+ exports.isServiceUnsupportedCode = isServiceUnsupportedCode;
8
+ exports.describeServiceRefusal = describeServiceRefusal;
9
+ exports.validateServiceSpec = validateServiceSpec;
10
+ exports.parseServiceSpecDocument = parseServiceSpecDocument;
11
+ exports.specPayload = specPayload;
12
+ exports.specExportDocument = specExportDocument;
13
+ exports.parseEnvEntries = parseEnvEntries;
14
+ exports.parseParamEntries = parseParamEntries;
15
+ exports.specFromFlags = specFromFlags;
16
+ exports.streamServiceLogs = streamServiceLogs;
17
+ exports.serviceTemplatesDir = serviceTemplatesDir;
18
+ exports.loadLocalServiceTemplate = loadLocalServiceTemplate;
19
+ exports.mergeServiceTemplate = mergeServiceTemplate;
20
+ exports.serviceDetailText = serviceDetailText;
21
+ exports.runningOnDedicatedRuntimeGuest = runningOnDedicatedRuntimeGuest;
22
+ const node_crypto_1 = require("node:crypto");
23
+ const node_fs_1 = require("node:fs");
24
+ const node_os_1 = require("node:os");
25
+ const node_path_1 = __importDefault(require("node:path"));
26
+ const socket_io_client_1 = require("socket.io-client");
27
+ const data_provider_1 = require("@skrr-ai/data-provider");
28
+ const agentic_stream_1 = require("./agentic-stream");
29
+ const first_party_harness_1 = require("./first-party-harness");
30
+ const format_1 = require("./format");
31
+ /**
32
+ * Declared services on a Dedicated Runtime — the CLI half of
33
+ * docs/architecture/dedicated-runtime-always-on-workloads-2026-09-21.md (§5.3
34
+ * spec, §5.4 wire surface, §5.6 agent proposals).
35
+ *
36
+ * A service is a spec the daemon keeps reconciling: command, restart policy,
37
+ * env, per-service cgroup limits. The CLI never talks to the machine's
38
+ * registry — every verb rides the same admitted channel the terminal and SSH
39
+ * lanes use, as a `dedicated:service:*` socket event relayed by the API to the
40
+ * lease's daemon. What crosses the wire is exactly what the design doc names;
41
+ * anything not in the spec is refused by the daemon, not by us.
42
+ *
43
+ * Wire vocabulary (client side, mirrored from daemon:service:*):
44
+ *
45
+ * client → API dedicated:service:list|get|create|update|action|logs|
46
+ * logs:stop|approve|secret {…payload, requestId}
47
+ * API → client dedicated:service:ack {requestId, ok, error?, code?, result?}
48
+ * dedicated:service:state {name, status, detail?, ts}
49
+ * dedicated:service:log {name, streamId, data(b64), ts}
50
+ * dedicated:service:log:closed {name, streamId, reason}
51
+ * dedicated:service:proposed {name, spec, createdBy}
52
+ * dedicated:templates:ack {requestId, ok, error?, code?, result?}
53
+ */
54
+ exports.DEDICATED_SERVICE_EVENTS = {
55
+ list: 'dedicated:service:list',
56
+ get: 'dedicated:service:get',
57
+ create: 'dedicated:service:create',
58
+ update: 'dedicated:service:update',
59
+ action: 'dedicated:service:action',
60
+ logs: 'dedicated:service:logs',
61
+ logsStop: 'dedicated:service:logs:stop',
62
+ approve: 'dedicated:service:approve',
63
+ secret: 'dedicated:service:secret',
64
+ /** Phase-2 verbs — wired now; an old daemon refuses them cleanly. */
65
+ exec: 'dedicated:service:exec',
66
+ templatesList: 'dedicated:templates:list',
67
+ templatesGet: 'dedicated:templates:get',
68
+ ack: 'dedicated:service:ack',
69
+ templatesAck: 'dedicated:templates:ack',
70
+ state: 'dedicated:service:state',
71
+ log: 'dedicated:service:log',
72
+ logClosed: 'dedicated:service:log:closed',
73
+ proposed: 'dedicated:service:proposed',
74
+ };
75
+ /** How long a verb has to answer before the machine is declared unreachable. */
76
+ exports.SERVICE_CALL_TIMEOUT_MS = 30_000;
77
+ /** §5.3 — the scope name `skrr-svc-<name>` derives from it. */
78
+ exports.SERVICE_NAME_PATTERN = /^[a-z0-9][a-z0-9-]{0,62}$/;
79
+ /** Secret names — the daemon's grammar; checked client-side so a bad name
80
+ * is a usage error here, not a refusal that names the wrong object. */
81
+ exports.SERVICE_SECRET_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_.-]{0,127}$/;
82
+ /** Leaf names a service may never claim (the wrapper routes them itself). */
83
+ exports.SERVICE_RESERVED_NAMES = new Set(['default', 'ssh', 'system']);
84
+ exports.SERVICE_RESTART_POLICIES = new Set(['always', 'on-failure', 'never']);
85
+ exports.SERVICE_ACTIONS = new Set(['start', 'stop', 'restart', 'remove']);
86
+ const ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
87
+ const ENV_MAX_KEYS = 64;
88
+ const ENV_VALUE_MAX_BYTES = 8 * 1024;
89
+ /**
90
+ * Fields the daemon owns, never the client. `export` strips them so a repo
91
+ * `service.json` round-trips through `apply` cleanly; create/apply payloads
92
+ * drop them silently rather than fail — a checked-in spec can still carry
93
+ * them from a previous export or an older daemon's write.
94
+ */
95
+ const DAEMON_OWNED_FIELDS = new Set(['createdBy', 'revision', 'createdAt', 'updatedAt']);
96
+ class DedicatedServiceError extends Error {
97
+ code;
98
+ constructor(message, code) {
99
+ super(message);
100
+ this.name = 'DedicatedServiceError';
101
+ this.code = code;
102
+ }
103
+ }
104
+ exports.DedicatedServiceError = DedicatedServiceError;
105
+ /** True for every refusal that says "this machine has no services support". */
106
+ function isServiceUnsupportedCode(code) {
107
+ const c = (code || '').toLowerCase();
108
+ return (c === 'dedicated_runtime_services_unsupported' ||
109
+ c === 'services_unsupported' ||
110
+ c === 'unsupported' ||
111
+ c === 'not_implemented' ||
112
+ c === 'unknown_verb' ||
113
+ c === 'capability_missing' ||
114
+ c === 'dedicated_runtime_capability_missing');
115
+ }
116
+ /**
117
+ * A service refusal in words that say what to do — the same register
118
+ * `describeSshStartRefusal` uses for the SSH lane. The capability-missing
119
+ * branch mirrors the sshBridge fallback: an old image or an old daemon is a
120
+ * named condition with a next step, not a crash.
121
+ */
122
+ function describeServiceRefusal(ack, context = {}) {
123
+ const code = ack.code || '';
124
+ if (isServiceUnsupportedCode(code)) {
125
+ return (ack.error ||
126
+ `This machine's daemon does not support the services_v1 extension${context.verb ? ` (${context.verb})` : ''}. ` +
127
+ `Its image may predate declared services — update it with \`skrr machines dedicated update-image ${context.leaseId || '<lease-id>'} --wait\`; \`skrr machines dedicated terminal\` and \`exec\` work in the meantime.`);
128
+ }
129
+ if (code === 'DEDICATED_RUNTIME_NOT_ASSIGNABLE') {
130
+ return ('The machine is not ready to take service commands (it must be running with a healthy workspace). ' +
131
+ `Check it with \`skrr machines dedicated show ${context.leaseId || '<lease-id>'}\`.`);
132
+ }
133
+ if (code === 'DEDICATED_RUNTIME_GENERATION_STALE') {
134
+ return 'The machine was replaced since its daemon registered. Wait a minute and try again.';
135
+ }
136
+ if (code === 'dedicated_runtime_quiesced') {
137
+ return 'The machine is checkpointing its workspace right now; try again in a moment.';
138
+ }
139
+ if (code === 'offline' || code === 'daemon_offline') {
140
+ return 'The machine’s daemon is not connected. Wait for it to come online and try again.';
141
+ }
142
+ if (code === 'conflict') {
143
+ return 'The service spec changed since you read it; fetch it again and re-apply your change.';
144
+ }
145
+ if (code === 'not_found') {
146
+ return context.name
147
+ ? `No service named ${context.name} on this machine.`
148
+ : 'The machine or the named service was not found.';
149
+ }
150
+ if (code === 'invalid' || code === 'validation') {
151
+ return `The daemon refused the spec: ${ack.error || 'invalid service spec'}`;
152
+ }
153
+ return ack.error ? `The service request failed: ${ack.error}` : 'The service request failed.';
154
+ }
155
+ /* ---------------------------------------------------------------- spec --- */
156
+ /** Validation that costs the daemon nothing to re-check — §5.3's table, enforced client-side first. */
157
+ function validateServiceSpec(doc, options = {}) {
158
+ const errors = [];
159
+ const { requireName = true, requireCommand = true } = options;
160
+ if (!doc || typeof doc !== 'object' || Array.isArray(doc)) {
161
+ return ['the spec must be a JSON object'];
162
+ }
163
+ const spec = doc;
164
+ if (spec.name !== undefined) {
165
+ if (typeof spec.name !== 'string' || !exports.SERVICE_NAME_PATTERN.test(spec.name)) {
166
+ errors.push('name must match ^[a-z0-9][a-z0-9-]{0,62}$');
167
+ }
168
+ else if (exports.SERVICE_RESERVED_NAMES.has(spec.name)) {
169
+ errors.push(`name "${spec.name}" is reserved (default, ssh, system are not services)`);
170
+ }
171
+ }
172
+ else if (requireName) {
173
+ errors.push('name is required');
174
+ }
175
+ const hasCommand = spec.command !== undefined;
176
+ const hasShell = spec.shell !== undefined;
177
+ if (hasCommand) {
178
+ if (!Array.isArray(spec.command) ||
179
+ spec.command.length === 0 ||
180
+ spec.command.some((w) => typeof w !== 'string' || w.length === 0)) {
181
+ errors.push('command must be a non-empty argv array of strings');
182
+ }
183
+ else if (spec.command.some((w) => w.includes('\0'))) {
184
+ errors.push('command arguments cannot contain null bytes');
185
+ }
186
+ }
187
+ if (hasShell && (typeof spec.shell !== 'string' || !spec.shell.trim())) {
188
+ errors.push('shell must be a non-empty string (run via sh -c)');
189
+ }
190
+ if (requireCommand && !hasCommand && !hasShell) {
191
+ errors.push('a command is required — argv after --, a "command" array, or a "shell" string');
192
+ }
193
+ if (spec.cwd !== undefined) {
194
+ if (typeof spec.cwd !== 'string' || !spec.cwd.startsWith('/')) {
195
+ errors.push('cwd must be an absolute path under the workspace (for example /workspace/repos/app)');
196
+ }
197
+ }
198
+ if (spec.env !== undefined) {
199
+ if (!spec.env || typeof spec.env !== 'object' || Array.isArray(spec.env)) {
200
+ errors.push('env must be an object of NAME=value pairs');
201
+ }
202
+ else {
203
+ const keys = Object.keys(spec.env);
204
+ if (keys.length > ENV_MAX_KEYS) {
205
+ errors.push(`env has ${keys.length} keys; at most ${ENV_MAX_KEYS} are allowed`);
206
+ }
207
+ for (const key of keys) {
208
+ if (!ENV_NAME_PATTERN.test(key)) {
209
+ errors.push(`env key "${key}" is not a POSIX variable name`);
210
+ }
211
+ const value = spec.env[key];
212
+ if (typeof value !== 'string') {
213
+ errors.push(`env.${key} must be a string`);
214
+ }
215
+ else if (Buffer.byteLength(value) > ENV_VALUE_MAX_BYTES) {
216
+ errors.push(`env.${key} is over 8 KiB`);
217
+ }
218
+ }
219
+ }
220
+ }
221
+ if (spec.restart !== undefined && !exports.SERVICE_RESTART_POLICIES.has(String(spec.restart))) {
222
+ errors.push('restart must be always, on-failure or never');
223
+ }
224
+ if (spec.stopTimeoutSeconds !== undefined) {
225
+ const t = spec.stopTimeoutSeconds;
226
+ if (typeof t !== 'number' || !Number.isFinite(t) || t < 1 || t > 120) {
227
+ errors.push('stopTimeoutSeconds must be between 1 and 120');
228
+ }
229
+ }
230
+ if (spec.backoff !== undefined) {
231
+ if (!spec.backoff || typeof spec.backoff !== 'object' || Array.isArray(spec.backoff)) {
232
+ errors.push('backoff must be an object {initialMs, maxMs, budget, windowMs}');
233
+ }
234
+ else {
235
+ for (const key of ['initialMs', 'maxMs', 'windowMs']) {
236
+ const v = spec.backoff[key];
237
+ if (v !== undefined && (typeof v !== 'number' || !Number.isFinite(v) || v < 0)) {
238
+ errors.push(`backoff.${key} must be a non-negative number`);
239
+ }
240
+ }
241
+ const budget = spec.backoff.budget;
242
+ if (budget !== undefined &&
243
+ (typeof budget !== 'number' || !Number.isInteger(budget) || budget < 0)) {
244
+ errors.push('backoff.budget must be a non-negative integer');
245
+ }
246
+ }
247
+ }
248
+ if (spec.limits !== undefined) {
249
+ if (!spec.limits || typeof spec.limits !== 'object' || Array.isArray(spec.limits)) {
250
+ errors.push('limits must be an object {memoryMax, cpuQuotaPercent, pidsMax}');
251
+ }
252
+ else {
253
+ const { memoryMax, cpuQuotaPercent, pidsMax } = spec.limits;
254
+ if (memoryMax !== undefined && (typeof memoryMax !== 'string' || !memoryMax.trim())) {
255
+ errors.push('limits.memoryMax must be a size string such as "512M" or "2G"');
256
+ }
257
+ if (cpuQuotaPercent !== undefined &&
258
+ (typeof cpuQuotaPercent !== 'number' ||
259
+ !Number.isFinite(cpuQuotaPercent) ||
260
+ cpuQuotaPercent <= 0)) {
261
+ errors.push('limits.cpuQuotaPercent must be a positive number');
262
+ }
263
+ if (pidsMax !== undefined &&
264
+ (typeof pidsMax !== 'number' || !Number.isInteger(pidsMax) || pidsMax <= 0)) {
265
+ errors.push('limits.pidsMax must be a positive integer');
266
+ }
267
+ }
268
+ }
269
+ if (spec.version !== undefined && spec.version !== 1) {
270
+ errors.push('version must be 1');
271
+ }
272
+ return errors;
273
+ }
274
+ /**
275
+ * Parse a `service.json` document, refusing with every problem named at once
276
+ * rather than the first — a file you are editing deserves the whole list.
277
+ */
278
+ function parseServiceSpecDocument(raw, source, options = {}) {
279
+ let doc;
280
+ try {
281
+ doc = JSON.parse(raw);
282
+ }
283
+ catch (err) {
284
+ throw new DedicatedServiceError(`${source} is not valid JSON: ${err instanceof Error ? err.message : err}`, 'SPEC_JSON');
285
+ }
286
+ const errors = validateServiceSpec(doc, options);
287
+ if (errors.length > 0) {
288
+ throw new DedicatedServiceError(`${source} is not a valid service spec:\n${errors.map((e) => ` - ${e}`).join('\n')}`, 'SPEC_INVALID');
289
+ }
290
+ return doc;
291
+ }
292
+ /** The spec a mutation carries: the document minus the fields the daemon owns. */
293
+ function specPayload(spec) {
294
+ const out = {};
295
+ for (const [key, value] of Object.entries(spec)) {
296
+ if (DAEMON_OWNED_FIELDS.has(key))
297
+ continue;
298
+ out[key] = value;
299
+ }
300
+ return out;
301
+ }
302
+ /**
303
+ * The repo `service.json` form of a stored spec — `export`'s output. The
304
+ * document the daemon returned minus its bookkeeping, unknown fields kept
305
+ * verbatim so forward fields survive the round-trip.
306
+ */
307
+ function specExportDocument(spec) {
308
+ return specPayload(spec);
309
+ }
310
+ /** `--env KEY=VALUE` entries, one flag instance each. */
311
+ function parseEnvEntries(entries) {
312
+ const env = {};
313
+ for (const entry of entries ?? []) {
314
+ const eq = entry.indexOf('=');
315
+ if (eq <= 0) {
316
+ throw new DedicatedServiceError(`--env expects NAME=VALUE, got "${entry}"`, 'SPEC_INVALID');
317
+ }
318
+ env[entry.slice(0, eq)] = entry.slice(eq + 1);
319
+ }
320
+ return env;
321
+ }
322
+ /** `name=value` pairs for --param; the value stays a string — templates interpolate text. */
323
+ function parseParamEntries(entries) {
324
+ return parseEnvEntries(entries);
325
+ }
326
+ /** A spec assembled from create/propose flags rather than a file. */
327
+ function specFromFlags(input) {
328
+ const spec = { version: 1 };
329
+ if (input.name)
330
+ spec.name = input.name;
331
+ if (input.argv && input.argv.length > 0)
332
+ spec.command = input.argv;
333
+ if (input.cwd)
334
+ spec.cwd = input.cwd;
335
+ if (input.env && Object.keys(input.env).length > 0)
336
+ spec.env = input.env;
337
+ if (input.restart)
338
+ spec.restart = input.restart;
339
+ if (input.notify !== undefined)
340
+ spec.notify = input.notify;
341
+ if (input.stopTimeoutSeconds !== undefined)
342
+ spec.stopTimeoutSeconds = input.stopTimeoutSeconds;
343
+ if (input.limits) {
344
+ const limits = {};
345
+ if (input.limits.memoryMax)
346
+ limits.memoryMax = input.limits.memoryMax;
347
+ if (input.limits.cpuQuotaPercent !== undefined)
348
+ limits.cpuQuotaPercent = input.limits.cpuQuotaPercent;
349
+ if (input.limits.pidsMax !== undefined)
350
+ limits.pidsMax = input.limits.pidsMax;
351
+ if (Object.keys(limits).length > 0)
352
+ spec.limits = limits;
353
+ }
354
+ return spec;
355
+ }
356
+ function socketOptions(token) {
357
+ return {
358
+ path: '/ws/agentic',
359
+ auth: {
360
+ token,
361
+ ...(0, data_provider_1.getClientIdentityAuth)(),
362
+ clientPlatform: 'cli',
363
+ clientOs: process.platform === 'darwin' ? 'macos' : process.platform,
364
+ },
365
+ transports: ['websocket'],
366
+ reconnection: false,
367
+ };
368
+ }
369
+ /**
370
+ * One admitted socket for a run of service verbs. `call` emits
371
+ * `dedicated:service:<verb>` with a fresh requestId and resolves on the
372
+ * matching `dedicated:service:ack`; push events (`state`, `log`,
373
+ * `log:closed`, `proposed`) are exposed via `on` for the follow loop.
374
+ */
375
+ class DedicatedServiceChannel {
376
+ options;
377
+ socket;
378
+ connected = false;
379
+ dead = false;
380
+ pending = new Map();
381
+ onDead = null;
382
+ constructor(options) {
383
+ this.options = options;
384
+ const connect = options.connect ?? socket_io_client_1.io;
385
+ this.socket = connect((0, agentic_stream_1.toWebSocketOrigin)(options.baseURL), socketOptions(options.token));
386
+ const onAck = (ack) => {
387
+ if (!ack?.requestId)
388
+ return;
389
+ const waiter = this.pending.get(ack.requestId);
390
+ if (!waiter)
391
+ return;
392
+ this.pending.delete(ack.requestId);
393
+ clearTimeout(waiter.timer);
394
+ waiter.resolve(ack);
395
+ };
396
+ this.socket.on(exports.DEDICATED_SERVICE_EVENTS.ack, onAck);
397
+ this.socket.on(exports.DEDICATED_SERVICE_EVENTS.templatesAck, onAck);
398
+ }
399
+ /** Resolves once the transport is up; rejects on connect failure. */
400
+ open() {
401
+ return new Promise((resolve, reject) => {
402
+ if (this.connected)
403
+ return resolve();
404
+ this.socket.on('connect', () => {
405
+ this.connected = true;
406
+ resolve();
407
+ });
408
+ this.socket.on('connect_error', (err) => {
409
+ if (this.connected)
410
+ return;
411
+ this.close();
412
+ reject(new DedicatedServiceError(`Could not connect to skrr: ${err.message}`, err.data?.code || 'SERVICE_CONNECT_FAILED'));
413
+ });
414
+ this.socket.on('disconnect', () => {
415
+ this.dead = true;
416
+ for (const [, waiter] of this.pending) {
417
+ clearTimeout(waiter.timer);
418
+ waiter.resolve({
419
+ ok: false,
420
+ error: 'The connection to skrr closed before the machine answered.',
421
+ code: 'disconnected',
422
+ });
423
+ }
424
+ this.pending.clear();
425
+ this.onDead?.();
426
+ });
427
+ });
428
+ }
429
+ /** A socket dying mid-follow ends the follow cleanly rather than hanging. */
430
+ setOnDead(handler) {
431
+ this.onDead = handler;
432
+ }
433
+ get isConnected() {
434
+ return this.connected && !this.dead;
435
+ }
436
+ /**
437
+ * Emit a verb and await its ack. A refusal ack is a DedicatedServiceError
438
+ * whose message is the actionable sentence; the raw code rides along so a
439
+ * caller can branch on `conflict` or a missing capability.
440
+ */
441
+ call(verb, payload, timeoutMs = exports.SERVICE_CALL_TIMEOUT_MS) {
442
+ return this.callEvent(`dedicated:service:${verb}`, exports.DEDICATED_SERVICE_EVENTS.ack, payload, {
443
+ leaseId: typeof payload.leaseId === 'string' ? payload.leaseId : undefined,
444
+ verb,
445
+ name: typeof payload.name === 'string' ? payload.name : undefined,
446
+ }, timeoutMs);
447
+ }
448
+ /**
449
+ * A read against the template gallery — `dedicated:templates:list|get`,
450
+ * answered on `dedicated:templates:ack`. Same requestId/ack discipline as
451
+ * the service verbs; the event family is the only difference.
452
+ */
453
+ callTemplates(verb, payload, timeoutMs = exports.SERVICE_CALL_TIMEOUT_MS) {
454
+ return this.callEvent(`dedicated:templates:${verb}`, exports.DEDICATED_SERVICE_EVENTS.templatesAck, payload, {
455
+ verb: `templates:${verb}`,
456
+ }, timeoutMs);
457
+ }
458
+ /** The shared request/ack round-trip. */
459
+ callEvent(event, ackEvent, payload, context, timeoutMs) {
460
+ const requestId = `svc-${(0, node_crypto_1.randomUUID)()}`;
461
+ return new Promise((resolve, reject) => {
462
+ if (this.dead) {
463
+ reject(new DedicatedServiceError('The connection to skrr is closed.', 'disconnected'));
464
+ return;
465
+ }
466
+ const timer = setTimeout(() => {
467
+ this.pending.delete(requestId);
468
+ reject(new DedicatedServiceError('The machine did not answer in time. Its daemon may be busy, offline, or older than the services_v1 extension.', 'SERVICE_CALL_TIMEOUT'));
469
+ }, timeoutMs);
470
+ this.pending.set(requestId, {
471
+ timer,
472
+ resolve: (ack) => {
473
+ if (!ack.ok) {
474
+ reject(new DedicatedServiceError(describeServiceRefusal(ack, context), ack.code || 'SERVICE_REFUSED'));
475
+ return;
476
+ }
477
+ resolve({ result: ack.result, ack });
478
+ },
479
+ });
480
+ this.socket.emit(event, { ...payload, requestId });
481
+ });
482
+ }
483
+ /** Subscribe to a server-pushed frame (`state`, `log`, `log:closed`, `proposed`). */
484
+ on(event, handler) {
485
+ this.socket.on(event, handler);
486
+ }
487
+ off(event, handler) {
488
+ this.socket.off(event, handler);
489
+ }
490
+ /** Emit without waiting for an ack (logs:stop — the stream may already be gone). */
491
+ send(event, payload) {
492
+ if (this.dead)
493
+ return;
494
+ this.socket.emit(event, payload);
495
+ }
496
+ close() {
497
+ for (const [, waiter] of this.pending)
498
+ clearTimeout(waiter.timer);
499
+ this.pending.clear();
500
+ this.dead = true;
501
+ try {
502
+ this.socket.disconnect();
503
+ }
504
+ catch {
505
+ /* a half-open socket refusing to close is not a failure */
506
+ }
507
+ }
508
+ }
509
+ exports.DedicatedServiceChannel = DedicatedServiceChannel;
510
+ /**
511
+ * `service logs` — tail the stored log, then follow live
512
+ * `dedicated:service:log` frames until `log:closed`, a SIGINT, or the socket
513
+ * dies. Resolves with the exit code the command should use (0 unless the
514
+ * stream failed). The follow is closed with `dedicated:service:logs:stop` so
515
+ * the daemon does not keep piping to a dead reader.
516
+ */
517
+ async function streamServiceLogs(options) {
518
+ const channel = new DedicatedServiceChannel(options);
519
+ const write = options.write ?? ((bytes) => options.output.write(bytes));
520
+ return new Promise((resolve, reject) => {
521
+ let streamId = null;
522
+ let finished = false;
523
+ const cleanupSignals = () => {
524
+ process.off('SIGINT', onSigint);
525
+ process.off('SIGTERM', onSigterm);
526
+ };
527
+ const finish = (code) => {
528
+ if (finished)
529
+ return;
530
+ finished = true;
531
+ cleanupSignals();
532
+ if (streamId && options.follow && !closedByServer) {
533
+ channel.send(exports.DEDICATED_SERVICE_EVENTS.logsStop, {
534
+ leaseId: options.leaseId,
535
+ name: options.name,
536
+ streamId,
537
+ });
538
+ }
539
+ channel.close();
540
+ resolve(code);
541
+ };
542
+ const onSigint = () => finish(0);
543
+ const onSigterm = () => finish(0);
544
+ const onLog = (frame) => {
545
+ if (finished || !frame)
546
+ return;
547
+ if (streamId && frame.streamId && frame.streamId !== streamId)
548
+ return;
549
+ if (frame.name && frame.name !== options.name)
550
+ return;
551
+ if (typeof frame.data === 'string' && frame.data.length > 0) {
552
+ write(Buffer.from(frame.data, 'base64'));
553
+ }
554
+ };
555
+ // Set when the SERVER ended the stream — logs:stop answers a follow the
556
+ // client ended; a stream the daemon already closed has nothing to stop.
557
+ let closedByServer = false;
558
+ const onClosed = (frame) => {
559
+ if (finished || !frame)
560
+ return;
561
+ if (streamId && frame.streamId && frame.streamId !== streamId)
562
+ return;
563
+ if (frame.name && frame.name !== options.name)
564
+ return;
565
+ closedByServer = true;
566
+ if (frame.reason) {
567
+ options.errorOutput.write(`[log stream closed: ${frame.reason}]\n`);
568
+ }
569
+ finish(0);
570
+ };
571
+ // An agent proposal arriving while the owner watches logs surfaces where
572
+ // the owner is looking — stderr, so it never mingles with log bytes.
573
+ const onProposed = (frame) => {
574
+ if (finished || !frame)
575
+ return;
576
+ const proposalName = frame.name ?? frame.spec?.name ?? '(unnamed)';
577
+ const by = frame.createdBy ? ` by ${frame.createdBy}` : '';
578
+ options.errorOutput.write(`[an agent proposed service ${proposalName}${by} — approve with ` +
579
+ `\`skrr machines dedicated service approve ${options.leaseId} ${proposalName}\`]\n`);
580
+ };
581
+ channel.setOnDead(() => {
582
+ if (finished)
583
+ return;
584
+ options.errorOutput.write('[lost the connection to skrr; the log stream has ended]\n');
585
+ finish(1);
586
+ });
587
+ channel
588
+ .open()
589
+ .then(async () => {
590
+ process.on('SIGINT', onSigint);
591
+ process.on('SIGTERM', onSigterm);
592
+ channel.on(exports.DEDICATED_SERVICE_EVENTS.log, onLog);
593
+ channel.on(exports.DEDICATED_SERVICE_EVENTS.logClosed, onClosed);
594
+ channel.on(exports.DEDICATED_SERVICE_EVENTS.proposed, onProposed);
595
+ const { result } = await channel.call('logs', {
596
+ leaseId: options.leaseId,
597
+ name: options.name,
598
+ follow: options.follow,
599
+ ...(options.tailBytes !== undefined ? { tailBytes: options.tailBytes } : {}),
600
+ }, exports.SERVICE_CALL_TIMEOUT_MS);
601
+ streamId = typeof result?.streamId === 'string' ? result.streamId : null;
602
+ // A non-follow call can return the whole tail in the ack itself.
603
+ if (typeof result?.data === 'string' && result.data.length > 0) {
604
+ write(Buffer.from(result.data, 'base64'));
605
+ }
606
+ if (!options.follow)
607
+ finish(0);
608
+ })
609
+ .catch((err) => {
610
+ // Reject BEFORE closing — disconnect() fires onDead → finish(1),
611
+ // which would otherwise win the settlement race and mask the refusal.
612
+ finished = true;
613
+ cleanupSignals();
614
+ reject(err);
615
+ channel.close();
616
+ });
617
+ });
618
+ }
619
+ function serviceTemplatesDir(home = (0, node_os_1.homedir)()) {
620
+ return node_path_1.default.join(home, '.skrr', 'templates');
621
+ }
622
+ /**
623
+ * `~/.skrr/templates/<name>.json`, or null when absent. A template file that
624
+ * will not parse is an error, not a miss — silent fall-through to the gallery
625
+ * would hide a broken local file.
626
+ */
627
+ function loadLocalServiceTemplate(name, home = (0, node_os_1.homedir)()) {
628
+ if (!exports.SERVICE_NAME_PATTERN.test(name)) {
629
+ throw new DedicatedServiceError(`Template name "${name}" is not valid (${exports.SERVICE_NAME_PATTERN}).`, 'TEMPLATE_NAME_INVALID');
630
+ }
631
+ const file = node_path_1.default.join(serviceTemplatesDir(home), `${name}.json`);
632
+ if (!(0, node_fs_1.existsSync)(file))
633
+ return null;
634
+ let doc;
635
+ try {
636
+ doc = JSON.parse((0, node_fs_1.readFileSync)(file, 'utf8'));
637
+ }
638
+ catch (err) {
639
+ throw new DedicatedServiceError(`${file} is not valid JSON: ${err instanceof Error ? err.message : err}`, 'TEMPLATE_JSON');
640
+ }
641
+ const template = doc;
642
+ if (!template ||
643
+ typeof template !== 'object' ||
644
+ typeof template.spec !== 'object' ||
645
+ !template.spec) {
646
+ throw new DedicatedServiceError(`${file} is not a service template — it needs a "spec" object.`, 'TEMPLATE_INVALID');
647
+ }
648
+ return template;
649
+ }
650
+ const PARAM_SLOT = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
651
+ function substitute(value, params, file) {
652
+ if (typeof value === 'string') {
653
+ return value.replace(PARAM_SLOT, (whole, key) => {
654
+ if (!(key in params)) {
655
+ throw new DedicatedServiceError(`Template ${file} uses \${${key}} but no value was given and the param has no default.`, 'TEMPLATE_PARAM_MISSING');
656
+ }
657
+ return params[key];
658
+ });
659
+ }
660
+ if (Array.isArray(value))
661
+ return value.map((v) => substitute(v, params, file));
662
+ if (value && typeof value === 'object') {
663
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [
664
+ k,
665
+ substitute(v, params, file),
666
+ ]));
667
+ }
668
+ return value;
669
+ }
670
+ /**
671
+ * Merge a template with its params into a concrete spec. Required params
672
+ * without a value or a default refuse before any substitution runs — a
673
+ * half-merged spec is a worse failure than a named missing param.
674
+ */
675
+ function mergeServiceTemplate(template, given, source = 'template') {
676
+ const declared = template.params ?? [];
677
+ const values = {};
678
+ for (const param of declared) {
679
+ if (!param || typeof param.name !== 'string')
680
+ continue;
681
+ if (param.name in given) {
682
+ values[param.name] = given[param.name];
683
+ }
684
+ else if (param.default !== undefined) {
685
+ values[param.name] = String(param.default);
686
+ }
687
+ else if (param.required) {
688
+ throw new DedicatedServiceError(`Template ${source} requires --param ${param.name}=<value>${param.description ? ` (${param.description})` : ''}`, 'TEMPLATE_PARAM_MISSING');
689
+ }
690
+ }
691
+ const undeclared = Object.keys(given).filter((k) => !declared.some((p) => p && p.name === k));
692
+ if (undeclared.length > 0) {
693
+ throw new DedicatedServiceError(`Template ${source} declares no param named ${undeclared.map((k) => `"${k}"`).join(', ')}.`, 'TEMPLATE_PARAM_UNKNOWN');
694
+ }
695
+ const merged = substitute(template.spec ?? {}, values, source);
696
+ return { version: 1, ...merged };
697
+ }
698
+ /* -------------------------------------------------------------- guest --- */
699
+ /**
700
+ * The wire's `detail` is a string or the daemon's structured object
701
+ * ({reason, exitCode, signal, restarts, detail}) — render the fields for a
702
+ * status line, never "[object Object]". Shared by the owner-lane and
703
+ * guest-local status commands so both read the same shape.
704
+ */
705
+ function serviceDetailText(detail) {
706
+ if (typeof detail === 'string')
707
+ return detail || undefined;
708
+ if (!detail || typeof detail !== 'object' || Array.isArray(detail))
709
+ return undefined;
710
+ const d = detail;
711
+ const parts = [];
712
+ if (typeof d.reason === 'string' && d.reason)
713
+ parts.push(d.reason);
714
+ if (typeof d.exitCode === 'number')
715
+ parts.push(`exit ${d.exitCode}`);
716
+ if (typeof d.signal === 'string' && d.signal)
717
+ parts.push(`signal ${d.signal}`);
718
+ if (typeof d.restarts === 'number')
719
+ parts.push(`${d.restarts} restarts`);
720
+ if (typeof d.backoffUntil === 'number')
721
+ parts.push(`backoff until ${(0, format_1.localStamp)(d.backoffUntil)}`);
722
+ if (typeof d.detail === 'string' && d.detail)
723
+ parts.push(d.detail);
724
+ return parts.join(', ') || undefined;
725
+ }
726
+ /** The daemon's workload-facing service socket on a Dedicated Runtime guest. */
727
+ exports.GUEST_SERVICE_SOCKET_PATH = '/run/skrr-dedicated-runtime/service-propose/service-propose.sock';
728
+ /**
729
+ * True when this process is running on a Dedicated Runtime guest — the
730
+ * workload-uid test, or the guest markers the image/daemon own. An SSH
731
+ * session may not carry the wrapper's env stamp, so the filesystem markers
732
+ * count too; the socket itself remains the authority.
733
+ */
734
+ function runningOnDedicatedRuntimeGuest(env = process.env, getuid = process.getuid?.bind(process), paths = {}) {
735
+ if ((0, first_party_harness_1.isDedicatedRuntimeWorkload)(env, getuid))
736
+ return true;
737
+ const socketPath = paths.socketPath ?? exports.GUEST_SERVICE_SOCKET_PATH;
738
+ const attestation = paths.attestationPath ?? '/etc/skrr-dedicated-runtime/attestation.json';
739
+ const runtimeDir = paths.runtimeDir ?? '/run/skrr-dedicated-runtime';
740
+ return (0, node_fs_1.existsSync)(socketPath) || (0, node_fs_1.existsSync)(attestation) || (0, node_fs_1.existsSync)(runtimeDir);
741
+ }