@north-light/cli 0.3.580 → 0.3.581
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 +20 -0
- package/dist/api/dto/imports.d.ts +142 -0
- package/dist/api/dto/imports.js +0 -0
- 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/clients/attach/session/connection.d.ts +3 -1
- package/dist/clients/attach/session/connection.js +1 -1
- package/dist/clients/attach/viewer.js +510 -510
- package/dist/commands/api-client.d.ts +7 -4
- package/dist/commands/api-client.js +3 -3
- package/dist/commands/sys/daemon.js +1 -1
- 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 +5 -0
- package/dist/commands/sys/import/start.js +4 -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/directory/person-credential.d.ts +36 -0
- package/dist/core/directory/person-credential.js +1 -0
- 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 +10 -10
- package/dist/core/graph/reader.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 +89 -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 +21 -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 +9 -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 +11 -0
- package/dist/core/runtime/broker.js +1 -1
- 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 +23 -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/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 +5 -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 +2 -2
- package/dist/daemon/crtrd.js +10 -10
- 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 +7 -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 +14 -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/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
|
@@ -7,6 +7,9 @@ export declare const LAUNCH_DEADLINE_MS: number;
|
|
|
7
7
|
export declare const LAUNCH_LOST_GRACE_MS = 120000;
|
|
8
8
|
/** The release the control plane runs, which is the release a wake names. */
|
|
9
9
|
export declare const acceptedRelease: () => string;
|
|
10
|
+
/** The machine environment the node's profile points at when this process is a cloud role (the machine is another host, reached
|
|
11
|
+
* through the remote machine runner), else null: a Mac's own crtrd runs its machine environment's nodes as processes. */
|
|
12
|
+
export declare function remoteMachineEnvironmentOf(profileId: string | null): Promise<string | null>;
|
|
10
13
|
/** Step 2: the `funding bill` check. A refusal writes `nodes.spend_refusal` and one refused `sandbox` row per node, limit and month,
|
|
11
14
|
* then throws `402 cap_exhausted`; no execution row is written and no runner is called. A pass clears `spend_refusal`. */
|
|
12
15
|
export declare function checkBilledWake(meta: NodeMeta): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var f=Object.defineProperty;var a=(e,
|
|
1
|
+
var f=Object.defineProperty;var a=(e,t)=>f(e,"name",{value:t,configurable:!0});import{liveExecutionForNode as m,endExecution as l,recordExecutionAwake as p,endLaunchingExecution as s,requestExecutionStop as w}from"../../../core/canvas/executions.js";import{openDb as h}from"../../../core/canvas/db.js";import{isCloudRole as _}from"../../../core/canvas/person-state.js";import{emitEvent as u}from"../../../core/events/emit.js";import{findRefusal as x,refusalError as E,refusedMark as g,writeCharge as v}from"../../../core/ledger/index.js";import{transition as c}from"../../../core/runtime/lifecycle.js";import{runnerForExecution as d}from"../../../core/runtime/runner/index.js";import{machineEnvironmentOfProfile as b}from"../../../core/runtime/runner/machine/environment.js";import{startCandidateOf as k}from"../../../core/runtime/start-check.js";import{settleSandboxBeforeWake as y}from"../ledger-sandbox.js";import{crtrVersion as I}from"../../../shared/crtr-version.js";import{resolveEnvironmentImage as S}from"../recipe-image.js";const L=45*6e4,A=12e4,D=a(()=>I(),"acceptedRelease");async function G(e){return e===null||!_()?null:await b(e)}a(G,"remoteMachineEnvironmentOf");async function q(e){const t=await k(e);if(t===null)return;const n={scope:"call",payer:t.payer,funding:"bill",sub:t.sub,grantee:t.grantee,provider:"blaxel"},r=await x(n),o=await h();if(r===null){await o.prepare("UPDATE nodes SET spend_refusal = NULL WHERE node_id = ? AND spend_refusal IS NOT NULL").run(e.node_id);return}throw await o.prepare("UPDATE nodes SET spend_refusal = ? WHERE node_id = ?").run(JSON.stringify({limit:r.limit.id,usd:r.usd,reset_at:r.reset_at,at:new Date().toISOString()}),e.node_id),await v({id:`refused:sandbox:${e.node_id}:${r.limit.id}:${r.month}`,kind:"sandbox",what:"wake",usd:null,payer:n.payer,funding:"bill",sub:n.sub,grantee:n.grantee,node:e.node_id,run:e.run_id??null,provider:"blaxel",refused:g(r)}),E(r)}a(q,"checkBilledWake");class J extends Error{static{a(this,"EnvironmentImagePending")}environmentId;imageError;constructor(t,n){super(`environment ${t} has no built image yet${n===null?"":` (${n})`}; the wake waits for the build.`),this.environmentId=t,this.imageError=n,this.name="EnvironmentImagePending"}}async function V(e){return await S(e)}a(V,"imageForWake");async function j(e){return e.release!==D()&&await w(e.nodeId,{afterTurn:!1,reason:"release"}),await d(e.runner).wake(e)==="no_broker"?(await l(e.executionId,{reason:"lost"}),"lost"):(await y(e.executionId,Date.now()),await p(e.executionId),"woken")}a(j,"wakeResident");async function z(e){const t=await d(e.runner).status(e);if(t.state!=="gone"&&t.state!=="failed")return!1;const n=await l(e.executionId,{reason:"lost"});return n&&u({level:"warn",event:"execution.lost",node_id:e.nodeId}),n}a(z,"endRemoteExecutionIfGone");async function K(e,t){const n=await m(e);if(n===null||n.state!=="launching"||n.runner==="process")return;const r=d(n.runner),o=t-Date.parse(n.startedAt),i=await r.status(n);if(i.state!=="unknown"){if(i.state==="running"){if(o<=L||!await s(n.executionId,{reason:"launch_timeout"}))return;await r.stop(n,{reason:"launch_timeout"}),await c(e,"crash",{reason:"launch_failed",outcome:{detail:{schema:"crtr.node-outcome-detail/v1",message:"the sandbox launch did not report within the launch deadline"}}}),u({level:"warn",event:"execution.launch_timeout",node_id:e});return}if(i.state==="failed"){if(!await s(n.executionId,{reason:"launch_failed"}))return;await c(e,"crash",{reason:"launch_failed",outcome:{detail:{schema:"crtr.node-outcome-detail/v1",message:`the sandbox start script failed: ${i.reason}`}}});return}if(o>A){if(!await s(n.executionId,{reason:"launch_lost"}))return;u({level:"warn",event:"execution.launch_lost",node_id:e})}}}a(K,"recoverLaunching");export{J as EnvironmentImagePending,L as LAUNCH_DEADLINE_MS,A as LAUNCH_LOST_GRACE_MS,D as acceptedRelease,q as checkBilledWake,z as endRemoteExecutionIfGone,V as imageForWake,K as recoverLaunching,G as remoteMachineEnvironmentOf,j as wakeResident};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var b=Object.defineProperty;var o=(r,
|
|
1
|
+
var b=Object.defineProperty;var o=(r,i)=>b(r,"name",{value:i,configurable:!0});import{clearFrozen as P,getNode as g}from"../../../core/canvas/index.js";import{isSafeNodeId as z}from"../../../core/canvas/paths.js";import{recordedPidLiveness as L}from"../../../core/canvas/pid.js";import{endExecution as v,hasAbsorbedProfilePauseStop as N,hasObservedExit as x,latestExecutionForNode as M,liveExecutionForNode as c}from"../../../core/canvas/executions.js";import{emitEvent as w}from"../../../core/events/emit.js";import{operationIdContext as D}from"../../../core/events/operation-id.js";import{readInterruptedTurn as F}from"../../../core/canvas/recovery.js";import{isProfilePaused as d}from"../../../core/profiles/manifest.js";import{isRecoveryHeld as I}from"../../../core/runtime/recovery-hold.js";import{hasUnseenWakeMail as p}from"../../../core/canvas/deliveries.js";import{hasIdleReleaseIntent as A,isParkedResident as C}from"../../../core/runtime/lifecycle.js";import{classifyDeadNode as S}from"../../fleet.js";import{applyParkEvent as T,hasPendingWake as U,parkEvent as W}from"../live-obligation.js";import{FreezeLaneReconciler as B}from"./freeze-lane.js";import{isInBackoff as E}from"./respawn-policy.js";import{asNodePerson as G,NodePersonUnresolvedError as k}from"./node-person.js";import{handleTerminatingRow as H}from"./terminating.js";import{wantsExecution as O}from"./wants-execution.js";import{enactRequestedStops as q}from"./requested-stops.js";import{endRemoteExecutionIfGone as j,recoverLaunching as J,wakeResident as K}from"./remote-wake.js";import{isDrained as Q}from"../../drain.js";function V(r){return r.status==="done"||r.status==="canceled"||r.status==="dead"}o(V,"isTerminal");const X=12e4;function Y(r,i){const e=Math.max(...[r.created,r.launched_at].map(n=>n==null?0:Date.parse(n)||0));return e>0&&i-e<X}o(Y,"isLaunchInFlight");async function R(r){const i=await c(r);return i!==null&&i.state==="running"&&i.pid!=null&&L(i.pid,i.processIdentity)==="dead"?i:null}o(R,"executionWithDeadPid");class we{static{o(this,"NodeLifecycleReconciler")}freezeLane=new B;async run(i,e){const n=[];await q(i);for(const s of e.rows)await D.fresh(async()=>{try{await G(s,()=>this.classifyRow(i,s,e,n))}catch(t){const l=s.node_id;w({level:t instanceof k?"warn":"error",event:t instanceof k?"node.lifecycle.person_unresolved":"node.lifecycle.classify_failed",...z(l)?{node_id:l}:{fields:{affected_node_id:l}},error:t})}});return await Q()?{stalled:0,waitingOnMail:0}:this.freezeLane.run(i,{fleet:e.fleet,candidates:n})}async classifyRow(i,e,n,s){const t=e.node_id;if(V(e)){if(H(e,await n.fleet.has(t)))return;if(C(e)&&!await n.fleet.has(t)&&!d(e.profile_id)&&!await I(t)&&!E(e,i)&&await p(t)){s.push({row:e,enact:"reopen"});return}if(e.pi_pid!=null){const a=await R(t);a!==null&&await v(a.executionId,{reason:"observed-dead"})}e.frozen_at!=null&&(e.final_report!=null||e.status==="dead"?await P(t):s.push({row:e,enact:"thaw"}));return}if(await n.fleet.has(t)){const a=await c(t);if(a?.runner!==void 0&&a.runner!=="process"&&a.state==="running"&&!d(e.profile_id)&&await p(t)){if(await K(a)==="lost")return}else a?.runner!==void 0&&a.runner!=="process"&&a.state==="running"&&await j(a);if(await n.fleet.has(t)){d(e.profile_id)?await n.fleet.interruptForProfilePause(t):await this.thawMark(e);return}}if(await J(t,i),d(e.profile_id)){await this.freezeLane.endEpisode(t);return}if(await I(t))return;const l=e.pi_pid==null?await M(t):null,f=l!==null&&x(l),m=l!==null&&!f&&N(l),h=e.pi_pid!=null?await R(t):null,_=f||m||h!==null;if(_&&!await n.fleet.hasProfilePauseInterruption(t)){const a=h??l;a!==null&&await v(a.executionId,{reason:"observed-dead"});const y=await c(t);if(y!==null&&y.executionId!==a?.executionId)return}if(!await O(e)){await this.thawMark(e),await this.settleParked(e);return}if(E(e,i))return;if(e.status==="idle"&&A(e)&&await p(t)){s.push({row:e,enact:"idle-release"});return}if(e.pi_pid==null&&(!f||e.frozen_at!=null)&&!m){if(e.respawn_not_before!=null){s.push({row:e,enact:"policy"});return}if(e.frozen_at!=null){s.push({row:e,enact:"thaw"});return}if(await n.fleet.isClaimed(t)||Y(e,i))return;w({level:"warn",event:"node.lifecycle.stranded_launch",node_id:t}),s.push({row:e,enact:"thaw"});return}if(!_)return;const u=S(await g(t),(await F(t))?.interrupted===!0,await U(t));if(u==="respawn-fresh"||u==="respawn-cycle"||u==="respawn-resume"){s.push({row:e,enact:"policy"});return}await n.fleet.applyDeadRowPolicy(t),await this.thawMark(e)}async thawMark(i){i.frozen_at!=null&&await P(i.node_id)}async settleParked(i){if(i.status!=="idle")return;await this.freezeLane.endEpisode(i.node_id);const e=await W(i.node_id,i.lifecycle);e!=="release"&&await T(i.node_id,e)}}export{X as LAUNCH_GRACE_MS,we as NodeLifecycleReconciler,Y as isLaunchInFlight};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { DetachedWorkLifecycle } from './broker-supervision.js';
|
|
2
|
+
import { type LanePass } from './throttled-lane.js';
|
|
3
|
+
export declare const PURGE_DUE_WORK_INTERVAL_MS: number;
|
|
4
|
+
/** Rows purged per pass; the rest wait for the next pass. */
|
|
5
|
+
export declare const PURGE_BUDGET = 1000;
|
|
6
|
+
export interface PurgeDueWorkContext {
|
|
7
|
+
lifecycle: DetachedWorkLifecycle;
|
|
8
|
+
}
|
|
9
|
+
export declare class PurgeDueWorkReconciler {
|
|
10
|
+
run(now: number, ctx: PurgeDueWorkContext): void;
|
|
11
|
+
/** One pass. Exposed for tests. */
|
|
12
|
+
pass(now: number, lane?: Pick<LanePass, 'holds'>): Promise<number>;
|
|
13
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
var a=Object.defineProperty;var c=(s,e)=>a(s,"name",{value:e,configurable:!0});import{emitEvent as n}from"../../core/events/emit.js";import{operationIdContext as i}from"../../core/events/operation-id.js";import{purgeSoftDeleted as d}from"../../core/storage/purge.js";import{runThrottledLane as p}from"./throttled-lane.js";const u=10*6e4,l=1e3,f=5*6e4;class D{static{c(this,"PurgeDueWorkReconciler")}run(e,t){if(!t.lifecycle.acceptsDetachedWork())return;const r=i.fresh(()=>p("purge-due-work","",e,u,f,async o=>{await this.pass(e,o)})).then(()=>{}).catch(o=>{i.fresh(()=>n({level:"error",event:"purge.due_work.failed",error:o}))});t.lifecycle.registerDetached(r)}async pass(e,t){const r=await d(e,l,t?.holds);return r>0&&n({level:"info",event:"purge.due_work.settled",outcome:"succeeded",fields:{purged:r}}),r}}export{l as PURGE_BUDGET,u as PURGE_DUE_WORK_INTERVAL_MS,D as PurgeDueWorkReconciler};
|
|
@@ -3,6 +3,7 @@ declare const CLAIMABLE: {
|
|
|
3
3
|
readonly nodes: "node_id";
|
|
4
4
|
readonly structured_requests: "request_id";
|
|
5
5
|
readonly human_requests: "request_id";
|
|
6
|
+
readonly import_runs: "run_id";
|
|
6
7
|
};
|
|
7
8
|
export type ClaimableTable = keyof typeof CLAIMABLE;
|
|
8
9
|
export declare class ClaimLostError extends Error {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var E=Object.defineProperty;var i=(e,r)=>E(e,"name",{value:r,configurable:!0});import{randomUUID as
|
|
2
|
-
WHERE ${a} = ? AND (claimed_by IS NULL OR lease_expires_at IS NULL OR lease_expires_at <= ?)`).run(n,new Date(Date.now()+t).toISOString(),r,
|
|
1
|
+
var E=Object.defineProperty;var i=(e,r)=>E(e,"name",{value:r,configurable:!0});import{randomUUID as d}from"node:crypto";import{openDb as _}from"../../core/canvas/db.js";const D={nodes:"node_id",structured_requests:"request_id",human_requests:"request_id",import_runs:"run_id"};class w extends Error{static{i(this,"ClaimLostError")}constructor(r,t){super(`row claim lost: ${r} ${t} is no longer held by this process`)}}async function L(e,r,t,u){const a=D[e],n=d(),o=await _(),p=new Date().toISOString();if((await o.prepare(`UPDATE ${e} SET claimed_by = ?, lease_expires_at = ?
|
|
2
|
+
WHERE ${a} = ? AND (claimed_by IS NULL OR lease_expires_at IS NULL OR lease_expires_at <= ?)`).run(n,new Date(Date.now()+t).toISOString(),r,p)).changes!==1)return!1;let s=!1;const c=setInterval(()=>{o.prepare(`UPDATE ${e} SET lease_expires_at = ? WHERE ${a} = ? AND claimed_by = ? AND lease_expires_at > ?`).run(new Date(Date.now()+t).toISOString(),r,n,new Date().toISOString()).then(l=>{l.changes!==1&&(s=!0)},()=>{s=!0})},Math.max(1,Math.floor(t/3)));c.unref();const m={async assertOwned(){if(s)throw new w(e,r);if(await(await _()).prepare(`SELECT 1 AS held FROM ${e} WHERE ${a} = ? AND claimed_by = ? AND lease_expires_at > ? FOR UPDATE`).get(r,n,new Date().toISOString())===void 0)throw s=!0,new w(e,r)}};try{await u(m)}finally{clearInterval(c),await o.prepare(`UPDATE ${e} SET claimed_by = NULL, lease_expires_at = NULL WHERE ${a} = ? AND claimed_by = ?`).run(r,n)}return!0}i(L,"withRowClaim");export{w as ClaimLostError,L as withRowClaim};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
var
|
|
2
|
-
WHERE node_id = ? AND EXISTS (SELECT 1 FROM reconciler_leases WHERE lane = ? AND key = ? AND owner = ? AND expires_at > ?)`).run(...e===null||
|
|
1
|
+
var M=Object.defineProperty;var l=(t,e)=>M(t,"name",{value:e,configurable:!0});import{randomUUID as y}from"node:crypto";import{openDb as d,withCanvasWrite as C}from"../../core/canvas/db.js";import{claimReconcilerLease as F,releaseReconcilerLease as O,renewReconcilerLease as x}from"../../core/canvas/reconciler-leases.js";import{getNode as R}from"../../core/canvas/canvas.js";import{BrokerClient as H,SocketTransport as D}from"../../core/broker-client/client.js";import{liveExecutionForNode as N}from"../../core/canvas/executions.js";import{runnerForExecution as W}from"../../core/runtime/runner/index.js";import{connectWssView as q}from"../../core/runtime/view-socket.js";import{openMachineView as U}from"../machine-link/forward.js";import{ViewLinkTransport as A}from"./view-transport.js";import{emitRunEvent as g,emitRunSettlement as T,emitRunStatus as j}from"../../core/runs/events.js";import{cancelOpenQuestions as J}from"../../core/runs/questions.js";import{emitEvent as c}from"../../core/events/emit.js";import{toRunOutcomeDTO as P}from"../api/map.js";import{readOutputResult as V}from"../../core/runtime/output-result.js";const B=JSON.stringify({input_tokens:0,output_tokens:0,cache_read_tokens:0,cache_write_tokens:0,cost_usd:0}),p=new Map,h="run-observer",E=1e4,Y=250;async function m(t,e,i){return await C(async n=>await n.prepare("SELECT 1 AS held FROM reconciler_leases WHERE lane = ? AND key = ? AND owner = ? AND expires_at > ? FOR SHARE").get(h,t,e,new Date().toISOString())===void 0?{held:!1}:{held:!0,value:await i()})}l(m,"whileHoldingLease");class v{static{l(this,"RunObserver")}nodeId;runId;owner;client;timer;status;piPid=null;outcomeRevision;openAssistant=!1;assistantStartSequence=0;block=null;thinking="";output="";pendingThinking="";pendingOutput="";flushTimer;closed=!1;lastRenewAt=Date.now();released=Promise.resolve();polling;frames=Promise.resolve();constructor(e,i,n,s,r){this.nodeId=e,this.runId=i,this.owner=n,this.status=s,this.outcomeRevision=r,this.timer=setInterval(()=>{this.pollOrClose()},250),this.timer.unref(),p.set(e,this)}static async start(e,i,n){const s=await d(),r=await s.prepare("SELECT body_json FROM run_events WHERE node_id = ? AND type = ? ORDER BY sequence_number DESC LIMIT 1").get(e,e===i?"node.settled":"child.settled"),a=await s.prepare("SELECT body_json FROM run_events WHERE node_id = ? AND type = 'node.status.changed' ORDER BY sequence_number DESC LIMIT 1").get(e),o=new v(e,i,n,a?JSON.parse(a.body_json).status:void 0,r?JSON.parse(r.body_json).outcome.revision:void 0);return await o.writeStream(null,null),await o.poll(),o}async pollOrClose(){try{await this.poll()}catch(e){this.close(),c({level:"error",event:"daemon.runs.observer_failed",node_id:this.nodeId,error:e})}}poll(){return this.polling===void 0&&(this.polling=this.pollOnce().finally(()=>{this.polling=void 0})),this.polling}async holdLease(){return Date.now()-this.lastRenewAt<E/3?!0:await x(h,this.nodeId,this.owner,E)?(this.lastRenewAt=Date.now(),!0):!1}async writeStream(e,i,n=!1){return(await(await d()).prepare(`UPDATE nodes SET ${n?"":`stream_message_seq = ${e===null?"NULL":"(SELECT event_cursor FROM nodes WHERE node_id = ?)"},`} stream_block = ?
|
|
2
|
+
WHERE node_id = ? AND EXISTS (SELECT 1 FROM reconciler_leases WHERE lane = ? AND key = ? AND owner = ? AND expires_at > ?)`).run(...e===null||n?[]:[this.runId],i===null?null:JSON.stringify(i),this.nodeId,h,this.nodeId,this.owner,new Date().toISOString())).changes===1}async pollOnce(){if(this.closed)return;if(!await this.holdLease()){this.close();return}const e=await R(this.nodeId);if(!e||e.run_id!==this.runId||!await I(this.runId)){this.close();return}this.piPid=e.pi_pid??null;const i=e.pi_pid===null?"dormant":e.status;if(this.status!==i){if(!(await m(this.nodeId,this.owner,()=>j(this.runId,this.nodeId,i,e.intent??null))).held){this.close();return}this.status=i}const n=await P(e);if(n&&n.revision!==this.outcomeRevision&&Date.now()-Date.parse(n.settled_at)<720*60*6e4){if(this.nodeId===this.runId){const a=await(await d()).prepare("SELECT node_id FROM nodes WHERE run_id = ? AND node_id != ? ORDER BY created DESC").all(this.runId,this.runId);for(const o of a)await k(o.node_id,this.runId),await p.get(o.node_id)?.poll()}this.outcomeRevision=n.revision;const s=this.nodeId===this.runId&&n.kind==="result"?await V(this.nodeId):void 0;if(!(await m(this.nodeId,this.owner,()=>this.nodeId===this.runId?T(this.runId,this.nodeId,"node.settled",{outcome:n,usage:JSON.parse(e.usage_json??B),...s===void 0?{}:{result:s}}):T(this.runId,this.nodeId,"child.settled",{child_node_id:this.nodeId,outcome:n}))).held){this.close();return}if(e.grantee&&e.grantee!=="app:terminal"){const a=this.nodeId===this.runId?{run_id:this.runId}:{node_id:this.nodeId};J(a,this.nodeId===this.runId?"the run settled":"the asking node was closed").catch(o=>c({level:"error",event:"daemon.runs.question_cancel_failed",node_id:this.nodeId,error:o}))}}e.pi_pid===null?this.close():await this.connect()}async settlePoll(){await this.polling?.catch(()=>{}),await this.frames,await this.released}async viewAdmitted(){const e=await N(this.nodeId);return!(e?.runner==="blaxel"&&e.idleAt!==null)}async dropView(){const e=this.client;e&&(this.client=void 0,await this.frames,await this.flushDeltas(),this.discardDeltas(),this.openAssistant=!1,this.block=null,await this.writeStream(null,null),e.close())}async connect(){if(this.closed)return;if(!await this.viewAdmitted()){await this.dropView();return}if(this.client)return;const e=await $(this.nodeId),i=await this.viewAdmitted();if(this.client||this.closed||!i){e.destroy();return}const n=new H(this.nodeId,{transport:e});this.client=n,n.on("connect",()=>n.send({type:"hello",role:"observer",client_id:y(),attends:!1}));const s=l(async r=>{if(!(this.closed||this.client!==n)){if(r.type==="message_start"&&r.message.role==="assistant"){if(this.discardDeltas(),this.openAssistant=!0,this.block=null,this.thinking="",this.output="",!await this.writeStream("run-cursor",null)){this.close();return}this.assistantStartSequence=Number((await(await d()).prepare("SELECT stream_message_seq FROM nodes WHERE node_id = ?").get(this.nodeId)).stream_message_seq)}if(r.type==="message_update"&&r.message.role==="assistant"&&this.openAssistant){const a=r.message.content;if(!Array.isArray(a))return;const o=a.at(-1);if(o&&typeof o=="object"&&"type"in o&&(o.type==="toolCall"?this.block={kind:"tool_call",name:"name"in o&&typeof o.name=="string"?o.name:""}:(o.type==="text"||o.type==="thinking")&&(this.block={kind:o.type})),!await this.writeStream(null,this.block,!0)){this.close();return}const f=a.filter(u=>u?.type==="thinking").map(u=>u.thinking).join(""),_=a.filter(u=>u?.type==="text").map(u=>u.text).join(""),L=f.startsWith(this.thinking)?f.slice(this.thinking.length):f,b=_.startsWith(this.output)?_.slice(this.output.length):_;this.thinking=f,this.output=_,this.pendingThinking+=L,this.pendingOutput+=b,(this.pendingThinking||this.pendingOutput)&&!this.flushTimer&&(this.flushTimer=setTimeout(()=>{this.flushDeltas().catch(u=>c({level:"error",event:"daemon.runs.observer_failed",node_id:this.nodeId,error:u}))},Y),this.flushTimer.unref())}r.type==="message_end"&&r.message.role==="assistant"&&(await this.flushDeltas(),this.openAssistant=!1,this.block=null,await this.writeStream(null,null))}},"handleFrame");n.on("frame",r=>{this.frames=this.frames.then(()=>s(r)).catch(a=>{this.close(),c({level:"error",event:"daemon.runs.observer_failed",node_id:this.nodeId,error:a})})}),n.on("error",()=>{}),n.on("close",()=>{this.client===n&&(this.client=void 0,this.discardDeltas(),this.openAssistant=!1,this.block=null,this.released=this.released.then(async()=>{await this.writeStream(null,null)}).catch(()=>{}))}),n.connect()}async flushDeltas(){this.flushTimer&&clearTimeout(this.flushTimer),this.flushTimer=void 0;const e=this.pendingThinking,i=this.pendingOutput;if(this.pendingThinking="",this.pendingOutput="",!this.openAssistant||!e&&!i)return;(await m(this.nodeId,this.owner,async()=>{if(await(await d()).prepare("SELECT 1 FROM run_events WHERE run_id = ? AND node_id = ? AND sequence_number >= ? AND type IN ('node.output_text.done', 'node.thinking.done', 'usage') LIMIT 1").get(this.runId,this.nodeId,this.assistantStartSequence)){this.openAssistant=!1;return}e&&await g(this.runId,this.nodeId,"node.thinking.delta",{delta:e}),i&&await g(this.runId,this.nodeId,"node.output_text.delta",{delta:i})})).held||this.close()}discardDeltas(){this.flushTimer&&clearTimeout(this.flushTimer),this.flushTimer=void 0,this.pendingThinking="",this.pendingOutput=""}close(){if(this.closed)return;this.closed=!0,this.discardDeltas(),clearInterval(this.timer),p.delete(this.nodeId),this.released=this.released.then(async()=>{await this.writeStream(null,null),await O(h,this.nodeId,this.owner)}).then(()=>{}).catch(i=>{c({level:"error",event:"daemon.runs.observer_release_failed",node_id:this.nodeId,error:i})});const e=this.client;this.client=void 0,e?.close()}}async function $(t){const e=await N(t);if(e===null)return new D(t,{recordsViewerFaults:!1});const i=await W(e.runner).viewerAddress(e);return i.kind==="machine"?new A(t,()=>U(i.environmentId,t)):i.kind==="unix"?new D(t,{recordsViewerFaults:!1}):new A(t,()=>q(i.url,i.headers))}l($,"viewTransport");async function he(t){const e=await(await d()).prepare("SELECT stream_message_seq FROM node_rows WHERE node_id = ? AND pi_pid IS NOT NULL").get(t);return e?.stream_message_seq==null?null:Number(e.stream_message_seq)}l(he,"openAssistantMessageStartSequence");async function pe(t){const e=await(await d()).prepare("SELECT stream_message_seq, stream_block FROM node_rows WHERE node_id = ? AND pi_pid IS NOT NULL").get(t);return e?.stream_message_seq==null||e.stream_block===null?null:JSON.parse(e.stream_block)}l(pe,"openAssistantBlock");async function fe(){const t=[...p.values()];for(const e of t)e.close();await Promise.all(t.map(e=>e.settlePoll())),await Promise.all([...w.values()].map(e=>e.catch(()=>{})))}l(fe,"closeRunObserversForTests");async function I(t){return await(await d()).prepare("SELECT 1 FROM nodes WHERE node_id = ? AND run_id = ? AND event_cursor IS NOT NULL").get(t,t)!==void 0}l(I,"runRootExists");const w=new Map;async function k(t,e){const i=w.get(t);if(i)return i;if(p.has(t))return;const n=G(t,e).finally(()=>w.delete(t));return w.set(t,n),n}l(k,"ensureRunObserver");async function G(t,e){if(!await I(e))return;const i=y();if(await F(h,t,i,E)!==void 0)try{if(t!==e){const n=await R(t);if(!await(await d()).prepare("SELECT 1 FROM run_events WHERE run_id = ? AND node_id = ? AND type = 'child.started' LIMIT 1").get(e,t)&&n&&Date.now()-Date.parse(n.created)<720*60*6e4&&!(await m(t,i,()=>g(e,t,"child.started",{child_node_id:t,kind:n.kind}))).held)return}await v.start(t,e,i)}catch(n){throw await O(h,t,i).catch(()=>{}),n}}l(G,"startRunObserver");async function _e(){const t=new Date(Date.now()-2592e6).toISOString(),e=await(await d()).prepare(`SELECT n.node_id, n.run_id, n.pi_pid, COALESCE(n.outcome_revision, 1) AS revision, e.body_json
|
|
3
3
|
FROM node_rows n
|
|
4
4
|
LEFT JOIN run_events e ON n.pi_pid IS NULL AND e.run_id = n.run_id AND e.node_id = n.node_id
|
|
5
5
|
AND e.type = CASE WHEN n.node_id = n.run_id THEN 'node.settled' ELSE 'child.settled' END
|
|
6
|
-
WHERE n.run_id IS NOT NULL AND (n.pi_pid IS NOT NULL OR (n.outcome_at IS NOT NULL AND n.outcome_at > ? AND n.outcome_reason != 'parked'))`).all(t),i=new Map;for(const
|
|
6
|
+
WHERE n.run_id IS NOT NULL AND (n.pi_pid IS NOT NULL OR (n.outcome_at IS NOT NULL AND n.outcome_at > ? AND n.outcome_reason != 'parked'))`).all(t),i=new Map;for(const s of e){const r=i.get(s.node_id)??{node_id:s.node_id,run_id:s.run_id,reported:!1};s.body_json!==null&&JSON.parse(s.body_json).outcome?.revision===s.revision&&(r.reported=!0),i.set(s.node_id,r)}const n=[...i.values()].filter(s=>!s.reported);for(const s of n)try{await k(s.node_id,s.run_id),S.delete(s.node_id)}catch(r){Q(s.node_id,r)}}l(_e,"pollRunObservers");const S=new Map;function Q(t,e){const i=e instanceof Error?e.message:String(e);S.get(t)!==i&&(S.set(t,i),c({level:"error",event:"daemon.runs.observer_failed",node_id:t,error:e}))}l(Q,"reportObserverFailure");export{fe as closeRunObserversForTests,k as ensureRunObserver,pe as openAssistantBlock,he as openAssistantMessageStartSequence,_e as pollRunObservers};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.581",
|
|
4
4
|
"description": "crtr — agent runtime with memory, plugins, and marketplaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -145,7 +145,7 @@
|
|
|
145
145
|
},
|
|
146
146
|
"license": "UNLICENSED",
|
|
147
147
|
"dependencies": {
|
|
148
|
-
"@north-light/identity": "0.3.
|
|
148
|
+
"@north-light/identity": "0.3.581",
|
|
149
149
|
"@earendil-works/chord": "1.1.0",
|
|
150
150
|
"@earendil-works/pi-agent-core": "1.1.0",
|
|
151
151
|
"@earendil-works/pi-ai": "1.1.0",
|
package/runtime.lock.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.581",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@north-light/cli",
|
|
9
|
-
"version": "0.3.
|
|
9
|
+
"version": "0.3.581",
|
|
10
10
|
"hasInstallScript": true,
|
|
11
11
|
"license": "UNLICENSED",
|
|
12
12
|
"workspaces": [
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"@milkdown/preset-gfm": "^7.22.3",
|
|
29
29
|
"@milkdown/prose": "^7.22.3",
|
|
30
30
|
"@milkdown/utils": "^7.22.3",
|
|
31
|
-
"@north-light/identity": "0.3.
|
|
31
|
+
"@north-light/identity": "0.3.581",
|
|
32
32
|
"cron-parser": "^5.6.0",
|
|
33
33
|
"esbuild": "^0.27.7",
|
|
34
34
|
"jose": "^6.2.12",
|
|
@@ -11956,17 +11956,17 @@
|
|
|
11956
11956
|
},
|
|
11957
11957
|
"packages/crouter-api": {
|
|
11958
11958
|
"name": "@north-light/api",
|
|
11959
|
-
"version": "0.3.
|
|
11959
|
+
"version": "0.3.581",
|
|
11960
11960
|
"license": "UNLICENSED"
|
|
11961
11961
|
},
|
|
11962
11962
|
"packages/crouter-env-docker": {
|
|
11963
11963
|
"name": "@north-light/env-docker",
|
|
11964
|
-
"version": "0.3.
|
|
11964
|
+
"version": "0.3.581",
|
|
11965
11965
|
"license": "UNLICENSED"
|
|
11966
11966
|
},
|
|
11967
11967
|
"packages/crouter-identity": {
|
|
11968
11968
|
"name": "@north-light/identity",
|
|
11969
|
-
"version": "0.3.
|
|
11969
|
+
"version": "0.3.581",
|
|
11970
11970
|
"license": "UNLICENSED",
|
|
11971
11971
|
"dependencies": {
|
|
11972
11972
|
"jose": "^6.2.1"
|
|
@@ -11974,21 +11974,21 @@
|
|
|
11974
11974
|
},
|
|
11975
11975
|
"packages/crouter-plugin": {
|
|
11976
11976
|
"name": "@north-light/plugin",
|
|
11977
|
-
"version": "0.3.
|
|
11977
|
+
"version": "0.3.581",
|
|
11978
11978
|
"license": "UNLICENSED",
|
|
11979
11979
|
"dependencies": {
|
|
11980
|
-
"@north-light/api": "0.3.
|
|
11981
|
-
"@north-light/identity": "0.3.
|
|
11980
|
+
"@north-light/api": "0.3.581",
|
|
11981
|
+
"@north-light/identity": "0.3.581"
|
|
11982
11982
|
}
|
|
11983
11983
|
},
|
|
11984
11984
|
"packages/crouter-sdk": {
|
|
11985
11985
|
"name": "@north-light/sdk",
|
|
11986
|
-
"version": "0.3.
|
|
11986
|
+
"version": "0.3.581",
|
|
11987
11987
|
"license": "UNLICENSED",
|
|
11988
11988
|
"dependencies": {
|
|
11989
|
-
"@north-light/api": "0.3.
|
|
11990
|
-
"@north-light/identity": "0.3.
|
|
11991
|
-
"@north-light/plugin": "0.3.
|
|
11989
|
+
"@north-light/api": "0.3.581",
|
|
11990
|
+
"@north-light/identity": "0.3.581",
|
|
11991
|
+
"@north-light/plugin": "0.3.581",
|
|
11992
11992
|
"jose": "^6.2.1"
|
|
11993
11993
|
},
|
|
11994
11994
|
"bin": {
|
|
@@ -9,10 +9,13 @@
|
|
|
9
9
|
import { spawn, spawnSync } from 'node:child_process';
|
|
10
10
|
import { createServer } from 'node:http';
|
|
11
11
|
import { randomBytes } from 'node:crypto';
|
|
12
|
-
import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, realpathSync, rmSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
12
|
+
import { chmodSync, closeSync, constants as fsConstants, createWriteStream, existsSync, fsyncSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, realpathSync, renameSync, rmSync, symlinkSync, unlinkSync, writeFileSync, writeSync } from 'node:fs';
|
|
13
13
|
import { tmpdir } from 'node:os';
|
|
14
|
-
import { dirname, join, resolve, sep } from 'node:path';
|
|
14
|
+
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
|
+
import { Readable } from 'node:stream';
|
|
17
|
+
import { pipeline } from 'node:stream/promises';
|
|
18
|
+
import { createZstdDecompress } from 'node:zlib';
|
|
16
19
|
|
|
17
20
|
export const EXIT_NO_BROKER = 3;
|
|
18
21
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
@@ -134,6 +137,171 @@ function gitAsync(cwd, args, timeoutMs, env) {
|
|
|
134
137
|
});
|
|
135
138
|
}
|
|
136
139
|
|
|
140
|
+
const archiveInvalid = (detail) => new StartFailure('import_archive_invalid', detail);
|
|
141
|
+
|
|
142
|
+
/** Create `dir` (which must lie lexically in `root`) one component at a time; no existing component may resolve outside `rootReal`. */
|
|
143
|
+
function ensureDir(root, rootReal, dir) {
|
|
144
|
+
let current = root;
|
|
145
|
+
for (const segment of dir.slice(root.length).split(sep).filter(Boolean)) {
|
|
146
|
+
current = join(current, segment);
|
|
147
|
+
if (!lstatExists(current)) mkdirSync(current);
|
|
148
|
+
if (!within(rootReal, realpathSync(current))) throw archiveInvalid(`${current} resolves outside ${root}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Each entry of a tar file: `{name, type, size, mode, linkname, offset}`; `offset` is where the data starts. Handles ustar, pax (`x`) and GNU long names (`L`, `K`). */
|
|
153
|
+
export function* tarEntries(fd, fileSize) {
|
|
154
|
+
const block = Buffer.alloc(512);
|
|
155
|
+
const text = (b, from, to) => { const slice = b.subarray(from, to); const end = slice.indexOf(0); return slice.subarray(0, end < 0 ? slice.length : end).toString('utf8'); };
|
|
156
|
+
const octal = (b, from, to) => {
|
|
157
|
+
if (b[from] & 0x80) { let n = 0n; for (let i = from + 1; i < to; i++) n = (n << 8n) | BigInt(b[i]); return Number(n); }
|
|
158
|
+
const t = text(b, from, to).trim();
|
|
159
|
+
return t === '' ? 0 : parseInt(t, 8);
|
|
160
|
+
};
|
|
161
|
+
const readAt = (buffer, position) => { const n = readSync(fd, buffer, 0, buffer.length, position); if (n !== buffer.length) throw archiveInvalid('the archive is truncated'); };
|
|
162
|
+
const readText = (position, size) => { const b = Buffer.alloc(size); readAt(b, position); return b; };
|
|
163
|
+
let position = 0;
|
|
164
|
+
let overrides = {};
|
|
165
|
+
let zeroBlocks = 0;
|
|
166
|
+
while (position + 512 <= fileSize) {
|
|
167
|
+
readAt(block, position);
|
|
168
|
+
position += 512;
|
|
169
|
+
if (block.every((x) => x === 0)) { if (++zeroBlocks === 2) return; continue; }
|
|
170
|
+
zeroBlocks = 0;
|
|
171
|
+
let sum = 0;
|
|
172
|
+
for (let i = 0; i < 512; i++) sum += i >= 148 && i < 156 ? 32 : block[i];
|
|
173
|
+
if (sum !== parseInt(text(block, 148, 156).trim() || 'x', 8)) throw archiveInvalid('a tar header fails its checksum');
|
|
174
|
+
const type = String.fromCharCode(block[156] || 48);
|
|
175
|
+
const size = octal(block, 124, 136);
|
|
176
|
+
if (!Number.isSafeInteger(size) || size < 0 || position + size > fileSize) throw archiveInvalid('a tar entry runs past the end of the archive');
|
|
177
|
+
const dataAt = position;
|
|
178
|
+
position += Math.ceil(size / 512) * 512;
|
|
179
|
+
if (type === 'x') {
|
|
180
|
+
const pax = readText(dataAt, size).toString('utf8');
|
|
181
|
+
for (let i = 0; i < pax.length;) {
|
|
182
|
+
const space = pax.indexOf(' ', i);
|
|
183
|
+
const length = Number(pax.slice(i, space));
|
|
184
|
+
if (!(length > 0)) throw archiveInvalid('a pax header is malformed');
|
|
185
|
+
const [key, ...value] = Buffer.from(pax).subarray(i, i + length - 1).toString('utf8').slice(space - i + 1).split('=');
|
|
186
|
+
overrides[key] = value.join('=');
|
|
187
|
+
i += length;
|
|
188
|
+
}
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
if (type === 'g') continue;
|
|
192
|
+
if (type === 'L') { overrides.path = text(readText(dataAt, size), 0, size); continue; }
|
|
193
|
+
if (type === 'K') { overrides.linkpath = text(readText(dataAt, size), 0, size); continue; }
|
|
194
|
+
const prefix = text(block, 345, 500);
|
|
195
|
+
const ustar = text(block, 257, 262) === 'ustar';
|
|
196
|
+
const name = overrides.path ?? (ustar && prefix ? `${prefix}/${text(block, 0, 100)}` : text(block, 0, 100));
|
|
197
|
+
const linkname = overrides.linkpath ?? text(block, 157, 257);
|
|
198
|
+
overrides = {};
|
|
199
|
+
yield { name, type, size, mode: octal(block, 100, 108) & 0o7777, linkname, offset: dataAt };
|
|
200
|
+
}
|
|
201
|
+
throw archiveInvalid('the archive has no end-of-archive marker');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** The target of an archive entry: the path map entry whose `from` (without its leading slash) is the longest leading run of the entry's segments, then
|
|
205
|
+
* the rest under that entry's `to`. Throws `import_archive_invalid` for an absolute name, a `..` segment, or a name under no prefix of the map. */
|
|
206
|
+
export function archiveTarget(pathMap, name) {
|
|
207
|
+
if (name.startsWith('/')) throw archiveInvalid(`entry ${name} has an absolute path`);
|
|
208
|
+
const segments = name.split('/').filter((x) => x !== '' && x !== '.');
|
|
209
|
+
if (segments.includes('..')) throw archiveInvalid(`entry ${name} escapes its prefix`);
|
|
210
|
+
let best = null;
|
|
211
|
+
for (const entry of pathMap) {
|
|
212
|
+
const from = entry.from.split('/').filter(Boolean);
|
|
213
|
+
if (from.length <= segments.length && from.every((x, i) => segments[i] === x) && (best === null || from.length > best.length)) best = { entry, length: from.length };
|
|
214
|
+
}
|
|
215
|
+
if (best === null) throw archiveInvalid(`entry ${name} lies under no prefix of the path map`);
|
|
216
|
+
const rest = segments.slice(best.length);
|
|
217
|
+
return { root: best.entry.to, path: join(best.entry.to, ...rest), rest };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Where an archive's entries for `root` are written first, beside it (so the final move is a rename on one filesystem). */
|
|
221
|
+
const stagingOf = (root) => join(dirname(root), `.${basename(root)}.import-staging`);
|
|
222
|
+
|
|
223
|
+
/** Move what was staged for `root` into it, replacing a same-named entry a retried or interrupted earlier attempt left. */
|
|
224
|
+
function placeStaged(stage, root) {
|
|
225
|
+
mkdirSync(root, { recursive: true });
|
|
226
|
+
for (const child of readdirSync(stage)) {
|
|
227
|
+
const to = join(root, child);
|
|
228
|
+
if (lstatExists(to)) rmSync(to, { recursive: true, force: true });
|
|
229
|
+
renameSync(join(stage, child), to);
|
|
230
|
+
}
|
|
231
|
+
rmSync(stage, { recursive: true, force: true });
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Download the node's archive and unpack every entry under its path-map target; nothing is written outside the targets, whatever the archive names.
|
|
235
|
+
* Entries are written to a staging directory beside each target and moved in only after the whole archive has been read, so an attempt that fails
|
|
236
|
+
* partway leaves no half-unpacked target and a retry starts clean. */
|
|
237
|
+
export async function unpackImportedFiles(files, scratch) {
|
|
238
|
+
const response = await fetch(files.url, { signal: AbortSignal.timeout(600_000) });
|
|
239
|
+
if (!response.ok || response.body === null) throw new StartFailure('import_archive_unreachable', `the archive download answered ${response.status}`);
|
|
240
|
+
const tarFile = join(scratch, 'import.tar');
|
|
241
|
+
await pipeline(Readable.fromWeb(response.body), createZstdDecompress(), createWriteStream(tarFile));
|
|
242
|
+
const targets = [...new Set(files.path_map.map((m) => m.to))];
|
|
243
|
+
for (const root of targets) rmSync(stagingOf(root), { recursive: true, force: true });
|
|
244
|
+
const fd = openSync(tarFile, 'r');
|
|
245
|
+
try {
|
|
246
|
+
const size = lstatSync(tarFile).size;
|
|
247
|
+
const stages = new Map();
|
|
248
|
+
const stageOf = (root) => {
|
|
249
|
+
if (!stages.has(root)) {
|
|
250
|
+
const stage = stagingOf(root);
|
|
251
|
+
mkdirSync(stage, { recursive: true });
|
|
252
|
+
stages.set(root, { stage, real: realpathSync(stage) });
|
|
253
|
+
}
|
|
254
|
+
return stages.get(root);
|
|
255
|
+
};
|
|
256
|
+
for (const entry of tarEntries(fd, size)) {
|
|
257
|
+
const { root, rest } = archiveTarget(files.path_map, entry.name);
|
|
258
|
+
const { stage, real } = stageOf(root);
|
|
259
|
+
const path = join(stage, ...rest);
|
|
260
|
+
if (entry.type === '5') { ensureDir(stage, real, path); continue; }
|
|
261
|
+
if (rest.length === 0) throw archiveInvalid(`entry ${entry.name} is a ${entry.type === '0' ? 'file' : 'link'} where the prefix directory belongs`);
|
|
262
|
+
ensureDir(stage, real, dirname(path));
|
|
263
|
+
if (entry.type === '0') {
|
|
264
|
+
if (lstatExists(path)) unlinkSync(path);
|
|
265
|
+
const out = openSync(path, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_EXCL | fsConstants.O_NOFOLLOW, 0o600);
|
|
266
|
+
try {
|
|
267
|
+
const buffer = Buffer.alloc(Math.min(1 << 20, Math.max(entry.size, 1)));
|
|
268
|
+
for (let done = 0; done < entry.size;) {
|
|
269
|
+
const n = readSync(fd, buffer, 0, Math.min(buffer.length, entry.size - done), entry.offset + done);
|
|
270
|
+
if (n === 0) throw archiveInvalid('the archive is truncated');
|
|
271
|
+
writeSync(out, buffer, 0, n);
|
|
272
|
+
done += n;
|
|
273
|
+
}
|
|
274
|
+
} finally { closeSync(out); }
|
|
275
|
+
chmodSync(path, entry.mode & 0o777);
|
|
276
|
+
} else if (entry.type === '2') {
|
|
277
|
+
// A link is kept only when it resolves, from where it lies, inside the same target (an absolute one is checked against the final target).
|
|
278
|
+
const resolved = resolve(dirname(path), entry.linkname);
|
|
279
|
+
if (entry.linkname.startsWith('/') ? !within(root, entry.linkname) : !within(stage, resolved)) throw archiveInvalid(`symlink ${entry.name} points outside its prefix`);
|
|
280
|
+
if (lstatExists(path)) unlinkSync(path);
|
|
281
|
+
symlinkSync(entry.linkname, path);
|
|
282
|
+
} else {
|
|
283
|
+
throw archiveInvalid(`entry ${entry.name} has type ${JSON.stringify(entry.type)}, which an import archive does not hold`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
for (const [root, { stage }] of stages) placeStaged(stage, root);
|
|
287
|
+
} finally {
|
|
288
|
+
closeSync(fd);
|
|
289
|
+
for (const root of targets) rmSync(stagingOf(root), { recursive: true, force: true });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/** The sandbox's own record that this node's archive was unpacked here: written after a complete unpack, synced to disk, so a sandbox that stopped and
|
|
294
|
+
* woke (or a restarted start script) never unpacks over the work done since. A fresh sandbox has none. */
|
|
295
|
+
const unpackMarker = (root, nodeId) => join(root, 'import-unpacked', nodeId);
|
|
296
|
+
|
|
297
|
+
function writeUnpackMarker(path) {
|
|
298
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
299
|
+
const fd = openSync(path, 'w');
|
|
300
|
+
try { writeSync(fd, `${new Date().toISOString()}\n`); fsyncSync(fd); } finally { closeSync(fd); }
|
|
301
|
+
const dir = openSync(dirname(path), 'r');
|
|
302
|
+
try { fsyncSync(dir); } finally { closeSync(dir); }
|
|
303
|
+
}
|
|
304
|
+
|
|
137
305
|
async function start(env) {
|
|
138
306
|
const need = (name) => { const v = env[name]; if (!v) throw new StartFailure('bad_environment', `${name} is not set`); return v; };
|
|
139
307
|
const apiUrl = need('CRTR_API_URL');
|
|
@@ -225,6 +393,21 @@ async function start(env) {
|
|
|
225
393
|
} finally { await relay?.close(); }
|
|
226
394
|
}
|
|
227
395
|
|
|
396
|
+
// 5.6b a hosted node's imported files (design-import, "Sandbox import step"). The control plane names the archive for as long as it exists, so a
|
|
397
|
+
// replacement sandbox gets it too; this sandbox unpacks only when it holds no marker of its own. The marker is written last: a start that fails
|
|
398
|
+
// anywhere before it (the download, a hostile entry, the report) leaves nothing half-unpacked in place and unpacks again on retry.
|
|
399
|
+
if (bundle.import_node_files) {
|
|
400
|
+
const marker = unpackMarker(root, bundle.node.id);
|
|
401
|
+
if (!existsSync(marker)) {
|
|
402
|
+
const scratch = mkdtempSync(join(tmpdir(), 'crtr-import-'));
|
|
403
|
+
try { await unpackImportedFiles(bundle.import_node_files, scratch); }
|
|
404
|
+
finally { rmSync(scratch, { recursive: true, force: true }); }
|
|
405
|
+
const reported = await api(apiUrl, token, 'POST', `/v1/executions/${executionId}/import-unpacked`, {}).catch((error) => ({ ok: false, status: String(error?.message ?? error) }));
|
|
406
|
+
if (!reported.ok) throw new StartFailure('import_report_failed', `reporting the unpack: ${reported.status}`);
|
|
407
|
+
writeUnpackMarker(marker);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
228
411
|
// 5.7 the managed worktree request (D102): the node's branch in the recipe repository's one clone, never a second checkout.
|
|
229
412
|
const worktree = bundle.launch.worktree;
|
|
230
413
|
if (worktree) {
|