@north-light/cli 0.3.580 → 0.3.582

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 (237) hide show
  1. package/dist/api/client.d.ts +6 -1
  2. package/dist/api/client.js +3 -3
  3. package/dist/api/contract.d.ts +1 -1
  4. package/dist/api/contract.js +1 -1
  5. package/dist/api/dto/execution-start.d.ts +14 -0
  6. package/dist/api/dto/health.d.ts +22 -0
  7. package/dist/api/dto/imports.d.ts +142 -0
  8. package/dist/api/dto/imports.js +0 -0
  9. package/dist/api/error-codes.d.ts +5 -0
  10. package/dist/api/error-codes.js +1 -1
  11. package/dist/api/errors.js +1 -1
  12. package/dist/api/index.d.ts +1 -0
  13. package/dist/api/index.js +1 -1
  14. package/dist/api/routes.d.ts +2 -0
  15. package/dist/api/routes.js +1 -1
  16. package/dist/build-root.d.ts +11 -4
  17. package/dist/build-root.js +1 -1
  18. package/dist/cli.js +7 -7
  19. package/dist/clients/attach/session/connection.d.ts +3 -1
  20. package/dist/clients/attach/session/connection.js +1 -1
  21. package/dist/clients/attach/viewer.js +529 -529
  22. package/dist/commands/api-client.d.ts +12 -4
  23. package/dist/commands/api-client.js +3 -3
  24. package/dist/commands/sys/daemon.js +1 -1
  25. package/dist/commands/sys/desktop.d.ts +5 -0
  26. package/dist/commands/sys/desktop.js +1 -0
  27. package/dist/commands/sys/import/move.d.ts +55 -0
  28. package/dist/commands/sys/import/move.js +8 -0
  29. package/dist/commands/sys/import/resume.d.ts +1 -0
  30. package/dist/commands/sys/import/resume.js +1 -0
  31. package/dist/commands/sys/import/start.d.ts +4 -0
  32. package/dist/commands/sys/import/start.js +1 -0
  33. package/dist/commands/sys/join.d.ts +2 -0
  34. package/dist/commands/sys/join.js +1 -1
  35. package/dist/commands/sys/machine/rotate.d.ts +1 -0
  36. package/dist/commands/sys/machine/rotate.js +1 -0
  37. package/dist/commands/sys/signin.d.ts +7 -0
  38. package/dist/commands/sys/signin.js +2 -0
  39. package/dist/commands/sys.js +1 -1
  40. package/dist/core/blobs/r2.d.ts +4 -0
  41. package/dist/core/blobs/r2.js +7 -4
  42. package/dist/core/canvas/daemon-owner.d.ts +1 -1
  43. package/dist/core/canvas/executions.d.ts +1 -1
  44. package/dist/core/canvas/node-tokens.d.ts +2 -0
  45. package/dist/core/canvas/node-tokens.js +3 -3
  46. package/dist/core/canvas/pid.d.ts +6 -0
  47. package/dist/core/canvas/pid.js +5 -5
  48. package/dist/core/command-plugins/compose.js +1 -1
  49. package/dist/core/command.d.ts +17 -8
  50. package/dist/core/command.js +9 -9
  51. package/dist/core/directory/issuer.d.ts +3 -0
  52. package/dist/core/directory/issuer.js +1 -1
  53. package/dist/core/directory/person-credential.d.ts +61 -0
  54. package/dist/core/directory/person-credential.js +1 -0
  55. package/dist/core/directory/reach.d.ts +3 -0
  56. package/dist/core/directory/reach.js +1 -0
  57. package/dist/core/grants/mirror.d.ts +3 -0
  58. package/dist/core/grants/mirror.js +2 -2
  59. package/dist/core/graph/bodies-boot-copy.d.ts +16 -2
  60. package/dist/core/graph/bodies-boot-copy.js +1 -1
  61. package/dist/core/graph/bodies.d.ts +13 -4
  62. package/dist/core/graph/bodies.js +1 -1
  63. package/dist/core/graph/deletion.d.ts +4 -1
  64. package/dist/core/graph/deletion.js +3 -3
  65. package/dist/core/graph/documents.d.ts +4 -0
  66. package/dist/core/graph/documents.js +9 -9
  67. package/dist/core/graph/reader.js +2 -2
  68. package/dist/core/graph/repo-sync/exchange.js +2 -2
  69. package/dist/core/graph/repo-sync/ref-format.d.ts +7 -2
  70. package/dist/core/graph/repo-sync/ref-format.js +2 -2
  71. package/dist/core/import/bundle/blob-keys.d.ts +10 -0
  72. package/dist/core/import/bundle/blob-keys.js +1 -0
  73. package/dist/core/import/bundle/layout.d.ts +90 -0
  74. package/dist/core/import/bundle/layout.js +5 -0
  75. package/dist/core/import/export/archive.d.ts +11 -0
  76. package/dist/core/import/export/archive.js +1 -0
  77. package/dist/core/import/export/cloud.d.ts +23 -0
  78. package/dist/core/import/export/cloud.js +1 -0
  79. package/dist/core/import/export/dump.d.ts +34 -0
  80. package/dist/core/import/export/dump.js +3 -0
  81. package/dist/core/import/export/files.d.ts +22 -0
  82. package/dist/core/import/export/files.js +1 -0
  83. package/dist/core/import/export/inline.d.ts +3 -0
  84. package/dist/core/import/export/inline.js +1 -0
  85. package/dist/core/import/export/source.d.ts +2 -0
  86. package/dist/core/import/export/source.js +1 -0
  87. package/dist/core/import/export/stores.d.ts +9 -0
  88. package/dist/core/import/export/stores.js +1 -0
  89. package/dist/core/import/load/bind.d.ts +19 -0
  90. package/dist/core/import/load/bind.js +15 -0
  91. package/dist/core/import/load/blobs.d.ts +10 -0
  92. package/dist/core/import/load/blobs.js +1 -0
  93. package/dist/core/import/load/catalog.d.ts +32 -0
  94. package/dist/core/import/load/catalog.js +9 -0
  95. package/dist/core/import/load/copy.d.ts +26 -0
  96. package/dist/core/import/load/copy.js +6 -0
  97. package/dist/core/import/load/path-map.d.ts +7 -0
  98. package/dist/core/import/load/path-map.js +1 -0
  99. package/dist/core/import/load/prepare.d.ts +37 -0
  100. package/dist/core/import/load/prepare.js +10 -0
  101. package/dist/core/import/load/rewrite.d.ts +39 -0
  102. package/dist/core/import/load/rewrite.js +13 -0
  103. package/dist/core/import/load/rollback.d.ts +3 -0
  104. package/dist/core/import/load/rollback.js +6 -0
  105. package/dist/core/import/load/run.d.ts +22 -0
  106. package/dist/core/import/load/run.js +3 -0
  107. package/dist/core/import/load/stage.d.ts +15 -0
  108. package/dist/core/import/load/stage.js +1 -0
  109. package/dist/core/import/load/stores.d.ts +24 -0
  110. package/dist/core/import/load/stores.js +2 -0
  111. package/dist/core/import/load/verify.d.ts +4 -0
  112. package/dist/core/import/load/verify.js +12 -0
  113. package/dist/core/io.js +4 -4
  114. package/dist/core/northlight/previews.d.ts +22 -0
  115. package/dist/core/northlight/previews.js +1 -0
  116. package/dist/core/providers/call.js +1 -1
  117. package/dist/core/providers/device.js +1 -1
  118. package/dist/core/providers/files.d.ts +2 -1
  119. package/dist/core/providers/files.js +1 -1
  120. package/dist/core/runtime/broker/cli-relay.js +1 -1
  121. package/dist/core/runtime/broker/model-adapter.js +3 -3
  122. package/dist/core/runtime/broker-ownership.d.ts +1 -0
  123. package/dist/core/runtime/broker-ownership.js +1 -0
  124. package/dist/core/runtime/broker.js +1 -1
  125. package/dist/core/runtime/first-run-offer.d.ts +7 -2
  126. package/dist/core/runtime/first-run-offer.js +10 -2
  127. package/dist/core/runtime/front-door.js +4 -4
  128. package/dist/core/runtime/revive.d.ts +5 -2
  129. package/dist/core/runtime/revive.js +2 -2
  130. package/dist/core/runtime/runner/blaxel/runner.js +3 -3
  131. package/dist/core/runtime/runner/index.d.ts +1 -0
  132. package/dist/core/runtime/runner/index.js +1 -1
  133. package/dist/core/runtime/runner/machine/call.d.ts +8 -0
  134. package/dist/core/runtime/runner/machine/call.js +3 -0
  135. package/dist/core/runtime/runner/machine/environment.d.ts +1 -0
  136. package/dist/core/runtime/runner/machine/environment.js +1 -0
  137. package/dist/core/runtime/runner/machine/executor.d.ts +29 -0
  138. package/dist/core/runtime/runner/machine/executor.js +1 -0
  139. package/dist/core/runtime/runner/machine/machine-executor.test.d.ts +1 -0
  140. package/dist/core/runtime/runner/machine/machine-executor.test.js +4 -0
  141. package/dist/core/runtime/runner/machine/ops.d.ts +127 -0
  142. package/dist/core/runtime/runner/machine/ops.js +1 -0
  143. package/dist/core/runtime/runner/machine/runner.d.ts +12 -0
  144. package/dist/core/runtime/runner/machine/runner.js +1 -0
  145. package/dist/core/runtime/runner/types.d.ts +7 -1
  146. package/dist/core/runtime/warm-pool.d.ts +2 -0
  147. package/dist/core/runtime/warm-pool.js +1 -1
  148. package/dist/core/secrets/env-rows.d.ts +1 -0
  149. package/dist/core/secrets/env-rows.js +1 -1
  150. package/dist/core/storage/purge.d.ts +6 -0
  151. package/dist/core/storage/purge.js +4 -0
  152. package/dist/core/subscription-state.d.ts +1 -0
  153. package/dist/core/subscription-state.js +1 -1
  154. package/dist/daemon/api/bridge.js +3 -3
  155. package/dist/daemon/api/handlers/attach.js +1 -1
  156. package/dist/daemon/api/handlers/crons.js +1 -1
  157. package/dist/daemon/api/handlers/daemon.js +1 -1
  158. package/dist/daemon/api/handlers/execution-start.js +2 -2
  159. package/dist/daemon/api/handlers/files.js +1 -1
  160. package/dist/daemon/api/handlers/imports.d.ts +2 -0
  161. package/dist/daemon/api/handlers/imports.js +1 -0
  162. package/dist/daemon/api/handlers/machine-credential.d.ts +2 -0
  163. package/dist/daemon/api/handlers/machine-credential.js +1 -0
  164. package/dist/daemon/api/handlers/profiles.js +1 -1
  165. package/dist/daemon/api/handlers/providers.js +1 -1
  166. package/dist/daemon/api/handlers/runs.js +1 -1
  167. package/dist/daemon/api/import-auth.d.ts +6 -0
  168. package/dist/daemon/api/import-auth.js +1 -0
  169. package/dist/daemon/api/operations.d.ts +2 -2
  170. package/dist/daemon/api/operations.js +1 -1
  171. package/dist/daemon/api/router.d.ts +7 -1
  172. package/dist/daemon/api/router.js +1 -1
  173. package/dist/daemon/api/server.js +1 -1
  174. package/dist/daemon/boot-migrations.js +1 -1
  175. package/dist/daemon/broker-signals/bus.d.ts +3 -2
  176. package/dist/daemon/broker-signals/bus.js +1 -1
  177. package/dist/daemon/cron/armed-context.d.ts +1 -0
  178. package/dist/daemon/cron/armed-context.js +1 -1
  179. package/dist/daemon/cron/closed.d.ts +31 -0
  180. package/dist/daemon/cron/closed.js +1 -0
  181. package/dist/daemon/cron/ended.js +2 -2
  182. package/dist/daemon/cron/lease-recovery.js +1 -1
  183. package/dist/daemon/cron/machine-exec.d.ts +11 -0
  184. package/dist/daemon/cron/machine-exec.js +1 -0
  185. package/dist/daemon/cron/machine-fire.d.ts +26 -0
  186. package/dist/daemon/cron/machine-fire.js +1 -0
  187. package/dist/daemon/cron/recurrence.d.ts +14 -0
  188. package/dist/daemon/cron/recurrence.js +1 -0
  189. package/dist/daemon/cron/run-token.d.ts +7 -0
  190. package/dist/daemon/cron/run-token.js +1 -0
  191. package/dist/daemon/cron-run.js +2 -2
  192. package/dist/daemon/crtrd-cli.js +1 -1
  193. package/dist/daemon/crtrd.d.ts +3 -3
  194. package/dist/daemon/crtrd.js +14 -14
  195. package/dist/daemon/drain.d.ts +25 -0
  196. package/dist/daemon/drain.js +9 -0
  197. package/dist/daemon/due-sources.d.ts +1 -1
  198. package/dist/daemon/due-sources.js +1 -1
  199. package/dist/daemon/due.js +8 -8
  200. package/dist/daemon/fleet.js +5 -5
  201. package/dist/daemon/import-dump-run.d.ts +1 -0
  202. package/dist/daemon/import-dump-run.js +5 -0
  203. package/dist/daemon/machine-daemon.d.ts +10 -0
  204. package/dist/daemon/machine-daemon.js +1 -0
  205. package/dist/daemon/machine-link/client.d.ts +29 -0
  206. package/dist/daemon/machine-link/client.js +1 -0
  207. package/dist/daemon/machine-link/config.d.ts +45 -0
  208. package/dist/daemon/machine-link/config.js +3 -0
  209. package/dist/daemon/machine-link/credential.d.ts +7 -0
  210. package/dist/daemon/machine-link/credential.js +2 -0
  211. package/dist/daemon/machine-link/forward.d.ts +16 -0
  212. package/dist/daemon/machine-link/forward.js +1 -0
  213. package/dist/daemon/machine-link/frames.d.ts +49 -0
  214. package/dist/daemon/machine-link/frames.js +1 -0
  215. package/dist/daemon/machine-link/holder.d.ts +73 -0
  216. package/dist/daemon/machine-link/holder.js +9 -0
  217. package/dist/daemon/machine-link/move-notify.d.ts +4 -0
  218. package/dist/daemon/machine-link/move-notify.js +1 -0
  219. package/dist/daemon/manage.d.ts +19 -3
  220. package/dist/daemon/manage.js +3 -3
  221. package/dist/daemon/reconcilers/cron-lane.js +1 -1
  222. package/dist/daemon/reconcilers/delivery-lane.js +2 -2
  223. package/dist/daemon/reconcilers/import-lane.d.ts +9 -0
  224. package/dist/daemon/reconcilers/import-lane.js +1 -0
  225. package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +1 -1
  226. package/dist/daemon/reconcilers/node-lifecycle/remote-wake.d.ts +3 -0
  227. package/dist/daemon/reconcilers/node-lifecycle/remote-wake.js +1 -1
  228. package/dist/daemon/reconcilers/node-lifecycle/tick.js +1 -1
  229. package/dist/daemon/reconcilers/purge-due-work.d.ts +13 -0
  230. package/dist/daemon/reconcilers/purge-due-work.js +1 -0
  231. package/dist/daemon/reconcilers/row-claim.d.ts +1 -0
  232. package/dist/daemon/reconcilers/row-claim.js +2 -2
  233. package/dist/daemon/runs/observer.js +3 -3
  234. package/package.json +2 -2
  235. package/packages/crouter-identity/package.json +1 -1
  236. package/runtime.lock.json +13 -13
  237. package/scripts/sandbox-start.mjs +185 -2
@@ -1,4 +1,5 @@
1
1
  import { CrtrClient } from '../api/index.js';
2
+ import { CrtrError } from '../core/errors.js';
2
3
  import type { CanvasSource } from '../core/canvas/source.js';
3
4
  import type { NodeStatus, SubscriptionRef } from '../core/canvas/types.js';
4
5
  import type { Scope } from '../types.js';
@@ -23,10 +24,6 @@ export declare function rethrowAsCliError(err: unknown, next?: string): never;
23
24
  * 404-only→null mapping as `ApiCanvasSource.orNull`, but returning the raw DTO
24
25
  * and rethrowing non-404s as CLI errors rather than swallowing them. */
25
26
  export declare function getNodeOrNull(nodeId: string): Promise<NodeDetailDTO | null>;
26
- /** Bounded tail of crtrd.log surfaced on a cold-start `/healthz` timeout.
27
- * Large enough to catch a real startup failure/stack trace,
28
- * small enough that a runaway daemon log can never leak unbounded into a CLI
29
- * error message. */
30
27
  export declare const COLD_START_DIAGNOSTIC_TAIL_BYTES = 4096;
31
28
  /** Read a bounded tail of crtrd's event log to enrich a `daemon_unavailable`
32
29
  * cold-start timeout, so the actual startup failure is not discarded behind a
@@ -37,6 +34,16 @@ export declare const COLD_START_DIAGNOSTIC_TAIL_BYTES = 4096;
37
34
  * contract — a missing/unreadable/empty log yields `undefined` so the base
38
35
  * message is used unchanged. */
39
36
  export declare function readColdStartDiagnostic(): string | undefined;
37
+ /** Where a `crtr` process outside a node reaches the control plane instead of a local daemon: a machine cron's run token goes to
38
+ * `CRTR_API_URL`; an imported Mac (`runtime/machine.json`) goes to its `apiUrl` with the signed-in person's bearer. Neither: undefined,
39
+ * and the local socket is used. `CRTR_API_SOCKET` (inside a node) is checked by the caller first and is never remote. */
40
+ export declare function remoteTarget(): {
41
+ client(timeoutMs?: number): CrtrClient;
42
+ } | undefined;
43
+ /** A move whose cloud run reached `done` but whose computer has not switched over: the account holds the records, the local daemon stays drained,
44
+ * and no command may run against either side until `crtr sys signin` finishes it (design D14). The record is deleted only once the machine-role daemon
45
+ * is up (`finishMove`, or that daemon's own start), so a `machine.json` written before a failed restart does not lift this. */
46
+ export declare function moveUnfinished(): CrtrError | undefined;
40
47
  /** The local-socket `CrtrClient` every CLI verb uses. Autostart is on by
41
48
  * default: on a cold socket the injected `onColdSocket` hook fires
42
49
  * `ensureDaemon()` (spawns crtrd detached via the branded host), then the
@@ -53,6 +60,7 @@ export declare function readColdStartDiagnostic(): string | undefined;
53
60
  * cannot outlast the initiating CLI command. */
54
61
  export declare function cliClient(opts?: {
55
62
  timeoutMs?: number;
63
+ local?: boolean;
56
64
  }): CrtrClient;
57
65
  export declare class ApiCanvasSource implements CanvasSource {
58
66
  private readonly client;
@@ -1,4 +1,4 @@
1
- var y=Object.defineProperty;var a=(r,t)=>y(r,"name",{value:t,configurable:!0});import{closeSync as g,existsSync as v,fstatSync as C,openSync as N,readFileSync as m,readSync as R}from"node:fs";import{dirname as T,join as l}from"node:path";import{gunzipSync as E}from"node:zlib";import{ApiError as u}from"../api/index.js";import{localClient as f}from"../api/node-transport.js";import{apiErrorToCliError as p}from"../core/io.js";import{autostartDisabled as w,daemonAutostartStartupFailure as A,ensureDaemon as M}from"../daemon/manage.js";import{DAEMON_STARTUP_WINDOW_MS as b}from"../daemon/startup-policy.js";import{readState as O}from"../core/config.js";import{resolveRemoteCanvasTarget as I}from"../core/remote-canvas-target.js";import{RemoteCanvasSource as x}from"../core/canvas/remote-canvas-source.js";import{BROKER_RELEASED_MARKER as D,BROKER_RELAY_GENERATION as F,crtrHome as k}from"../core/canvas/paths.js";import{envTestNodeSocketWindowMs as L,isSandboxedNode as B}from"../shared/env.js";import{ownerOnly as W}from"../core/errors.js";function d(r){return{...r,fault:r.fault??null}}a(d,"withRowFault");function K(r,t){throw r instanceof u?p(r,t):r}a(K,"rethrowAsCliError");async function it(r){try{return await _().getNode(r)}catch(t){if(t instanceof u&&t.status===404)return null;K(t)}}a(it,"getNodeOrNull");const P=4096,j=20;function G(){let r;try{r=N(l(k(),"crtrd.log"),"r");const{size:t}=C(r),s=Math.max(0,t-P),n=t-s;if(n<=0)return;const e=Buffer.alloc(n);R(r,e,0,n,s);const o=e.toString("utf8").split(`
2
- `).filter(c=>c.trim()!=="").slice(-j).join(`
1
+ var v=Object.defineProperty;var c=(r,t)=>v(r,"name",{value:t,configurable:!0});import{closeSync as S,existsSync as R,fstatSync as N,openSync as T,readFileSync as m,readSync as E}from"node:fs";import{dirname as O,join as l}from"node:path";import{gunzipSync as A}from"node:zlib";import{CrtrClient as p,ApiError as u}from"../api/index.js";import{localClient as f}from"../api/node-transport.js";import{northlightFetch as x,notSignedIn as M,readPersonCredential as I}from"../core/directory/person-credential.js";import{isMoveCommitted as F,moveUnfinishedError as D,readMachineConfig as U}from"../daemon/machine-link/config.js";import{northlightUnreachable as P}from"../core/directory/reach.js";import{apiErrorToCliError as w}from"../core/io.js";import{autostartDisabled as b,daemonAutostartStartupFailure as k,ensureDaemon as L}from"../daemon/manage.js";import{DAEMON_STARTUP_WINDOW_MS as _}from"../daemon/startup-policy.js";import{readState as K}from"../core/config.js";import{resolveRemoteCanvasTarget as W}from"../core/remote-canvas-target.js";import{RemoteCanvasSource as B}from"../core/canvas/remote-canvas-source.js";import{BROKER_RELEASED_MARKER as j,BROKER_RELAY_GENERATION as z,crtrHome as G}from"../core/canvas/paths.js";import{envTestNodeSocketWindowMs as q,isSandboxedNode as H}from"../shared/env.js";import{CrtrError as Y,ownerOnly as $}from"../core/errors.js";import{ExitCode as J}from"../types.js";function d(r){return{...r,fault:r.fault??null}}c(d,"withRowFault");function V(r,t){throw r instanceof u?w(r,t):r}c(V,"rethrowAsCliError");async function Rt(r){try{return await y().getNode(r)}catch(t){if(t instanceof u&&t.status===404)return null;V(t)}}c(Rt,"getNodeOrNull");const X="crtr_run_",Q=4096,Z=20;function tt(){let r;try{r=T(l(G(),"crtrd.log"),"r");const{size:t}=N(r),e=Math.max(0,t-Q),s=t-e;if(s<=0)return;const n=Buffer.alloc(s);E(r,n,0,s,e);const o=n.toString("utf8").split(`
2
+ `).filter(a=>a.trim()!=="").slice(-Z).join(`
3
3
  `).trim();return o===""?void 0:`crtrd.log (tail):
4
- ${o}`}catch{return}finally{if(r!==void 0)try{g(r)}catch{}}}a(G,"readColdStartDiagnostic");function _(r){const t=process.env.CRTR_API_SOCKET;if(t){const s=a(()=>{if(!v(t))try{const e=T(t),i=m(l(e,D),"utf8"),o=m(l(e,F),"utf8");if(i===o&&i!=="")return new u(503,"broker_released","This node's broker has been released, so crtr commands from its background jobs are not available.")}catch(e){if(e.code!=="ENOENT")throw e}},"availabilityFailure"),n=s();if(n!==void 0)throw p(n,"Run the command from the node's own turn instead.");return f({autostart:!1,availabilityFailure:s,coldStartPollWindowMs:(w()?L():void 0)??b,...r?.timeoutMs!==void 0?{timeoutMs:r.timeoutMs}:{}})}return w()?f({autostart:!1,...r?.timeoutMs!==void 0?{timeoutMs:r.timeoutMs}:{}}):f({...r?.timeoutMs!==void 0?{timeoutMs:r.timeoutMs}:{},onColdSocket:a(async()=>{M()},"onColdSocket"),coldStartDiagnostic:G,coldStartFailure:A,coldStartPollWindowMs:b})}a(_,"cliClient");const z=2e3;class S{static{a(this,"ApiCanvasSource")}client;rosterCache=null;rosterInFlight=null;constructor(t=_()){this.client=t}async roster(){const t=Date.now();return this.rosterCache!==null&&t-this.rosterCache.at<z?this.rosterCache.index:this.rosterInFlight!==null?this.rosterInFlight:(this.rosterInFlight=this.client.canvasRoster().then(s=>{const n={snapshot:s,subscriptions:new Map,subscribers:new Map};for(const e of s.edges){const i={node_id:e.to_id,active:e.active,created:e.created},o={node_id:e.from_id,active:e.active,created:e.created},c=n.subscriptions.get(e.from_id);c===void 0?n.subscriptions.set(e.from_id,[i]):c.push(i);const h=n.subscribers.get(e.to_id);h===void 0?n.subscribers.set(e.to_id,[o]):h.push(o)}return this.rosterCache={at:Date.now(),index:n},n}).finally(()=>{this.rosterInFlight=null}),this.rosterInFlight)}async orNull(t){try{return await t()}catch(s){if(s instanceof u&&s.status===404)return null;throw s}}async getNode(t){const s=await this.orNull(()=>this.client.getNode(t));return s===null?null:d(s)}async getRow(t){const s=await this.orNull(()=>this.client.getNode(t));return s===null?null:d(s)}async listNodes(t){const s=t?.status,n=t?.under===void 0?{}:{under:t.under,under_inclusive:!0};let e;if(Array.isArray(s)){const i=new Set(s);e=(await this.client.listNodes(n)).filter(c=>i.has(c.status))}else e=await this.client.listNodes({...s!==void 0?{status:s}:{},...n});return e.map(i=>d(i))}async browseSnapshot(){const t=await this.client.canvasBrowseCompressed(),s=JSON.parse(E(t).toString("utf8")),n={subscriptions:new Map,subscribers:new Map};for(const e of s.edges){const i=n.subscriptions.get(e.from_id)??[];i.push({node_id:e.to_id,active:e.active,created:e.created}),n.subscriptions.set(e.from_id,i);const o=n.subscribers.get(e.to_id)??[];o.push({node_id:e.from_id,active:e.active,created:e.created}),n.subscribers.set(e.to_id,o)}return this.rosterCache={at:Date.now(),index:n},s.nodes.map(e=>d(e))}async panelRoster(){const t=await this.roster();return t.snapshot!==void 0?t.snapshot:(this.rosterCache=null,(await this.roster()).snapshot)}async subscriptionsOf(t){return this.roster().then(s=>s.subscriptions.get(t)??[])}async subscribersOf(t){return this.roster().then(s=>s.subscribers.get(t)??[])}async view(t){const s=new Set([t]),n=[],e=(await this.subscriptionsOf(t)).map(i=>i.node_id);for(;e.length>0;){const i=e.shift();if(!s.has(i)){s.add(i),n.push(i);for(const o of await this.subscriptionsOf(i))s.has(o.node_id)||e.push(o.node_id)}}return n}async ticketCountsForNodes(t){return(await this.client.attentionCounts([...t])).counts}async ticketCountsForView(t){return this.ticketCountsForNodes([t,...await this.view(t)])}async newestReport(t){return(await this.client.getReports(t,{limit:1}))[0]??null}async hasActiveLiveSubscription(t){const s=(await this.subscriptionsOf(t)).filter(n=>n.active);for(const n of s){const e=await this.getRow(n.node_id);if(e&&(e.status==="active"||e.status==="idle"))return!0}return!1}}function ot(r,t=B()){const s=r?.scope??"user";if(t){if(r?.canvasName!==void 0)throw W("--canvas selects the person's remote canvas targets, which an app's node cannot reach","Omit --canvas; this node reads its own runtime's canvas.");return new S}const n=r?.canvasName??O(s).activeCanvas??null;if(!n)return new S;const{previewEndpoint:e,relayToken:i}=I(n,s);return new x(e,i)}a(ot,"cliCanvasSource");export{S as ApiCanvasSource,P as COLD_START_DIAGNOSTIC_TAIL_BYTES,ot as cliCanvasSource,_ as cliClient,it as getNodeOrNull,G as readColdStartDiagnostic,K as rethrowAsCliError};
4
+ ${o}`}catch{return}finally{if(r!==void 0)try{S(r)}catch{}}}c(tt,"readColdStartDiagnostic");function et(){if(process.env.CRTR_API_SOCKET)return;const r=process.env.CRTR_API_TOKEN,t=process.env.CRTR_API_URL;if(r!==void 0&&r.startsWith(X)&&t)return{client:c(i=>new p({baseUrl:t,headers:{authorization:`Bearer ${r}`},...i!==void 0?{timeoutMs:i}:{}}),"client")};const e=U();if(e===void 0)return;if(I()===void 0)throw M("this computer is imported into Northlight, so crtr needs your sign-in");const s=x(e.directoryUrl,e.apiUrl),n=c((i,o)=>s(i,o).catch(a=>{throw a instanceof Y?g(a):a}),"authed");return{client:c(i=>nt(new p({baseUrl:e.apiUrl,fetch:n,...i!==void 0?{timeoutMs:i}:{}}),e.apiUrl),"client")}}c(et,"remoteTarget");function nt(r,t){return new Proxy(r,{get(e,s){const n=Reflect.get(e,s,e);return typeof n!="function"?n:(...i)=>{const o=n.apply(e,i);return o instanceof Promise?o.catch(a=>{throw a instanceof u&&a.code==="request_timeout"?g(P(a,t)):a}):o}}})}c(nt,"unreachableOnTimeout");function g(r){const t=typeof r.details?.next=="string"?r.details.next:void 0;return new u(r.exitCode===J.NETWORK?503:401,r.code,r.message,{error:r.code,message:r.message,...t===void 0?{}:{next:t}})}c(g,"asApiError");function rt(){return F()?D():void 0}c(rt,"moveUnfinished");function y(r){if(r?.local!==!0){const s=rt();if(s!==void 0)throw s}const t=r?.local===!0?void 0:et();if(t!==void 0)return t.client(r?.timeoutMs);const e=process.env.CRTR_API_SOCKET;if(e){const s=c(()=>{if(!R(e))try{const i=O(e),o=m(l(i,j),"utf8"),a=m(l(i,z),"utf8");if(o===a&&o!=="")return new u(503,"broker_released","This node's broker has been released, so crtr commands from its background jobs are not available.")}catch(i){if(i.code!=="ENOENT")throw i}},"availabilityFailure"),n=s();if(n!==void 0)throw w(n,"Run the command from the node's own turn instead.");return f({autostart:!1,availabilityFailure:s,coldStartPollWindowMs:(b()?q():void 0)??_,...r?.timeoutMs!==void 0?{timeoutMs:r.timeoutMs}:{}})}return b()?f({autostart:!1,...r?.timeoutMs!==void 0?{timeoutMs:r.timeoutMs}:{}}):f({...r?.timeoutMs!==void 0?{timeoutMs:r.timeoutMs}:{},onColdSocket:c(async()=>{L()},"onColdSocket"),coldStartDiagnostic:tt,coldStartFailure:k,coldStartPollWindowMs:_})}c(y,"cliClient");const st=2e3;class C{static{c(this,"ApiCanvasSource")}client;rosterCache=null;rosterInFlight=null;constructor(t=y()){this.client=t}async roster(){const t=Date.now();return this.rosterCache!==null&&t-this.rosterCache.at<st?this.rosterCache.index:this.rosterInFlight!==null?this.rosterInFlight:(this.rosterInFlight=this.client.canvasRoster().then(e=>{const s={snapshot:e,subscriptions:new Map,subscribers:new Map};for(const n of e.edges){const i={node_id:n.to_id,active:n.active,created:n.created},o={node_id:n.from_id,active:n.active,created:n.created},a=s.subscriptions.get(n.from_id);a===void 0?s.subscriptions.set(n.from_id,[i]):a.push(i);const h=s.subscribers.get(n.to_id);h===void 0?s.subscribers.set(n.to_id,[o]):h.push(o)}return this.rosterCache={at:Date.now(),index:s},s}).finally(()=>{this.rosterInFlight=null}),this.rosterInFlight)}async orNull(t){try{return await t()}catch(e){if(e instanceof u&&e.status===404)return null;throw e}}async getNode(t){const e=await this.orNull(()=>this.client.getNode(t));return e===null?null:d(e)}async getRow(t){const e=await this.orNull(()=>this.client.getNode(t));return e===null?null:d(e)}async listNodes(t){const e=t?.status,s=t?.under===void 0?{}:{under:t.under,under_inclusive:!0};let n;if(Array.isArray(e)){const i=new Set(e);n=(await this.client.listNodes(s)).filter(a=>i.has(a.status))}else n=await this.client.listNodes({...e!==void 0?{status:e}:{},...s});return n.map(i=>d(i))}async browseSnapshot(){const t=await this.client.canvasBrowseCompressed(),e=JSON.parse(A(t).toString("utf8")),s={subscriptions:new Map,subscribers:new Map};for(const n of e.edges){const i=s.subscriptions.get(n.from_id)??[];i.push({node_id:n.to_id,active:n.active,created:n.created}),s.subscriptions.set(n.from_id,i);const o=s.subscribers.get(n.to_id)??[];o.push({node_id:n.from_id,active:n.active,created:n.created}),s.subscribers.set(n.to_id,o)}return this.rosterCache={at:Date.now(),index:s},e.nodes.map(n=>d(n))}async panelRoster(){const t=await this.roster();return t.snapshot!==void 0?t.snapshot:(this.rosterCache=null,(await this.roster()).snapshot)}async subscriptionsOf(t){return this.roster().then(e=>e.subscriptions.get(t)??[])}async subscribersOf(t){return this.roster().then(e=>e.subscribers.get(t)??[])}async view(t){const e=new Set([t]),s=[],n=(await this.subscriptionsOf(t)).map(i=>i.node_id);for(;n.length>0;){const i=n.shift();if(!e.has(i)){e.add(i),s.push(i);for(const o of await this.subscriptionsOf(i))e.has(o.node_id)||n.push(o.node_id)}}return s}async ticketCountsForNodes(t){return(await this.client.attentionCounts([...t])).counts}async ticketCountsForView(t){return this.ticketCountsForNodes([t,...await this.view(t)])}async newestReport(t){return(await this.client.getReports(t,{limit:1}))[0]??null}async hasActiveLiveSubscription(t){const e=(await this.subscriptionsOf(t)).filter(s=>s.active);for(const s of e){const n=await this.getRow(s.node_id);if(n&&(n.status==="active"||n.status==="idle"))return!0}return!1}}function Nt(r,t=H()){const e=r?.scope??"user";if(t){if(r?.canvasName!==void 0)throw $("--canvas selects the person's remote canvas targets, which an app's node cannot reach","Omit --canvas; this node reads its own runtime's canvas.");return new C}const s=r?.canvasName??K(e).activeCanvas??null;if(!s)return new C;const{previewEndpoint:n,relayToken:i}=W(s,e);return new B(n,i)}c(Nt,"cliCanvasSource");export{C as ApiCanvasSource,Q as COLD_START_DIAGNOSTIC_TAIL_BYTES,Nt as cliCanvasSource,y as cliClient,Rt as getNodeOrNull,rt as moveUnfinished,tt as readColdStartDiagnostic,et as remoteTarget,V as rethrowAsCliError};
@@ -1 +1 @@
1
- var w=Object.defineProperty;var o=(t,e)=>w(t,"name",{value:e,configurable:!0});import{defineLeaf as u,defineBranch as f}from"../../core/command.js";import{InputError as d}from"../../core/io.js";import{parseDurationMs as g}from"../../core/wake.js";import{DaemonNotPreparedError as y,probeDaemonHealth as p,probeErrorMessage as b,spawnDaemon as h,stopDaemonProcess as v,sweepStrayDaemons as _}from"../../daemon/manage.js";import{isDaemonRunning as k,readPidfile as l,isPidAlive as m}from"../../daemon/pidfile.js";import{cliClient as T}from"../api-client.js";import{localClient as c}from"../../api/node-transport.js";import{defineSysBranch as q}from"./branch.js";import{selectedGeneration as x}from"./shared.js";const I=u({name:"start",description:"start the daemon in the background",whenToUse:"bringing the crtrd supervisor up for the first time in a session so node window-exits get handled (no-op only when its Unix API socket already serves)",help:{name:"sys daemon start",summary:"start the crtrd supervisor daemon in the background (no-op only if its Unix API socket serves)",params:[{kind:"flag",name:"prepare",type:"bool",required:!1,constraint:"Hold normal daemon work after storage preparation until `sys daemon admit`; omit for ordinary startup."},{kind:"flag",name:"force",type:"bool",required:!1,default:!1,constraint:"Start even while `crtr sys join` holds the daemon down. Joining then fails `daemon_running`; use only to clear a join that is stuck."}],output:[{name:"started",type:"boolean",required:!0,constraint:"True when a new daemon process was spawned; false when one was already running."},{name:"pid",type:"number",required:!1,constraint:"PID of the newly spawned daemon."},{name:"existing_pid",type:"number",required:!1,constraint:"PID of the already-running daemon (when started=false)."},{name:"running",type:"boolean",required:!1,constraint:"True only when a live pidfile owner does not serve its Unix API socket."},{name:"serving",type:"boolean",required:!1,constraint:"False only when a live pidfile owner does not answer /healthz."},{name:"probe_error",type:"string",required:!1,constraint:"The final /healthz probe failure when a live owner missed readiness."},{name:"startup_phase",type:"string",required:!1,constraint:"The observed daemon phase when it was reachable."}],outputKind:"object",effects:["Refuses `join_in_progress` while a live `crtr sys join` holds the daemon down, unless `--force`. `sys daemon restart` and `ensureDaemon` stand down the same way.","Spawns dist/daemon/crtrd-cli.js detached + unref'd; it outlives this process.","Writes a pidfile at CRTR_PIDFILE if set, otherwise crtrHome()/crtrd.pid."]},run:o(async t=>{try{return await h({targetPhase:t.prepare===!0?"prepared":"ready",ignoreJoinLock:t.force===!0})}catch(e){throw e instanceof y?new d({error:"daemon_not_prepared",message:e.message,next:"Stop the live daemon, then start a new one with `crtr sys daemon start --prepare` before setup."}):e}},"run")}),D=100;async function S(t,e){const a=Date.now(),r=a+e;let s="initializing",i="the daemon socket was not reachable";for(;Date.now()<r;){try{const n=await p(Math.min(1e3,Math.max(1,r-Date.now())));if(s=n.startup_phase,i=n.startup_blocked??`daemon is ${n.startup_phase}`,n.startup_phase==="blocked")throw new d({error:"startup_blocked",message:n.startup_blocked??"daemon startup is blocked",received:n.startup_phase,field:"phase",next:"Repair the reported blocker paths with `crtr sys migrate`, then retry the await."});if(n.startup_phase===t)return{startup_phase:t,waited_ms:Date.now()-a};if(t==="prepared"&&n.startup_phase==="ready")throw new d({error:"await_missed_phase",message:"daemon is already ready and cannot return to prepared in this process",received:n.startup_phase,field:"phase",next:"Start a new daemon with `crtr sys daemon start --prepare` before awaiting prepared."})}catch(n){if(n instanceof d)throw n;i=n instanceof Error?n.message:String(n)}await new Promise(n=>setTimeout(n,Math.min(D,Math.max(0,r-Date.now()))))}throw new d({error:"await_timeout",message:`daemon did not reach ${t} within ${e}ms (last phase: ${s}; last observation: ${i})`,received:s,field:"phase",next:"Inspect `crtr sys daemon status` and resolve any reported startup blocker before retrying."})}o(S,"awaitDaemonPhase");const E=u({name:"await",description:"wait for a daemon startup phase",whenToUse:"a caller started or was given a daemon that must reach prepared or ready before its next step.",help:{name:"sys daemon await",summary:"wait for the running daemon to report one startup phase",params:[{kind:"flag",name:"phase",type:"enum",choices:["prepared","ready"],required:!0,constraint:"The phase that permits the caller\u2019s next step."},{kind:"flag",name:"timeout",type:"string",required:!1,default:"5m",constraint:"Maximum wait as a positive duration such as 90s, 5m, or 1h30m."}],output:[{name:"startup_phase",type:"string",required:!0,constraint:"The requested phase."},{name:"waited_ms",type:"number",required:!0,constraint:"Elapsed wall-clock time before the phase was observed."}],outputKind:"object",effects:["Read-only: polls /healthz until the requested phase is observed or the caller-bounded timeout expires."]},run:o(async t=>{const e=t.timeout,a=g(e);if(a===null||a<=0)throw new d({error:"invalid_duration",message:`not a positive duration: ${e}`,received:e,field:"timeout",next:"Use a positive duration such as 90s, 5m, or 1h30m."});return S(t.phase,a)},"run")}),R=u({name:"admit",description:"admit normal work after daemon preparation",whenToUse:"the caller has completed its setup against a prepared daemon and is ready for normal daemon work to start.",help:{name:"sys daemon admit",summary:"ask a prepared daemon to start normal services",params:[],output:[{name:"admitted",type:"boolean",required:!0,constraint:"True only when this request began admission."},{name:"already_admitted",type:"boolean",required:!0,constraint:"True when admission had already begun or completed."},{name:"startup_phase",type:"string",required:!0,constraint:"The phase after admission settles."}],outputKind:"object",effects:["Starts normal daemon services when the daemon is prepared."]},run:o(async t=>await c({autostart:!1}).admitDaemon(),"run")});async function P(){try{return(await c({autostart:!1}).status()).generation_id??void 0}catch{return}}o(P,"servingGenerationId");const A=u({name:"status",description:"check whether the daemon is running",whenToUse:"operator/dev diagnostic \u2014 probing whether the crtrd supervisor process is alive while troubleshooting revive/supervision behavior",help:{name:"sys daemon status",summary:"check whether the crtrd supervisor daemon is currently running",params:[],output:[{name:"running",type:"boolean",required:!0,constraint:"True when the daemon pidfile owner is alive."},{name:"serving",type:"boolean",required:!1,constraint:"True when the live daemon answers /healthz; false when one probe does not."},{name:"pid",type:"number",required:!1,constraint:"PID of the running daemon."},{name:"probe_error",type:"string",required:!1,constraint:"The failed /healthz observation when the live owner did not answer."},{name:"startup_phase",type:"string",required:!1,constraint:"The observed daemon startup phase when the socket answered."},{name:"serving_generation",type:"string",required:!1,constraint:"Id of the runtime generation the daemon process was launched from. Absent when the daemon reports none (it predates the field, or runs from a development checkout) or its status could not be read."},{name:"selected_generation",type:"string",required:!1,constraint:"Id of the selected runtime generation. Absent when none is selected or it cannot be read."},{name:"generation_drift",type:"string",required:!1,constraint:"`none` when the serving and selected generation ids are equal; `differs` when they are not, meaning the daemon runs older bytes than the install selected until `sys daemon restart`; `unknown` when either id is absent. Never `none` on missing evidence."}],outputKind:"object",effects:["Read-only: reads the pidfile, probes the pid, and requests /healthz and /v1/status without autostart."]},run:o(async t=>{const e=l();if(!(e!==null&&m(e)))return{running:!1};try{const r=await p(),s=await P(),i=x().generation?.id;return{running:!0,serving:r.ok,pid:e,...r.startup_phase===void 0?{}:{startup_phase:r.startup_phase},...s===void 0?{}:{serving_generation:s},...i===void 0?{}:{selected_generation:i},generation_drift:s===void 0||i===void 0?"unknown":s===i?"none":"differs"}}catch(r){return{running:!0,serving:!1,pid:e,probe_error:b(r)}}},"run")}),j=u({name:"restart",description:"put the selected runtime generation in service via a daemon-owned handover",whenToUse:"adopting a newly installed runtime generation \u2014 the routine restart. Use this instead of stop+start: the daemon answers before it hands over, so a caller running inside a node (whose broker the handover tears down) still gets a settled result and is resumed automatically.",help:{name:"sys daemon restart",summary:"ask the running daemon to replace itself with a successor on the selected runtime generation",params:[],output:[{name:"restarted",type:"boolean",required:!0,constraint:"True when a running daemon accepted the handover. False when none was running \u2014 one was started instead."},{name:"started",type:"boolean",required:!1,constraint:"True when no daemon was running and this call started one (restarted=false)."},{name:"pid",type:"number",required:!1,constraint:"The daemon that accepted the handover, or the one just started."},{name:"grace_ms",type:"number",required:!1,constraint:"How long the daemon waits after answering before teardown begins."},{name:"note",type:"string",required:!1,constraint:"What happens next to the calling node. Read it before acting on an interrupted turn."}],outputKind:"object",effects:["The daemon acknowledges immediately, then after a short grace tears its whole broker fleet down, releases its claim, spawns a successor on the runtime generation currently selected, and exits.","Each torn-down broker hands its foreground bash commands to the file-backed background job system; they keep running and report completion by urgent inbox message.","Every node it tore down \u2014 including the caller \u2014 is resolved by the successor through the ordinary row-driven supervision tick.","The calling process is killed as part of that teardown, AFTER this result is returned."]},run:o(async t=>{if(!k()){const e=await h();return{restarted:!1,started:e.started,...e.pid!==void 0?{pid:e.pid}:{},...e.existing_pid!==void 0?{pid:e.existing_pid}:{},note:"No daemon was running, so there was nothing to hand over; one was started on the selected runtime generation."}}try{const e=await T().restartDaemon();return{restarted:e.accepted,pid:e.predecessor_pid,grace_ms:e.grace_ms,note:"Handover accepted. If you are running inside a node, your broker is about to be torn down and resumed on the new generation \u2014 THIS RESTART ALREADY SUCCEEDED. When your turn resumes with this result in the transcript, do not re-run it."}}catch(e){throw new d({error:"daemon_restart_failed",message:`the daemon did not accept a handover: ${e.message}`,next:"Run `crtr sys daemon status` to inspect the daemon."})}},"run")}),U=u({name:"stop",description:"stop the running daemon",whenToUse:"shutting the crtrd supervisor down for good, ending auto-revival of nodes on window exit. Not the way to pick up a new runtime generation \u2014 `restart` is, because stopping kills every broker of this epoch, including the caller's own, so this command cannot report its result to an agent that runs it.",help:{name:"sys daemon stop",summary:"stop the crtrd supervisor daemon and confirm it exited",params:[],output:[{name:"stopped",type:"boolean",required:!0,constraint:"True when a daemon (recorded or stray) was found and killed. False when none was running."},{name:"pid",type:"number",required:!1,constraint:"The recorded pidfile pid that exited (when one was recorded and alive)."},{name:"strays_killed",type:"array",required:!1,constraint:"PIDs of stray crtrd daemons (alive but not in the pidfile) swept alongside the recorded one \u2014 the backstop against a daemon that survived a prior stop."},{name:"force_killed",type:"boolean",required:!1,constraint:"True when the recorded daemon ignored SIGTERM and had to be SIGKILLed."}],outputKind:"object",effects:["Sends SIGTERM to the recorded pid and waits for it to exit and release its pidfile, escalating to SIGKILL if it does not.","Then sweeps any OTHER live crtrd daemon process (SIGTERM, escalating to SIGKILL after 2s) so a stray daemon orphaned from the pidfile cannot keep supervising."]},run:o(async t=>{const e=l(),a=e!==null&&m(e);try{let r=!1;a&&(r=await v(e)==="killed");const s=_(null);return!a&&s.length===0?{stopped:!1}:{stopped:!0,...a?{pid:e}:{},...s.length>0?{strays_killed:s}:{},...r?{force_killed:!0}:{}}}catch(r){throw new d({error:"daemon_stop_failed",message:`failed to stop daemon${a?` pid ${e}`:""}: ${r.message}`,next:"Run `crtr sys daemon status` to inspect the daemon before retrying."})}},"run")}),L=f({name:"daemon",description:"manage the crtrd supervisor process",whenToUse:"managing the crtrd supervisor that auto-revives nodes on window exit \u2014 start it, restart it onto a new runtime generation, check its status, or stop it",help:{name:"sys daemon",summary:"manage the crtrd canvas supervisor daemon",model:"crtrd is a thin background daemon that polls active+idle nodes and acts on window exit: crashed windows become dead; refresh-yield windows get a fresh respawn. It holds no orchestration logic \u2014 just process supervision. These are operator/dev self-management, not something a normal agent turn needs to check. Picking up a newly installed runtime generation is `restart` (a handover the daemon performs on itself); `stop` is the terminal one, and it kills the caller along with the rest of the fleet."},children:[I,E,R,j,A,U]});function W(){return q([L])}o(W,"registerDaemonControlSys");export{L as daemonBranch,W as registerDaemonControlSys};
1
+ var w=Object.defineProperty;var o=(t,e)=>w(t,"name",{value:e,configurable:!0});import{defineLeaf as u,defineBranch as f}from"../../core/command.js";import{InputError as d}from"../../core/io.js";import{parseDurationMs as g}from"../../core/wake.js";import{DaemonNotPreparedError as y,probeDaemonHealth as p,probeErrorMessage as b,spawnDaemon as h,stopDaemonProcess as v,sweepStrayDaemons as _}from"../../daemon/manage.js";import{isDaemonRunning as k,readPidfile as l,isPidAlive as m}from"../../daemon/pidfile.js";import{cliClient as T}from"../api-client.js";import{localClient as c}from"../../api/node-transport.js";import{defineSysBranch as q}from"./branch.js";import{selectedGeneration as x}from"./shared.js";const I=u({name:"start",description:"start the daemon in the background",whenToUse:"bringing the crtrd supervisor up for the first time in a session so node window-exits get handled (no-op only when its Unix API socket already serves)",help:{name:"sys daemon start",summary:"start the crtrd supervisor daemon in the background (no-op only if its Unix API socket serves)",params:[{kind:"flag",name:"prepare",type:"bool",required:!1,constraint:"Hold normal daemon work after storage preparation until `sys daemon admit`; omit for ordinary startup."},{kind:"flag",name:"force",type:"bool",required:!1,default:!1,constraint:"Start even while `crtr sys join` holds the daemon down. Joining then fails `daemon_running`; use only to clear a join that is stuck."}],output:[{name:"started",type:"boolean",required:!0,constraint:"True when a new daemon process was spawned; false when one was already running."},{name:"pid",type:"number",required:!1,constraint:"PID of the newly spawned daemon."},{name:"existing_pid",type:"number",required:!1,constraint:"PID of the already-running daemon (when started=false)."},{name:"running",type:"boolean",required:!1,constraint:"True only when a live pidfile owner does not serve its Unix API socket."},{name:"serving",type:"boolean",required:!1,constraint:"False only when a live pidfile owner does not answer /healthz."},{name:"probe_error",type:"string",required:!1,constraint:"The final /healthz probe failure when a live owner missed readiness."},{name:"startup_phase",type:"string",required:!1,constraint:"The observed daemon phase when it was reachable."}],outputKind:"object",effects:["Refuses `join_in_progress` while a live `crtr sys join` holds the daemon down, unless `--force`. `sys daemon restart` and `ensureDaemon` stand down the same way.","Spawns dist/daemon/crtrd-cli.js detached + unref'd; it outlives this process.","Writes a pidfile at CRTR_PIDFILE if set, otherwise crtrHome()/crtrd.pid."]},run:o(async t=>{try{return await h({targetPhase:t.prepare===!0?"prepared":"ready",ignoreJoinLock:t.force===!0})}catch(e){throw e instanceof y?new d({error:"daemon_not_prepared",message:e.message,next:"Stop the live daemon, then start a new one with `crtr sys daemon start --prepare` before setup."}):e}},"run")}),D=100;async function S(t,e){const a=Date.now(),r=a+e;let s="initializing",i="the daemon socket was not reachable";for(;Date.now()<r;){try{const n=await p(Math.min(1e3,Math.max(1,r-Date.now())));if(s=n.startup_phase,i=n.startup_blocked??`daemon is ${n.startup_phase}`,n.startup_phase==="blocked")throw new d({error:"startup_blocked",message:n.startup_blocked??"daemon startup is blocked",received:n.startup_phase,field:"phase",next:"Repair the reported blocker paths with `crtr sys migrate`, then retry the await."});if(n.startup_phase===t)return{startup_phase:t,waited_ms:Date.now()-a};if(t==="prepared"&&n.startup_phase==="ready")throw new d({error:"await_missed_phase",message:"daemon is already ready and cannot return to prepared in this process",received:n.startup_phase,field:"phase",next:"Start a new daemon with `crtr sys daemon start --prepare` before awaiting prepared."})}catch(n){if(n instanceof d)throw n;i=n instanceof Error?n.message:String(n)}await new Promise(n=>setTimeout(n,Math.min(D,Math.max(0,r-Date.now()))))}throw new d({error:"await_timeout",message:`daemon did not reach ${t} within ${e}ms (last phase: ${s}; last observation: ${i})`,received:s,field:"phase",next:"Inspect `crtr sys daemon status` and resolve any reported startup blocker before retrying."})}o(S,"awaitDaemonPhase");const E=u({name:"await",description:"wait for a daemon startup phase",whenToUse:"a caller started or was given a daemon that must reach prepared or ready before its next step.",help:{name:"sys daemon await",summary:"wait for the running daemon to report one startup phase",params:[{kind:"flag",name:"phase",type:"enum",choices:["prepared","ready"],required:!0,constraint:"The phase that permits the caller\u2019s next step."},{kind:"flag",name:"timeout",type:"string",required:!1,default:"5m",constraint:"Maximum wait as a positive duration such as 90s, 5m, or 1h30m."}],output:[{name:"startup_phase",type:"string",required:!0,constraint:"The requested phase."},{name:"waited_ms",type:"number",required:!0,constraint:"Elapsed wall-clock time before the phase was observed."}],outputKind:"object",effects:["Read-only: polls /healthz until the requested phase is observed or the caller-bounded timeout expires."]},run:o(async t=>{const e=t.timeout,a=g(e);if(a===null||a<=0)throw new d({error:"invalid_duration",message:`not a positive duration: ${e}`,received:e,field:"timeout",next:"Use a positive duration such as 90s, 5m, or 1h30m."});return S(t.phase,a)},"run")}),R=u({name:"admit",description:"admit normal work after daemon preparation",whenToUse:"the caller has completed its setup against a prepared daemon and is ready for normal daemon work to start.",help:{name:"sys daemon admit",summary:"ask a prepared daemon to start normal services",params:[],output:[{name:"admitted",type:"boolean",required:!0,constraint:"True only when this request began admission."},{name:"already_admitted",type:"boolean",required:!0,constraint:"True when admission had already begun or completed."},{name:"startup_phase",type:"string",required:!0,constraint:"The phase after admission settles."}],outputKind:"object",effects:["Starts normal daemon services when the daemon is prepared."]},run:o(async t=>await c({autostart:!1}).admitDaemon(),"run")});async function P(){try{return(await c({autostart:!1}).status()).generation_id??void 0}catch{return}}o(P,"servingGenerationId");const A=u({name:"status",description:"check whether the daemon is running",whenToUse:"operator/dev diagnostic \u2014 probing whether the crtrd supervisor process is alive while troubleshooting revive/supervision behavior",help:{name:"sys daemon status",summary:"check whether the crtrd supervisor daemon is currently running",params:[],output:[{name:"running",type:"boolean",required:!0,constraint:"True when the daemon pidfile owner is alive."},{name:"serving",type:"boolean",required:!1,constraint:"True when the live daemon answers /healthz; false when one probe does not."},{name:"pid",type:"number",required:!1,constraint:"PID of the running daemon."},{name:"probe_error",type:"string",required:!1,constraint:"The failed /healthz observation when the live owner did not answer."},{name:"startup_phase",type:"string",required:!1,constraint:"The observed daemon startup phase when the socket answered."},{name:"serving_generation",type:"string",required:!1,constraint:"Id of the runtime generation the daemon process was launched from. Absent when the daemon reports none (it predates the field, or runs from a development checkout) or its status could not be read."},{name:"selected_generation",type:"string",required:!1,constraint:"Id of the selected runtime generation. Absent when none is selected or it cannot be read."},{name:"generation_drift",type:"string",required:!1,constraint:"`none` when the serving and selected generation ids are equal; `differs` when they are not, meaning the daemon runs older bytes than the install selected until `sys daemon restart`; `unknown` when either id is absent. Never `none` on missing evidence."}],outputKind:"object",effects:["Read-only: reads the pidfile, probes the pid, and requests /healthz and /v1/status without autostart."]},run:o(async t=>{const e=l();if(!(e!==null&&m(e)))return{running:!1};try{const r=await p(),s=await P(),i=x().generation?.id;return{running:!0,serving:r.ok,pid:e,...r.startup_phase===void 0?{}:{startup_phase:r.startup_phase},...s===void 0?{}:{serving_generation:s},...i===void 0?{}:{selected_generation:i},generation_drift:s===void 0||i===void 0?"unknown":s===i?"none":"differs"}}catch(r){return{running:!0,serving:!1,pid:e,probe_error:b(r)}}},"run")}),j=u({name:"restart",description:"put the selected runtime generation in service via a daemon-owned handover",whenToUse:"adopting a newly installed runtime generation \u2014 the routine restart. Use this instead of stop+start: the daemon answers before it hands over, so a caller running inside a node (whose broker the handover tears down) still gets a settled result and is resumed automatically.",help:{name:"sys daemon restart",summary:"ask the running daemon to replace itself with a successor on the selected runtime generation",params:[],output:[{name:"restarted",type:"boolean",required:!0,constraint:"True when a running daemon accepted the handover. False when none was running \u2014 one was started instead."},{name:"started",type:"boolean",required:!1,constraint:"True when no daemon was running and this call started one (restarted=false)."},{name:"pid",type:"number",required:!1,constraint:"The daemon that accepted the handover, or the one just started."},{name:"grace_ms",type:"number",required:!1,constraint:"How long the daemon waits after answering before teardown begins."},{name:"note",type:"string",required:!1,constraint:"What happens next to the calling node. Read it before acting on an interrupted turn."}],outputKind:"object",effects:["The daemon acknowledges immediately, then after a short grace tears its whole broker fleet down, releases its claim, spawns a successor on the runtime generation currently selected, and exits.","Each torn-down broker hands its foreground bash commands to the file-backed background job system; they keep running and report completion by urgent inbox message.","Every node it tore down \u2014 including the caller \u2014 is resolved by the successor through the ordinary row-driven supervision tick.","The calling process is killed as part of that teardown, AFTER this result is returned."]},run:o(async t=>{if(!k()){const e=await h();return{restarted:!1,started:e.started,...e.pid!==void 0?{pid:e.pid}:{},...e.existing_pid!==void 0?{pid:e.existing_pid}:{},note:"No daemon was running, so there was nothing to hand over; one was started on the selected runtime generation."}}try{const e=await T({local:!0}).restartDaemon();return{restarted:e.accepted,pid:e.predecessor_pid,grace_ms:e.grace_ms,note:"Handover accepted. If you are running inside a node, your broker is about to be torn down and resumed on the new generation \u2014 THIS RESTART ALREADY SUCCEEDED. When your turn resumes with this result in the transcript, do not re-run it."}}catch(e){throw new d({error:"daemon_restart_failed",message:`the daemon did not accept a handover: ${e.message}`,next:"Run `crtr sys daemon status` to inspect the daemon."})}},"run")}),U=u({name:"stop",description:"stop the running daemon",whenToUse:"shutting the crtrd supervisor down for good, ending auto-revival of nodes on window exit. Not the way to pick up a new runtime generation \u2014 `restart` is, because stopping kills every broker of this epoch, including the caller's own, so this command cannot report its result to an agent that runs it.",help:{name:"sys daemon stop",summary:"stop the crtrd supervisor daemon and confirm it exited",params:[],output:[{name:"stopped",type:"boolean",required:!0,constraint:"True when a daemon (recorded or stray) was found and killed. False when none was running."},{name:"pid",type:"number",required:!1,constraint:"The recorded pidfile pid that exited (when one was recorded and alive)."},{name:"strays_killed",type:"array",required:!1,constraint:"PIDs of stray crtrd daemons (alive but not in the pidfile) swept alongside the recorded one \u2014 the backstop against a daemon that survived a prior stop."},{name:"force_killed",type:"boolean",required:!1,constraint:"True when the recorded daemon ignored SIGTERM and had to be SIGKILLed."}],outputKind:"object",effects:["Sends SIGTERM to the recorded pid and waits for it to exit and release its pidfile, escalating to SIGKILL if it does not.","Then sweeps any OTHER live crtrd daemon process (SIGTERM, escalating to SIGKILL after 2s) so a stray daemon orphaned from the pidfile cannot keep supervising."]},run:o(async t=>{const e=l(),a=e!==null&&m(e);try{let r=!1;a&&(r=await v(e)==="killed");const s=_(null);return!a&&s.length===0?{stopped:!1}:{stopped:!0,...a?{pid:e}:{},...s.length>0?{strays_killed:s}:{},...r?{force_killed:!0}:{}}}catch(r){throw new d({error:"daemon_stop_failed",message:`failed to stop daemon${a?` pid ${e}`:""}: ${r.message}`,next:"Run `crtr sys daemon status` to inspect the daemon before retrying."})}},"run")}),L=f({name:"daemon",description:"manage the crtrd supervisor process",whenToUse:"managing the crtrd supervisor that auto-revives nodes on window exit \u2014 start it, restart it onto a new runtime generation, check its status, or stop it",help:{name:"sys daemon",summary:"manage the crtrd canvas supervisor daemon",model:"crtrd is a thin background daemon that polls active+idle nodes and acts on window exit: crashed windows become dead; refresh-yield windows get a fresh respawn. It holds no orchestration logic \u2014 just process supervision. These are operator/dev self-management, not something a normal agent turn needs to check. Picking up a newly installed runtime generation is `restart` (a handover the daemon performs on itself); `stop` is the terminal one, and it kills the caller along with the rest of the fleet."},children:[I,E,R,j,A,U]});function W(){return q([L])}o(W,"registerDaemonControlSys");export{L as daemonBranch,W as registerDaemonControlSys};
@@ -0,0 +1,5 @@
1
+ export declare const DESKTOP_DMG_URL = "https://downloads.northlight.ai/desktop/mac/Northlight-Desktop.dmg";
2
+ /** Northlight Desktop's Developer ID team (Vallum Security Inc.). */
3
+ export declare const DESKTOP_TEAM_ID = "BRL7SHLNLU";
4
+ export declare const sysDesktopInstallLeaf: import("../../core/command.js").LeafDef;
5
+ export declare const sysDesktopBranch: import("../../core/command.js").BranchDef;
@@ -0,0 +1 @@
1
+ var x=Object.defineProperty;var n=(t,e)=>x(t,"name",{value:e,configurable:!0});import{execFile as T}from"node:child_process";import{existsSync as f,mkdtempSync as $,readFileSync as g,rmSync as I}from"node:fs";import{homedir as O,tmpdir as P}from"node:os";import{join as d}from"node:path";import{promisify as L}from"node:util";import{defineBranch as M,defineLeaf as U}from"../../core/command.js";import{CrtrError as j,general as p,usage as C}from"../../core/errors.js";import{atomicWriteJson as E}from"../../core/fs-utils.js";import{northlightUnreachable as w}from"../../core/directory/reach.js";import{notSignedIn as y,readPersonCredential as k,personAccount as F}from"../../core/directory/person-credential.js";import{readMachineConfig as R}from"../../daemon/machine-link/config.js";import{ExitCode as B}from"../../types.js";const D=L(T),S="https://downloads.northlight.ai/desktop/mac/Northlight-Desktop.dmg",_="BRL7SHLNLU",N="Northlight Desktop.app",J="/Applications",m=d(O(),"Library","Application Support","Northlight Desktop"),A=3e4,K=12e4,W=/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,b=n(t=>new Promise(e=>setTimeout(e,t)),"sleep"),h=n(()=>d(J,N),"appPath"),l=n(async(t,e)=>(await D(t,e,{maxBuffer:16*1024*1024})).stdout,"sh");async function q(){try{return await l("pgrep",["-f",`${h()}/Contents/MacOS/`]),!0}catch{return!1}}n(q,"desktopRunning");function v(t){try{return JSON.parse(g(d(m,"machine.json"),"utf8"))?.owner===t}catch{return!1}}n(v,"desktopPaired");async function G(t){const e=d(t,"Northlight-Desktop.dmg");try{await l("curl",["-fsSL","--max-time","600","-o",e,S])}catch(o){const a=String(o.stderr??"");throw/Could not resolve|Failed to connect|timed out|SSL|Connection|resolve host/i.test(a)?w(o,S):p(`the Desktop download failed: ${a.trim()||o.message}`)}return e}n(G,"download");async function H(t,e){await l("hdiutil",["attach","-nobrowse","-readonly","-mountpoint",e,t]);try{const o=d(e,N);try{await l("codesign",["--verify","--deep","--strict",o])}catch(i){throw p(`the downloaded Desktop failed its signature check, so it was not installed: ${String(i.stderr??i.message).trim()}`)}const a=(await D("codesign",["-dv","--verbose=4",o])).stderr,s=/^TeamIdentifier=(\S+)$/m.exec(a)?.[1];if(s!==_)throw p(`the downloaded Desktop is signed by team ${s??"none"}, not Northlight's ${_}, so it was not installed`);await l("ditto",[o,h()])}finally{await l("hdiutil",["detach",e,"-quiet"]).catch(()=>{})}}n(H,"installFrom");async function z(){const t=d(m,"install.json");for(const e=Date.now()+A;Date.now()<e;await b(500))try{const o=JSON.parse(g(t,"utf8")).install_id;if(typeof o=="string"&&W.test(o))return o}catch{}throw p(`Desktop opened but did not write ${t} within ${A/1e3} seconds`,{next:"Open Northlight Desktop yourself, then run `crtr sys desktop install` again."})}n(z,"waitForInstallId");async function V(t,e){const o=k(),a=`${t.replace(/\/+$/,"")}/v1/developer/desktop-enrollment`;let s;try{s=await fetch(a,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${o.secret}`},body:JSON.stringify({install_id:e}),signal:AbortSignal.timeout(3e4)})}catch(c){throw w(c,a)}const i=await s.text();let r={};try{r=i===""?{}:JSON.parse(i)}catch{}if(s.status===401){const c=typeof r.error=="object"?r.error?.code:r.error;if(c==="credential_revoked"||c==="credential_expired"||c==="credential_unknown")throw y(`the sign-in on this computer is no longer valid (${String(c)})`)}if(!s.ok||typeof r.device_code!="string"||typeof r.expires_in!="number"){const c=typeof r.error=="object"&&typeof r.error?.message=="string"?r.error.message:`HTTP ${s.status}`;throw p(`Northlight refused to pair Desktop: ${c}`)}return{device_code:r.device_code,expires_in:r.expires_in}}n(V,"mintPairing");const u=n(()=>l("open",["-a",h()]),"openApp"),Q=U({name:"install",description:"download, install, open and pair Northlight Desktop to this account",whenToUse:"a `device` command answered that no Northlight app is paired to this account, and the person said yes to installing it",help:{name:"sys desktop install",summary:"install Northlight Desktop in Applications and pair it to the account crtr is signed in to, with no code and no Allow click",guide:"macOS only, and only on a computer signed in with `crtr sys signin` and moved to Northlight. Downloads Desktop from downloads.northlight.ai, checks it is signed by Northlight, installs it, opens it, pairs it, then waits up to two minutes until Desktop on this Mac has registered to the account. Safe to run again: a Desktop on this Mac that is already registered to the account is only opened. macOS permission prompts (Full Disk Access, Messages) still appear when a feature needs them.",params:[],output:[{name:"installed",type:"boolean",required:!0,constraint:"True when this run installed Desktop; false when it was already installed and paired."}],outputKind:"object",effects:["Downloads and mounts the Desktop image, then copies the app to `/Applications/Northlight Desktop.app` (a copy that is running is left as it is and only paired). Opens Desktop. Calls `POST /v1/developer/desktop-enrollment` with the stored sign-in and writes `~/Library/Application Support/Northlight Desktop/pairing.json` (mode 0600), which Desktop deletes when it reads it."]},run:n(async()=>{if(process.platform!=="darwin")throw C("Northlight Desktop is a Mac app: this command only runs on macOS");const t=k();if(t===void 0||R()===void 0)throw y("this computer is not signed in to Northlight");const{id:e}=await F(t.directoryUrl);if(f(h())&&v(e))return await u(),{installed:!1};if(!(f(h())&&await q())){const i=$(d(P(),"crtr-desktop-"));try{await H(await G(i),d(i,"mnt"))}finally{I(i,{recursive:!0,force:!0})}}await u();const a=await z(),s=await V(t.directoryUrl,a);E(d(m,"pairing.json"),{issuer:t.directoryUrl.replace(/\/+$/,""),device_code:s.device_code},{mode:384}),await u();for(const i=Date.now()+K;Date.now()<i;await b(3e3))if(v(e))return{installed:!0};throw new j("desktop_not_paired","Desktop opened but did not pair.",B.GENERAL,{next:"If Desktop is signed in to another account, sign out there and run `crtr sys desktop install` again."})},"run"),render:n(t=>`<desktop installed="${t.installed}"/>`,"render")}),lt=M({name:"desktop",description:"Northlight Desktop on this Mac",whenToUse:"Northlight Desktop must be installed and paired to this account",help:{name:"sys desktop",summary:"install and pair Northlight Desktop"},children:[Q]});export{S as DESKTOP_DMG_URL,_ as DESKTOP_TEAM_ID,lt as sysDesktopBranch,Q as sysDesktopInstallLeaf};
@@ -0,0 +1,55 @@
1
+ import { ImportCloud } from '../../../core/import/export/cloud.js';
2
+ import { type MoveRecord } from '../../../daemon/machine-link/config.js';
3
+ import { type SignInPrompt } from '../../../core/directory/person-credential.js';
4
+ /** The directory a sign-in or move uses when none is typed: `runtime/machine.json`'s, else the stored sign-in's, else Northlight's (design D5). */
5
+ export declare function defaultDirectoryUrl(): string;
6
+ /** End the daemon's drain and confirm the daemon answers `drained: false`, retrying a lost answer: the daemon persists its drain before it waits for
7
+ * busy nodes, so a request that timed out or was lost may have left one on. Throws `drain_not_ended` when it cannot be confirmed ended.
8
+ * The daemon refuses to end a drain while a move record exists (the cloud outcome may be unknown), so every caller here has a positive answer that
9
+ * the run failed or never loaded: the record is settled (deleted) first, and no other path ends a computer's drain. */
10
+ export declare function endDrainConfirmed(client: {
11
+ endDaemonDrain(): Promise<unknown>;
12
+ }): Promise<void>;
13
+ /** The drain's end once the exporter stops: a run that never reached `done` ends it, confirmed. A run that reached `done` leaves it on — a hosted
14
+ * runtime for hosting's quiesce, a computer because its records now live in the account and local agents must not carry on against the local database. */
15
+ export declare function settleDrainAtEnd(done: boolean, client: {
16
+ endDaemonDrain(): Promise<unknown>;
17
+ }): Promise<void>;
18
+ export interface ExportStep {
19
+ name: string;
20
+ timings: string[];
21
+ }
22
+ /** One export: open the run (or use hosting's), upload blobs, drain, dump, upload rows and stores, load, wait for `done`. `step.name` is the step in
23
+ * progress, so a failure can name it. The drain ends here when the run did not reach `done`, and stays on when it did. */
24
+ export declare function exportRun(kind: 'machine' | 'hosted', cloud: ImportCloud, existingRun: string | undefined, step: ExportStep, onStarted?: (runId: string) => void, onFailed?: () => void): Promise<string>;
25
+ /** Activation, the second commit boundary: the asking node's message first (stored in the account as mail; the node can wake only once the machine
26
+ * link is up), then the machine credential (re-minted with `rotate` when an earlier attempt already minted one), `machine.json`, a restart of
27
+ * crtrd in machine role, and the move record's deletion. Safe to repeat. */
28
+ export declare function finishMove(move: MoveRecord): Promise<void>;
29
+ /** Move this computer into the signed-in person's account. Before the cloud run reaches `done`, any failure undoes the move: the drain ends, the
30
+ * sign-in is revoked and deleted, the move record goes, and `notify` (a node) is told through the local daemon. After `done` the move record says
31
+ * `committed` and activation is retried three times over a minute; still failing, the record, the sign-in and the drain stay for `crtr sys signin`. */
32
+ export declare function runMove(opts: {
33
+ directoryUrl: string;
34
+ apiUrl?: string;
35
+ notify: string | null;
36
+ }): Promise<{
37
+ runId: string;
38
+ account: string;
39
+ timings: string;
40
+ }>;
41
+ /** Where an earlier move stands before a new sign-in or move starts, and when the local daemon starts. A `running` record whose exporter is alive
42
+ * refuses; one whose exporter died is settled against the control plane, and only its positive answer moves it: a run that reached `done` becomes
43
+ * `committed` (returned, so the caller finishes the switch-over); a run still loading there refuses until it settles; a run that failed or never
44
+ * loaded is a failed move, undone here (record deleted and drain ended and confirmed, sign-in revoked, the asking node told). Any other outcome —
45
+ * the control plane unreachable or refusing, no working sign-in — leaves the record and the drain as they are and throws. With no working sign-in
46
+ * `show` signs back in first, and only to the account that owns the move (another account is refused, nothing written); without `show` (the daemon) it throws `not_signed_in`.
47
+ * Answers the committed record, or undefined when no move is left (an `activated` record only awaits its notification, which the machine daemon sends). */
48
+ export declare function settleEarlierMove(show?: (prompt: SignInPrompt & {
49
+ complete: string;
50
+ }) => void): Promise<MoveRecord | undefined>;
51
+ /** The local daemon's start is a starter of the move record (design-crtr-northlight-signin component 4): a `running` record whose exporter died (a crash,
52
+ * a reboot) leaves the daemon drained and the asking node untold until someone runs `crtr sys signin`, so the daemon settles it itself once it serves.
53
+ * Retries every minute while the settle cannot finish (the control plane unreachable, the run still loading); a run that reached `done` stays `committed`
54
+ * for `crtr sys signin`. Returns when there is nothing to settle, the exporter is alive, or `stopped()` says the daemon is closing. */
55
+ export declare function settleMoveAtDaemonStart(stopped: () => boolean, onRetry: (error: unknown) => void): Promise<void>;
@@ -0,0 +1,8 @@
1
+ var Y=Object.defineProperty;var l=(e,t)=>Y(e,"name",{value:t,configurable:!0});import{mkdtempSync as q,readFileSync as U,readdirSync as R,rmSync as K}from"node:fs";import{tmpdir as X}from"node:os";import{join as p}from"node:path";import{CrtrError as s,general as Q}from"../../../core/errors.js";import{ExitCode as h}from"../../../types.js";import{cliClient as T}from"../../api-client.js";import{ImportCloud as x,STORE_FILE as Z,STORE_ORDER as tt}from"../../../core/import/export/cloud.js";import{Uploader as et,localObjects as F,reviewSourceObject as rt}from"../../../core/import/export/files.js";import{bundleKeys as $}from"../../../core/import/bundle/layout.js";import{crtrVersion as nt}from"../../../shared/crtr-version.js";import{nodeArchive as ot}from"../../../core/import/export/archive.js";import{buildSourceJson as it}from"../../../core/import/export/source.js";import{runDaemonBodiesDump as at,runDaemonImportDump as st,runDaemonInstallId as ct,spawnDaemon as dt,stopAllDaemons as lt}from"../../../daemon/manage.js";import{deleteMoveRecord as N,moveUnfinishedError as ut,readMachineConfig as ft,readMoveRecord as A,writeMachineConfig as mt,writeMachineCredential as wt,writeMoveRecord as M}from"../../../daemon/machine-link/config.js";import{notifyMoveFinished as pt}from"../../../daemon/machine-link/move-notify.js";import{controlPlaneUrl as ht,forgetSignIn as gt,personAccount as G,personBearer as D,readPersonCredential as yt,signIn as _t,signOut as Et}from"../../../core/directory/person-credential.js";import{NORTHLIGHT_DIRECTORY_URL as vt}from"../../../core/directory/issuer.js";import{isPidAlive as L}from"../../../core/canvas/pid.js";const g=l(e=>new Promise(t=>setTimeout(t,e)),"sleep");function Kt(){return ft()?.directoryUrl??yt()?.directoryUrl??vt}l(Kt,"defaultDirectoryUrl");async function B(e){let t="the daemon still reports a drain";for(let r=1;r<=3;r++){try{if((await e.endDaemonDrain()).drained===!1)return}catch(n){t=n instanceof Error?n.message:String(n)}r<3&&await g(1e3)}throw new s("drain_not_ended",`the local daemon's pause could not be confirmed ended: ${t}`,h.GENERAL,{next:"Run `crtr sys import resume`."})}l(B,"endDrainConfirmed");async function $t(e,t){e||await B(t)}l($t,"settleDrainAtEnd");const St=3e4;async function bt(e,t,r){let n=2e3;for(;;){let i;try{i=await e.get(t)}catch(c){const u=c instanceof s?c.details?.status:void 0;if(c instanceof s&&(c.code==="not_signed_in"||typeof u=="number"&&u<500&&u!==408&&u!==429))throw new s("move_outcome_unknown",`could not confirm whether Northlight finished loading this computer's records: ${c.message}`,h.GENERAL,{next:"Run `crtr sys signin` to settle the move."});process.stderr.write(`waiting for Northlight: ${c instanceof Error?c.message:String(c)}
2
+ `),await g(n),n=Math.min(n*2,St);continue}if(n=2e3,i.state==="done")return;if(i.state==="failed"||i.state==="rolled_back")throw Q(`the import failed at ${i.failure?.step??i.step??"an unknown step"}: ${i.failure?.message??i.state}`,{code:i.failure?.code??"import_failed"});if(i.state==="uploading"&&r!==void 0)throw r;await g(2e3)}}l(bt,"awaitRunOutcome");async function Ut(e,t,r,n,i=()=>{},c=()=>{}){let u=Date.now();const o=l((m,f)=>{const w=Date.now(),v=`${m}=${((w-u)/1e3).toFixed(1)}s`;n.timings.push(v),process.stderr.write(`${m} ${((w-u)/1e3).toFixed(1)}s
3
+ `),u=w,n.name=f},"next"),d=q(p(X(),"crtr-import-")),S=T({local:!0});let y=!1,I=!1,_=!1,E;try{n.name="start";const{installId:m}=await ct(),f=r===void 0?await t.start({source_install_id:m,source_kind:e,release:nt()}):{run_id:r};i(f.run_id),n.name="blobs";const w=new et(t,f.run_id,a=>process.stderr.write(`${a}
4
+ `));await w.upload(F()),await at(d);for(const a of R(p(d,"rows","bodies")))await w.uploadBytes($.bodyRows(f.run_id,a.replace(/\.jsonl\.zst$/,"")),U(p(d,"rows","bodies",a)));o("blobs","drain"),y=!0,await S.drainDaemon({settle_limit_ms:6e5}),o("drain","dump"),await w.upload(F());const v=await st(d,e),k=[];for(const a of v.requests){const b=rt(a.id,a.dir,a.file);b!==null&&k.push(b)}await w.upload(k),o("dump","upload");for(const a of R(p(d,"rows"),{withFileTypes:!0}))a.isFile()&&await w.uploadBytes($.rows(f.run_id,a.name.replace(/\.jsonl\.zst$/,"")),U(p(d,"rows",a.name)));const V=new Set(R(p(d,"rows","bodies")).filter(a=>a.startsWith("pre-")));for(const a of R(p(d,"rows","bodies")))V.has(a)||await w.uploadBytes($.bodyRows(f.run_id,a.replace(/\.jsonl\.zst$/,"")),U(p(d,"rows","bodies",a)));const O=[];if(e==="hosted")for(const a of v.runningNodes){const{data:b,prefixes:W}=await ot(a);O.push(...W),await w.uploadBytes($.nodeFiles(f.run_id,a.node),b)}await w.uploadBytes($.source(f.run_id),Buffer.from(JSON.stringify(it(v.source,O))));const H=JSON.parse(U(p(d,Z),"utf8"));for(const a of tt)await t.putStore(f.run_id,a,H[a]);o("upload","load"),_=!0;let C;try{await t.load(f.run_id)}catch(a){C=a}try{await bt(t,f.run_id,C)}catch(a){throw a instanceof s&&a.code==="move_outcome_unknown"||(_=!1),a}return _=!1,I=!0,o("load","restart"),f.run_id}catch(m){throw E=m,m}finally{if(K(d,{recursive:!0,force:!0}),!I&&!_&&c(),y&&!_)try{await $t(I,S)}catch(m){if(E===void 0)throw m;const f=m instanceof s?m.code:"drain_not_ended";throw new s(f,`${E instanceof Error?E.message:String(E)} (and ${m instanceof Error?m.message:String(m)})`,h.GENERAL,{next:"Run `crtr sys import resume`."})}}}l(Ut,"exportRun");async function Rt(e){await pt(e);const t=new x(e.apiUrl,()=>D(e.directoryUrl));let r;try{r=await t.machineCredential(e.runId)}catch(n){if(!(n instanceof s)||n.details?.code!=="credential_exists")throw n;r=await t.machineCredential(e.runId,!0)}wt(r.credential),mt({apiUrl:e.apiUrl,environmentId:r.environment_id,runId:e.runId,directoryUrl:e.directoryUrl,account:e.account}),await lt(),await dt(),N()}l(Rt,"finishMove");const P=3,Nt=3e4;async function Xt(e){const t={name:"sign-in",timings:[]};let r,n="",i;try{const o=await G(e.directoryUrl),d=e.apiUrl??await ht(e.directoryUrl),S=new x(d,()=>D(e.directoryUrl));i={apiUrl:d,directoryUrl:e.directoryUrl,account:o,notify:e.notify,notified:!1,pid:process.pid},await Ut("machine",S,void 0,t,y=>{n=y,M({...i,runId:y,state:"running"})},N)}catch(o){throw o instanceof s&&o.code==="move_outcome_unknown"?o:await j(t.name,o,e.notify)}r={...i,runId:n,state:"committed"},M(r);let c;const u=Date.now();for(let o=1;o<=P;o++)try{return await Rt(r),t.timings.push(`restart=${((Date.now()-u)/1e3).toFixed(1)}s`),process.stderr.write(`restart ${((Date.now()-u)/1e3).toFixed(1)}s
5
+ `),{runId:r.runId,account:r.account.display,timings:t.timings.join(" ")}}catch(d){c=d,process.stderr.write(`switch-over attempt ${o} failed: ${d instanceof Error?d.message:String(d)}
6
+ `),o<P&&await g(Nt),r=A()??r}throw ut(c instanceof Error?c.message:String(c))}l(Xt,"runMove");async function j(e,t,r){const n=t instanceof Error?t.message:String(t),i=t instanceof s&&t.code==="drain_not_ended"?"crtr is still local, but its pause could not be ended: run `crtr sys import resume`":"crtr is still local and working as before",c=`The move to Northlight failed at ${e}: ${n}. ${i}.`,u=await J(c,r),o=t instanceof s&&t.code!=="error"?t.code:t instanceof s&&typeof t.details?.code=="string"?t.details.code:"move_failed";return new s(o,`the move to Northlight failed at ${e}: ${n}. ${i}, and ${z(u)}`,h.GENERAL)}l(j,"undoMove");const z=l(e=>e===void 0?"this computer is signed out.":`this computer's sign-in was removed here but could not be revoked at Northlight (${e}); revoke it on your Northlight account page.`,"signedOutText");async function J(e,t){N();let r;try{await Et()}catch(n){gt(),r=n instanceof Error?n.message:String(n),process.stderr.write(`could not revoke the sign-in at Northlight: ${r}
7
+ `)}if(t===null)return r;try{await T({local:!0}).sendMessage(t,{from:null,body:`${e} Tell the person the move failed.${r===void 0?"":` ${z(r)}`}`})}catch(n){process.stderr.write(`could not message ${t}: ${n instanceof Error?n.message:String(n)}
8
+ `)}return r}l(J,"dropMove");async function It(e){const t=A();if(t===void 0||t.state==="activated")return;const r=l(async()=>{await _t(t.directoryUrl,e,{accept:l(async o=>o.account.id===t.account.id?void 0:new s("signin_other_account",`This computer's move belongs to the Northlight account ${t.account.display}. Sign in to that account.`,h.USAGE),"accept")})},"signBackIn");if(t.state==="committed"){if(e!==void 0)try{await G(t.directoryUrl)}catch(o){if(!(o instanceof s&&o.code==="not_signed_in"))throw o;await r()}return t}if(t.pid>0&&L(t.pid))throw new s("move_running",`a move to Northlight is already running (process ${t.pid}).`,h.USAGE,{next:"Wait for it to finish; the node that asked is messaged when it does."});const n=l(()=>new x(t.apiUrl,()=>D(t.directoryUrl)).get(t.runId),"ask");let i;try{i=await n()}catch(o){if(!(o instanceof s&&o.code==="not_signed_in")||e===void 0)throw o;await r(),i=await n()}if(i.state==="done"){const o={...t,state:"committed"};return M(o),o}if(i.state==="loading"||i.state==="committed")throw new s("move_running","Northlight is still loading this computer's records from an earlier move.",h.USAGE,{next:"Retry in a minute."});if(i.state!=="failed"&&i.state!=="rolled_back"&&i.state!=="uploading")throw new s("move_outcome_unknown","Northlight did not report a settled outcome for this move.",h.GENERAL,{next:"Run `crtr sys signin` to settle the move."});const c=i.failure?.step??"an earlier step",u=i.failure==null?"the process that was moving this computer stopped before Northlight had the records":i.failure.message;N();try{await B(T({local:!0}))}catch(o){throw await j(c,new s("drain_not_ended",`${u} (and ${o instanceof Error?o.message:String(o)})`,h.GENERAL),t.notify)}await J(`The move to Northlight failed at ${c}: ${u}. crtr is still local and working as before.`,t.notify)}l(It,"settleEarlierMove");const Tt=6e4,xt=5e3;async function Qt(e,t){for(await g(xt);!e();){const r=A();if(r===void 0||r.state!=="running"||r.pid>0&&L(r.pid))return;try{await It();return}catch(n){if(t(n),n instanceof s&&n.code==="not_signed_in")return}await g(Tt)}}l(Qt,"settleMoveAtDaemonStart");export{Kt as defaultDirectoryUrl,B as endDrainConfirmed,Ut as exportRun,Rt as finishMove,Xt as runMove,$t as settleDrainAtEnd,It as settleEarlierMove,Qt as settleMoveAtDaemonStart};
@@ -0,0 +1 @@
1
+ export declare const resume: import("../../../core/command.js").LeafDef;
@@ -0,0 +1 @@
1
+ var n=Object.defineProperty;var t=(e,a)=>n(e,"name",{value:a,configurable:!0});import{defineLeaf as r}from"../../../core/command.js";import{cliClient as s}from"../../api-client.js";const m=r({name:"resume",description:"end the pause an import left on this daemon",whenToUse:"an import failed or its exporter was killed, so this runtime stayed paused and should carry on",help:{name:"sys import resume",summary:"end the drain so the daemon takes wakes, cron fires and deliveries again",guide:"Safe to repeat: when the daemon is not drained it says so and exits 0. Refused (409 `conflict`) while a move record exists: the account may hold this computer's records, so agents here must not carry on against the local database until `crtr sys signin` settles the move.",params:[],output:[{name:"was_drained",type:"boolean",required:!0,constraint:"Whether the daemon was drained before this call."}],outputKind:"object",effects:["Deletes the `drain` row of `runtime_state`; nothing else changes. Refused while `runtime/import/move.json` exists."]},run:t(async()=>({was_drained:(await s({local:!0}).endDaemonDrain()).was_drained===!0}),"run"),render:t(e=>e.was_drained===!0?'<import drain="ended"/>':'<import drain="none">the daemon was not drained</import>',"render")});export{m as resume};
@@ -0,0 +1,4 @@
1
+ /** Set by `crtr sys signin` on the detached exporter it starts from inside a node: the signin process, which the exporter outlives before it drains,
2
+ * so it is never in a broker's process tree. */
3
+ export declare const WAIT_FOR_PID_ENV = "CRTR_IMPORT_WAIT_PID";
4
+ export declare const sysImportBranch: import("../../../core/command.js").BranchDef;
@@ -0,0 +1 @@
1
+ var g=Object.defineProperty;var i=(e,t)=>g(e,"name",{value:t,configurable:!0});import{defineBranch as w,defineLeaf as y}from"../../../core/command.js";import{general as v,usage as r}from"../../../core/errors.js";import{ImportCloud as k}from"../../../core/import/export/cloud.js";import{ensureSignedIn as p}from"../../../core/directory/person-credential.js";import{isPidAlive as _}from"../../../core/canvas/pid.js";import{showSignIn as d}from"../signin.js";import{insideBroker as R}from"../join.js";import{defaultDirectoryUrl as f,exportRun as T,settleEarlierMove as b,finishMove as I,runMove as O}from"./move.js";import{resume as N}from"./resume.js";const q="CRTR_IMPORT_WAIT_PID";async function U(e){for(;_(e);)await new Promise(t=>setTimeout(t,200))}i(U,"waitForExit");const x=y({name:"start",description:"export this runtime into the person's Northlight account",whenToUse:"moving this computer or hosted runtime's canvas into Northlight: run it from a plain terminal, not from inside a node",help:{name:"sys import start",summary:"upload blobs, pause the daemon, dump rows and file stores, and have the control plane load them",guide:"Order: open the run, upload blobs and request files, drain the daemon (turns in flight settle for up to 10 minutes), upload them again, dump every table and the file stores, upload the rows, request the load and wait for `done` or `failed`, printing each step's time. Before `done`, a failure ends the drain and this runtime carries on as before; a computer's move also signs it out. On `done` the daemon stays drained: a hosted runtime until hosting quiesces it (`crtr sys import resume` ends it), a computer until it has switched over \u2014 machine credential, `runtime/machine.json`, daemon restarted in machine role \u2014 which is retried three times over a minute and otherwise finished by `crtr sys signin`. A machine import signs the person in first when this computer has no stored sign-in. `crtr sys signin` normally starts a computer's move itself. With `--run`, hosting has already opened the run: the exporter uses it and reads that run's import token from the environment variable `CRTR_IMPORT_TOKEN`.",params:[{kind:"flag",name:"kind",type:"enum",choices:["machine","hosted"],required:!0,constraint:"`machine` for a person's computer, `hosted` for a hosted runtime (also archives running nodes' files)."},{kind:"flag",name:"api-url",type:"string",required:!1,constraint:"Control plane base URL (http or https). Required with `--kind hosted`; with `--kind machine` the default is the control plane the person's sign-in is for."},{kind:"flag",name:"run",type:"string",required:!1,constraint:"An import run hosting opened for this runtime (only with `--kind hosted`); its import token is read from `CRTR_IMPORT_TOKEN`."},{kind:"flag",name:"directory-url",type:"string",required:!1,constraint:"Directory base URL (http or https) the person signs in against, with `--kind machine`. Default: the one in `runtime/machine.json`, else the stored sign-in's, else Northlight's."},{kind:"flag",name:"notify",type:"string",required:!1,constraint:"With `--kind machine`: a node id told by message when the move fails or finishes."}],output:[{name:"run_id",type:"string",required:!0,constraint:"The import run."},{name:"state",type:"string",required:!0,constraint:"Final state of the run, `done`."},{name:"timings",type:"string",required:!0,constraint:"Per-step seconds."}],outputKind:"object",effects:["Drains the local daemon (no new wake or cron fire); the drain is a `runtime_state` row, so it survives a daemon restart. Ended on a failure before `done`; kept after `done`.","Machine: writes `runtime/import/move.json` (0600) from the run's start until the switch-over, then deletes it. On `done`: writes `runtime/machine.json` and `runtime/secrets/machine-credential` (0600) and restarts the daemon in machine role. On a failure before `done`: revokes and deletes the stored sign-in.","Machine with `--notify`: sends that node one message when the move fails or finishes.","Opens a temporary directory holding the opened file stores (0600), removed before the command ends."]},run:i(async e=>{const t=e.kind,o=e.run===void 0?void 0:String(e.run),n=e.apiUrl===void 0?void 0:String(e.apiUrl),h=e.notify===void 0?null:String(e.notify);if(t==="hosted"){if(n===void 0)throw r("--kind hosted needs --api-url: the control plane hosting opened the run on");if(h!==null)throw r("--notify names the node that asked for a computer's move: use it only with --kind machine")}else if(o!==void 0)throw r("--run takes an existing run, which only a hosted import has: use --kind hosted");const s=Number(process.env[q]??"");if(Number.isInteger(s)&&s>0&&await U(s),R())throw v("import_inside_node: run this from a plain terminal; a node's broker ends with the daemon restart",{code:"import_inside_node"});if(t==="machine"){const a=await b(d);if(a!==void 0)return await I(a),{run_id:a.runId,state:"done",timings:""};const l=e.directoryUrl===void 0?f():String(e.directoryUrl);await p(l,d);const c=await O({directoryUrl:l,...n===void 0?{}:{apiUrl:n},notify:h});return{run_id:c.runId,state:"done",timings:c.timings}}const m=(o===void 0?await p(f(),d):process.env.CRTR_IMPORT_TOKEN??"").trim();if(m==="")throw r("set CRTR_IMPORT_TOKEN to the run's import token");const u={name:"start",timings:[]};return{run_id:await T("hosted",new k(n,m),o,u),state:"done",timings:u.timings.join(" ")}},"run"),render:i(e=>`<import run="${e.run_id}" state="${e.state}" timings="${e.timings}"/>`,"render")}),W=w({name:"import",description:"move this runtime into the person's Northlight account",whenToUse:"importing this computer or hosted runtime into Northlight",help:{name:"sys import",summary:"export this runtime to Northlight",model:"The exporter pauses the local daemon, uploads a bundle the control plane loads into the shared database, and on a computer switches it to the machine role."},children:[x,N]});export{q as WAIT_FOR_PID_ENV,W as sysImportBranch};
@@ -1 +1,3 @@
1
+ /** Whether a broker (a node's engine) is an ancestor of this process, or cannot be ruled out. Stopping the daemon kills every broker, so a join run from one would die mid-way. */
2
+ export declare function insideBroker(): boolean;
1
3
  export declare const sysJoinLeaf: import("../../core/command.js").LeafDef;
@@ -1 +1 @@
1
- var a=Object.defineProperty;var n=(e,r)=>a(e,"name",{value:r,configurable:!0});import{defineLeaf as o}from"../../core/command.js";import{existsSync as s}from"node:fs";import{join as d}from"node:path";import{pluginsDir as l}from"../../core/scope.js";import{joinDirectory as c}from"../../core/directory/join.js";import{apiSocketPath as u}from"../../core/canvas/paths.js";import{listProcessTable as h}from"../../core/canvas/pid.js";import{daemonAnswers as m}from"../../core/layout-migrate/index.js";import{envNodeId as p}from"../../shared/env.js";import{isDaemonRunning as f}from"../../daemon/pidfile.js";import{findDaemonPids as w,spawnDaemon as y,stopAllDaemons as g}from"../../daemon/manage.js";function k(){const e=h();if(e===null||(p()??"")!=="")return!0;const r=new Map(e.map(t=>[t.pid,t])),i=new Set;for(let t=process.ppid;t>1&&!i.has(t);t=r.get(t)?.ppid??0)if(i.add(t),r.get(t)?.command.includes("broker-cli"))return!0;return!1}n(k,"insideBroker");function b(){if(!(process.platform==="linux"&&s("/run/crtr/launcher.sock")))return{running:n(async()=>f()||w().length>0||await m(u()),"running"),stop:n(async()=>{await g()},"stop"),start:n(async()=>{await y()},"start"),firstSync:n(async()=>{const e=l("user"),r=Date.now()+2e4;for(;e&&Date.now()<r&&!s(d(e,"cloud","commands.json"));)await new Promise(i=>setTimeout(i,250))},"firstSync"),insideNode:k}}n(b,"daemonControl");const T=o({name:"join",description:"register this runtime with its directory using an enrollment token, offline",whenToUse:"on your own computer, with a token from `<issuer>/account/machines`: run it from a plain terminal, with any viewers open. It stops the daemon, joins, and starts it again. Also at a hosted runtime's first boot, before the daemon starts, with the enrollment token provisioning passed in.",help:{name:"sys join",summary:"exchange a directory enrollment token for a new runtime client key and record the returned identity",guide:"Generates a new ES256 client key, sends only its public key to `PUT <directory>/v1/runtimes/registration` with the token as bearer, and learns the runtime id, client id, issuer, owner and URL from the answer. Only after that answer does it write the key file. It writes no settings and touches no canvas.db: the daemon's next start replaces this install's person with the owner everywhere in canvas.db, sets the user settings `runtime.issuer` and `runtime.audience` from the key file, and rewrites the legacy local person's profile grantees to `app:terminal`, all before it serves anything. On your own computer a running daemon is no obstacle: join takes a `join.lock` in the runtime directory (so no open viewer's redial can autostart a daemon while it works), registers with the directory while the daemon still serves, stops the daemon, writes the identity, and starts the daemon again, whose start applies the change. Run it from a plain terminal, not from inside a node: a node's broker dies with the daemon, so join refuses `join_inside_node` instead. Where the launcher owns the daemon (a hosted container) join keeps the offline contract and refuses `daemon_running`. Presenting the token of the stored successful registration again finishes those local writes without calling the directory. Any other token generates a new key and registers again.",params:[{kind:"flag",name:"directory",type:"string",required:!0,constraint:"Directory base URL (http or https) that minted the token."},{kind:"flag",name:"token",type:"string",required:!0,sensitive:!0,constraint:"Single-use enrollment token. Sensitive: never echoed in output or errors."},{kind:"flag",name:"name",type:"string",required:!1,constraint:"Label shown for this machine on the account page. Default: this computer's hostname."}],output:[{name:"runtime",type:"string",required:!0,constraint:"Runtime principal from the directory, `runtime:<id>`."},{name:"client_id",type:"string",required:!0,constraint:"OAuth client id registered for the new key."},{name:"issuer",type:"string",required:!0,constraint:"Canonical issuer, written to `runtime.issuer`."},{name:"owner",type:"string",required:!0,constraint:"Owner principal, `user:<id>`, that replaces this install's own person at the next daemon start."},{name:"canvas_rewrite",type:"string",required:!0,constraint:"Always `next daemon start`: when canvas.db identity fields are rewritten."},{name:"url",type:"string",required:!0,constraint:"Runtime URL, written to `runtime.audience`."},{name:"registered",type:"boolean",required:!0,constraint:"False when the token matched the stored registration and the directory was not called."},{name:"daemon",type:"string",required:!0,constraint:"`restarted` (a daemon was stopped and started on the joined settings), `started` (none was running; join started one and waited, up to 20s, for its first grant sync to install the account's provider plugins, so `crtr cloud \u2026` resolves), `not_running` (none was started: a hosted container's launcher owns its daemon), or `restart_failed` (joined, but the new daemon did not start: see `daemon_error`, then run `crtr sys daemon start`)."},{name:"daemon_error",type:"string",required:!1,constraint:"Why the new daemon did not start, with `daemon: restart_failed`."}],outputKind:"object",effects:["Holds `<runtime>/join.lock` (its pid) from the start until the daemon is back; `ensureDaemon` and `sys daemon start` (without `--force`) decline to start a daemon while a live process holds it (including a start already in flight when the join began). A lock whose holder died is reclaimed.","A running daemon is stopped (SIGTERM, then SIGKILL, plus strays) after the directory accepts the registration and before the local writes, then started again. Every broker dies with it and is revived by the new daemon. If the join fails after the stop, the daemon is started again anyway. With no daemon running join starts one anyway, because a bare `crtr cloud \u2026` is an unknown command (it never autostarts a daemon) until a daemon's grant sync has installed the provider plugins. Refuses `join_inside_node` when run from inside a node and a daemon is running; refuses `daemon_running` where a container launcher owns the daemon.","On a new token: calls the directory once; on success writes `<runtime>/secrets/runtime-client.json` (mode 0600) with the private key, registration and the token's SHA-256, replacing any earlier key file. A refused or failed call writes nothing.","Sends name, platform, crtr version, installed plugin ids and sandbox state with the public key. Before the request it writes `runtime-client.pending.json` (mode 0600); a rerun with the same token and directory reuses that key, so a lost reply is retried safely. The file is deleted on success or a refusal.","Sets `runtime.issuer` and `runtime.audience` in user settings. When the directory says this machine is not the account's default (`is_default: false`) it replaces the whole `runtime` setting with `{issuer, audience, unsandboxed: true}`: no app listener, and only `app:terminal` nodes run.","Rewrites the legacy local person's profile grantees to `app:terminal`.","At the next daemon start, before any listener, the person this install minted (or still the legacy local person) is replaced by the owner in every canvas.db column that holds a person: node, token, grant, cron, audit and usage subjects and grantees, the person's identity, and queued cron births. References to the install's local terminal grant are cleared and that local grant mirror is deleted so grant sync installs the directory's. Free text is unchanged. The start after that changes nothing."]},run:n(async e=>({...await c(e.directory,e.token,e.name||void 0,b()),canvas_rewrite:"next daemon start"}),"run")});export{T as sysJoinLeaf};
1
+ var a=Object.defineProperty;var n=(e,r)=>a(e,"name",{value:r,configurable:!0});import{defineLeaf as o}from"../../core/command.js";import{existsSync as s}from"node:fs";import{join as d}from"node:path";import{pluginsDir as l}from"../../core/scope.js";import{joinDirectory as c}from"../../core/directory/join.js";import{apiSocketPath as u}from"../../core/canvas/paths.js";import{listProcessTable as h}from"../../core/canvas/pid.js";import{daemonAnswers as m}from"../../core/layout-migrate/index.js";import{envNodeId as p}from"../../shared/env.js";import{isDaemonRunning as f}from"../../daemon/pidfile.js";import{findDaemonPids as w,spawnDaemon as y,stopAllDaemons as g}from"../../daemon/manage.js";function k(){const e=h();if(e===null||(p()??"")!=="")return!0;const r=new Map(e.map(t=>[t.pid,t])),i=new Set;for(let t=process.ppid;t>1&&!i.has(t);t=r.get(t)?.ppid??0)if(i.add(t),r.get(t)?.command.includes("broker-cli"))return!0;return!1}n(k,"insideBroker");function b(){if(!(process.platform==="linux"&&s("/run/crtr/launcher.sock")))return{running:n(async()=>f()||w().length>0||await m(u()),"running"),stop:n(async()=>{await g()},"stop"),start:n(async()=>{await y()},"start"),firstSync:n(async()=>{const e=l("user"),r=Date.now()+2e4;for(;e&&Date.now()<r&&!s(d(e,"cloud","commands.json"));)await new Promise(i=>setTimeout(i,250))},"firstSync"),insideNode:k}}n(b,"daemonControl");const T=o({name:"join",description:"register this runtime with its directory using an enrollment token, offline",whenToUse:"on your own computer, with a token from `<issuer>/account/machines`: run it from a plain terminal, with any viewers open. It stops the daemon, joins, and starts it again. Also at a hosted runtime's first boot, before the daemon starts, with the enrollment token provisioning passed in.",help:{name:"sys join",summary:"exchange a directory enrollment token for a new runtime client key and record the returned identity",guide:"Generates a new ES256 client key, sends only its public key to `PUT <directory>/v1/runtimes/registration` with the token as bearer, and learns the runtime id, client id, issuer, owner and URL from the answer. Only after that answer does it write the key file. It writes no settings and touches no canvas.db: the daemon's next start replaces this install's person with the owner everywhere in canvas.db, sets the user settings `runtime.issuer` and `runtime.audience` from the key file, and rewrites the legacy local person's profile grantees to `app:terminal`, all before it serves anything. On your own computer a running daemon is no obstacle: join takes a `join.lock` in the runtime directory (so no open viewer's redial can autostart a daemon while it works), registers with the directory while the daemon still serves, stops the daemon, writes the identity, and starts the daemon again, whose start applies the change. Run it from a plain terminal, not from inside a node: a node's broker dies with the daemon, so join refuses `join_inside_node` instead. Where the launcher owns the daemon (a hosted container) join keeps the offline contract and refuses `daemon_running`. Presenting the token of the stored successful registration again finishes those local writes without calling the directory. Any other token generates a new key and registers again.",params:[{kind:"flag",name:"directory",type:"string",required:!0,constraint:"Directory base URL (http or https) that minted the token."},{kind:"flag",name:"token",type:"string",required:!0,sensitive:!0,constraint:"Single-use enrollment token. Sensitive: never echoed in output or errors."},{kind:"flag",name:"name",type:"string",required:!1,constraint:"Label shown for this machine on the account page. Default: this computer's hostname."}],output:[{name:"runtime",type:"string",required:!0,constraint:"Runtime principal from the directory, `runtime:<id>`."},{name:"client_id",type:"string",required:!0,constraint:"OAuth client id registered for the new key."},{name:"issuer",type:"string",required:!0,constraint:"Canonical issuer, written to `runtime.issuer`."},{name:"owner",type:"string",required:!0,constraint:"Owner principal, `user:<id>`, that replaces this install's own person at the next daemon start."},{name:"canvas_rewrite",type:"string",required:!0,constraint:"Always `next daemon start`: when canvas.db identity fields are rewritten."},{name:"url",type:"string",required:!0,constraint:"Runtime URL, written to `runtime.audience`."},{name:"registered",type:"boolean",required:!0,constraint:"False when the token matched the stored registration and the directory was not called."},{name:"daemon",type:"string",required:!0,constraint:"`restarted` (a daemon was stopped and started on the joined settings), `started` (none was running; join started one and waited, up to 20s, for its first grant sync to install the account's provider plugins, so `crtr cloud \u2026` resolves), `not_running` (none was started: a hosted container's launcher owns its daemon), or `restart_failed` (joined, but the new daemon did not start: see `daemon_error`, then run `crtr sys daemon start`)."},{name:"daemon_error",type:"string",required:!1,constraint:"Why the new daemon did not start, with `daemon: restart_failed`."}],outputKind:"object",effects:["Holds `<runtime>/join.lock` (its pid) from the start until the daemon is back; `ensureDaemon` and `sys daemon start` (without `--force`) decline to start a daemon while a live process holds it (including a start already in flight when the join began). A lock whose holder died is reclaimed.","A running daemon is stopped (SIGTERM, then SIGKILL, plus strays) after the directory accepts the registration and before the local writes, then started again. Every broker dies with it and is revived by the new daemon. If the join fails after the stop, the daemon is started again anyway. With no daemon running join starts one anyway, because a bare `crtr cloud \u2026` is an unknown command (it never autostarts a daemon) until a daemon's grant sync has installed the provider plugins. Refuses `join_inside_node` when run from inside a node and a daemon is running; refuses `daemon_running` where a container launcher owns the daemon.","On a new token: calls the directory once; on success writes `<runtime>/secrets/runtime-client.json` (mode 0600) with the private key, registration and the token's SHA-256, replacing any earlier key file. A refused or failed call writes nothing.","Sends name, platform, crtr version, installed plugin ids and sandbox state with the public key. Before the request it writes `runtime-client.pending.json` (mode 0600); a rerun with the same token and directory reuses that key, so a lost reply is retried safely. The file is deleted on success or a refusal.","Sets `runtime.issuer` and `runtime.audience` in user settings. When the directory says this machine is not the account's default (`is_default: false`) it replaces the whole `runtime` setting with `{issuer, audience, unsandboxed: true}`: no app listener, and only `app:terminal` nodes run.","Rewrites the legacy local person's profile grantees to `app:terminal`.","At the next daemon start, before any listener, the person this install minted (or still the legacy local person) is replaced by the owner in every canvas.db column that holds a person: node, token, grant, cron, audit and usage subjects and grantees, the person's identity, and queued cron births. References to the install's local terminal grant are cleared and that local grant mirror is deleted so grant sync installs the directory's. Free text is unchanged. The start after that changes nothing."]},run:n(async e=>({...await c(e.directory,e.token,e.name||void 0,b()),canvas_rewrite:"next daemon start"}),"run")});export{k as insideBroker,T as sysJoinLeaf};
@@ -0,0 +1 @@
1
+ export declare const sysMachineBranch: import("../../../core/command.js").BranchDef;
@@ -0,0 +1 @@
1
+ var c=Object.defineProperty;var r=(e,n)=>c(e,"name",{value:n,configurable:!0});import{defineBranch as h,defineLeaf as d}from"../../../core/command.js";import{general as o,usage as m}from"../../../core/errors.js";import{personBearer as l}from"../../../core/directory/person-credential.js";import{readMachineConfig as p,writeMachineCredential as u}from"../../../daemon/machine-link/config.js";const w=d({name:"rotate",description:"replace this Mac's machine credential",whenToUse:"the credential this imported Mac presents on its link to Northlight may have leaked, or the console revoked it",help:{name:"sys machine rotate",summary:"ask the control plane for a new machine credential for this Mac and store it",params:[],output:[{name:"environment_id",type:"string",required:!0,constraint:"The machine environment the new credential belongs to."}],outputKind:"object",effects:["Calls `POST /v1/imports/<run>/machine-credential?rotate=1`, which replaces the stored hash for this machine environment, then writes the new secret to `runtime/secrets/machine-credential` (mode 0600). Authenticates with the person's sign-in (`crtr sys signin`; `not_signed_in` without one). Fails when `runtime/machine.json` is missing: this install was not imported as a machine.","The old credential stops opening links as soon as the control plane has replaced it, and the link it already holds is closed within 15 seconds; the Mac reconnects with the new credential."]},run:r(async()=>{const e=p();if(e===void 0)throw m("this install was not imported as a machine: runtime/machine.json is missing");const n=await l(e.directoryUrl),s=new URL(`/v1/imports/${encodeURIComponent(e.runId)}/machine-credential?rotate=1`,e.apiUrl),i=await fetch(s,{method:"POST",headers:{authorization:`Bearer ${n}`},signal:AbortSignal.timeout(3e4)}),a=await i.text();if(!i.ok)throw o(`the control plane answered ${i.status}: ${a.slice(0,300)}`);const t=JSON.parse(a);if(typeof t.credential!="string"||typeof t.environment_id!="string")throw o("the control plane answered without a credential");return u(t.credential),{environment_id:t.environment_id}},"run"),render:r(e=>`<machine environment="${e.environment_id}" credential="rotated"/>`,"render")}),k=h({name:"machine",description:"this Mac's link to Northlight after import",whenToUse:"an imported Mac's machine credential must be replaced",help:{name:"sys machine",summary:"manage the machine credential of an imported Mac",model:"After import the Mac runs `crtrd --role machine`: it holds one outbound link to Northlight, authenticated by a machine credential stored in `runtime/secrets/machine-credential`, and executes the runner calls Northlight sends over it."},children:[w]});export{k as sysMachineBranch};
@@ -0,0 +1,7 @@
1
+ import { type SignInPrompt } from '../../core/directory/person-credential.js';
2
+ /** Print the pairing code and open the confirm page: what the person sees while `signIn` polls. */
3
+ export declare function showSignIn(prompt: SignInPrompt & {
4
+ complete: string;
5
+ }): void;
6
+ export declare const sysSigninLeaf: import("../../core/command.js").LeafDef;
7
+ export declare const sysSignoutLeaf: import("../../core/command.js").LeafDef;
@@ -0,0 +1,2 @@
1
+ var y=Object.defineProperty;var o=(e,n)=>y(e,"name",{value:n,configurable:!0});import{spawn as c}from"node:child_process";import{mkdirSync as k,openSync as _}from"node:fs";import{dirname as d,join as b}from"node:path";import{defineLeaf as h}from"../../core/command.js";import{CrtrError as u}from"../../core/errors.js";import{signIn as m,signOut as T}from"../../core/directory/person-credential.js";import{moveRecordPath as N,readMachineConfig as l}from"../../daemon/machine-link/config.js";import{sanitizedDaemonEnv as S,spawnDaemon as p,stopAllDaemons as g}from"../../daemon/manage.js";import{northlightUnreachable as U}from"../../core/directory/reach.js";import{envNodeId as A}from"../../shared/env.js";import{ExitCode as f}from"../../types.js";import{insideBroker as v}from"./join.js";import{cliClient as C}from"../api-client.js";import{defaultDirectoryUrl as $,finishMove as O,runMove as R,settleEarlierMove as I}from"./import/move.js";import{WAIT_FOR_PID_ENV as P}from"./import/start.js";function E(e){const[n,t]=process.platform==="darwin"?["open",[e]]:["xdg-open",[e]];try{c(n,t,{stdio:"ignore",detached:!0}).on("error",()=>{}).unref()}catch{}}o(E,"openBrowser");function a(e){process.stderr.write(`Confirm this code at ${e.verificationUri}: ${e.userCode}
2
+ `),E(e.complete)}o(a,"showSignIn");function x(e){return async n=>{let t;try{t=await fetch(new URL(`/v1/imports/${e.runId}`,e.apiUrl),{headers:{authorization:`Bearer ${n.bearer}`},signal:AbortSignal.timeout(3e4)})}catch(r){return U(r,e.apiUrl)}if(!t.ok)return t.status===404?new u("signin_other_account",`This computer's crtr belongs to the Northlight account ${e.account?.display??"it was moved into"}. Sign in to that account.`,f.USAGE):new u("signin_unchecked",`the control plane did not say which account owns this computer (HTTP ${t.status}); nothing was written.`,f.GENERAL,{next:"Retry `crtr sys signin`."})}}o(x,"sameAccountOnly");function w(e){const n=b(d(N()),"move.log");k(d(n),{recursive:!0});const t=_(n,"a",384),r=S();for(const s of["CRTR_API_SOCKET","CRTR_API_TOKEN","CRTR_API_URL"])delete r[s];r[P]=String(process.pid);const i=[...process.execArgv,process.argv[1],"sys","import","start","--kind","machine",...e===void 0?[]:["--notify",e]];c(process.execPath,i,{detached:!0,stdio:["ignore",t,t],env:r}).unref()}o(w,"startDetachedMove");const D=o(e=>`Signed in as ${e}. crtr is now moving this computer's records to Northlight; every agent here pauses for seconds to minutes. End your turn now; this conversation carries on when the move finishes.`,"MOVING"),ee=h({name:"signin",description:"sign this computer's crtr in to the person's Northlight account",whenToUse:"the person agreed to sign in to Northlight (a Northlight command needs the account, or they asked), or a command answered `not_signed_in` or `move_unfinished`",help:{name:"sys signin",summary:"pair this computer with the person's Northlight account in the browser, then move this computer's records into it",guide:"Prints a code and opens the directory's confirm page; the person signs in or signs up there if asked, checks the code and clicks Allow. The command waits for that. On a computer whose records are still local it then moves them into the account: run from a terminal it prints each step and returns when done; run from inside a node it returns at once and the node is messaged when the move finishes or fails. A computer already moved accepts only the account it moved into. For an unfinished move (`move_unfinished`), it checks the account's move result and finishes the switch-over if the records arrived. If the stored sign-in is missing or revoked, it first pairs only the account recorded for that move. Until the result is known, agents remain paused. The sign-in ends after 90 days without use, when revoked on the account page, or with `crtr sys signout`.",params:[{kind:"flag",name:"directory-url",type:"string",required:!1,constraint:"Directory base URL (http or https). Default: the one in `runtime/machine.json`, else the stored sign-in's, else Northlight's."}],output:[{name:"account",type:"string",required:!0,constraint:"The account this computer is now signed in as."},{name:"moved",type:"boolean",required:!1,constraint:"Present when this command moved the records, or finished an earlier move's switch-over."},{name:"moving",type:"boolean",required:!1,constraint:"Present when the move runs on in its own process (run from inside a node): end the turn; the node is messaged when it finishes or fails."}],outputKind:"object",effects:["Calls the directory (`POST /v1/developer/logins`, then polls `/v1/developer/logins/poll`) and, once approved, writes `runtime/secrets/person-credential` (0600) and replaces the cached bearer. A declined, abandoned or refused sign-in writes nothing and leaves an existing sign-in as it was.","On a computer whose records are local: the move of `crtr sys import start --kind machine` (drains the daemon, uploads, restarts it in machine role); a move confirmed failed before the account holds the records ends the drain and signs this computer out again. If the account's result is unknown, the move record and drain remain for recovery. From inside a node it runs as a detached process logging to `runtime/import/move.log`.","On a moved computer: restarts the daemon so it reconnects to Northlight."]},run:o(async e=>{const n=e.directoryUrl===void 0?$():String(e.directoryUrl),t=await I(a);if(t!==void 0)return v()?(w(void 0),{account:t.account.display,moving:!0}):(await O(t),{account:t.account.display,moved:!0});const r=l();if(r!==void 0){const{account:s}=await m(n,a,{accept:x(r)});return await g(),await p(),{account:s}}const{account:i}=await m(n,a);return v()?(w(A()),{account:i,moving:!0}):(await R({directoryUrl:n,notify:null}),{account:i,moved:!0})},"run"),render:o(e=>e.moving===!0?`<signin account="${e.account}" moving="true">${D(String(e.account))}</signin>`:`<signin account="${e.account}"${e.moved===!0?' moved="true"':""}/>`,"render")});async function q(){return await g(),await p(),(await C({local:!0}).healthz()).signed_in===!1}o(q,"stopMovedComputer");const te=h({name:"signout",description:"sign this computer's crtr out, revoke its credential, and stop its agents",whenToUse:"this computer should stop acting as the person on Northlight, or its stored sign-in may have leaked",help:{name:"sys signout",summary:"revoke the stored credential, delete it from this computer, and on a moved computer stop every agent and cron here",params:[],output:[{name:"signed_out",type:"boolean",required:!0,constraint:"False when no sign-in was stored."},{name:"revoked",type:"boolean",required:!0,constraint:"True when the directory ended the credential now; false when it was already ended or none was stored."},{name:"stopped",type:"boolean",required:!0,constraint:"True when this computer's records are in Northlight and its agents and running crons have stopped; false on a computer whose records are local, where nothing else is tied to the sign-in."}],outputKind:"object",effects:["Calls `POST /v1/developer/token/revoke` with the stored secret, then deletes `runtime/secrets/person-credential` and `person-bearer.json`. A bearer already issued stays valid for at most 5 minutes.","On a computer whose records are in Northlight: restarts the daemon, which stops every agent and running cron here and takes no new work until `crtr sys signin`; agents keep their mail on Northlight and wake after it. Commands answer `not_signed_in`."]},run:o(async()=>{const{signedOut:e,revoked:n}=await T(),t=l()===void 0?!1:await q();return{signed_out:e,revoked:n,stopped:t}},"run"),render:o(e=>`<signout signed_out="${e.signed_out}" revoked="${e.revoked}" stopped="${e.stopped}"/>`,"render")});export{a as showSignIn,ee as sysSigninLeaf,te as sysSignoutLeaf};
@@ -1 +1 @@
1
- var s=Object.defineProperty;var r=(o,m)=>s(o,"name",{value:m,configurable:!0});import{defineSysBranch as t}from"./sys/branch.js";import{configBranch as f}from"./sys/config.js";import{sysEnvBranch as e}from"./sys/env.js";import{daemonBranch as i}from"./sys/daemon.js";import{sysDoctorLeaf as a}from"./sys/doctor.js";import{sysFeedbackLeaf as p}from"./sys/feedback.js";import{sysLogsLeaf as n}from"./sys/logs.js";import{sysRetryDeliveryLeaf as y}from"./sys/retry-delivery.js";import{sysWorktreesLeaf as c}from"./sys/worktrees.js";import{sysSpacesLeaf as L}from"./sys/spaces.js";import{sysLimitsBranch as h}from"./sys/limits.js";import{sysSpendBranch as B}from"./sys/spend.js";import{sysSettingsLeaf as S}from"./sys/settings.js";import{sysConnectLeaf as d}from"./sys/connect.js";import{sysSetupLeaf as g}from"./sys/setup.js";import{tutorialBranch as u}from"./sys/tutorial/branch.js";import{supportBranch as D}from"./sys/support.js";import{contextBranch as b}from"./sys/context.js";import{sysSyncBranch as k}from"./sys/sync.js";import{sysMigrateLeaf as l}from"./sys/migrate.js";import{sysJoinLeaf as v}from"./sys/join.js";import{sysDbBranch as x}from"./sys/db.js";import{sysUpdateLeaf as C,sysVersionLeaf as E}from"./sys/update.js";function _(){return t([S,f,e,d,g,u,a,b,p,n,c,y,L,h,B,D,C,E,l,v,k,x,i])}r(_,"registerSys");export{_ as registerSys};
1
+ var s=Object.defineProperty;var r=(o,m)=>s(o,"name",{value:m,configurable:!0});import{defineSysBranch as t}from"./sys/branch.js";import{configBranch as f}from"./sys/config.js";import{sysEnvBranch as i}from"./sys/env.js";import{daemonBranch as e}from"./sys/daemon.js";import{sysDoctorLeaf as a}from"./sys/doctor.js";import{sysFeedbackLeaf as p}from"./sys/feedback.js";import{sysLogsLeaf as n}from"./sys/logs.js";import{sysRetryDeliveryLeaf as y}from"./sys/retry-delivery.js";import{sysWorktreesLeaf as c}from"./sys/worktrees.js";import{sysSpacesLeaf as L}from"./sys/spaces.js";import{sysLimitsBranch as h}from"./sys/limits.js";import{sysSpendBranch as B}from"./sys/spend.js";import{sysSettingsLeaf as S}from"./sys/settings.js";import{sysConnectLeaf as g}from"./sys/connect.js";import{sysSetupLeaf as u}from"./sys/setup.js";import{tutorialBranch as d}from"./sys/tutorial/branch.js";import{supportBranch as D}from"./sys/support.js";import{contextBranch as k}from"./sys/context.js";import{sysSyncBranch as b}from"./sys/sync.js";import{sysMigrateLeaf as l}from"./sys/migrate.js";import{sysJoinLeaf as v}from"./sys/join.js";import{sysDbBranch as x}from"./sys/db.js";import{sysImportBranch as M}from"./sys/import/start.js";import{sysMachineBranch as C}from"./sys/machine/rotate.js";import{sysDesktopBranch as E}from"./sys/desktop.js";import{sysSigninLeaf as F,sysSignoutLeaf as I}from"./sys/signin.js";import{sysUpdateLeaf as J,sysVersionLeaf as R}from"./sys/update.js";function er(){return t([S,f,i,g,u,d,a,k,p,n,c,y,L,h,B,D,J,R,l,v,b,x,M,C,F,I,E,e])}r(er,"registerSys");export{er as registerSys};
@@ -7,3 +7,7 @@ export interface R2Config {
7
7
  }
8
8
  /** S3-compatible object store (R2) over `fetch`: path-style requests signed with SigV4, region `auto`. */
9
9
  export declare function createR2BlobStore(config: R2Config): BlobStore;
10
+ /** The R2 settings of this process, or `null` when it uses the directory store. Same variables as `blobStore()`. */
11
+ export declare function r2ConfigFromEnv(): R2Config | null;
12
+ /** A SigV4 query-signed URL that lets its holder PUT one object at `key` until `expiresSeconds` pass. */
13
+ export declare function presignPutUrl(config: R2Config, key: string, expiresSeconds: number, now?: Date): string;
@@ -1,4 +1,7 @@
1
- var I=Object.defineProperty;var c=(a,o)=>I(a,"name",{value:o,configurable:!0});import{createHash as U,createHmac as C}from"node:crypto";import{assertBlobKey as g}from"./index.js";import{blobFault as z}from"./fault.js";const T=c(a=>U("sha256").update(a).digest("hex"),"sha256"),d=c((a,o)=>C("sha256",a).update(o).digest(),"hmac"),$=c(a=>encodeURIComponent(a).replace(/[!'()*]/g,o=>`%${o.charCodeAt(0).toString(16).toUpperCase()}`),"encode");function G(a){const o=new URL(a.endpoint);async function u(t,e,n,r,s){const l=`/${$(a.bucket)}${e===""?"":`/${e.split("/").map($).join("/")}`}`,h=[...n].sort(([i],[w])=>i<w?-1:1).map(([i,w])=>`${$(i)}=${$(w)}`).join("&"),m=new Date().toISOString().replace(/[:-]|\.\d{3}/g,""),y=m.slice(0,8),B=T(r??""),p={host:o.host,"x-amz-content-sha256":B,"x-amz-date":m},S=Object.keys(p).sort(),b=S.map(i=>`${i}:${p[i]}
2
- `).join(""),x=S.join(";"),A=`${y}/auto/s3/aws4_request`,j=[t,l,h,b,x,B].join(`
3
- `),E=["AWS4-HMAC-SHA256",m,A,T(j)].join(`
4
- `),K=d(d(d(d(`AWS4${a.secretAccessKey}`,y),"auto"),"s3"),"aws4_request"),q=C("sha256",K).update(E).digest("hex");p.authorization=`AWS4-HMAC-SHA256 Credential=${a.accessKeyId}/${A}, SignedHeaders=${x}, Signature=${q}`;const{host:R,...H}=p;return s&&(H.range=`bytes=${s.start}-${s.start+s.length-1}`),await fetch(`${o.origin}${l}${h?`?${h}`:""}`,{method:t,headers:H,...r?{body:new Uint8Array(r)}:{}})}c(u,"send");async function f(t,e){if(!t.ok)throw new Error(`R2 ${e} failed (${t.status}): ${(await t.text()).slice(0,300)}`);return t}return c(f,"ok"),{async put(t,e){z("put"),g(t),await f(await u("PUT",t,[],typeof e=="string"?Buffer.from(e,"utf8"):Buffer.from(e)),`put ${t}`)},async get(t,e){g(t);const n=await u("GET",t,[],void 0,e);return n.status===416?(await n.arrayBuffer(),Buffer.alloc(0)):n.status===404?(await n.arrayBuffer(),null):Buffer.from(await(await f(n,`get ${t}`)).arrayBuffer())},async list(t){const e=[];let n;do{const r=[["list-type","2"],["prefix",t],...n?[["continuation-token",n]]:[]],s=await(await f(await u("GET","",r),`list ${t}`)).text();for(const l of s.matchAll(/<Key>([^<]*)<\/Key>/g))e.push(l[1].replace(/&amp;/g,"&").replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/&quot;/g,'"').replace(/&apos;/g,"'"));n=/<IsTruncated>true<\/IsTruncated>/.test(s)?/<NextContinuationToken>([^<]*)<\/NextContinuationToken>/.exec(s)?.[1]:void 0}while(n);return e.sort((r,s)=>Buffer.compare(Buffer.from(r),Buffer.from(s)))},async delete(t){g(t);const e=await u("DELETE",t,[]);e.status!==404?await f(e,`delete ${t}`):await e.arrayBuffer()}}}c(G,"createR2BlobStore");export{G as createR2BlobStore};
1
+ var q=Object.defineProperty;var c=(e,a)=>q(e,"name",{value:a,configurable:!0});import{createHash as z,createHmac as R}from"node:crypto";import{assertBlobKey as y}from"./index.js";import{blobFault as I}from"./fault.js";const E=c(e=>z("sha256").update(e).digest("hex"),"sha256"),p=c((e,a)=>R("sha256",e).update(a).digest(),"hmac"),f=c(e=>encodeURIComponent(e).replace(/[!'()*]/g,a=>`%${a.charCodeAt(0).toString(16).toUpperCase()}`),"encode");function X(e){const a=new URL(e.endpoint);async function i(t,s,n,r,o){const m=`/${f(e.bucket)}${s===""?"":`/${s.split("/").map(f).join("/")}`}`,l=[...n].sort(([h],[C])=>h<C?-1:1).map(([h,C])=>`${f(h)}=${f(C)}`).join("&"),g=new Date().toISOString().replace(/[:-]|\.\d{3}/g,""),w=g.slice(0,8),S=E(r??""),d={host:a.host,"x-amz-content-sha256":S,"x-amz-date":g},$=Object.keys(d).sort(),A=$.map(h=>`${h}:${d[h]}
2
+ `).join(""),_=$.join(";"),T=`${w}/auto/s3/aws4_request`,B=[t,m,l,A,_,S].join(`
3
+ `),K=["AWS4-HMAC-SHA256",g,T,E(B)].join(`
4
+ `),H=p(p(p(p(`AWS4${e.secretAccessKey}`,w),"auto"),"s3"),"aws4_request"),j=R("sha256",H).update(K).digest("hex");d.authorization=`AWS4-HMAC-SHA256 Credential=${e.accessKeyId}/${T}, SignedHeaders=${_}, Signature=${j}`;const{host:U,...x}=d;return o&&(x.range=`bytes=${o.start}-${o.start+o.length-1}`),await fetch(`${a.origin}${m}${l?`?${l}`:""}`,{method:t,headers:x,...r?{body:new Uint8Array(r)}:{}})}c(i,"send");async function u(t,s){if(!t.ok)throw new Error(`R2 ${s} failed (${t.status}): ${(await t.text()).slice(0,300)}`);return t}return c(u,"ok"),{async put(t,s){I("put"),y(t),await u(await i("PUT",t,[],typeof s=="string"?Buffer.from(s,"utf8"):Buffer.from(s)),`put ${t}`)},async get(t,s){y(t);const n=await i("GET",t,[],void 0,s);return n.status===416?(await n.arrayBuffer(),Buffer.alloc(0)):n.status===404?(await n.arrayBuffer(),null):Buffer.from(await(await u(n,`get ${t}`)).arrayBuffer())},async list(t){const s=[];let n;do{const r=[["list-type","2"],["prefix",t],...n?[["continuation-token",n]]:[]],o=await(await u(await i("GET","",r),`list ${t}`)).text();for(const m of o.matchAll(/<Key>([^<]*)<\/Key>/g))s.push(m[1].replace(/&amp;/g,"&").replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/&quot;/g,'"').replace(/&apos;/g,"'"));n=/<IsTruncated>true<\/IsTruncated>/.test(o)?/<NextContinuationToken>([^<]*)<\/NextContinuationToken>/.exec(o)?.[1]:void 0}while(n);return s.sort((r,o)=>Buffer.compare(Buffer.from(r),Buffer.from(o)))},async delete(t){y(t);const s=await i("DELETE",t,[]);s.status!==404?await u(s,`delete ${t}`):await s.arrayBuffer()}}}c(X,"createR2BlobStore");function O(){const e=process.env.CRTR_R2_BUCKET;if(!e)return null;const a=c(i=>{const u=process.env[i];if(!u)throw new Error(`${i} is required when CRTR_R2_BUCKET is set`);return u},"need");return{bucket:e,endpoint:a("CRTR_R2_ENDPOINT"),accessKeyId:a("CRTR_R2_ACCESS_KEY_ID"),secretAccessKey:a("CRTR_R2_SECRET_ACCESS_KEY")}}c(O,"r2ConfigFromEnv");function P(e,a,i,u=new Date){y(a);const t=new URL(e.endpoint),s=`/${f(e.bucket)}/${a.split("/").map(f).join("/")}`,n=u.toISOString().replace(/[:-]|\.\d{3}/g,""),r=n.slice(0,8),o=`${r}/auto/s3/aws4_request`,l=[["X-Amz-Algorithm","AWS4-HMAC-SHA256"],["X-Amz-Credential",`${e.accessKeyId}/${o}`],["X-Amz-Date",n],["X-Amz-Expires",String(i)],["X-Amz-SignedHeaders","host"]].sort(([$],[A])=>$<A?-1:1).map(([$,A])=>`${f($)}=${f(A)}`).join("&"),g=["PUT",s,l,`host:${t.host}
5
+ `,"host","UNSIGNED-PAYLOAD"].join(`
6
+ `),w=["AWS4-HMAC-SHA256",n,o,E(g)].join(`
7
+ `),S=p(p(p(p(`AWS4${e.secretAccessKey}`,r),"auto"),"s3"),"aws4_request"),d=R("sha256",S).update(w).digest("hex");return`${t.origin}${s}?${l}&X-Amz-Signature=${d}`}c(P,"presignPutUrl");export{X as createR2BlobStore,P as presignPutUrl,O as r2ConfigFromEnv};
@@ -15,7 +15,7 @@ export interface DaemonOwnerClaim {
15
15
  * process running both roles); the cloud's `--role api` and `--role reconcile` processes are several per database by design, so
16
16
  * they never claim, never release, and never read the row. The caller (`crtrd.ts`) checks this before
17
17
  * `claimDaemonOwnership` / `releaseDaemonOwnership` / `daemonOwnershipClaimed`. */
18
- export declare function daemonOwnershipApplies(role: 'api' | 'reconcile' | undefined): boolean;
18
+ export declare function daemonOwnershipApplies(role: 'api' | 'reconcile' | 'machine' | undefined): boolean;
19
19
  /** Every terminal, non-retriable way `claimDaemonOwnership` can lose. A loss
20
20
  * never deletes or otherwise mutates the incumbent row — only a positively
21
21
  * DEAD incumbent is ever CAS-replaced. */
@@ -8,7 +8,7 @@ export interface Execution {
8
8
  startedAt: string;
9
9
  endedAt: string | null;
10
10
  exitReason: string | null;
11
- runner: 'process' | 'blaxel';
11
+ runner: 'process' | 'blaxel' | 'machine';
12
12
  sandboxName: string | null;
13
13
  attempt: number;
14
14
  /** The release the wake accepted. */
@@ -10,6 +10,8 @@ export interface NodePrincipal {
10
10
  scopes: string[] | null;
11
11
  }
12
12
  export declare function mintNodeToken(db: CanvasDb, executionId: string, nodeId: string, issuedAt: string): Promise<string>;
13
+ /** Mints the node token of a `launching` execution in its own canvas write: what a runner's `start` hands to the process it launches. */
14
+ export declare const mintExecutionToken: (executionId: string, nodeId: string) => Promise<string>;
13
15
  /** A stopped or superseded execution can never authenticate, even after restart. */
14
16
  export declare function resolveNodeToken(token: string): Promise<NodePrincipal | null>;
15
17
  export declare function revokeExitedNodeToken(executionId: string): Promise<void>;
@@ -1,5 +1,5 @@
1
- var E=Object.defineProperty;var r=(n,e)=>E(n,"name",{value:e,configurable:!0});import{createHash as g,randomBytes as l}from"node:crypto";import{openDb as s}from"./db.js";import{terminalGrant as k}from"../grants/mirror.js";import{personIdAsync as w}from"./person.js";import{uidForApp as N}from"../spaces/users.js";const p=r(n=>g("sha256").update(n).digest("hex"),"hash");async function x(n,e,o,i){const t=await n.prepare("SELECT grantee, sub, grant_id, client_id, scopes_json, run_id FROM node_rows WHERE node_id = ?").get(o);if(!t)throw new Error(`cannot mint node token: missing node ${o}`);const a=t.run_id&&t.run_id!==o?await n.prepare("SELECT sub, grant_id, client_id FROM nodes WHERE node_id = ?").get(t.run_id):void 0,d=a?.sub??t.sub??await w(),_=t.grantee==="app:terminal"?await k(d):null;if(t.grantee==="app:terminal"&&!_)throw new Error("cannot mint node token: terminal grant is not active");const u=_?.id??a?.grant_id??t.grant_id??null,c=l(32).toString("base64url");if(!await n.prepare("SELECT 1 AS ok FROM executions WHERE execution_id = ? AND node_id = ? AND state = 'launching'").get(e,o))throw new Error(`cannot mint node token: execution ${e} is not launching`);return await n.prepare('INSERT INTO node_tokens (broker_process_id, node_id, token_hash, grantee, uid, sub, "grant", client_id, scopes_json, issued_at, revoked_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)').run(e,o,p(c),t.grantee,await N(t.grantee),d,u,a?.client_id??t.client_id,t.scopes_json,i),c}r(x,"mintNodeToken");async function A(n){if(!/^[A-Za-z0-9_-]{43}$/.test(n))return null;const e=await(await s()).prepare(`SELECT t.node_id, t.grantee, t.uid, t.sub, t."grant", t.client_id, t.scopes_json, n.run_id
1
+ var E=Object.defineProperty;var r=(n,e)=>E(n,"name",{value:e,configurable:!0});import{createHash as g,randomBytes as l}from"node:crypto";import{openDb as s,withCanvasWrite as k}from"./db.js";import{terminalGrant as w}from"../grants/mirror.js";import{personIdAsync as N}from"./person.js";import{uidForApp as m}from"../spaces/users.js";const p=r(n=>g("sha256").update(n).digest("hex"),"hash");async function S(n,e,o,i){const t=await n.prepare("SELECT grantee, sub, grant_id, client_id, scopes_json, run_id FROM node_rows WHERE node_id = ?").get(o);if(!t)throw new Error(`cannot mint node token: missing node ${o}`);const a=t.run_id&&t.run_id!==o?await n.prepare("SELECT sub, grant_id, client_id FROM nodes WHERE node_id = ?").get(t.run_id):void 0,d=a?.sub??t.sub??await N(),c=t.grantee==="app:terminal"?await w(d):null;if(t.grantee==="app:terminal"&&!c)throw new Error("cannot mint node token: terminal grant is not active");const u=c?.id??a?.grant_id??t.grant_id??null,_=l(32).toString("base64url");if(!await n.prepare("SELECT 1 AS ok FROM executions WHERE execution_id = ? AND node_id = ? AND state = 'launching'").get(e,o))throw new Error(`cannot mint node token: execution ${e} is not launching`);return await n.prepare('INSERT INTO node_tokens (broker_process_id, node_id, token_hash, grantee, uid, sub, "grant", client_id, scopes_json, issued_at, revoked_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL)').run(e,o,p(_),t.grantee,await m(t.grantee),d,u,a?.client_id??t.client_id,t.scopes_json,i),_}r(S,"mintNodeToken");const D=r(async(n,e)=>await k(o=>S(o,n,e,new Date().toISOString())),"mintExecutionToken");async function v(n){if(!/^[A-Za-z0-9_-]{43}$/.test(n))return null;const e=await(await s()).prepare(`SELECT t.node_id, t.grantee, t.uid, t.sub, t."grant", t.client_id, t.scopes_json, n.run_id
2
2
  FROM node_tokens t JOIN executions e ON e.execution_id = t.broker_process_id
3
3
  JOIN nodes n ON n.node_id = t.node_id
4
- WHERE t.token_hash = ? AND t.revoked_at IS NULL AND e.state != 'exited'`).get(p(n));return e?{app:e.grantee,node:e.node_id,run:e.run_id,uid:e.uid,sub:e.sub,grant:e.grant,client_id:e.client_id,scopes:e.scopes_json===null?null:JSON.parse(e.scopes_json)}:null}r(A,"resolveNodeToken");async function D(n){await m(await s(),"broker_process_id",n,new Date().toISOString())}r(D,"revokeExitedNodeToken");async function b(n){await(await s()).prepare(`UPDATE node_tokens SET revoked_at = ? WHERE revoked_at IS NULL
5
- AND broker_process_id IN (SELECT execution_id FROM executions WHERE daemon_epoch IS DISTINCT FROM ?)`).run(new Date().toISOString(),n)}r(b,"revokePriorEpochNodeTokens");async function m(n,e,o,i){await n.prepareUncached(`UPDATE node_tokens SET revoked_at = ? WHERE ${e} = ? AND revoked_at IS NULL`).run(i,o)}r(m,"revokeNodeTokens");export{x as mintNodeToken,A as resolveNodeToken,D as revokeExitedNodeToken,m as revokeNodeTokens,b as revokePriorEpochNodeTokens};
4
+ WHERE t.token_hash = ? AND t.revoked_at IS NULL AND e.state != 'exited'`).get(p(n));return e?{app:e.grantee,node:e.node_id,run:e.run_id,uid:e.uid,sub:e.sub,grant:e.grant,client_id:e.client_id,scopes:e.scopes_json===null?null:JSON.parse(e.scopes_json)}:null}r(v,"resolveNodeToken");async function b(n){await h(await s(),"broker_process_id",n,new Date().toISOString())}r(b,"revokeExitedNodeToken");async function I(n){await(await s()).prepare(`UPDATE node_tokens SET revoked_at = ? WHERE revoked_at IS NULL
5
+ AND broker_process_id IN (SELECT execution_id FROM executions WHERE daemon_epoch IS DISTINCT FROM ?)`).run(new Date().toISOString(),n)}r(I,"revokePriorEpochNodeTokens");async function h(n,e,o,i){await n.prepareUncached(`UPDATE node_tokens SET revoked_at = ? WHERE ${e} = ? AND revoked_at IS NULL`).run(i,o)}r(h,"revokeNodeTokens");export{D as mintExecutionToken,S as mintNodeToken,v as resolveNodeToken,b as revokeExitedNodeToken,h as revokeNodeTokens,I as revokePriorEpochNodeTokens};
@@ -153,6 +153,8 @@ export declare function composeIdentity(pid: number, lstart: string, deps?: {
153
153
  * probe itself failed; callers preserve that as unknown rather than absence. */
154
154
  export interface PsLivenessProcess {
155
155
  zombie: boolean;
156
+ /** The process group the process is in. */
157
+ pgid: number;
156
158
  identity: string;
157
159
  command: string;
158
160
  }
@@ -173,6 +175,7 @@ interface ProcStat {
173
175
  comm: string;
174
176
  state: string;
175
177
  ppid: number;
178
+ pgid: number;
176
179
  starttime: string;
177
180
  }
178
181
  export declare function parseProcStat(raw: string): ProcStat | null;
@@ -233,6 +236,9 @@ export declare const procfsBackendForTest: {
233
236
  * effort: returns `[]` (never throws) on any `ps` failure, exactly like
234
237
  * `killProcessGroup`'s ESRCH-swallowing. */
235
238
  export declare function descendantPids(rootPid: number): number[];
239
+ /** The pids of the live (not zombie) processes in process group `pgid`, from one process-table sample. `null` when the table cannot be read: an unreadable
240
+ * table is unknown, never an empty group. */
241
+ export declare function pidsInProcessGroup(pgid: number): number[] | null;
236
242
  /** Capture a portable process-identity fingerprint — `ps`'s `lstart` column,
237
243
  * the process's full wall-clock start time — for each of `pids`, in ONE
238
244
  * batched `ps -p <list>` call. `lstart` is a standard format keyword on BOTH