@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.
- package/dist/api/client.d.ts +6 -1
- package/dist/api/client.js +3 -3
- package/dist/api/contract.d.ts +1 -1
- package/dist/api/contract.js +1 -1
- package/dist/api/dto/execution-start.d.ts +14 -0
- package/dist/api/dto/health.d.ts +22 -0
- package/dist/api/dto/imports.d.ts +142 -0
- package/dist/api/dto/imports.js +0 -0
- package/dist/api/error-codes.d.ts +5 -0
- package/dist/api/error-codes.js +1 -1
- package/dist/api/errors.js +1 -1
- package/dist/api/index.d.ts +1 -0
- package/dist/api/index.js +1 -1
- package/dist/api/routes.d.ts +2 -0
- package/dist/api/routes.js +1 -1
- package/dist/build-root.d.ts +11 -4
- package/dist/build-root.js +1 -1
- package/dist/cli.js +7 -7
- package/dist/clients/attach/session/connection.d.ts +3 -1
- package/dist/clients/attach/session/connection.js +1 -1
- package/dist/clients/attach/viewer.js +529 -529
- package/dist/commands/api-client.d.ts +12 -4
- package/dist/commands/api-client.js +3 -3
- package/dist/commands/sys/daemon.js +1 -1
- package/dist/commands/sys/desktop.d.ts +5 -0
- package/dist/commands/sys/desktop.js +1 -0
- package/dist/commands/sys/import/move.d.ts +55 -0
- package/dist/commands/sys/import/move.js +8 -0
- package/dist/commands/sys/import/resume.d.ts +1 -0
- package/dist/commands/sys/import/resume.js +1 -0
- package/dist/commands/sys/import/start.d.ts +4 -0
- package/dist/commands/sys/import/start.js +1 -0
- package/dist/commands/sys/join.d.ts +2 -0
- package/dist/commands/sys/join.js +1 -1
- package/dist/commands/sys/machine/rotate.d.ts +1 -0
- package/dist/commands/sys/machine/rotate.js +1 -0
- package/dist/commands/sys/signin.d.ts +7 -0
- package/dist/commands/sys/signin.js +2 -0
- package/dist/commands/sys.js +1 -1
- package/dist/core/blobs/r2.d.ts +4 -0
- package/dist/core/blobs/r2.js +7 -4
- package/dist/core/canvas/daemon-owner.d.ts +1 -1
- package/dist/core/canvas/executions.d.ts +1 -1
- package/dist/core/canvas/node-tokens.d.ts +2 -0
- package/dist/core/canvas/node-tokens.js +3 -3
- package/dist/core/canvas/pid.d.ts +6 -0
- package/dist/core/canvas/pid.js +5 -5
- package/dist/core/command-plugins/compose.js +1 -1
- package/dist/core/command.d.ts +17 -8
- package/dist/core/command.js +9 -9
- package/dist/core/directory/issuer.d.ts +3 -0
- package/dist/core/directory/issuer.js +1 -1
- package/dist/core/directory/person-credential.d.ts +61 -0
- package/dist/core/directory/person-credential.js +1 -0
- package/dist/core/directory/reach.d.ts +3 -0
- package/dist/core/directory/reach.js +1 -0
- package/dist/core/grants/mirror.d.ts +3 -0
- package/dist/core/grants/mirror.js +2 -2
- package/dist/core/graph/bodies-boot-copy.d.ts +16 -2
- package/dist/core/graph/bodies-boot-copy.js +1 -1
- package/dist/core/graph/bodies.d.ts +13 -4
- package/dist/core/graph/bodies.js +1 -1
- package/dist/core/graph/deletion.d.ts +4 -1
- package/dist/core/graph/deletion.js +3 -3
- package/dist/core/graph/documents.d.ts +4 -0
- package/dist/core/graph/documents.js +9 -9
- package/dist/core/graph/reader.js +2 -2
- package/dist/core/graph/repo-sync/exchange.js +2 -2
- package/dist/core/graph/repo-sync/ref-format.d.ts +7 -2
- package/dist/core/graph/repo-sync/ref-format.js +2 -2
- package/dist/core/import/bundle/blob-keys.d.ts +10 -0
- package/dist/core/import/bundle/blob-keys.js +1 -0
- package/dist/core/import/bundle/layout.d.ts +90 -0
- package/dist/core/import/bundle/layout.js +5 -0
- package/dist/core/import/export/archive.d.ts +11 -0
- package/dist/core/import/export/archive.js +1 -0
- package/dist/core/import/export/cloud.d.ts +23 -0
- package/dist/core/import/export/cloud.js +1 -0
- package/dist/core/import/export/dump.d.ts +34 -0
- package/dist/core/import/export/dump.js +3 -0
- package/dist/core/import/export/files.d.ts +22 -0
- package/dist/core/import/export/files.js +1 -0
- package/dist/core/import/export/inline.d.ts +3 -0
- package/dist/core/import/export/inline.js +1 -0
- package/dist/core/import/export/source.d.ts +2 -0
- package/dist/core/import/export/source.js +1 -0
- package/dist/core/import/export/stores.d.ts +9 -0
- package/dist/core/import/export/stores.js +1 -0
- package/dist/core/import/load/bind.d.ts +19 -0
- package/dist/core/import/load/bind.js +15 -0
- package/dist/core/import/load/blobs.d.ts +10 -0
- package/dist/core/import/load/blobs.js +1 -0
- package/dist/core/import/load/catalog.d.ts +32 -0
- package/dist/core/import/load/catalog.js +9 -0
- package/dist/core/import/load/copy.d.ts +26 -0
- package/dist/core/import/load/copy.js +6 -0
- package/dist/core/import/load/path-map.d.ts +7 -0
- package/dist/core/import/load/path-map.js +1 -0
- package/dist/core/import/load/prepare.d.ts +37 -0
- package/dist/core/import/load/prepare.js +10 -0
- package/dist/core/import/load/rewrite.d.ts +39 -0
- package/dist/core/import/load/rewrite.js +13 -0
- package/dist/core/import/load/rollback.d.ts +3 -0
- package/dist/core/import/load/rollback.js +6 -0
- package/dist/core/import/load/run.d.ts +22 -0
- package/dist/core/import/load/run.js +3 -0
- package/dist/core/import/load/stage.d.ts +15 -0
- package/dist/core/import/load/stage.js +1 -0
- package/dist/core/import/load/stores.d.ts +24 -0
- package/dist/core/import/load/stores.js +2 -0
- package/dist/core/import/load/verify.d.ts +4 -0
- package/dist/core/import/load/verify.js +12 -0
- package/dist/core/io.js +4 -4
- package/dist/core/northlight/previews.d.ts +22 -0
- package/dist/core/northlight/previews.js +1 -0
- package/dist/core/providers/call.js +1 -1
- package/dist/core/providers/device.js +1 -1
- package/dist/core/providers/files.d.ts +2 -1
- package/dist/core/providers/files.js +1 -1
- package/dist/core/runtime/broker/cli-relay.js +1 -1
- package/dist/core/runtime/broker/model-adapter.js +3 -3
- package/dist/core/runtime/broker-ownership.d.ts +1 -0
- package/dist/core/runtime/broker-ownership.js +1 -0
- package/dist/core/runtime/broker.js +1 -1
- package/dist/core/runtime/first-run-offer.d.ts +7 -2
- package/dist/core/runtime/first-run-offer.js +10 -2
- package/dist/core/runtime/front-door.js +4 -4
- package/dist/core/runtime/revive.d.ts +5 -2
- package/dist/core/runtime/revive.js +2 -2
- package/dist/core/runtime/runner/blaxel/runner.js +3 -3
- package/dist/core/runtime/runner/index.d.ts +1 -0
- package/dist/core/runtime/runner/index.js +1 -1
- package/dist/core/runtime/runner/machine/call.d.ts +8 -0
- package/dist/core/runtime/runner/machine/call.js +3 -0
- package/dist/core/runtime/runner/machine/environment.d.ts +1 -0
- package/dist/core/runtime/runner/machine/environment.js +1 -0
- package/dist/core/runtime/runner/machine/executor.d.ts +29 -0
- package/dist/core/runtime/runner/machine/executor.js +1 -0
- package/dist/core/runtime/runner/machine/machine-executor.test.d.ts +1 -0
- package/dist/core/runtime/runner/machine/machine-executor.test.js +4 -0
- package/dist/core/runtime/runner/machine/ops.d.ts +127 -0
- package/dist/core/runtime/runner/machine/ops.js +1 -0
- package/dist/core/runtime/runner/machine/runner.d.ts +12 -0
- package/dist/core/runtime/runner/machine/runner.js +1 -0
- package/dist/core/runtime/runner/types.d.ts +7 -1
- package/dist/core/runtime/warm-pool.d.ts +2 -0
- package/dist/core/runtime/warm-pool.js +1 -1
- package/dist/core/secrets/env-rows.d.ts +1 -0
- package/dist/core/secrets/env-rows.js +1 -1
- package/dist/core/storage/purge.d.ts +6 -0
- package/dist/core/storage/purge.js +4 -0
- package/dist/core/subscription-state.d.ts +1 -0
- package/dist/core/subscription-state.js +1 -1
- package/dist/daemon/api/bridge.js +3 -3
- package/dist/daemon/api/handlers/attach.js +1 -1
- package/dist/daemon/api/handlers/crons.js +1 -1
- package/dist/daemon/api/handlers/daemon.js +1 -1
- package/dist/daemon/api/handlers/execution-start.js +2 -2
- package/dist/daemon/api/handlers/files.js +1 -1
- package/dist/daemon/api/handlers/imports.d.ts +2 -0
- package/dist/daemon/api/handlers/imports.js +1 -0
- package/dist/daemon/api/handlers/machine-credential.d.ts +2 -0
- package/dist/daemon/api/handlers/machine-credential.js +1 -0
- package/dist/daemon/api/handlers/profiles.js +1 -1
- package/dist/daemon/api/handlers/providers.js +1 -1
- package/dist/daemon/api/handlers/runs.js +1 -1
- package/dist/daemon/api/import-auth.d.ts +6 -0
- package/dist/daemon/api/import-auth.js +1 -0
- package/dist/daemon/api/operations.d.ts +2 -2
- package/dist/daemon/api/operations.js +1 -1
- package/dist/daemon/api/router.d.ts +7 -1
- package/dist/daemon/api/router.js +1 -1
- package/dist/daemon/api/server.js +1 -1
- package/dist/daemon/boot-migrations.js +1 -1
- package/dist/daemon/broker-signals/bus.d.ts +3 -2
- package/dist/daemon/broker-signals/bus.js +1 -1
- package/dist/daemon/cron/armed-context.d.ts +1 -0
- package/dist/daemon/cron/armed-context.js +1 -1
- package/dist/daemon/cron/closed.d.ts +31 -0
- package/dist/daemon/cron/closed.js +1 -0
- package/dist/daemon/cron/ended.js +2 -2
- package/dist/daemon/cron/lease-recovery.js +1 -1
- package/dist/daemon/cron/machine-exec.d.ts +11 -0
- package/dist/daemon/cron/machine-exec.js +1 -0
- package/dist/daemon/cron/machine-fire.d.ts +26 -0
- package/dist/daemon/cron/machine-fire.js +1 -0
- package/dist/daemon/cron/recurrence.d.ts +14 -0
- package/dist/daemon/cron/recurrence.js +1 -0
- package/dist/daemon/cron/run-token.d.ts +7 -0
- package/dist/daemon/cron/run-token.js +1 -0
- package/dist/daemon/cron-run.js +2 -2
- package/dist/daemon/crtrd-cli.js +1 -1
- package/dist/daemon/crtrd.d.ts +3 -3
- package/dist/daemon/crtrd.js +14 -14
- package/dist/daemon/drain.d.ts +25 -0
- package/dist/daemon/drain.js +9 -0
- package/dist/daemon/due-sources.d.ts +1 -1
- package/dist/daemon/due-sources.js +1 -1
- package/dist/daemon/due.js +8 -8
- package/dist/daemon/fleet.js +5 -5
- package/dist/daemon/import-dump-run.d.ts +1 -0
- package/dist/daemon/import-dump-run.js +5 -0
- package/dist/daemon/machine-daemon.d.ts +10 -0
- package/dist/daemon/machine-daemon.js +1 -0
- package/dist/daemon/machine-link/client.d.ts +29 -0
- package/dist/daemon/machine-link/client.js +1 -0
- package/dist/daemon/machine-link/config.d.ts +45 -0
- package/dist/daemon/machine-link/config.js +3 -0
- package/dist/daemon/machine-link/credential.d.ts +7 -0
- package/dist/daemon/machine-link/credential.js +2 -0
- package/dist/daemon/machine-link/forward.d.ts +16 -0
- package/dist/daemon/machine-link/forward.js +1 -0
- package/dist/daemon/machine-link/frames.d.ts +49 -0
- package/dist/daemon/machine-link/frames.js +1 -0
- package/dist/daemon/machine-link/holder.d.ts +73 -0
- package/dist/daemon/machine-link/holder.js +9 -0
- package/dist/daemon/machine-link/move-notify.d.ts +4 -0
- package/dist/daemon/machine-link/move-notify.js +1 -0
- package/dist/daemon/manage.d.ts +19 -3
- package/dist/daemon/manage.js +3 -3
- package/dist/daemon/reconcilers/cron-lane.js +1 -1
- package/dist/daemon/reconcilers/delivery-lane.js +2 -2
- package/dist/daemon/reconcilers/import-lane.d.ts +9 -0
- package/dist/daemon/reconcilers/import-lane.js +1 -0
- package/dist/daemon/reconcilers/node-lifecycle/freeze-lane.js +1 -1
- package/dist/daemon/reconcilers/node-lifecycle/remote-wake.d.ts +3 -0
- package/dist/daemon/reconcilers/node-lifecycle/remote-wake.js +1 -1
- package/dist/daemon/reconcilers/node-lifecycle/tick.js +1 -1
- package/dist/daemon/reconcilers/purge-due-work.d.ts +13 -0
- package/dist/daemon/reconcilers/purge-due-work.js +1 -0
- package/dist/daemon/reconcilers/row-claim.d.ts +1 -0
- package/dist/daemon/reconcilers/row-claim.js +2 -2
- package/dist/daemon/runs/observer.js +3 -3
- package/package.json +2 -2
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +13 -13
- package/scripts/sandbox-start.mjs +185 -2
package/dist/daemon/due.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
var D=Object.defineProperty;var a=(n,e)=>D(n,"name",{value:e,configurable:!0});import{openDb as E}from"../core/canvas/db.js";import{isPidPresent as R}from"../core/canvas/pid.js";import{isProfilePaused as p}from"../core/profiles/manifest.js";import{hasUnseenWakeMail as f}from"../core/canvas/deliveries.js";import{listPendingProviderRetries as
|
|
2
|
-
WHERE status IN ('active', 'idle') AND kind != 'human' AND ${
|
|
3
|
-
AND pi_pid IS NULL AND respawn_not_before IS NOT NULL`),async e=>!p(e.profile_id)&&await m({node_id:e.node_id,status:e.status,intent:e.intent}))).map(e=>
|
|
4
|
-
WHERE deadline_at IS NOT NULL AND outcome_at IS NULL AND status IN ('active', 'idle') AND kind != 'human'`)).filter(e=>!p(e.profile_id)).map(e=>
|
|
5
|
-
WHERE kind = 'blaxel' AND recipe_revision > COALESCE(image_revision, 0) AND ${
|
|
1
|
+
var D=Object.defineProperty;var a=(n,e)=>D(n,"name",{value:e,configurable:!0});import{openDb as E}from"../core/canvas/db.js";import{isPidPresent as R}from"../core/canvas/pid.js";import{isProfilePaused as p}from"../core/profiles/manifest.js";import{hasUnseenWakeMail as f}from"../core/canvas/deliveries.js";import{listPendingProviderRetries as g}from"../core/canvas/recovery.js";import{listSubmitRequestedRequests as x}from"../core/human/requests.js";import{storageConfig as I}from"../core/storage/client.js";import{emitEvent as O}from"../core/events/emit.js";import{wantsExecution as m}from"./reconcilers/node-lifecycle/wants-execution.js";import{dueAtMs as y}from"./reconcilers/fault-retry-backstop.js";import{UNUSED_UPLOAD_TTL_MS as T}from"./reconcilers/storage-due-work.js";import{RECIPE_HAS_CONTENT_SQL as A}from"./api/environment-of.js";import{busyOnlyLanes as b,collectDue as h,declareBusyOnly as o,dueSourceLanes as M,registerDueSource as i}from"./due-sources.js";import{filterAsync as v}from"../core/async-array.js";const _=a(async(n,...e)=>await(await E()).prepare(n).all(...e),"rows"),r=a(async n=>await(await E()).prepare(n).get(),"first"),u=a(n=>{if(n==null)return null;const e=Date.parse(n);return Number.isFinite(e)?e:null},"iso"),c=a(n=>{let e=null;for(const t of n)t!==null&&(e===null||t<e)&&(e=t);return e},"earliest"),L="node_id NOT IN (SELECT node_id FROM warm_pool)";let N=!1;async function S(){if(N)return;N=!0,o("brokerSupervision","acts only on live broker processes, which already make busy true"),o("controllerDeath","acts only on live controller processes"),o("pollRunObservers","follows live pids only"),o("bashDeadline","acts only on running bash jobs, which already make busy true"),o("storageMaintenance","retention and placement housekeeping: never time-critical, and it must not wake or hold a sleeping runtime"),o("purgeDueWork","physical purge of objects soft-deleted over 30 days ago: housekeeping, and a tombstone a live object references stays due forever, so it must not wake or hold a sleeping runtime"),o("ledgerSandbox","bills awake time of executions that are busy or just exited; a sleeping runtime owes nothing until it wakes, and the lane must not wake it"),o("ledgerStorage","monthly storage billing: never time-critical, and it must not wake or hold a sleeping runtime"),o("ledgerR2","moves closed months to R2: housekeeping that must not wake or hold a sleeping runtime"),i("nodeLifecycle",{nextDue:a(async()=>c((await v(await _(`SELECT node_id, status, intent, profile_id, respawn_not_before FROM node_rows
|
|
2
|
+
WHERE status IN ('active', 'idle') AND kind != 'human' AND ${L}
|
|
3
|
+
AND pi_pid IS NULL AND respawn_not_before IS NOT NULL`),async e=>!p(e.profile_id)&&await m({node_id:e.node_id,status:e.status,intent:e.intent}))).map(e=>u(e.respawn_not_before))),"nextDue")}),i("nodeDeadline",{nextDue:a(async()=>c((await _(`SELECT deadline_at, profile_id FROM node_rows
|
|
4
|
+
WHERE deadline_at IS NOT NULL AND outcome_at IS NULL AND status IN ('active', 'idle') AND kind != 'human'`)).filter(e=>!p(e.profile_id)).map(e=>u(e.deadline_at))),"nextDue")}),i("structuredRequests",{nextDue:a(async e=>await r("SELECT 1 FROM structured_requests WHERE state != 'pending' AND effects_done_at IS NULL LIMIT 1")!==void 0?e:(await r("SELECT MIN(deadline_at) AS at FROM structured_requests WHERE state = 'pending' AND deadline_at IS NOT NULL"))?.at??null,"nextDue")}),i("recipeImage",{nextDue:a(async e=>c((await _(`SELECT image_building_at FROM environments
|
|
5
|
+
WHERE kind = 'blaxel' AND recipe_revision > COALESCE(image_revision, 0) AND ${A}`)).map(t=>t.image_building_at===null?e:Math.max(e,u(t.image_building_at)??e))),"nextDue")}),i("cronLane",{nextDue:a(async()=>{const e=(await _(`SELECT fire_at, profile FROM crons
|
|
6
6
|
WHERE state = 'active' AND run_state = 'idle'
|
|
7
|
-
AND NOT (held = 1 AND recur IS NULL AND expires_at IS NULL)`)).filter(
|
|
8
|
-
AND NOT EXISTS (SELECT 1 FROM upload_uses x WHERE x.upload_id = u.upload_id)`))?.at,l=
|
|
7
|
+
AND NOT (held = 1 AND recur IS NULL AND expires_at IS NULL)`)).filter(s=>!p(s.profile)).map(s=>u(s.fire_at)),t=(await _("SELECT expires_at FROM crons WHERE expires_at IS NOT NULL AND run_state = 'idle'")).map(s=>u(s.expires_at));return c([...e,...t])},"nextDue")});const n=a(async e=>async()=>(await r(`SELECT MIN(next_attempt_at) AS at FROM ${e} WHERE state = 'pending' AND canceled_at IS NULL AND next_attempt_at IS NOT NULL`))?.at??null,"attempts");i("deliveryLane",{nextDue:await n("deliveries")}),i("actionLane",{nextDue:await n("daemon_actions")}),i("pendingReviewSubmit",{nextDue:a(async e=>(await x()).length>0?e:null,"nextDue")}),i("storageDueWork",{nextDue:a(async e=>{if(I()===null)return null;if(await r("SELECT 1 FROM uploads WHERE delete_pending_since IS NOT NULL LIMIT 1")!==void 0||await r("SELECT 1 FROM shares WHERE status = 'off' AND object_deleted_at IS NULL LIMIT 1")!==void 0)return e;const t=(await r("SELECT MIN(expires_at) AS at FROM shares WHERE status = 'on' AND expires_at IS NOT NULL"))?.at,s=(await r(`SELECT MIN(created_at) AS at FROM uploads u WHERE delete_pending_since IS NULL
|
|
8
|
+
AND NOT EXISTS (SELECT 1 FROM upload_uses x WHERE x.upload_id = u.upload_id)`))?.at,l=u(s);return c([u(t),l===null?null:l+T])},"nextDue")}),i("importLane",{nextDue:a(async e=>await r(`SELECT 1 FROM import_runs WHERE state IN ('loading', 'committed') AND (claimed_by IS NULL OR lease_expires_at IS NULL OR lease_expires_at <= '${new Date(e).toISOString()}')`)!==void 0?e:null,"nextDue")}),i("faultRetryBackstop",{nextDue:a(async()=>c((await g()).map(e=>y(e))),"nextDue")})}a(S,"installDueSources");const w=12e4;async function C(n){const e=await _(`SELECT n.node_id, n.status, n.intent, n.profile_id, n.pi_pid, n.frozen_at, n.respawn_not_before, n.created, n.launched_at,
|
|
9
9
|
n.turn_open_at, n.turn_closed_at,
|
|
10
10
|
(SELECT x.started_at FROM executions x WHERE x.node_id = n.node_id AND x.state != 'exited') AS exec_started_at
|
|
11
11
|
FROM node_rows n
|
|
12
|
-
WHERE n.status IN ('active', 'idle') AND n.kind != 'human' AND n.${
|
|
12
|
+
WHERE n.status IN ('active', 'idle') AND n.kind != 'human' AND n.${L}`);for(const t of e){if(p(t.profile_id)||!await m({node_id:t.node_id,status:t.status,intent:t.intent}))continue;if(t.pi_pid!==null&&R(t.pi_pid)){if(t.turn_open_at!==null&&t.turn_closed_at===null)return!0;const d=u(t.exec_started_at);if(t.turn_open_at===null&&d!==null&&n-d<w||await f(t.node_id))return!0;continue}if(t.respawn_not_before!==null)continue;if(t.pi_pid!==null||t.frozen_at!==null||t.status==="active")return!0;const l=Math.max(...[t.created,t.launched_at,t.exec_started_at].map(d=>u(d)??0));if(l>0&&n-l<w||t.status==="idle"&&await f(t.node_id))return!0}return!1}a(C,"nodesOweWork");async function K(n=Date.now()){await S();const{dueNow:e,nextDueMs:t}=await h(n,(l,d)=>{O({level:"warn",event:"daemon.due.source_failed",error:d,fields:{lane:l}})});return{busy:e||await C(n)||await r("SELECT 1 FROM crons WHERE run_state = 'running' LIMIT 1")!==void 0||await r("SELECT 1 FROM jobs j JOIN objects o ON o.id = j.object WHERE j.status = 'running' AND o.deleted_at IS NULL LIMIT 1")!==void 0,next_due_at:t===null?null:new Date(t).toISOString()}}a(K,"dueReport");async function V(){return await S(),[...M(),...b()]}a(V,"coveredLanes");export{w as START_GRACE_MS,V as coveredLanes,K as dueReport,S as installDueSources};
|
package/dist/daemon/fleet.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var P=Object.defineProperty;var l=(i,e)=>P(i,"name",{value:e,configurable:!0});import{getNode as w,getRow as R}from"../core/canvas/index.js";import{revokeExitedNodeToken as T}from"../core/canvas/node-tokens.js";import{PROFILE_PAUSE_STOP_REASON as _,absorbProfilePauseStop as N,endExecution as y,hasLaunchingExecution as S,hasProfilePauseStop as F,hasRunningRemoteExecution as b,liveExecutionForNode as g,liveExecutionNodeIds as C,markProfilePauseStop as k,recordExecutionExit as O,takeObservedExit as M}from"../core/canvas/executions.js";import{isProfilePaused as z}from"../core/profiles/manifest.js";import{fullName as p}from"../core/canvas/labels.js";import{isSafeNodeId as o}from"../core/canvas/paths.js";import{hasIdleReleaseIntent as A,transition as v}from"../core/runtime/lifecycle.js";import{publishNotice as L}from"../core/runtime/close.js";import{readFault as B,readInterruptedTurn as D,readTransportPoisonVerdict as W}from"../core/canvas/recovery.js";import{hasPendingWake as q}from"./reconcilers/live-obligation.js";import{clearRespawnPolicy as h,deferRespawnWhileOffline as G,MAX_RESPAWN_ATTEMPTS as H,OFFLINE_RESPAWN_RETRY_MS as Y,recordRespawnPolicy as U,respawnBackoffMs as V}from"./reconcilers/node-lifecycle/respawn-policy.js";import{probeOnline as j}from"../core/runtime/connectivity.js";import{runnerForExecution as K}from"../core/runtime/runner/index.js";import{reviveNode as X}from"../core/runtime/revive.js";import{isStartRefusal as J}from"../core/runtime/start-check.js";import{pushUrgent as m}from"../core/feed/feed.js";import{emitEvent as u}from"../core/events/emit.js";import{brokerThresholdsForDaemon as
|
|
1
|
+
var P=Object.defineProperty;var l=(i,e)=>P(i,"name",{value:e,configurable:!0});import{getNode as w,getRow as R}from"../core/canvas/index.js";import{revokeExitedNodeToken as T}from"../core/canvas/node-tokens.js";import{PROFILE_PAUSE_STOP_REASON as _,absorbProfilePauseStop as N,endExecution as y,hasLaunchingExecution as S,hasProfilePauseStop as F,hasRunningRemoteExecution as b,liveExecutionForNode as g,liveExecutionNodeIds as C,markProfilePauseStop as k,recordExecutionExit as O,takeObservedExit as M}from"../core/canvas/executions.js";import{isProfilePaused as z}from"../core/profiles/manifest.js";import{fullName as p}from"../core/canvas/labels.js";import{isSafeNodeId as o}from"../core/canvas/paths.js";import{hasIdleReleaseIntent as A,transition as v}from"../core/runtime/lifecycle.js";import{publishNotice as L}from"../core/runtime/close.js";import{readFault as B,readInterruptedTurn as D,readTransportPoisonVerdict as W}from"../core/canvas/recovery.js";import{hasPendingWake as q}from"./reconcilers/live-obligation.js";import{clearRespawnPolicy as h,deferRespawnWhileOffline as G,MAX_RESPAWN_ATTEMPTS as H,OFFLINE_RESPAWN_RETRY_MS as Y,recordRespawnPolicy as U,respawnBackoffMs as V}from"./reconcilers/node-lifecycle/respawn-policy.js";import{probeOnline as j}from"../core/runtime/connectivity.js";import{runnerForExecution as K}from"../core/runtime/runner/index.js";import{reviveNode as X}from"../core/runtime/revive.js";import{isStartRefusal as J}from"../core/runtime/start-check.js";import{MachineOfflineError as Q}from"../core/runtime/runner/machine/ops.js";import{pushUrgent as m}from"../core/feed/feed.js";import{emitEvent as u}from"../core/events/emit.js";import{brokerThresholdsForDaemon as Z}from"../core/runtime/fleet.js";import{isRecoveryHeld as I}from"../core/runtime/recovery-hold.js";async function E(i,e){const r=await B(i.node_id),t=r===null?"":`The broker reported a ${r.kind} fault on ${r.link} (${r.op}) before it exited: ${r.message}
|
|
2
2
|
|
|
3
3
|
`,s=e===void 0?"The daemon did not observe an exit status.":`The observed exit status was code ${e.code??"null"}, signal ${e.signal??"null"}.`;return`\u26A0 Spawn failed \u2014 \`${i.name}\` (${i.kind}) never started.
|
|
4
4
|
|
|
@@ -6,16 +6,16 @@ Its pi vehicle exited before the session came up (no pi_session_id was ever reco
|
|
|
6
6
|
|
|
7
7
|
`+t+`Inspect canonical event diagnostics with \`crtr sys logs --node ${i.node_id}\`.
|
|
8
8
|
|
|
9
|
-
If the work still needs doing, re-spawn it.`}l(
|
|
9
|
+
If the work still needs doing, re-spawn it.`}l(E,"bootFailureBody");async function $(i,e){await m(i.node_id,await E(i,e),{from:i.node_id})}l($,"surfaceBootFailure");function x(i){return`\u26A0 Out of memory \u2014 \`${i.name}\` (${i.kind}) was killed because its app's processes together passed the app's memory limit.
|
|
10
10
|
|
|
11
|
-
Every process of the run was stopped. The daemon does not restart it, because it would reach the same limit. Reduce what the run holds in memory, or ask the person to raise the app's limit (\`crtr sys limits set --app ${i.grantee??"app:terminal"} --memory <size>\`), then revive the node.`}l(x,"outOfMemoryBody");async function
|
|
11
|
+
Every process of the run was stopped. The daemon does not restart it, because it would reach the same limit. Reduce what the run holds in memory, or ask the person to raise the app's limit (\`crtr sys limits set --app ${i.grantee??"app:terminal"} --memory <size>\`), then revive the node.`}l(x,"outOfMemoryBody");async function ee(i){const e=await W(i.node_id),r=e===null?"":`
|
|
12
12
|
|
|
13
13
|
Provider rotation observed ${e.consecutivePasses} consecutive route-queue passes with no HTTP response (tried ${e.routes.join(", ")}). Check network and provider transport before reviving.`,t=`\u26A0 Crash loop \u2014 \`${i.name}\` (${i.kind}) kept dying without settling a turn.
|
|
14
14
|
|
|
15
15
|
Its session started successfully, but six consecutive relaunches made no progress, so the daemon stopped retrying.`+r+`
|
|
16
16
|
|
|
17
|
-
Fix the crash cause, then revive the node if the work still needs doing.`;await m(i.node_id,t,{from:i.node_id})}l(
|
|
17
|
+
Fix the crash cause, then revive the node if the work still needs doing.`;await m(i.node_id,t,{from:i.node_id})}l(ee,"surfaceCrashLoop");async function te(i,e){const r=e instanceof Error?e.message:String(e),t=`\u26A0 Recovery failed \u2014 \`${i.name}\` (${i.kind}) could not be relaunched.
|
|
18
18
|
|
|
19
19
|
The daemon refused this recovery before starting a broker: ${r}
|
|
20
20
|
|
|
21
|
-
The node is now dead. Fix the recorded launch configuration, then revive it if the work still needs doing.`;await m(i.node_id,t,{from:i.node_id})}l(
|
|
21
|
+
The node is now dead. Fix the recorded launch configuration, then revive it if the work still needs doing.`;await m(i.node_id,t,{from:i.node_id})}l(te,"surfaceRecoveryFailure");function re(i){return i?.cycle_pending!==!0}l(re,"retryResumeMode");function ie(i,e,r=!0){return i===null||i.status==="done"||i.status==="canceled"||i.status==="dead"?"forget":i.intent==="refresh"?"respawn-fresh":A(i)?"dormant":i.lifecycle==="resident"&&!e&&!r&&i.pi_session_id!=null?"release-idle":i.pi_session_id!=null?re(i)?"respawn-resume":"respawn-cycle":e||i.fork_from!=null?"respawn-fresh":"boot-failure"}l(ie,"classifyDeadNode");const se=3e4;class Pe{static{l(this,"DaemonFleet")}#a;#n;#r;#o;#e=new Map;#t=new Set;#l=Number.NEGATIVE_INFINITY;constructor(e){this.#a=e.epoch,this.#n=e.enqueue,this.#r=e.now??Date.now,this.#o=e.probeOnline??(()=>j())}epoch(){return this.#a}register(e,r){if(this.#e.has(e))throw new Error(`fleet: ${e} already has a live broker entry \u2014 refusing a second launch (programming error)`);if(r.pid===null)throw new Error(`fleet: refusing to register ${e} with no pid`);this.#t.delete(e),this.#e.set(e,{pid:r.pid,handle:r})}async has(e){return this.#e.has(e)||await b(e)}async isClaimed(e){return this.#e.has(e)||this.#t.has(e)||await b(e)||await S(e,this.#a)}get(e){return this.#e.get(e)}size(){return this.#e.size}async#c(){const e=await C();for(const r of this.#e.keys())e.add(r);for(const r of this.#t)e.add(r);return e}async occupancy(){return(await this.#c()).size}async reserveSlot(e){if(this.#t.has(e)||this.#e.has(e))return!0;const{automaticReviveCap:r}=Z(),t=await this.#c();if(this.#t.has(e)||this.#e.has(e))return!0;if(t.delete(e),t.size<r)return this.#t.has(e)||this.#t.add(e),!0;const s=this.#r();return s-this.#l>=se&&(this.#l=s,u({level:"warn",event:"broker.capacity.revive_deferred",fields:{live_count:t.size,hard_cap:r}})),!1}releaseReservation(e){this.#t.delete(e)}entries(){return this.#e.entries()}async interruptForProfilePause(e,r=this.#e.get(e)?.pid){if(r===void 0){const t=await g(e);if(t===null||t.runner==="process"||t.state!=="running")return;await k(e),(await K(t.runner).stop(t,{reason:_})).confirmed&&await y(t.executionId,{reason:_});return}await k(e);try{process.kill(r,"SIGTERM")}catch{}}async hasProfilePauseInterruption(e){return await F(e)}forget(e){this.#e.delete(e),this.#t.delete(e)}#i=new Set;async exitsSettled(){for(;this.#i.size>0;)await Promise.allSettled([...this.#i])}async onChildExit(e,r,t){const s=this.#u(e,r,t);this.#i.add(s);try{await s}finally{this.#i.delete(s)}}async#u(e,r,t){const s=t??(await g(e))?.executionId;t&&await T(t),this.#e.delete(e),s!==void 0&&(await O(s,r),await y(s,{reason:"exited"})),this.#n(async()=>{const a=await w(e),n=a?.launched_at==null?Number.NaN:Date.parse(a.launched_at),d=Number.isFinite(n)?Math.max(0,this.#r()-n):null,f=a?.respawn_failures??0;this.#f(e,r,d,f)})}async deliverExit(e,r){await this.onChildExit(e,r)}async applyDeadRowPolicy(e){await this.#h(e,await M(e))}async terminalizeIfStillNonTerminal(e,r,t,s,a){const n=await w(e);return n===null||n.status==="done"||n.status==="canceled"||n.status==="dead"?!1:(await this.#s(e,n,{event:r,reason:s,detail:a,wake:`Child crashed \u2014 ${p(n)} (${e}) could not be relaunched and is now dead. It stays dead until you revive it \u2014 \`crtr node lifecycle revive ${e}\`.`,surface:l(()=>te(n,t),"surface")}),!0)}dispose(){}#f(e,r,t,s){u({level:"info",event:"broker.exit.observed",...o(e)?{node_id:e}:{},fields:{...o(e)?{}:{affected_node_id:e},code:r.code,signal:r.signal,uptime_ms:t,consecutive_failures:s}})}async#h(e,r){return this.#d(e,r)}async#d(e,r){const t=await w(e);if(t!==null&&z(t.profile_id))return await h(e),"forget";if(await N(e))return await h(e),"forget";if(await I(e))return"dormant";const s=(await D(e))?.interrupted===!0;if(r?.oomKilled===!0&&t!==null&&t.status!=="done"&&t.status!=="canceled"&&t.status!=="dead")return await h(e),await this.#s(e,t,{event:"broker.out_of_memory",reason:"out_of_memory",detail:{schema:"crtr.node-outcome-detail/v1",message:x(t)},wake:`Child out of memory \u2014 ${p(t)} (${e}) used more memory than its app is allowed and is now dead. It is not restarted automatically. Revive it with \`crtr node lifecycle revive ${e}\` once the work needs less memory or the cap is raised (\`crtr sys limits set\`).`,surface:l(async()=>{await m(e,x(t),{from:e})},"surface")}),"forget";const a=ie(t,s,await q(e));switch(a){case"forget":await h(e);break;case"dormant":await h(e);break;case"release-idle":await h(e),await v(e,"release"),u({level:"info",event:"broker.recovery.released",...o(e)?{node_id:e}:{},fields:{...o(e)?{}:{affected_node_id:e}}});break;case"boot-failure":await this.#s(e,t,{event:"broker.boot.failed",reason:"boot_failed",detail:{schema:"crtr.node-outcome-detail/v1",message:await E(t,r)},wake:`Child crashed \u2014 ${p(t)} (${e}) exited before its session ever came up and is now dead. It stays dead until you revive it \u2014 \`crtr node lifecycle revive ${e}\`.`,surface:l(()=>$(t,r),"surface")});break;case"respawn-fresh":case"respawn-cycle":case"respawn-resume":await this.#p(e,t,a,s);break}return a}async#p(e,r,t,s){const a=await R(e);if(a===null)return;const n=a.respawn_failures??0;if(!await this.#o()){await G(e,n,this.#r()),u({level:"info",event:"broker.respawn.offline_deferred",...o(e)?{node_id:e}:{},fields:{...o(e)?{}:{affected_node_id:e},action:t,retry_in_ms:Y,consecutive_failures:n}});return}const d=a.respawn_not_before==null,f=d?await U(a,this.#r()):{failures:n,notBefore:a.respawn_not_before,exhausted:n>=H};if(f.exhausted){const c=`Child crashed \u2014 ${p(r)} (${e}) kept dying without settling a turn (${f.failures} consecutive launches) and is now dead. It stays dead until you revive it \u2014 \`crtr node lifecycle revive ${e}\`.`;await this.#s(e,r,{event:"broker.respawn.exhausted",reason:"crash_looped",detail:{schema:"crtr.node-outcome-detail/v1",message:c,respawn_failures:f.failures},wake:c,...r.pi_session_id!=null?{surface:l(()=>ee(r),"surface")}:{}});return}if(d&&f.notBefore!==null){u({level:"info",event:"broker.respawn.deferred",...o(e)?{node_id:e}:{},fields:{...o(e)?{}:{affected_node_id:e},action:t,backoff_ms:V(f.failures),consecutive_failures:f.failures}});return}u({level:"info",event:"broker.respawn.started",...o(e)?{node_id:e}:{},fields:{...o(e)?{}:{affected_node_id:e},action:t,consecutive_failures:f.failures}});try{await X(e,{capacity:"freeze",resume:t==="respawn-resume",wakeReason:t==="respawn-resume"&&s?"runtime-restart-abort":void 0,recovery:!0})}catch(c){if(J(c)||c instanceof Q){await h(e),await v(e,"release"),u({level:"warn",event:"node.start.refused",node_id:e,error:c,fields:{enact:"respawn"}});return}u({level:"error",event:"broker.respawn.failed",...o(e)?{node_id:e}:{},error:c,fields:{...o(e)?{}:{affected_node_id:e},action:t}}),await this.terminalizeIfStillNonTerminal(e,"broker.respawn.refused",c,"launch_failed",{schema:"crtr.node-outcome-detail/v1",message:c instanceof Error?c.message:String(c)})}}async#s(e,r,t){u({level:"error",event:t.event,...o(e)?{node_id:e}:{fields:{affected_node_id:e}}}),await v(e,"crash",{reason:t.reason,outcome:{detail:t.detail}});try{await(t.surface??(()=>$(r)))()}catch(s){u({level:"error",event:"broker.boot_failure_surface.failed",...o(e)?{node_id:e}:{fields:{affected_node_id:e}},error:s})}await L(e,"notice.crash",t.wake,{reason:"child-crashed",child:e})}}export{Pe as DaemonFleet,ie as classifyDeadNode,re as retryResumeMode,$ as surfaceBootFailure};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runImportDump(out: string | undefined, kind: string | undefined): Promise<number>;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
var a=Object.defineProperty;var s=(r,t)=>a(r,"name",{value:t,configurable:!0});import{closeAllDbs as n,openDb as d}from"../core/canvas/db.js";import{chmodSync as m,writeFileSync as p}from"node:fs";import{join as c}from"node:path";import{dumpBodiesPre as f,dumpRows as l}from"../core/import/export/dump.js";import{STORE_FILE as w}from"../core/import/export/cloud.js";import{readStores as u}from"../core/import/export/stores.js";async function v(r,t){try{if(t==="install-id"){const o=await(await d()).prepare("SELECT value FROM canvas_meta WHERE key = 'install_id'").get();if(o===void 0)throw new Error("this canvas database has no install id");return process.stdout.write(`${JSON.stringify({installId:o.value})}
|
|
2
|
+
`),0}if(t==="bodies"&&r!==void 0&&r!=="")return process.stdout.write(`${JSON.stringify(await f(r))}
|
|
3
|
+
`),0;if(r===void 0||t!=="machine"&&t!=="hosted")throw new Error("usage: import-dump <out dir> machine|hosted|bodies|install-id");const e=await l(r,t),i=c(r,w);return p(i,JSON.stringify(await u()),{mode:384}),m(i,384),process.stdout.write(`${JSON.stringify(e)}
|
|
4
|
+
`),0}catch(e){return process.stderr.write(`${e.message}
|
|
5
|
+
`),1}finally{await n()}}s(v,"runImportDump");export{v as runImportDump};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { HealthDTO } from '../api/index.js';
|
|
2
|
+
/** `/healthz` of the machine role. The daemon is up and serving whatever the link does, so `ok` stays true (a start waiting for it must not fail on an offline Mac);
|
|
3
|
+
* the link is its own fact, and the phase reads `ready` only while it is up. `signed_in` is false on a computer with no person sign-in, which opens no link
|
|
4
|
+
* and runs nothing. No brokers are reconciled here: this role holds no database. */
|
|
5
|
+
export declare function machineHealth(linkConnected: boolean, signedIn: boolean): HealthDTO;
|
|
6
|
+
/** Start the machine role and return its teardown. Throws when `runtime/machine.json` is missing, or the machine credential is while the person is signed
|
|
7
|
+
* in: a machine role with nothing to link to must not look like a running daemon. With no person sign-in (`crtr sys signout`, or the file gone any other
|
|
8
|
+
* way) it opens no link and stops every broker and running machine cron of this home before it serves `/healthz` (design-crtr-northlight-signin
|
|
9
|
+
* component 5): the control plane's start and wake calls then get `machine_offline`, and nodes wait with their mail for the next sign-in. */
|
|
10
|
+
export declare function runMachineDaemon(): Promise<() => Promise<void>>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var _=Object.defineProperty;var c=(e,n)=>_(e,"name",{value:n,configurable:!0});import{chmodSync as M,existsSync as S,mkdirSync as h,unlinkSync as g,writeFileSync as I,rmSync as v}from"node:fs";import{createServer as k}from"node:http";import{dirname as T}from"node:path";import{apiSocketPath as E}from"../core/canvas/paths.js";import{bindDaemonEventSource as R}from"../core/events/source.js";import{emitEvent as f}from"../core/events/emit.js";import{createMachineExecutor as P,stopAllBrokers as x}from"../core/runtime/runner/machine/executor.js";import{readPersonCredential as C}from"../core/directory/person-credential.js";import{socketDir as F}from"../core/layout.js";import{isDaemonRunning as G,pidfilePath as u,readPidfile as b}from"./pidfile.js";import{cancelMachineCron as N,runMachineCron as D,stopOrphanedMachineCrons as O}from"./cron/machine-exec.js";import{startMachineLink as U}from"./machine-link/client.js";import{deleteMoveRecord as w,readMachineConfig as Y,readMachineCredential as y,readMoveRecord as A,writeMoveRecord as z}from"./machine-link/config.js";import{notifyMoveFinished as H}from"./machine-link/move-notify.js";function j(e,n){return{ok:!0,daemon_up:!0,store_accessible:!1,brokers_reconciled:!1,link_connected:e,signed_in:n,startup_phase:e?"ready":"recovering"}}c(j,"machineHealth");async function se(){const e=Y();if(e===void 0)throw new Error("crtrd --role machine needs runtime/machine.json: this install has not been imported as a machine");const n=C()!==void 0;if(n&&y(),G())throw new Error("a crtrd is already running for this home");R(),n||(await x(),await O(),f({level:"info",event:"daemon.machine.signed_out",fields:{environment:e.environmentId}})),h(F(),{recursive:!0,mode:493});const a=E();S(a)&&g(a);let r;const t=k((s,d)=>{const p=(s.method??"GET")==="GET"&&(s.url??"/").split("?")[0]==="/healthz";d.writeHead(p?200:404,{"content-type":"application/json; charset=utf-8"}),d.end(JSON.stringify(p?j(r?.connected()===!0,n):{error:{code:"not_found",message:"a machine-role crtrd serves only /healthz",origin:"runtime"}}))});await new Promise((s,d)=>{t.once("error",d),t.listen(a,()=>{t.off("error",d),M(a,384),s()})}),h(T(u()),{recursive:!0}),I(u(),String(process.pid),"utf8");let i;await L(e.runId,n,()=>i!==void 0);const o=P({apiUrl:e.apiUrl,environmentId:e.environmentId});r=n?U({apiUrl:e.apiUrl,environmentId:e.environmentId,credential:y,handlers:{start:o.start,wake:o.wake,stop:o.stop,status:o.status,delete:o.delete,viewer:o.viewer,"cron.run":D,"cron.cancel":N}}):void 0,f({level:"info",event:"daemon.machine.started",fields:{environment:e.environmentId}});const l=c(()=>i??=(async()=>{r?.stop(),await new Promise(s=>{t.close(()=>s()),t.closeAllConnections()}),b()===process.pid&&(v(a,{force:!0}),v(u(),{force:!0})),process.off("SIGINT",m),process.off("SIGTERM",m)})(),"teardown"),m=c(()=>{l().then(()=>process.exit(0),()=>process.exit(1))},"onSignal");return process.on("SIGINT",m),process.on("SIGTERM",m),l}c(se,"runMachineDaemon");const B=5e3,J=3e5;async function L(e,n,a){let r;try{r=A()}catch(i){f({level:"warn",event:"daemon.machine.move_record_unreadable",fields:{error:i.message}});return}if(r===void 0||r.state==="running"||r.runId!==e)return;if(r.notified||r.notify===null){w();return}if(r.state==="committed"&&(r={...r,state:"activated"},z(r)),!n)return;const t=r;(async()=>{let i=B;for(;;){try{await H(t),w();return}catch(o){f({level:"warn",event:"daemon.machine.move_notify_failed",fields:{node:t.notify??"",error:o.message}})}if(await new Promise(o=>{setTimeout(o,i).unref()}),a())return;i=Math.min(i*2,J)}})()}c(L,"settleFinishedMove");export{j as machineHealth,se as runMachineDaemon};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Socket } from 'node:net';
|
|
2
|
+
import type { MachineCronCancelArgs, MachineCronCancelResult, MachineCronRunArgs, MachineCronRunResult, MachineOp } from '../../core/runtime/runner/machine/ops.js';
|
|
3
|
+
export declare const LINK_BACKOFF_MIN_MS = 1000;
|
|
4
|
+
export declare const LINK_BACKOFF_MAX_MS = 60000;
|
|
5
|
+
/** Bytes written to a view.sock that its peer has not yet accepted, above which the channel is closed. */
|
|
6
|
+
export declare const VIEW_WRITE_LIMIT_BYTES: number;
|
|
7
|
+
/** One function per op; a machine-role crtrd injects the executor and the cron runner. */
|
|
8
|
+
export type MachineHandlers = {
|
|
9
|
+
[O in MachineOp]?: (args: never, signal: AbortSignal) => Promise<unknown>;
|
|
10
|
+
} & {
|
|
11
|
+
'cron.run'?: (args: MachineCronRunArgs) => Promise<MachineCronRunResult>;
|
|
12
|
+
'cron.cancel'?: (args: MachineCronCancelArgs) => Promise<MachineCronCancelResult>;
|
|
13
|
+
};
|
|
14
|
+
export interface MachineLinkOptions {
|
|
15
|
+
apiUrl: string;
|
|
16
|
+
environmentId: string;
|
|
17
|
+
credential: () => string;
|
|
18
|
+
handlers: MachineHandlers;
|
|
19
|
+
/** Opens a node's view.sock; default `connectViewSocket`. */
|
|
20
|
+
openView?: (node: string) => Socket;
|
|
21
|
+
backoffMinMs?: number;
|
|
22
|
+
backoffMaxMs?: number;
|
|
23
|
+
}
|
|
24
|
+
export interface MachineLink {
|
|
25
|
+
stop(): void;
|
|
26
|
+
connected(): boolean;
|
|
27
|
+
}
|
|
28
|
+
export declare function linkUrl(apiUrl: string, environmentId: string): string;
|
|
29
|
+
export declare function startMachineLink(options: MachineLinkOptions): MachineLink;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var b=Object.defineProperty;var c=(o,d)=>b(o,"name",{value:d,configurable:!0});import{WebSocket as w}from"ws";import{emitEvent as E}from"../../core/events/emit.js";import{connectViewSocket as A}from"../../core/runtime/view-socket.js";import{CLOSE_REVOKED as O,LINK_MAX_PAYLOAD_BYTES as T,parseFrame as x}from"./frames.js";const L=1e3,N=6e4,B=4*1024*1024,C=1024*1024,V=8*1024*1024;function K(o,d){const l=new URL(`/v1/machines/${encodeURIComponent(d)}/link`,o);return l.protocol=l.protocol==="https:"?"wss:":l.protocol==="http:"?"ws:":l.protocol,l.toString()}c(K,"linkUrl");function Y(o){const d=o.backoffMinMs??L,l=o.backoffMaxMs??N,S=o.openView??A;let v=!1,f,s=d,p;const k=c((_,t)=>{E({level:"error",event:_,fields:{environment:o.environmentId},error:t})},"fault");let m;const y=c(()=>{if(v)return;if(m!==void 0){if(o.credential()===m){p=setTimeout(y,s),s=Math.min(s*2,l);return}m=void 0}const _=o.credential(),t=new w(K(o.apiUrl,o.environmentId),{headers:{authorization:`Bearer ${_}`},maxPayload:T});f=t;const i=new Map,h=new Map,a=c(e=>{t.readyState===w.OPEN&&t.send(JSON.stringify(e))},"send"),M=c(async e=>{const n=o.handlers[e.op];if(n===void 0){a({type:"result",call_id:e.call_id,ok:!1,error:`this machine does not execute ${e.op}`});return}const r=new AbortController;t.once("close",()=>r.abort(new Error("the machine link closed")));try{a({type:"result",call_id:e.call_id,ok:!0,value:await n(e.args,r.signal)??null})}catch(u){a({type:"result",call_id:e.call_id,ok:!1,error:u instanceof Error?u.message:String(u)})}},"runCall"),g=c(e=>{const n=S(e.node);i.set(e.channel,n),n.once("connect",()=>a({type:"view.ready",channel:e.channel})),n.on("data",r=>{t.readyState===w.OPEN&&(t.send(JSON.stringify({type:"view.data",channel:e.channel,bytes:r.toString("base64")}),()=>{t.bufferedAmount<C&&n.resume()}),t.bufferedAmount>B&&n.pause())}),n.once("error",r=>{i.delete(e.channel)&&a({type:"view.close",channel:e.channel,error:r.message})}),n.once("close",()=>{i.delete(e.channel)&&a({type:"view.close",channel:e.channel})})},"openChannel"),I=c((e,n)=>{const r=i.get(e);if(r===void 0||r.write(n))return;const u=(h.get(e)??0)+n.length;if(u>V){i.delete(e),h.delete(e),r.destroy(),a({type:"view.close",channel:e,error:"the broker is not draining the viewer channel"});return}h.has(e)||r.once("drain",()=>h.delete(e)),h.set(e,u)},"writeToChannel");t.on("open",()=>{s=d}),t.on("message",e=>{const n=x(e);if(n!==null)try{switch(n.type){case"call":M(n);return;case"view.open":g(n);return;case"view.data":I(n.channel,Buffer.from(n.bytes,"base64"));return;case"view.close":{const r=i.get(n.channel);i.delete(n.channel),r?.destroy();return}default:return}}catch(r){k("daemon.machine_link.frame_failed",r)}}),t.on("error",e=>k("daemon.machine_link.client_error",e)),t.on("close",e=>{for(const n of i.values())n.destroy();i.clear(),f===t&&(f=void 0),!v&&(e===O&&(E({level:"error",event:"daemon.machine_link.revoked",fields:{environment:o.environmentId}}),m=_),p=setTimeout(y,s),s=Math.min(s*2,l))})},"connect");return y(),{stop(){v=!0,p!==void 0&&clearTimeout(p),f?.close()},connected:c(()=>f?.readyState===w.OPEN,"connected")}}c(Y,"startMachineLink");export{N as LINK_BACKOFF_MAX_MS,L as LINK_BACKOFF_MIN_MS,V as VIEW_WRITE_LIMIT_BYTES,K as linkUrl,Y as startMachineLink};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { CrtrError } from '../../core/errors.js';
|
|
2
|
+
/** The Northlight account, as the directory names it (`account` of the sign-in poll). */
|
|
3
|
+
export interface NorthlightAccount {
|
|
4
|
+
id: string;
|
|
5
|
+
display: string;
|
|
6
|
+
}
|
|
7
|
+
/** `directoryUrl` is the directory the person signs in against (`crtr sys signin`).
|
|
8
|
+
* `account` is the account the records moved into, kept only to name it when another account signs in; a Mac moved before it existed has none. */
|
|
9
|
+
export interface MachineConfig {
|
|
10
|
+
apiUrl: string;
|
|
11
|
+
environmentId: string;
|
|
12
|
+
runId: string;
|
|
13
|
+
directoryUrl: string;
|
|
14
|
+
account?: NorthlightAccount;
|
|
15
|
+
}
|
|
16
|
+
export declare const machineConfigPath: () => string;
|
|
17
|
+
export declare const machineCredentialPath: () => string;
|
|
18
|
+
/** The machine config, or undefined when this install is not an imported machine. A present but malformed file throws. */
|
|
19
|
+
export declare function readMachineConfig(): MachineConfig | undefined;
|
|
20
|
+
export declare function writeMachineConfig(config: MachineConfig): void;
|
|
21
|
+
export declare function readMachineCredential(): string;
|
|
22
|
+
export declare function writeMachineCredential(secret: string): void;
|
|
23
|
+
/** A move to Northlight in progress. `running`: the cloud run has not reached `done`, so a failure undoes the move. `committed`: the account holds the
|
|
24
|
+
* records and only the switch-over (machine credential, `machine.json`, restart) is left. `activated`: the machine-role daemon is up, so the switch-over is
|
|
25
|
+
* complete and commands run; the record stays only until `notified` — the asking node's message is stored in the account — is true, then it is deleted. */
|
|
26
|
+
export interface MoveRecord {
|
|
27
|
+
runId: string;
|
|
28
|
+
apiUrl: string;
|
|
29
|
+
directoryUrl: string;
|
|
30
|
+
account: NorthlightAccount;
|
|
31
|
+
notify: string | null;
|
|
32
|
+
state: 'running' | 'committed' | 'activated';
|
|
33
|
+
notified: boolean;
|
|
34
|
+
/** The exporter's process, so a later `crtr sys signin` can tell a running move from one whose exporter was killed. */
|
|
35
|
+
pid: number;
|
|
36
|
+
}
|
|
37
|
+
export declare const moveRecordPath: () => string;
|
|
38
|
+
/** The move record, or undefined when no move is running or unfinished. A present but malformed file throws. */
|
|
39
|
+
export declare function readMoveRecord(): MoveRecord | undefined;
|
|
40
|
+
export declare function writeMoveRecord(record: MoveRecord): void;
|
|
41
|
+
export declare function deleteMoveRecord(): void;
|
|
42
|
+
/** Whether a move's records are in the Northlight account but this computer has not switched over (the record says `committed`). */
|
|
43
|
+
export declare const isMoveCommitted: () => boolean;
|
|
44
|
+
/** The one `move_unfinished` error: every command, `crtr sys import resume` and a move whose switch-over failed answer it alike (exit code USAGE, as `not_signed_in`). */
|
|
45
|
+
export declare const moveUnfinishedError: (reason?: string) => CrtrError;
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
var y=Object.defineProperty;var e=(r,t)=>y(r,"name",{value:t,configurable:!0});import{chmodSync as l,existsSync as n,mkdirSync as s,readFileSync as o,rmSync as h,writeFileSync as u}from"node:fs";import{dirname as p,join as i}from"node:path";import{runtimeRoot as f,secretsDir as g}from"../../core/layout.js";import{CrtrError as v}from"../../core/errors.js";import{atomicWriteJson as U}from"../../core/fs-utils.js";import{ExitCode as x}from"../../types.js";const c=e(()=>i(f(),"machine.json"),"machineConfigPath"),m=e(()=>i(g(),"machine-credential"),"machineCredentialPath");function J(){const r=c();if(!n(r))return;const t=JSON.parse(o(r,"utf8"));if(typeof t.apiUrl!="string"||typeof t.environmentId!="string"||typeof t.runId!="string"||typeof t.directoryUrl!="string")throw new Error(`${r} must hold {apiUrl, environmentId, runId, directoryUrl}`);const a=t.account!==void 0&&typeof t.account?.id=="string"&&typeof t.account.display=="string"?{id:t.account.id,display:t.account.display}:void 0;return{apiUrl:t.apiUrl,environmentId:t.environmentId,runId:t.runId,directoryUrl:t.directoryUrl,...a===void 0?{}:{account:a}}}e(J,"readMachineConfig");function N(r){s(p(c()),{recursive:!0}),u(c(),`${JSON.stringify(r,null,2)}
|
|
2
|
+
`)}e(N,"writeMachineConfig");function j(){const r=m();if(!n(r))throw new Error(`no machine credential at ${r}`);return o(r,"utf8").trim()}e(j,"readMachineCredential");function O(r){const t=m();s(p(t),{recursive:!0}),u(t,`${r}
|
|
3
|
+
`,{mode:384}),l(t,384)}e(O,"writeMachineCredential");const d=e(()=>i(f(),"import","move.json"),"moveRecordPath");function w(){const r=d();if(!n(r))return;const t=JSON.parse(o(r,"utf8"));if(typeof t.runId!="string"||typeof t.apiUrl!="string"||typeof t.directoryUrl!="string"||t.state!=="running"&&t.state!=="committed"&&t.state!=="activated"||typeof t.account?.id!="string"||typeof t.account.display!="string")throw new Error(`${r} is not a move record`);return{runId:t.runId,apiUrl:t.apiUrl,directoryUrl:t.directoryUrl,account:{id:t.account.id,display:t.account.display},notify:typeof t.notify=="string"?t.notify:null,state:t.state,notified:t.notified===!0,pid:typeof t.pid=="number"?t.pid:0}}e(w,"readMoveRecord");function P(r){U(d(),r,{mode:384})}e(P,"writeMoveRecord");function b(){h(d(),{force:!0})}e(b,"deleteMoveRecord");const F=e(()=>w()?.state==="committed","isMoveCommitted"),k=e(r=>new v("move_unfinished",`crtr's records are already in your Northlight account, but this computer has not switched over${r===void 0?".":`: ${r}`}`,x.USAGE,{next:"Run `crtr sys signin` to finish."}),"moveUnfinishedError");export{b as deleteMoveRecord,F as isMoveCommitted,c as machineConfigPath,m as machineCredentialPath,d as moveRecordPath,k as moveUnfinishedError,J as readMachineConfig,j as readMachineCredential,w as readMoveRecord,N as writeMachineConfig,O as writeMachineCredential,P as writeMoveRecord};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** True when the environment has an unrevoked credential. */
|
|
2
|
+
export declare function hasMachineCredential(environmentId: string): Promise<boolean>;
|
|
3
|
+
/** Mint (or replace) the environment's credential and return the secret. */
|
|
4
|
+
export declare function mintMachineCredential(environmentId: string): Promise<string>;
|
|
5
|
+
export declare function revokeMachineCredential(environmentId: string): Promise<void>;
|
|
6
|
+
/** True when `token` is the environment's current, unrevoked credential. */
|
|
7
|
+
export declare function verifyMachineCredential(environmentId: string, token: string): Promise<boolean>;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
var i=Object.defineProperty;var t=(e,n)=>i(e,"name",{value:n,configurable:!0});import{createHash as o,randomBytes as s}from"node:crypto";import{openDb as a}from"../../core/canvas/db.js";const r=t(e=>o("sha256").update(e).digest("hex"),"hash");async function E(e){return await(await a()).prepare("SELECT 1 AS ok FROM machine_credentials WHERE environment = ? AND revoked_at IS NULL").get(e)!==void 0}t(E,"hasMachineCredential");async function _(e){const n=`mc_${s(32).toString("base64url")}`;return await(await a()).prepare(`INSERT INTO machine_credentials (environment, token_hash, issued_at, revoked_at) VALUES (?, ?, ?, NULL)
|
|
2
|
+
ON CONFLICT (environment) DO UPDATE SET token_hash = EXCLUDED.token_hash, issued_at = EXCLUDED.issued_at, revoked_at = NULL`).run(e,r(n),new Date().toISOString()),n}t(_,"mintMachineCredential");async function m(e){await(await a()).prepare("UPDATE machine_credentials SET revoked_at = ? WHERE environment = ? AND revoked_at IS NULL").run(new Date().toISOString(),e)}t(m,"revokeMachineCredential");async function p(e,n){return await(await a()).prepare("SELECT 1 AS ok FROM machine_credentials WHERE environment = ? AND token_hash = ? AND revoked_at IS NULL").get(e,r(n))!==void 0}t(p,"verifyMachineCredential");export{E as hasMachineCredential,_ as mintMachineCredential,m as revokeMachineCredential,p as verifyMachineCredential};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'node:http';
|
|
2
|
+
import type { Duplex } from 'node:stream';
|
|
3
|
+
import type { BrokerViewLink } from '../../core/runtime/view-socket.js';
|
|
4
|
+
import { type MachineLinkRefusal } from './holder.js';
|
|
5
|
+
/** Unsent bytes on a viewer WebSocket above which the channel is closed (a viewer that cannot keep up is dropped, never buffered without bound). */
|
|
6
|
+
export declare const VIEW_BUFFER_LIMIT_BYTES: number;
|
|
7
|
+
/** A viewer channel to a node on a machine: over the link this process holds, or forwarded to the API process that does. */
|
|
8
|
+
export declare function openMachineView(environmentId: string, nodeId: string): BrokerViewLink;
|
|
9
|
+
/** One readiness check: the Mac connected the node's view.sock within `timeoutMs`. */
|
|
10
|
+
export declare function probeMachineView(environmentId: string, nodeId: string, timeoutMs: number): Promise<boolean>;
|
|
11
|
+
/** Wait for the node's broker on the Mac to accept a viewer, retrying while its view.sock refuses; false on the deadline or when the launch exits. */
|
|
12
|
+
export declare function waitForMachineView(environmentId: string, nodeId: string, exited: Promise<unknown> | undefined, waitMs: number): Promise<boolean>;
|
|
13
|
+
/** The environment id of a forwarded viewer upgrade path, or null for any other path. */
|
|
14
|
+
export declare function machineViewEnvironment(req: IncomingMessage): string | null;
|
|
15
|
+
/** The holder's side: authenticate the forwarding API process, open a channel on the link this process holds and pipe it. Returns a refusal for the caller to write, or null once upgraded. */
|
|
16
|
+
export declare function handleMachineViewUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer, environmentId: string): MachineLinkRefusal | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var x=Object.defineProperty;var a=(s,e)=>x(s,"name",{value:e,configurable:!0});import{EventEmitter as b}from"node:events";import{createHmac as A,timingSafeEqual as I}from"node:crypto";import{WebSocket as l,WebSocketServer as P}from"ws";import{openDb as R}from"../../core/canvas/db.js";import{loadSecretsKey as k}from"../../core/secrets/key.js";import{LINK_MAX_PAYLOAD_BYTES as _,parseFrame as v}from"./frames.js";import{apiAddress as B,heldLinkOf as g,MachineViewLink as E,UNROUTABLE_PREFIX as O}from"./holder.js";const w=8*1024*1024,T=1013,D=/^\/v1\/machines\/([A-Za-z0-9_-]+)\/view$/,L=new P({noServer:!0,maxPayload:_}),S=a(s=>A("sha256",k().key).update(`machine-view:${s}`).digest("hex"),"forwardToken");function $(s,e){const r=g(s);return r!==null?new E(r,e):new q(s,e)}a($,"openMachineView");class q extends b{static{a(this,"ForwardedViewLink")}environmentId;nodeId;ws;ended=!1;failed=!1;queued=[];queuedBytes=0;constructor(e,r){super(),this.environmentId=e,this.nodeId=r,this.dial().catch(t=>this.finish(t instanceof Error?t.message:String(t)))}get destroyed(){return this.ended}write(e){return this.ended?!1:this.ws?.readyState!==l.OPEN?(this.queuedBytes+=e.length,this.queuedBytes>w?(this.finish("machine view: the holder did not accept the channel before its queue filled"),!1):(this.queued.push(e),!0)):this.ws.bufferedAmount>w?(this.finish("machine view: the holder is not draining the channel"),!1):(this.sendData(e),!0)}destroy(){return this.ended||(this.ws?.readyState===l.OPEN&&this.ws.send(JSON.stringify({type:"view.close",channel:""})),this.finish(void 0)),this}sendData(e){this.ws?.send(JSON.stringify({type:"view.data",channel:"",bytes:Buffer.from(e).toString("base64")}))}async dial(){const e=await(await R()).prepare("SELECT api_address, lease_expires_at FROM machine_links WHERE environment = ?").get(this.environmentId);if(e===void 0||Date.parse(e.lease_expires_at)<=Date.now())throw new Error(`machine_offline: machine ${this.environmentId} has no live link`);if(e.api_address.startsWith(O))throw new Error(`machine_unreachable: the API process holding the link wrote ${e.api_address}; set CRTR_API_ADDRESS or listen on TCP`);if(e.api_address===B())throw new Error(`machine_offline: the link row names this API process (${e.api_address}) but it holds no link`);if(this.ended)return;const r=new URL(`/v1/machines/${encodeURIComponent(this.environmentId)}/view`,e.api_address.includes("://")?e.api_address:`ws://${e.api_address}`);r.protocol=r.protocol==="https:"?"wss:":r.protocol==="http:"?"ws:":r.protocol,r.searchParams.set("node",this.nodeId);const t=new l(r.toString(),{headers:{authorization:`Bearer ${S(this.environmentId)}`},maxPayload:_});this.ws=t,t.on("open",()=>{for(const n of this.queued.splice(0))this.sendData(n)}),t.on("unexpected-response",(n,i)=>{i.resume(),this.finish(i.statusCode===409?"machine_offline: the holder no longer holds the link":`machine view refused by ${e.api_address}: ${i.statusCode}`)}),t.on("message",n=>{const i=v(n);i!==null&&(i.type==="view.ready"?this.emit("connect"):i.type==="view.data"?this.emit("data",Buffer.from(i.bytes,"base64")):i.type==="view.close"&&this.finish(i.error))}),t.on("error",n=>this.finish(n.message)),t.on("close",()=>this.finish("machine view forward closed"))}finish(e){this.ended||(this.ended=!0,this.ws?.close(),e!==void 0&&!this.failed&&(this.failed=!0,this.emit("error",Object.assign(new Error(e),{code:"ECONNRESET"}))),this.emit("close",this.failed))}}function M(s,e,r){return new Promise(t=>{const n=$(s,e),i=setTimeout(()=>{t(!1),n.destroy()},r);n.on("connect",()=>{clearTimeout(i),t(!0),n.destroy()}),n.on("error",()=>{}),n.on("close",()=>{clearTimeout(i),t(!1)})})}a(M,"probeMachineView");async function J(s,e,r,t){const n=Date.now()+t;let i=!1;for(r?.then(()=>{i=!0});Date.now()<=n&&!i;){if(await M(s,e,Math.max(1,Math.min(2e3,n-Date.now()))))return!0;await new Promise(c=>setTimeout(c,250))}return!1}a(J,"waitForMachineView");function Y(s){const e=D.exec(new URL(s.url??"/","http://runtime.local").pathname);return e===null?null:e[1]}a(Y,"machineViewEnvironment");function K(s,e,r,t){const n=s.headers.authorization,i=/^Bearer (\S+)$/.exec(Array.isArray(n)?n[0]??"":n??"")?.[1]??"",c=Buffer.from(S(t)),p=Buffer.from(i);if(p.length!==c.length||!I(p,c))return{status:401,text:"Unauthorized",code:"unauthorized",message:"machine view forward credential refused"};const f=new URL(s.url??"/","http://runtime.local").searchParams.get("node");if(f===null||f==="")return{status:400,text:"Bad Request",code:"bad_request",message:"node is required"};const y=g(t);return y===null?{status:409,text:"Conflict",code:"machine_offline",message:"this API process holds no link for the machine"}:(L.handleUpgrade(s,e,r,d=>{const o=new E(y,f),u=a(h=>{d.readyState===l.OPEN&&d.send(JSON.stringify(h))},"send");o.on("connect",()=>u({type:"view.ready",channel:""})),o.on("data",h=>{if(d.bufferedAmount>w){o.destroy(),d.close(T,"viewer too slow");return}u({type:"view.data",channel:"",bytes:h.toString("base64")})}),o.on("error",h=>u({type:"view.close",channel:"",error:h.message})),o.on("close",()=>d.close()),d.on("message",h=>{const m=v(h);m?.type==="view.data"?o.write(Buffer.from(m.bytes,"base64")):m?.type==="view.close"&&o.destroy()}),d.on("close",()=>o.destroy()),d.on("error",()=>o.destroy())}),null)}a(K,"handleMachineViewUpgrade");export{w as VIEW_BUFFER_LIMIT_BYTES,K as handleMachineViewUpgrade,Y as machineViewEnvironment,$ as openMachineView,M as probeMachineView,J as waitForMachineView};
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { MachineOp } from '../../core/runtime/runner/machine/ops.js';
|
|
2
|
+
export type MachineFrame =
|
|
3
|
+
/** holder → Mac: run `op`. */
|
|
4
|
+
{
|
|
5
|
+
type: 'call';
|
|
6
|
+
call_id: string;
|
|
7
|
+
op: MachineOp;
|
|
8
|
+
args: unknown;
|
|
9
|
+
}
|
|
10
|
+
/** Mac → holder: the call's outcome. */
|
|
11
|
+
| {
|
|
12
|
+
type: 'result';
|
|
13
|
+
call_id: string;
|
|
14
|
+
ok: true;
|
|
15
|
+
value: unknown;
|
|
16
|
+
} | {
|
|
17
|
+
type: 'result';
|
|
18
|
+
call_id: string;
|
|
19
|
+
ok: false;
|
|
20
|
+
error: string;
|
|
21
|
+
}
|
|
22
|
+
/** holder → Mac: open a viewer channel to `node`'s view.sock. */
|
|
23
|
+
| {
|
|
24
|
+
type: 'view.open';
|
|
25
|
+
channel: string;
|
|
26
|
+
node: string;
|
|
27
|
+
}
|
|
28
|
+
/** Mac → holder: the channel's view.sock accepted the connection. */
|
|
29
|
+
| {
|
|
30
|
+
type: 'view.ready';
|
|
31
|
+
channel: string;
|
|
32
|
+
}
|
|
33
|
+
/** Either way: base64 bytes of the view.sock stream. */
|
|
34
|
+
| {
|
|
35
|
+
type: 'view.data';
|
|
36
|
+
channel: string;
|
|
37
|
+
bytes: string;
|
|
38
|
+
}
|
|
39
|
+
/** Either way: the channel ended; `error` when it failed (the Mac could not reach the view.sock). */
|
|
40
|
+
| {
|
|
41
|
+
type: 'view.close';
|
|
42
|
+
channel: string;
|
|
43
|
+
error?: string;
|
|
44
|
+
};
|
|
45
|
+
export declare const LINK_MAX_PAYLOAD_BYTES: number;
|
|
46
|
+
/** WebSocket close codes of the holder: a newer link took the environment's row; the credential is revoked (the Mac does not reconnect with it). */
|
|
47
|
+
export declare const CLOSE_SUPERSEDED = 4001;
|
|
48
|
+
export declare const CLOSE_REVOKED = 4003;
|
|
49
|
+
export declare function parseFrame(data: Buffer | ArrayBuffer | Buffer[]): MachineFrame | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var o=Object.defineProperty;var l=(r,t)=>o(r,"name",{value:t,configurable:!0});const a=33554432,f=4001,i=4003;function p(r){const t=Array.isArray(r)?Buffer.concat(r):Buffer.isBuffer(r)?r:Buffer.from(new Uint8Array(r));try{const e=JSON.parse(t.toString("utf8"));if(typeof e!="object"||e===null)return null;const n=l((...u)=>u.every(c=>typeof e[c]=="string"),"strings");switch(e.type){case"call":return n("call_id","op")?e:null;case"result":return n("call_id")&&(e.ok===!0||e.ok===!1&&n("error"))?e:null;case"view.open":return n("channel","node")?e:null;case"view.ready":case"view.close":return n("channel")?e:null;case"view.data":return n("channel","bytes")?e:null;default:return null}}catch{return null}}l(p,"parseFrame");export{i as CLOSE_REVOKED,f as CLOSE_SUPERSEDED,a as LINK_MAX_PAYLOAD_BYTES,p as parseFrame};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import type { IncomingMessage } from 'node:http';
|
|
3
|
+
import type { Duplex } from 'node:stream';
|
|
4
|
+
import { WebSocket } from 'ws';
|
|
5
|
+
import type { BrokerViewLink } from '../../core/runtime/view-socket.js';
|
|
6
|
+
import { type MachineFrame } from './frames.js';
|
|
7
|
+
export declare const LINK_LEASE_MS = 45000;
|
|
8
|
+
export declare const LINK_RENEW_MS = 15000;
|
|
9
|
+
export declare const LINK_PING_MS = 30000;
|
|
10
|
+
/** Prefix of the address an API process writes when nothing tells it where other API processes can reach it; the viewer forward refuses it. */
|
|
11
|
+
export declare const UNROUTABLE_PREFIX = "unroutable:";
|
|
12
|
+
/** The TCP listener's bound `host:port`, used as this process's address when `CRTR_API_ADDRESS` is not set. */
|
|
13
|
+
export declare const setApiAddress: (address: string) => void;
|
|
14
|
+
/** The address other API processes find this one at, written to `machine_links.api_address`: `CRTR_API_ADDRESS`, else the listener's bound address, else an unroutable marker. */
|
|
15
|
+
export declare const apiAddress: () => string;
|
|
16
|
+
export declare class HeldLink extends EventEmitter {
|
|
17
|
+
readonly environmentId: string;
|
|
18
|
+
readonly generation: number;
|
|
19
|
+
readonly ws: WebSocket;
|
|
20
|
+
private readonly token;
|
|
21
|
+
private readonly timers;
|
|
22
|
+
private unsubscribe;
|
|
23
|
+
private claiming;
|
|
24
|
+
private closed;
|
|
25
|
+
private alive;
|
|
26
|
+
readonly channels: Map<string, MachineViewLink>;
|
|
27
|
+
constructor(environmentId: string, generation: number, ws: WebSocket, token: string);
|
|
28
|
+
start(): void;
|
|
29
|
+
private fault;
|
|
30
|
+
send(frame: MachineFrame): boolean;
|
|
31
|
+
/** Renew the row, my sent calls' leases and `last_seen_at`; a lost generation or a revoked credential closes the link. */
|
|
32
|
+
renew(): Promise<void>;
|
|
33
|
+
/** The credential this link authenticated with is still the environment's current one: rotation or revocation evicts the link. */
|
|
34
|
+
private credentialValid;
|
|
35
|
+
/** Claim this environment's pending calls (fenced by this link's generation) and forward them. */
|
|
36
|
+
claim(): Promise<void>;
|
|
37
|
+
/** `sent` is whether the call was written to the link: a call that was sent may have run, one that was not ran nowhere. */
|
|
38
|
+
private failCall;
|
|
39
|
+
/** This link's generation is still the one in the environment's `machine_links` row. */
|
|
40
|
+
private isCurrentGeneration;
|
|
41
|
+
/** Give a claimed, unsent call back to `pending`. */
|
|
42
|
+
private release;
|
|
43
|
+
/** Settle a call this link holds; a call another holder owns or one already settled is left as it is. A result settles only
|
|
44
|
+
* while this link is the current generation (`requireCurrent`); a failure for a dropped link settles whatever the row now says. */
|
|
45
|
+
private settle;
|
|
46
|
+
private onMessage;
|
|
47
|
+
private teardown;
|
|
48
|
+
}
|
|
49
|
+
/** A viewer channel to one node's view.sock on the Mac, as the relay's broker-side link. */
|
|
50
|
+
export declare class MachineViewLink extends EventEmitter implements BrokerViewLink {
|
|
51
|
+
private readonly link;
|
|
52
|
+
private ended;
|
|
53
|
+
private failed;
|
|
54
|
+
private readonly channel;
|
|
55
|
+
constructor(link: HeldLink, nodeId: string);
|
|
56
|
+
get destroyed(): boolean;
|
|
57
|
+
write(line: string | Buffer): boolean;
|
|
58
|
+
destroy(): this;
|
|
59
|
+
/** The channel is over: `error` names a failure (the relay records a fault), none is a clean end. */
|
|
60
|
+
finish(error: string | undefined): void;
|
|
61
|
+
}
|
|
62
|
+
/** The link this process holds for `environmentId`, or null (no link, or another API process holds it). */
|
|
63
|
+
export declare const heldLinkOf: (environmentId: string) => HeldLink | null;
|
|
64
|
+
/** The environment id of a machine-link upgrade path, or null for any other path. */
|
|
65
|
+
export declare function machineLinkEnvironment(req: IncomingMessage): string | null;
|
|
66
|
+
export interface MachineLinkRefusal {
|
|
67
|
+
status: number;
|
|
68
|
+
text: string;
|
|
69
|
+
code: string;
|
|
70
|
+
message: string;
|
|
71
|
+
}
|
|
72
|
+
/** Authenticate the machine credential and take over the environment's link. Returns a refusal for the caller to write, or null once upgraded. */
|
|
73
|
+
export declare function handleMachineLinkUpgrade(req: IncomingMessage, socket: Duplex, head: Buffer, environmentId: string): Promise<MachineLinkRefusal | null>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
var N=Object.defineProperty;var s=(i,e)=>N(i,"name",{value:e,configurable:!0});import{EventEmitter as u}from"node:events";import{hostname as k}from"node:os";import{randomUUID as R}from"node:crypto";import{WebSocket as T,WebSocketServer as I}from"ws";import{openDb as r}from"../../core/canvas/db.js";import{emitEvent as _}from"../../core/events/emit.js";import{brokerSignalBus as L}from"../broker-signals/bus.js";import{verifyMachineCredential as E}from"./credential.js";import{CLOSE_REVOKED as y,CLOSE_SUPERSEDED as p,LINK_MAX_PAYLOAD_BYTES as O,parseFrame as b}from"./frames.js";const U=45e3,C=15e3,x=3e4,M=1e3,w=10,P=new I({noServer:!0,maxPayload:O}),c=new Map,H="unroutable:";let g;const q=s(i=>{g=i},"setApiAddress"),W=s(()=>process.env.CRTR_API_ADDRESS??g??`${H}${k()}:${process.pid}`,"apiAddress"),m=s(()=>new Date(Date.now()+U).toISOString(),"lease");class F extends u{static{s(this,"HeldLink")}environmentId;generation;ws;token;timers=[];unsubscribe;claiming=!1;closed=!1;alive=!0;channels=new Map;constructor(e,t,n,a){super(),this.environmentId=e,this.generation=t,this.ws=n,this.token=a}start(){const{ws:e}=this;e.on("message",t=>{this.onMessage(t).catch(n=>this.fault("daemon.machine_link.frame_failed",n))}),e.on("pong",()=>{this.alive=!0}),e.on("close",()=>{this.teardown().catch(t=>this.fault("daemon.machine_link.teardown_failed",t))}),e.on("error",()=>{}),this.timers.push(setInterval(()=>{this.renew().catch(t=>this.fault("daemon.machine_link.renew_failed",t))},C)),this.timers.push(setInterval(()=>{if(!this.alive){e.terminate();return}this.alive=!1;try{e.ping()}catch{}},x)),this.timers.push(setInterval(()=>{this.claim().catch(t=>this.fault("daemon.machine_link.claim_failed",t))},M)),L.subscribe(t=>{t===this.environmentId&&this.claim().catch(n=>this.fault("daemon.machine_link.claim_failed",n))},{kinds:["machine"],onListenStart:s(()=>{this.claim().catch(t=>this.fault("daemon.machine_link.claim_failed",t))},"onListenStart")}).then(t=>{this.closed?t():this.unsubscribe=t})}fault(e,t){_({level:"error",event:e,fields:{environment:this.environmentId},error:t})}send(e){return this.ws.readyState!==T.OPEN?!1:(this.ws.send(JSON.stringify(e)),!0)}async renew(){if(this.closed)return;const e=await r(),t=m();if((await e.prepare("UPDATE machine_links SET lease_expires_at = ? WHERE environment = ? AND generation = ?").run(t,this.environmentId,this.generation)).changes===0){this.ws.close(p,"superseded");return}await e.prepare("UPDATE machine_calls SET lease_expires_at = ? WHERE environment = ? AND state = 'sent' AND claimed_generation = ?").run(t,this.environmentId,this.generation),await e.prepare("UPDATE environments SET last_seen_at = ? WHERE object = ?").run(new Date().toISOString(),this.environmentId),await this.credentialValid()||this.ws.close(y,"revoked")}async credentialValid(){return await E(this.environmentId,this.token)}async claim(){if(!(this.closed||this.claiming)){this.claiming=!0;try{for(;;){const e=await(await r()).prepare(`UPDATE machine_calls SET state = 'sent', claimed_generation = ?, lease_expires_at = ?
|
|
2
|
+
WHERE call_id IN (SELECT call_id FROM machine_calls WHERE environment = ? AND state = 'pending'
|
|
3
|
+
AND EXISTS (SELECT 1 FROM machine_links WHERE environment = ? AND generation = ?)
|
|
4
|
+
ORDER BY created_at FOR UPDATE SKIP LOCKED LIMIT ${w})
|
|
5
|
+
RETURNING call_id, op, args_json`).all(this.generation,m(),this.environmentId,this.environmentId,this.generation),t=!this.closed&&await this.isCurrentGeneration();for(const n of e)t?this.send({type:"call",call_id:n.call_id,op:n.op,args:JSON.parse(n.args_json)})||await this.failCall(n.call_id,"machine_offline","link closed before the call was sent",!1):await this.release(n.call_id);if(e.length<w)return}}finally{this.claiming=!1}}}async failCall(e,t,n,a){await this.settle(e,"failed",{code:t,message:n,sent:a},!1)}async isCurrentGeneration(){return await(await r()).prepare("SELECT 1 AS live FROM machine_links WHERE environment = ? AND generation = ?").get(this.environmentId,this.generation)!==void 0}async release(e){await(await r()).prepare("UPDATE machine_calls SET state = 'pending', claimed_generation = NULL, lease_expires_at = NULL WHERE call_id = ? AND state = 'sent' AND claimed_generation = ?").run(e,this.generation)}async settle(e,t,n,a){await B(e,t,n,this.environmentId,this.generation,a)}async onMessage(e){const t=b(e);if(t!==null)switch(t.type){case"result":t.ok?await this.settle(t.call_id,"done",t.value??null,!0):await this.settle(t.call_id,"failed",{code:"machine_call_failed",message:t.error},!0);return;case"view.ready":this.channels.get(t.channel)?.emit("connect");return;case"view.data":this.channels.get(t.channel)?.emit("data",Buffer.from(t.bytes,"base64"));return;case"view.close":this.channels.get(t.channel)?.finish(t.error);return;default:return}}async teardown(){if(this.closed)return;this.closed=!0;for(const n of this.timers)clearInterval(n);this.unsubscribe?.(),c.get(this.environmentId)===this&&c.delete(this.environmentId);for(const n of[...this.channels.values()])n.finish("machine link closed");const e=await r(),t=await e.prepare("SELECT call_id FROM machine_calls WHERE environment = ? AND state = 'sent' AND claimed_generation = ?").all(this.environmentId,this.generation);for(const{call_id:n}of t)await this.failCall(n,"machine_offline","link dropped before the result",!0);await e.prepare("DELETE FROM machine_links WHERE environment = ? AND generation = ?").run(this.environmentId,this.generation)}}async function B(i,e,t,n,a,l){const o=await r();(await o.prepare(`UPDATE machine_calls SET state = ?, result_json = ?, finished_at = ?
|
|
6
|
+
WHERE call_id = ? AND environment = ? AND claimed_generation = ? AND state = 'sent'
|
|
7
|
+
${l?"AND EXISTS (SELECT 1 FROM machine_links WHERE environment = ? AND generation = ?)":""}`).run(e,JSON.stringify(t),new Date().toISOString(),i,n,a,...l?[n,a]:[])).changes>0&&await o.prepare("SELECT pg_notify(?, ?)").get("crtr_signal",`machine_call:${i}`)}s(B,"withNotify");class ee extends u{static{s(this,"MachineViewLink")}link;ended=!1;failed=!1;channel=R();constructor(e,t){super(),this.link=e,e.channels.set(this.channel,this),e.send({type:"view.open",channel:this.channel,node:t})||queueMicrotask(()=>this.finish("machine link closed"))}get destroyed(){return this.ended}write(e){return!this.ended&&this.link.send({type:"view.data",channel:this.channel,bytes:Buffer.from(e).toString("base64")})}destroy(){return this.ended||(this.link.send({type:"view.close",channel:this.channel}),this.finish(void 0)),this}finish(e){this.ended||(this.ended=!0,this.link.channels.delete(this.channel),e!==void 0&&!this.failed&&(this.failed=!0,this.emit("error",Object.assign(new Error(e),{code:"ECONNRESET"}))),this.emit("close",this.failed))}}const te=s(i=>c.get(i)??null,"heldLinkOf"),K=/^\/v1\/machines\/([A-Za-z0-9_-]+)\/link$/;function ne(i){const e=K.exec(new URL(i.url??"/","http://runtime.local").pathname);return e===null?null:e[1]}s(ne,"machineLinkEnvironment");async function ie(i,e,t,n){const a=i.headers.authorization,l=/^Bearer (\S+)$/.exec(Array.isArray(a)?a[0]??"":a??"")?.[1];if(l===void 0||!await E(n,l))return{status:401,text:"Unauthorized",code:"unauthorized",message:"machine credential refused"};const o=new Date().toISOString(),h=await r(),v=await h.prepare(`INSERT INTO machine_links (environment, generation, api_address, connected_at, lease_expires_at) VALUES (?, 1, ?, ?, ?)
|
|
8
|
+
ON CONFLICT (environment) DO UPDATE SET generation = machine_links.generation + 1, api_address = EXCLUDED.api_address, connected_at = EXCLUDED.connected_at, lease_expires_at = EXCLUDED.lease_expires_at
|
|
9
|
+
RETURNING generation`).get(n,W(),o,m());await h.prepare("UPDATE environments SET last_seen_at = ? WHERE object = ?").run(o,n);const f=Number(v.generation);return e.destroyed?(await h.prepare("DELETE FROM machine_links WHERE environment = ? AND generation = ?").run(n,f),null):(P.handleUpgrade(i,e,t,S=>{const d=new F(n,f,S,l),D=c.get(n);c.set(n,d),D?.ws.close(p,"superseded"),d.start(),d.claim().catch(A=>_({level:"error",event:"daemon.machine_link.claim_failed",fields:{environment:n},error:A}))}),null)}s(ie,"handleMachineLinkUpgrade");export{F as HeldLink,U as LINK_LEASE_MS,x as LINK_PING_MS,C as LINK_RENEW_MS,ee as MachineViewLink,H as UNROUTABLE_PREFIX,W as apiAddress,ie as handleMachineLinkUpgrade,te as heldLinkOf,ne as machineLinkEnvironment,q as setApiAddress};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type MoveRecord } from './config.js';
|
|
2
|
+
/** Tell the node that asked for the move that it finished, through the control plane with the person's bearer (`POST /v1/nodes/:id/messages`): the node
|
|
3
|
+
* now lives in the account, so the message is stored there as mail and wakes it once the machine link is up. Then the record says `notified`. */
|
|
4
|
+
export declare function notifyMoveFinished(move: MoveRecord): Promise<MoveRecord>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var i=Object.defineProperty;var n=(t,r)=>i(t,"name",{value:r,configurable:!0});import{CrtrClient as o}from"../../api/index.js";import{northlightFetch as e}from"../../core/directory/person-credential.js";import{writeMoveRecord as c}from"./config.js";async function l(t){if(t.notified||t.notify===null)return t;await new o({baseUrl:t.apiUrl,fetch:e(t.directoryUrl,t.apiUrl)}).sendMessage(t.notify,{from:null,body:`The move to Northlight finished; crtr on this computer now runs with the account ${t.account.display}. Retry the command that needed the account and carry on.`});const r={...t,notified:!0};return c(r),r}n(l,"notifyMoveFinished");export{l as notifyMoveFinished};
|
package/dist/daemon/manage.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type HealthDTO, type StartupPhaseDTO } from '../api/index.js';
|
|
2
|
+
import type { DumpResult } from '../core/import/export/dump.js';
|
|
2
3
|
/** Env keys that must NEVER reach the daemon process. Restarting crtrd is
|
|
3
4
|
* overwhelmingly done from inside an agent node's own bash tool (`crtr sys
|
|
4
5
|
* daemon stop && start`), which inherits that node's FULL env — its identity
|
|
@@ -46,10 +47,25 @@ export interface DaemonExportSqliteResult {
|
|
|
46
47
|
ms: number;
|
|
47
48
|
userVersion: number;
|
|
48
49
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
export interface DaemonExportSqliteResult {
|
|
51
|
+
path: string;
|
|
52
|
+
rows: number;
|
|
53
|
+
ms: number;
|
|
54
|
+
userVersion: number;
|
|
55
|
+
}
|
|
56
|
+
/** Run the selected generation's `crtrd-cli export-sqlite` and wait for it. */
|
|
52
57
|
export declare function runDaemonExportSqlite(): Promise<DaemonExportSqliteResult>;
|
|
58
|
+
/** Run the selected generation's `crtrd-cli import-dump`: the row files of the running canvas into `out`/rows. */
|
|
59
|
+
export declare function runDaemonImportDump(out: string, kind: 'machine' | 'hosted'): Promise<DumpResult>;
|
|
60
|
+
/** Run `crtrd-cli import-dump <out> bodies`: every `bodies` row into `out`/rows/bodies (the pre-pause dump; the later `runDaemonImportDump` adds the rows written since). */
|
|
61
|
+
export declare function runDaemonBodiesDump(out: string): Promise<{
|
|
62
|
+
parts: string[];
|
|
63
|
+
count: number;
|
|
64
|
+
}>;
|
|
65
|
+
/** Run `crtrd-cli import-dump <out> install-id`: this install's id, read from the running canvas. */
|
|
66
|
+
export declare function runDaemonInstallId(): Promise<{
|
|
67
|
+
installId: string;
|
|
68
|
+
}>;
|
|
53
69
|
export interface SpawnDaemonOptions {
|
|
54
70
|
/** The phase that proves a daemon launched by this call is available. */
|
|
55
71
|
targetPhase?: 'ready' | 'prepared';
|