@north-light/cli 0.3.494 → 0.3.496
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/contract.d.ts +1 -1
- package/dist/api/contract.js +1 -1
- package/dist/api/dto/limits.d.ts +15 -1
- package/dist/builtin-memory/crouter-sdk/release-notes.md +9 -1
- package/dist/commands/node/create.js +1 -1
- package/dist/commands/sys/limits.js +3 -3
- package/dist/core/peer/client.d.ts +1 -1
- package/dist/core/peer/directory.d.ts +1 -1
- package/dist/core/peer/token.d.ts +0 -2
- package/dist/core/peer/token.js +2 -2
- package/dist/core/runtime/fleet.d.ts +12 -0
- package/dist/core/runtime/fleet.js +1 -1
- package/dist/core/runtime/host.js +3 -3
- package/dist/core/runtime/memory-admission.d.ts +38 -0
- package/dist/core/runtime/memory-admission.js +1 -0
- package/dist/core/runtime/revive.js +2 -2
- package/dist/core/runtime/spawn.js +2 -2
- package/dist/daemon/api/handlers/limits.js +1 -1
- package/dist/daemon/api/principal.d.ts +2 -2
- package/dist/daemon/api/router.d.ts +1 -1
- package/dist/daemon/peer/ingress.d.ts +2 -2
- package/dist/daemon/peer/relay-client.d.ts +1 -1
- package/dist/daemon/reconcilers/broker-supervision.js +4 -4
- package/dist/launcher/limits/cgroup.d.ts +5 -1
- package/dist/launcher/limits/cgroup.js +2 -2
- package/dist/launcher/limits/manager.d.ts +27 -4
- package/dist/launcher/limits/manager.js +1 -1
- package/dist/launcher/limits/policy.d.ts +43 -0
- package/dist/launcher/limits/policy.js +1 -1
- package/dist/launcher/main.js +2 -2
- package/dist/launcher/spec.d.ts +31 -1
- package/docs/sdk/release-notes.md +9 -1
- package/package.json +2 -2
- package/packages/crouter-identity/dist/index.d.ts +8 -92
- package/packages/crouter-identity/dist/index.js +9 -94
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +12 -12
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var k=Object.defineProperty;var o=(e,t)=>k(e,"name",{value:t,configurable:!0});import{existsSync as w}from"node:fs";import{runtimeRoot as a}from"../../../core/layout.js";import{launcherRequest as l}from"../../../launcher/client.js";import{LAUNCHER_SOCKET as v}from"../../../launcher/spec.js";import{bareApp as u,budgetFor as S}from"../../../launcher/limits/policy.js";import{LimitInputError as g,applyChange as A,readSettings as d,removeOverride as E,writeSettings as c}from"../../../launcher/limits/settings.js";import{ownerOnly as R}from"../router.js";function y(){return process.platform==="linux"&&w(v)}o(y,"hasLauncher");function m(e){throw{status:400,code:"invalid_request",message:e}}o(m,"bad");function f(e,t,r=null){const s=t?.apps.find(p=>p.app===u(e)),i=r?.apps.find(p=>p.app===u(e))?.bytes??null;if(s)return{app:`app:${s.app}`,memory_bytes:s.memory_bytes,disk_bytes:s.disk_bytes,memory_source:s.memory_source,disk_source:s.disk_source,disk_used_bytes:s.disk_used_bytes??i};const n=S(e,d(a()));return{app:`app:${u(e)}`,memory_bytes:n.memoryBytes,disk_bytes:n.diskBytes,memory_source:n.memorySource,disk_source:n.diskSource,disk_used_bytes:i}}o(f,"appRow");async function b(){if(!y())return null;const e=await l({operation:"limits.show"});return"limits"in e?e.limits:null}o(b,"report");const T=5e3;async function $(){if(!y())return null;let e;try{const t=await Promise.race([l({operation:"disk.usage"}),new Promise(r=>{e=setTimeout(()=>r(null),T)})]);return t!==null&&"disk"in t?t.disk:null}catch{return null}finally{clearTimeout(e)}}o($,"diskUsage");function P(e){return e===null?void 0:{total_bytes:e.total_bytes,used_bytes:e.used_bytes,user_bytes:e.user_bytes,runtime_bytes:e.runtime_bytes}}o(P,"dataDisk");async function U(){const[e,t]=await Promise.all([b(),$()]),r=d(a()),s=new Set([...e?.apps.map(n=>n.app)??[],...Object.keys(r.apps)]);return{status:200,body:{memory_enforced:e?.memory.available??!1,memory_reason:e?e.memory.reason:"this host has no launcher, so memory limits are not enforced",disk_enforced:e?.disk.available??!1,disk_reason:e?e.disk.reason:"this host has no launcher, so disk limits are not enforced",apps:[...s].sort().map(n=>f(n,e,t)),...t===null?{}:{data_disk:P(t)}}}}o(U,"handleShow");function _(e){const t=u(decodeURIComponent(e.params.app??""));return/^[a-zA-Z0-9][a-zA-Z0-9._-]{0,127}$/.test(t)||m(`app must be an app id such as app:terminal; received ${e.params.app??""}`),t}o(_,"appParam");async function h(e){let t=!1;return y()&&(await l({operation:"limits.apply",app:e}),t=!0),{app:`app:${e}`,applied:t,limits:f(e,await b())}}o(h,"apply");async function C(e){const t=_(e),r=e.body;(r===null||typeof r!="object"||Array.isArray(r))&&m("body must be an object with memory_bytes and/or disk_bytes");const{memory_bytes:s,disk_bytes:i}=r;s===void 0&&i===void 0&&m("set at least one of memory_bytes and disk_bytes");try{const n=A(d(a()),t,{...s===void 0?{}:{memory_bytes:s},...i===void 0?{}:{disk_bytes:i}});c(a(),n)}catch(n){throw n instanceof g&&m(n.message),n}return{status:200,body:await h(t)}}o(C,"handleSet");async function I(e){const t=_(e),r=e.query.get("fields"),s=r===null||r===""?["memory","disk"]:r.split(",");return s.every(i=>i==="memory"||i==="disk")||m(`fields must be memory, disk or memory,disk; received ${r}`),c(a(),E(d(a()),t,s)),{status:200,body:await h(t)}}o(I,"handleRemove");const K=R([{method:"GET",pattern:"/v1/limits",handler:U},{method:"PUT",pattern:"/v1/limits/apps/:app",handler:C},{method:"DELETE",pattern:"/v1/limits/apps/:app",handler:I}]);export{K as limitsRoutes};
|
|
@@ -31,7 +31,7 @@ export declare class AppPrincipalResolver {
|
|
|
31
31
|
}) => Promise<void>, jwksUrl?: string);
|
|
32
32
|
private load;
|
|
33
33
|
isReady(): boolean;
|
|
34
|
-
/** The directory key set, for the peer ingress's
|
|
34
|
+
/** The directory key set, for the peer ingress's call-token check. */
|
|
35
35
|
get jwks(): JwksCache;
|
|
36
36
|
close(): void;
|
|
37
37
|
private readonly ledger;
|
|
@@ -49,7 +49,7 @@ export declare class AppPrincipalResolver {
|
|
|
49
49
|
* there is no `sub` or `jti` requirement. Every failure is 401. */
|
|
50
50
|
verifyDirectoryRead(authorization: string | undefined, purpose: string): Promise<void>;
|
|
51
51
|
/** Verify a bearer once as a call token: ES256 signature against the JWKS mirror and every check of
|
|
52
|
-
* `verifyCallToken` (
|
|
52
|
+
* `verifyCallToken` (a token with no `act` is refused). The caller routes on `chain[0]`. Authorizes nothing.
|
|
53
53
|
* 503 before the mirror loads; every other failure is 401. */
|
|
54
54
|
verifyCall(authorization: string | undefined): Promise<CallToken>;
|
|
55
55
|
resolve(authorization: string | undefined, scope?: string): Promise<AppPrincipal>;
|
|
@@ -50,7 +50,7 @@ export type ForwardedPrincipal = {
|
|
|
50
50
|
place: RunPlace;
|
|
51
51
|
};
|
|
52
52
|
};
|
|
53
|
-
/** A
|
|
53
|
+
/** A peer call token with no envelope: the calling machine itself (`peer.*` rows only), never a person or an app. */
|
|
54
54
|
export type MachinePrincipal = {
|
|
55
55
|
class: 'machine';
|
|
56
56
|
via: string;
|
|
@@ -3,11 +3,11 @@ import { type CallToken, type JwksCache } from '@north-light/identity';
|
|
|
3
3
|
import { Router } from '../api/router.js';
|
|
4
4
|
import type { GrantSync } from '../../core/grants/cover.js';
|
|
5
5
|
export interface PeerIngressContext {
|
|
6
|
-
/** The directory the
|
|
6
|
+
/** The directory the peer call tokens come from. */
|
|
7
7
|
issuer: string;
|
|
8
8
|
/** Its key set (the app listener's cache, or the laptop's own). */
|
|
9
9
|
jwks: JwksCache;
|
|
10
|
-
/** The audience
|
|
10
|
+
/** The audience peer call tokens must carry: `machineUrl()` on the laptop, `runtime.audience` on the hosted runtime. Null before it is known: every request is refused 503. */
|
|
11
11
|
audience(): string | null;
|
|
12
12
|
/** The account owner (`user:<id>`) this runtime belongs to. Null when it has none: no relation reaches the machine and every request is refused 403. */
|
|
13
13
|
owner(): string | null;
|
|
@@ -8,7 +8,7 @@ export declare const TOKEN_REFRESH_LEAD_MS = 60000;
|
|
|
8
8
|
export declare const SILENCE_LIMIT_MS = 60000;
|
|
9
9
|
export declare const HELLO_TIMEOUT_MS = 10000;
|
|
10
10
|
export interface RelayClientOptions {
|
|
11
|
-
/** The directory (`iss` of the
|
|
11
|
+
/** The directory (`iss` of the peer call tokens). */
|
|
12
12
|
issuer: string;
|
|
13
13
|
/** The relay origin from the machine's reach (`https://relay.northlight.ai`); also the token audience. */
|
|
14
14
|
relayOrigin: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
var q=Object.defineProperty;var p=(s,e)=>q(s,"name",{value:e,configurable:!0});import{existsSync as Y}from"node:fs";import{LAUNCHER_SOCKET as H}from"../../launcher/spec.js";import{launcherRequest as K}from"../../launcher/client.js";import{execFile as V}from"node:child_process";import{promisify as z}from"node:util";import{getNode as N,isWarmSpare as j}from"../../core/canvas/index.js";import{isSafeNodeId as u}from"../../core/canvas/paths.js";import{contextDir as J}from"../../core/canvas/node-agent-paths.js";import{fullName as C}from"../../core/canvas/labels.js";import{recordedPidLiveness as
|
|
2
|
-
`)){const i=a.trim();if(i==="")continue;const n=i.split(/\s+/);if(n.length<3)continue;const o=Number(n[0]),r=Number(n[1]),l=Number(n[2]);if(!Number.isFinite(o)||!Number.isFinite(r)||!Number.isFinite(l))continue;e.set(o,l);const c=t.get(r);c===void 0?t.set(r,[o]):c.push(o)}return{byPid:e,childrenOf:t}}p(B,"parsePsTree");function
|
|
3
|
-
`).map(async m=>{if(m!=="")try{await P("tmux",["display-message","-C","-d","10000","-c",m,c],{encoding:"utf8",timeout:2e3})}catch{try{await P("tmux",["display-message","-c",m,c],{encoding:"utf8",timeout:2e3})}catch(h){f({level:"error",event:"broker.capacity.tmux_display_failed",error:h,fields:{client:m}})}}}))}catch(d){f({level:"error",event:"broker.capacity.tmux_list_clients_failed",error:d})}}async handleYieldStall(e,t,a,i){const n=e.node_id,o=L(t.pid,e.pi_pid_identity);if(e.intent!=="refresh"||F(e)!==null||o===null){o!==null&&this.yieldStalls.delete(o);return}let r=this.yieldStalls.get(o);if(r===void 0){r={since:i},this.yieldStalls.set(o,r);return}if(
|
|
4
|
-
`)[0]??"").slice(0,200),r=t.providerError,l=r===void 0?"":` Provider: ${r.provider}/${r.model}.`+(r.status===void 0?"":` HTTP status: ${r.status}.`)+(r.errno===void 0?"":` Errno: ${r.errno}.`)+(r.code===void 0?"":` Code: ${r.code}.`)+(r.body===void 0?"":` Body: ${r.body.slice(0,200)}.`)+(r.ratelimit===void 0?"":` Rate limit: ${r.ratelimit.scope??"account"}${r.ratelimit.claim===void 0?"":` (${r.ratelimit.claim})`}${r.ratelimit.model===void 0?"":` on ${r.ratelimit.model}`}.`),c=t.kind==="context-overflow",d=
|
|
1
|
+
var q=Object.defineProperty;var p=(s,e)=>q(s,"name",{value:e,configurable:!0});import{existsSync as Y}from"node:fs";import{LAUNCHER_SOCKET as H}from"../../launcher/spec.js";import{launcherRequest as K}from"../../launcher/client.js";import{execFile as V}from"node:child_process";import{promisify as z}from"node:util";import{getNode as N,isWarmSpare as j}from"../../core/canvas/index.js";import{isSafeNodeId as u}from"../../core/canvas/paths.js";import{contextDir as J}from"../../core/canvas/node-agent-paths.js";import{fullName as C}from"../../core/canvas/labels.js";import{recordedPidLiveness as D}from"../../core/canvas/pid.js";import{transition as Q}from"../../core/runtime/lifecycle.js";import{readFault as I,readLiveTurn as E,recordFault as X}from"../../core/canvas/recovery.js";import{errorClassFromFault as Z}from"../../core/fault-classifier.js";import{nextFaultRetry as ee}from"../../core/runtime/fault-recovery.js";import{publishNotice as R}from"../../core/runtime/close.js";import{headlessBrokerHost as k}from"../../core/runtime/host.js";import{emitEvent as f}from"../../core/events/emit.js";import{operationIdContext as O}from"../../core/events/operation-id.js";import{hasUnseenWakeMail as te,resetStaleExecutionMail as ne}from"../../core/canvas/deliveries.js";import{activeBackgroundBashJobs as re}from"../../core/bash-jobs.js";import{focusOf as ie}from"../../core/runtime/placement.js";import{hasAttachedBrokerViewers as ae}from"../api/viewer-presence.js";import{deliverLive as se}from"../../core/runtime/interactive-deliver.js";import{BrokerUnreachableError as oe}from"../../core/runtime/broker-request.js";import{formatCard as le,PARK_SUMMARY_PROMPT as de}from"../../shared/generated-context.js";import{brokerThresholdsForDaemon as ue}from"../../core/runtime/fleet.js";import{readConfig as ce}from"../../core/config.js";import{beginPendingPark as fe,consumePendingPark as y,pendingParkIds as pe,pendingParkStartedAt as me}from"../park-pending.js";import{shouldParkAfterSummary as he}from"../park-activity.js";import{applyParkEvent as b,parkEvent as _e}from"./live-obligation.js";import{memoryDeniedRecently as ge}from"../../core/runtime/memory-admission.js";import{envNoDaemonAutostart as ke,envTestUnattendedParkMs as ve,parkSummaryGraceMs as ye,envUnattendedParkMs as be}from"../../shared/env.js";const P=z(V);function Se(s){return E(s)?.busy===!0}p(Se,"liveBusy");function ut(s){const e=E(s)?.progressAt;if(e==null)return null;const t=Date.parse(e);return Number.isNaN(t)?null:t}p(ut,"liveTurnProgressMs");function L(s,e){return e===null?null:`${s}:${e}`}p(L,"incarnationOf");function F(s){const e=E(s.node_id),t=e?.busy===!0&&e.progressAt!==null?Date.parse(e.progressAt):Number.NaN,a=s.launched_at===null||s.launched_at===void 0?Number.NaN:Date.parse(s.launched_at);return!Number.isFinite(t)||!Number.isFinite(a)||t<a?null:t}p(F,"busySinceForIncarnation");const G=3*6e4,W=2e4,x=20*6e4,we=2,Ne=5*6e4;function Pe(s,e){return s===null||s.parent!=null?!1:s.lifecycle==="terminal"?!0:e==="cap_exhausted"&&s.run_id===s.node_id}p(Pe,"settlesOnFatalProviderFault");function Ae(){const s=be();if(s!==void 0)return s;const e=ce("user").lifecycle.unattendedParkMs;return ke()?ve()??e:e}p(Ae,"unattendedParkMsForDaemon");function U(){return ye()}p(U,"parkSummaryGraceMsForDaemon");function $e(s,e,t,a){return s!==!0||e!=="refresh"||t?"leave":a===null||a<G?"pending":"kill"}p($e,"yieldStallVerdict");function Me(s){return!s.busy||s.quietForMs===null||s.quietForMs<x?"leave":s.cpuPercent===null||s.hasDescendants!==!1&&s.cpuPercent>we?"pending":"wedged"}p(Me,"wedgeVerdict");function B(s){const e=new Map,t=new Map;for(const a of s.split(`
|
|
2
|
+
`)){const i=a.trim();if(i==="")continue;const n=i.split(/\s+/);if(n.length<3)continue;const o=Number(n[0]),r=Number(n[1]),l=Number(n[2]);if(!Number.isFinite(o)||!Number.isFinite(r)||!Number.isFinite(l))continue;e.set(o,l);const c=t.get(r);c===void 0?t.set(r,[o]):c.push(o)}return{byPid:e,childrenOf:t}}p(B,"parsePsTree");function Te(s,e){const{byPid:t,childrenOf:a}=B(s);if(!t.has(e))return null;let i=0;const n=[e],o=new Set;for(;n.length>0;){const r=n.pop();if(o.has(r))continue;o.add(r),i+=t.get(r)??0;const l=a.get(r);l!==void 0&&n.push(...l)}return i}p(Te,"sumTreeCpu");function Ee(s,e){const{byPid:t,childrenOf:a}=B(s);return t.has(e)?(a.get(e)??[]).length>0:null}p(Ee,"treeHasDescendants");class ct{static{p(this,"BrokerSupervisionReconciler")}yieldStalls=new Map;wedgeNotifiedAt=new Map;fatalFaultNotifiedAt=new Map;unattendedSince=new Map;sampleTree;signal;lastOverCapWarnAt=Number.NEGATIVE_INFINITY;constructor(e={}){this.sampleTree=e.readTreeSample??(t=>this.readTreeSample(t)),this.signal=e.signal??((t,a)=>{if(process.platform==="linux"&&Y(H)){if(a!=="SIGTERM"&&a!=="SIGKILL")throw new Error(`unsupported launcher signal ${a}`);return K({operation:"stop",pid:t,signal:a}).then(()=>{})}process.kill(t,a)})}async runFleetPreamble(e){return ue()}warnIfSaturated(e){if(e.fleet.size()<e.thresholds.warning||!e.lifecycle.acceptsDetachedWork())return;const t=O.fresh(()=>this.warnOverCapInTmux(e.fleet.size(),e.thresholds,e.frozen));e.lifecycle.registerDetached(t)}async run(e,t){const a=Ae();for(const i of t.rows){const n=i.node_id,o=t.fleet.get(n);if(o===void 0||i.status!=="active"&&i.status!=="idle"){this.unattendedSince.delete(n);continue}await O.fresh(async()=>{try{await this.handleYieldStall(i,o,t.fleet,e),await this.handleWedgeDetection(i,o,t.fleet,e),this.handleFatalFault(n),this.handleUnattendedParking(n,N(n),o.pid,e,t,a)}catch(r){this.unattendedSince.delete(n),f({level:"error",event:"broker.supervision.failed",...u(n)?{node_id:n}:{fields:{affected_node_id:n}},error:r})}})}for(const i of this.unattendedSince.keys())t.fleet.has(i)||this.unattendedSince.delete(i);for(const i of pe())t.fleet.has(i)||y(i);ne(e)}parkResetCondition(e,t,a,i){if(t.pi_session_id==null||t.pi_pid!==a||t.status==="done"||t.status==="canceled"||t.status==="dead"||t.intent!==null||re(J(e)).length>0||ie(e)!==null||ae(e))return!0;const n=I(e);return n!==null&&n.retry.disposition==="auto"&&n.retry.by==="daemon"&&(n.kind==="connection"||n.kind==="rate-limit"||n.kind==="overloaded")&&(n.retry.nextAt!==void 0||ee(n,i)!==null)?!0:te(e)}handleUnattendedParking(e,t,a,i,n,o){if(j(e)){this.unattendedSince.delete(e),y(e);return}if(t===null){this.unattendedSince.delete(e),y(e);return}const r=this.parkResetCondition(e,t,a,i),l=me(e);if(l!==void 0){if(i-l<U())return;const h=y(e);if(h===void 0||(this.unattendedSince.delete(e),!he(e,t,h)))return;f({level:"warn",event:"broker.park.summary_expired",...u(e)?{node_id:e}:{},fields:{...u(e)?{}:{affected_node_id:e},grace_ms:U()}}),b(e,"park"),k.teardown(e);return}if(r||Se(e)){this.unattendedSince.delete(e);return}const c=this.unattendedSince.get(e);if(c===void 0){this.unattendedSince.set(e,i);return}const d=_e(e,t.lifecycle),m=d!=="cancel"&&ge(t.grantee??"app:terminal",i)?0:o;if(!(i-c<m)){if(this.unattendedSince.delete(e),d==="release"){b(e,d),k.teardown(e);return}if(d==="cancel"){b(e,d),k.teardown(e);return}if(t.grantee!==void 0&&t.grantee!=="app:terminal"){b(e,"park"),k.teardown(e);return}n.lifecycle.acceptsDetachedWork()&&(fe(e,i),n.lifecycle.registerDetached(this.requestParkSummary(e)))}}async requestParkSummary(e){try{const t=await se(e,le("park",{},de),{origin:{type:"runtime",kind:"park"}});f({level:"info",event:"broker.park.summary_requested",...u(e)?{node_id:e}:{},fields:{...u(e)?{}:{affected_node_id:e},route:t}})}catch(t){if(y(e)===void 0)return;if(t instanceof oe){b(e,"park"),k.teardown(e);return}f({level:"error",event:"broker.park.summary_failed",...u(e)?{node_id:e}:{fields:{affected_node_id:e}},error:t})}}async warnOverCapInTmux(e,t,a){const i=Date.now();if(i-this.lastOverCapWarnAt<Ne)return;this.lastOverCapWarnAt=i;const{warning:n,automaticReviveCap:o}=t,r=a.stalled+a.waitingOnMail;f({level:"warn",event:"broker.capacity.warning",fields:{live_count:e,warning_threshold:n,hard_cap:o,frozen_stalled:a.stalled,frozen_waiting_on_mail:a.waitingOnMail}});const l=r===0?"":` \u2014 ${r} frozen (${a.stalled} stalled, ${a.waitingOnMail} waiting on mail)`,c=`\u26A0 crtrd: ${e} live brokers (warning ${n}; hard cap ${o})${l} \u2014 possible broker leak. ps -ax | grep broker-cli.js`;try{const d=await P("tmux",["list-clients","-F","#{client_name}"],{encoding:"utf8",timeout:2e3});if(typeof d.stdout!="string")return;await Promise.allSettled(d.stdout.split(`
|
|
3
|
+
`).map(async m=>{if(m!=="")try{await P("tmux",["display-message","-C","-d","10000","-c",m,c],{encoding:"utf8",timeout:2e3})}catch{try{await P("tmux",["display-message","-c",m,c],{encoding:"utf8",timeout:2e3})}catch(h){f({level:"error",event:"broker.capacity.tmux_display_failed",error:h,fields:{client:m}})}}}))}catch(d){f({level:"error",event:"broker.capacity.tmux_list_clients_failed",error:d})}}async handleYieldStall(e,t,a,i){const n=e.node_id,o=L(t.pid,e.pi_pid_identity);if(e.intent!=="refresh"||F(e)!==null||o===null){o!==null&&this.yieldStalls.delete(o);return}let r=this.yieldStalls.get(o);if(r===void 0){r={since:i},this.yieldStalls.set(o,r);return}if($e(!0,e.intent,!1,i-r.since)!=="kill")return;const l=N(n);if(!(a.get(n)!==t||l?.pi_pid!==t.pid||l.pi_pid_identity!==e.pi_pid_identity||D(t.pid,e.pi_pid_identity)!=="alive"))if(r.termedAt===void 0){if(!await this.signalYieldStall(n,t.pid,"SIGTERM"))return;r.termedAt=i,f({level:"warn",event:"broker.yield_stall.sigterm",...u(n)?{node_id:n}:{},fields:{...u(n)?{}:{affected_node_id:n},pid:t.pid,elapsed_ms:i-r.since,grace_ms:G}})}else i-r.termedAt>=W&&await this.signalYieldStall(n,t.pid,"SIGKILL")&&f({level:"warn",event:"broker.yield_stall.sigkill",...u(n)?{node_id:n}:{},fields:{...u(n)?{}:{affected_node_id:n},pid:t.pid,elapsed_ms:i-r.termedAt,grace_ms:W,stall_elapsed_ms:i-r.since}})}async signalYieldStall(e,t,a){try{return await this.signal(t,a),!0}catch(i){return f({level:"error",event:"broker.yield_stall.signal_failed",...u(e)?{node_id:e}:{},error:i,fields:{...u(e)?{}:{affected_node_id:e},pid:t,signal:a}}),!1}}async readTreeSample(e){try{const{stdout:t}=await P("ps",["-ax","-o","pid=,ppid=,pcpu="],{encoding:"utf8",timeout:2e3});if(typeof t!="string")return null;const a=Te(t,e),i=Ee(t,e);return a===null||i===null?null:{cpuPercent:a,hasDescendants:i}}catch{return null}}async handleWedgeDetection(e,t,a,i){const n=e.node_id,o=L(t.pid,e.pi_pid_identity),r=F(e);if(r===null||o===null){o!==null&&this.wedgeNotifiedAt.delete(o);return}const l=i-r,c=l>=x?await this.sampleTree(t.pid):null,d=N(n),m=d===null?null:F(d);if(a.get(n)!==t||d===null||d.pi_pid!==t.pid||d.pi_pid_identity!==e.pi_pid_identity||m!==r)return;const h=c?.cpuPercent??null,_=c?.hasDescendants??null;if(Me({busy:!0,quietForMs:l,cpuPercent:h,hasDescendants:_})!=="wedged"||this.wedgeNotifiedAt.get(o)===r)return;this.wedgeNotifiedAt.set(o,r);const S=C(d),w=Math.round((l??x)/6e4),A=_??!0,v=!A&&D(t.pid,e.pi_pid_identity)!=="alive"?"pid_identity_unproven":null;let $=!1,M=!1,g;if(A)g=`Child wedged \u2014 ${S} (${n}) is alive and mid-turn but has shown NO engine progress for ${w}+ minutes and its process is near-idle. It will NOT recover on its own \u2014 read \`crtr canvas read wedged-child-on-runaway-bash\` and kill the runaway SUBPROCESS (not this node) so its turn can resume with no lost context.`;else if(v!==null)g=`Child wedged \u2014 ${S} (${n}) is alive and mid-turn but has shown NO engine progress for ${w}+ minutes and it has no subprocess to kill (the engine itself stalled). The daemon did not SIGTERM the broker because its PID identity cannot be confirmed alive.`;else{$=!0;try{await this.signal(t.pid,"SIGTERM"),M=!0,g=`Child wedged \u2014 ${S} (${n}) is alive and mid-turn but has shown NO engine progress for ${w}+ minutes and it has no subprocess to kill (the engine itself stalled). The daemon sent SIGTERM to the broker and will resume a cleanly aborted session with a continuation, or start a recovery cycle if the abort cannot settle.`}catch(T){f({level:"error",event:"broker.wedge.signal_failed",...u(n)?{node_id:n}:{},error:T,fields:{...u(n)?{}:{affected_node_id:n},pid:t.pid,signal:"SIGTERM"}}),g=`Child wedged \u2014 ${S} (${n}) is alive and mid-turn but has shown NO engine progress for ${w}+ minutes and it has no subprocess to kill (the engine itself stalled). The daemon attempted SIGTERM to the broker, but the signal call failed.`}}f({level:"error",event:"broker.wedge.detected",...u(n)?{node_id:n}:{},fields:{...u(n)?{}:{affected_node_id:n},pid:t.pid,quiet_ms:l,cpu_percent:h,has_descendants:A,kick_attempted:$,kicked:M,...v===null?{}:{kick_suppression_reason:v}}});try{X(n,{link:"daemon\u2192node",op:"engine wedge",kind:"wedged",retry:{disposition:"manual"},message:g})}catch(T){f({level:"error",event:"broker.wedge.fault_record_failed",...u(n)?{node_id:n}:{fields:{affected_node_id:n}},error:T})}R(n,"notice.crash",g,{reason:"child-wedged",child:n,quiet_ms:l,cpu_percent:h,kick_attempted:$,kicked:M,...v===null?{}:{kick_suppression_reason:v}})}handleFatalFault(e){const t=I(e);if(t===null){this.fatalFaultNotifiedAt.delete(e);return}if(t.retry.disposition!=="fatal"||t.link!=="pi\u2192provider"||this.fatalFaultNotifiedAt.get(e)===t.since)return;const a=N(e),i=a!==null?C(a):e,n=t.kind==="auth",o=(t.message.split(`
|
|
4
|
+
`)[0]??"").slice(0,200),r=t.providerError,l=r===void 0?"":` Provider: ${r.provider}/${r.model}.`+(r.status===void 0?"":` HTTP status: ${r.status}.`)+(r.errno===void 0?"":` Errno: ${r.errno}.`)+(r.code===void 0?"":` Code: ${r.code}.`)+(r.body===void 0?"":` Body: ${r.body.slice(0,200)}.`)+(r.ratelimit===void 0?"":` Rate limit: ${r.ratelimit.scope??"account"}${r.ratelimit.claim===void 0?"":` (${r.ratelimit.claim})`}${r.ratelimit.model===void 0?"":` on ${r.ratelimit.model}`}.`),c=t.kind==="context-overflow",d=Pe(a,typeof r?.code=="string"?r.code:void 0),m=c||d,h=c?`Child failed \u2014 ${i} (${e}) exceeded its model context window and is now dead. Shrink the task or loaded context, or choose a larger-context model before spawning it again. Error: ${o}`:`Child hit a fatal provider fault \u2014 ${i} (${e}) got a non-retryable ${t.kind} error and will NOT recover on its own; `+(d?"its terminal run has failed. ":"it stays alive, and waking it clears the fault as soon as one turn completes \u2014 a wake that hits the same cause fails again. "+(n?"Its provider credential is dead \u2014 re-authenticate (`/login`, or re-auth crtr); the child recovers on its next wake once the auth reloads. ":""))+`Error: ${o}.${l}`;m&&(Q(e,"crash",{reason:c?"context_overflow":r?.code==="cap_exhausted"?"cap_exhausted":"provider_fatal",outcome:{detail:{schema:"crtr.node-outcome-detail/v1",message:t.message,fault_kind:t.kind,error_class:Z(t).class}}}),k.teardown(e).then(_=>{_.confirmed||f({level:"error",event:"fault.fatal.teardown_failed",node_id:e,error:_.error??new Error("Broker teardown not confirmed")})},_=>f({level:"error",event:"fault.fatal.teardown_failed",node_id:e,error:_}))),this.fatalFaultNotifiedAt.set(e,t.since),f({level:"error",event:"fault.fatal.fanout",...u(e)?{node_id:e}:{},fields:{...u(e)?{}:{affected_node_id:e},kind:t.kind,link:t.link,terminal_overflow:c,terminal_provider_fault:d}}),R(e,"notice.crash",h,{reason:c?"child-context-overflow":"child-fault-fatal",child:e,kind:t.kind,message:o,...r===void 0?{}:{provider_error:r}})}}export{ct as BrokerSupervisionReconciler,we as WEDGE_CPU_MAX_PERCENT,x as WEDGE_QUIET_MS,G as YIELD_STALL_GRACE_MS,U as parkSummaryGraceMsForDaemon,Pe as settlesOnFatalProviderFault,Te as sumTreeCpu,Ee as treeHasDescendants,Ae as unattendedParkMsForDaemon,Me as wedgeVerdict,$e as yieldStallVerdict};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type AppBudget } from './policy.js';
|
|
2
2
|
export declare const CGROUP_ROOT = "/sys/fs/cgroup";
|
|
3
3
|
export declare class Cgroups {
|
|
4
|
-
|
|
4
|
+
readonly root: string;
|
|
5
5
|
private readonly procMeminfo;
|
|
6
6
|
/** Why per-app memory limits are off on this host, or null when they work. */
|
|
7
7
|
unavailable: string | null;
|
|
@@ -15,6 +15,10 @@ export declare class Cgroups {
|
|
|
15
15
|
/** Create a run group that kills as one when the kernel's OOM killer picks any member. */
|
|
16
16
|
createRun(app: string, run: string): string;
|
|
17
17
|
oomKilled(dir: string): boolean;
|
|
18
|
+
/** Bytes the group and its descendants hold now, or null when unreadable. */
|
|
19
|
+
memoryCurrent(dir: string): number | null;
|
|
20
|
+
/** The group's `memory.max` in bytes; null when it has no cap or the file is missing. */
|
|
21
|
+
memoryLimit(dir: string): number | null;
|
|
18
22
|
kill(dir: string): void;
|
|
19
23
|
/** Remove an empty run group; a group still dying is retried by the caller. */
|
|
20
24
|
remove(dir: string): boolean;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var
|
|
2
|
-
`).includes(String(t))}catch{return!1}}sweep(){let r=0;try{for(const t of
|
|
1
|
+
var l=Object.defineProperty;var m=(n,r)=>l(n,"name",{value:r,configurable:!0});import{existsSync as p,mkdirSync as u,readdirSync as c,readFileSync as s,rmdirSync as y,writeFileSync as a}from"node:fs";import{appCgroupPath as h,appCgroupValues as f,appsMemoryMax as d,parseMemoryEvents as $,runCgroupPath as g,wasOomKilled as x}from"./policy.js";const b="/sys/fs/cgroup";function o(n,r){a(n,r)}m(o,"write");class S{static{m(this,"Cgroups")}root;procMeminfo;unavailable=null;constructor(r=b,t="/proc/meminfo"){this.root=r,this.procMeminfo=t}guestMemoryBytes(){try{const t=s(`${this.root}/memory.max`,"utf8").trim();if(/^\d+$/.test(t))return Number(t)}catch{}const r=/^MemTotal:\s+(\d+)\s+kB/m.exec(s(this.procMeminfo,"utf8"));if(!r)throw new Error("cannot read guest memory");return Number(r[1])*1024}init(){try{if(!s(`${this.root}/cgroup.controllers`,"utf8").split(/\s+/).includes("memory"))throw new Error("memory controller is not available");const t=`${this.root}/apps`;return u(t,{recursive:!0}),s(`${this.root}/cgroup.subtree_control`,"utf8").split(/\s+/).includes("memory")||o(`${this.root}/cgroup.subtree_control`,"+memory +pids"),o(`${t}/cgroup.subtree_control`,"+memory +pids"),o(`${t}/memory.max`,String(d(this.guestMemoryBytes()))),this.unavailable=null,!0}catch(r){return this.unavailable=r.message,!1}}applyApp(r,t){if(this.unavailable!==null)return;const e=h(this.root,r);u(e,{recursive:!0}),o(`${e}/cgroup.subtree_control`,"+memory +pids");const i=f(t);o(`${e}/memory.high`,i.memoryHigh);try{o(`${e}/memory.swap.max`,i.swapMax)}catch{}o(`${e}/memory.max`,i.memoryMax),o(`${e}/pids.max`,i.pidsMax)}createRun(r,t){const e=g(this.root,r,t);return u(e),o(`${e}/memory.oom.group`,"1"),e}oomKilled(r){try{return x($(s(`${r}/memory.events`,"utf8")))}catch{return!1}}memoryCurrent(r){try{const t=Number(s(`${r}/memory.current`,"utf8").trim());return Number.isFinite(t)?t:null}catch{return null}}memoryLimit(r){try{const t=s(`${r}/memory.max`,"utf8").trim();return/^\d+$/.test(t)?Number(t):null}catch{return null}}kill(r){try{o(`${r}/cgroup.kill`,"1")}catch{}}remove(r){try{return y(r),!0}catch(t){return t.code==="ENOENT"}}exists(r){return p(r)}contains(r,t){try{return s(`${r}/cgroup.procs`,"utf8").split(`
|
|
2
|
+
`).includes(String(t))}catch{return!1}}sweep(){let r=0;try{for(const t of c(`${this.root}/apps`,{withFileTypes:!0}))if(t.isDirectory())for(const e of c(`${this.root}/apps/${t.name}`,{withFileTypes:!0}))e.isDirectory()&&this.remove(`${this.root}/apps/${t.name}/${e.name}`)&&(r+=1)}catch{}return r}}function N(n,r){a(`/proc/${n}/oom_score_adj`,String(r))}m(N,"setOomScoreAdj");export{b as CGROUP_ROOT,S as Cgroups,N as setOomScoreAdj};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Cgroups } from './cgroup.js';
|
|
2
|
-
import { type AppBudget, type PurgeMode, type TmpfsSizes } from './policy.js';
|
|
2
|
+
import { type AppBudget, type MemoryAdmission, type PurgeMode, type TmpfsSizes } from './policy.js';
|
|
3
3
|
import type { PurgeResult } from './purge-worker.js';
|
|
4
4
|
import { ProjectQuota } from './quota.js';
|
|
5
|
-
import type { LimitEvent, LimitsReport } from '../spec.js';
|
|
5
|
+
import type { DiskUsageReport, LimitEvent, LimitsReport } from '../spec.js';
|
|
6
6
|
export interface RunPlan {
|
|
7
7
|
/** The run's cgroup directory, or null when per-app memory limits are off. */
|
|
8
8
|
cgroupDir: string | null;
|
|
@@ -37,10 +37,14 @@ export interface ManagerOptions {
|
|
|
37
37
|
purge?: (cacheDir: string, mode: PurgeMode, uid: number, gid: number, now: number) => Promise<PurgeResult>;
|
|
38
38
|
/** Fraction of the apps volume in use. */
|
|
39
39
|
diskUsedFraction?: () => number;
|
|
40
|
+
/** Bytes under a directory; the default shells out to `du -x`. */
|
|
41
|
+
walk?: (dir: string) => Promise<number | null>;
|
|
40
42
|
setOom?: (pid: number, value: number) => void;
|
|
41
43
|
now?: () => number;
|
|
42
44
|
}
|
|
43
45
|
export declare function spawnPurge(cacheDir: string, mode: PurgeMode, uid: number, gid: number, now: number): Promise<PurgeResult>;
|
|
46
|
+
/** Bytes under one directory on its own filesystem, or null when the walk failed or timed out. */
|
|
47
|
+
export declare function walkBytes(dir: string): Promise<number | null>;
|
|
44
48
|
export declare class LimitsManager {
|
|
45
49
|
private readonly options;
|
|
46
50
|
readonly cgroups: Cgroups;
|
|
@@ -53,6 +57,13 @@ export declare class LimitsManager {
|
|
|
53
57
|
private readonly diskUsedFraction;
|
|
54
58
|
private readonly setOom;
|
|
55
59
|
private readonly now;
|
|
60
|
+
private readonly walk;
|
|
61
|
+
private diskUsage;
|
|
62
|
+
private diskUsageRun;
|
|
63
|
+
/** Admitted broker launches that have not reached `prepareRun` yet, by node. */
|
|
64
|
+
private readonly tickets;
|
|
65
|
+
/** Live broker runs by run group directory. Non-broker launches (tool calls, background jobs) never appear. */
|
|
66
|
+
private readonly brokerRuns;
|
|
56
67
|
constructor(options: ManagerOptions);
|
|
57
68
|
/** Queue an event for the daemon to emit with the next reply. */
|
|
58
69
|
note(event: LimitEvent): void;
|
|
@@ -61,8 +72,14 @@ export declare class LimitsManager {
|
|
|
61
72
|
budget(app: string): AppBudget;
|
|
62
73
|
/** Write the app's memory cap and disk quota when they differ from what was last applied. */
|
|
63
74
|
applyApp(app: string, uid: number, force?: boolean): AppBudget;
|
|
64
|
-
/**
|
|
65
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Whether one more broker of `app` fits in its memory now. Measured here because only the launcher can read the cgroup
|
|
77
|
+
* files; an admit issues a ticket that holds the boot reservation until that node's broker launches or the ticket expires.
|
|
78
|
+
* Never throws and never writes a limit: cgroups that cannot be read admit.
|
|
79
|
+
*/
|
|
80
|
+
admitBroker(app: string, nodeId: string): MemoryAdmission;
|
|
81
|
+
/** Everything a launch needs: the app's limits applied, a fresh run group, the sandbox tmpfs sizes. A broker launch (`broker`) joins the admission ledger and consumes its node's ticket. */
|
|
82
|
+
prepareRun(uid: number, nodeId: string, broker?: boolean): RunPlan;
|
|
66
83
|
/** An app already past 80 % of its quota loses its cache before the sandbox starts. */
|
|
67
84
|
private purgeIfNearQuota;
|
|
68
85
|
/** Called when the sandbox's first process exists (its pid is bwrap's). */
|
|
@@ -76,6 +93,12 @@ export declare class LimitsManager {
|
|
|
76
93
|
/** crtrd must outlive every sandbox when memory runs out. */
|
|
77
94
|
protectDaemon(pid: number, value: number): void;
|
|
78
95
|
show(): LimitsReport;
|
|
96
|
+
/**
|
|
97
|
+
* What the data volume holds, per app. A directory walk rather than project quota, so it works
|
|
98
|
+
* on hosts whose kernel cannot enforce quota. Cached for a minute; concurrent callers share one walk.
|
|
99
|
+
*/
|
|
100
|
+
diskUsageReport(): Promise<DiskUsageReport>;
|
|
101
|
+
private measureDisk;
|
|
79
102
|
purgeApp(app: string, uid: number, mode: PurgeMode): Promise<CachePassEntry | null>;
|
|
80
103
|
/** One cache pass over every registered app. */
|
|
81
104
|
cachePass(): Promise<CachePassEntry[]>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var D=Object.defineProperty;var d=(c,t)=>D(c,"name",{value:t,configurable:!0});import{spawn as w}from"node:child_process";import{randomBytes as A}from"node:crypto";import{existsSync as S,statfsSync as _}from"node:fs";import{dirname as B,join as b}from"node:path";import{fileURLToPath as N}from"node:url";import{Cgroups as O,setOomScoreAdj as P}from"./cgroup.js";import{ADMISSION_TICKET_MS as q,APP_QUOTA_CLEANUP_FRACTION as U,BOOT_RESERVATION_BYTES as M,BOOT_WINDOW_MS as T,CONVERSATIONS_DISK_BYTES as I,appCgroupPath as C,appProjectId as v,bareApp as g,bootPendingBytes as E,budgetFor as F,conversationsProjectId as x,memoryAdmission as L,passesFraction as K,runGroupName as j,selectCachePurges as Q,tmpfsSizes as G}from"./policy.js";import{ProjectQuota as $}from"./quota.js";import{readSettings as z}from"./settings.js";function J(c,t,s,i,e){const r=b(B(N(import.meta.url)),"purge-worker.js");return new Promise((o,n)=>{const a=w("/bin/setpriv",["--reuid",String(s),"--regid",String(i),"--groups",String(i),"--no-new-privs","--",process.execPath,r,c,t,String(e)],{stdio:["ignore","pipe","pipe"]});let h="",l="";a.stdout.on("data",u=>{h+=String(u)}),a.stderr.on("data",u=>{l+=String(u)}),a.once("error",n),a.once("close",u=>{if(u!==0)return n(new Error(`purge worker exited ${u}: ${l.trim().slice(0,200)}`));try{o(JSON.parse(h))}catch(y){n(y)}})})}d(J,"spawnPurge");function V(c){return new Promise(t=>{if(!S(c))return t(null);const s=w("du",["-x","-B1","-s",c],{stdio:["ignore","pipe","ignore"],timeout:6e4});let i="";s.stdout.on("data",e=>{i+=String(e)}),s.once("error",()=>t(null)),s.once("close",(e,r)=>{const o=Number(i.split(/\s/)[0]);t(r===null&&(e===0||e===1)&&i.length>0&&Number.isFinite(o)?o:null)})})}d(V,"walkBytes");const W=6e4;function Y(c){const t=_(c),s=Number(t.blocks);return s>0?1-Number(t.bavail)/s:0}d(Y,"appsDiskUsedFraction");class at{static{d(this,"LimitsManager")}options;cgroups;quota;events=[];appliedMemory=new Map;appliedDisk=new Map;purging=new Set;purge;diskUsedFraction;setOom;now;walk;diskUsage=null;diskUsageRun=null;tickets=new Map;brokerRuns=new Map;constructor(t){this.options=t,this.cgroups=t.cgroups??new O,this.quota=t.quota??new $(t.appsRoot,t.stateDir),this.purge=t.purge??J,this.diskUsedFraction=t.diskUsedFraction??(()=>Y(t.appsRoot)),this.setOom=t.setOom??P,this.now=t.now??Date.now,this.walk=t.walk??V}note(t){this.events.push(t),this.events.length>200&&this.events.splice(0,this.events.length-200)}takeEvents(){return this.events.splice(0,this.events.length)}init(){this.cgroups.init()?this.cgroups.sweep():this.note({level:"warn",event:"limits.memory.unavailable",fields:{reason:this.cgroups.unavailable}}),this.quota.init()||this.note({level:"warn",event:"limits.disk.unavailable",fields:{reason:this.quota.unavailable}})}budget(t){return F(t,z(this.options.runtimeRoot))}applyApp(t,s,i=!1){const e=this.budget(t),r=g(t);if(this.cgroups.unavailable===null){const o=JSON.stringify([e.memoryBytes,e.pids]);if(i||this.appliedMemory.get(r)!==o)try{this.cgroups.applyApp(r,e),this.appliedMemory.set(r,o),this.note({level:"info",event:"limits.memory.applied",fields:{app:r,memory_bytes:e.memoryBytes,pids:e.pids}})}catch(n){this.note({level:"warn",event:"limits.memory.failed",fields:{app:r,error:String(n)}})}}if(this.quota.unavailable===null)try{if(this.quota.tag(r,s,{uid:0,gid:0})){const o=String(e.diskBytes);(i||this.appliedDisk.get(s)!==o)&&(this.quota.setLimit(v(s),e.diskBytes),this.quota.setLimit(x(s),I),this.appliedDisk.set(s,o),this.note({level:"info",event:"limits.disk.applied",fields:{app:r,disk_bytes:e.diskBytes}}))}}catch(o){this.note({level:"warn",event:"limits.disk.failed",fields:{app:r,error:String(o)}})}return e}admitBroker(t,s){const i={verdict:"admitted"};if(this.cgroups.unavailable!==null)return i;const e=this.now();for(const[m,f]of this.tickets)e-f.issuedAt>=q&&this.tickets.delete(m);if(this.tickets.has(s))return i;const r=g(t),o=this.cgroups.root,n=`${o}/apps`,a=C(o,r),h=[...this.tickets.values()],l=[...this.brokerRuns].map(([m,f])=>({dir:m,...f})),u=d((m,f,k)=>({max:f,current:this.cgroups.memoryCurrent(m)??0,pending:h.filter(p=>k(p.app)).length*M+l.filter(p=>k(p.app)&&e-p.startedAt<T).reduce((p,R)=>p+E(this.cgroups.memoryCurrent(R.dir)??0),0),hasBroker:h.some(p=>k(p.app))||l.some(p=>k(p.app))}),"level"),y=L({app:u(a,this.cgroups.exists(a)?this.cgroups.memoryLimit(a):this.budget(r).memoryBytes,m=>m===r),apps:u(n,this.cgroups.memoryLimit(n),()=>!0)});return y.verdict==="admitted"&&this.tickets.set(s,{app:r,issuedAt:e}),y}prepareRun(t,s,i=!1){const e=this.options.users.appFor(t)??String(t),r=this.applyApp(e,t);let o=null;if(this.cgroups.unavailable===null)try{o=this.cgroups.createRun(e,j(s,A(4).toString("hex")))}catch(n){this.note({level:"warn",event:"limits.memory.failed",nodeId:s,fields:{app:e,error:String(n)}})}return i&&(this.tickets.delete(s),o!==null&&this.brokerRuns.set(o,{app:g(e),nodeId:s,startedAt:this.now()})),this.purgeIfNearQuota(e,t,r),{cgroupDir:o,app:g(e),nodeId:s,budget:r,tmpfs:G(r)}}purgeIfNearQuota(t,s,i){if(this.quota.unavailable!==null||i.diskBytes===null)return;const e=this.quota.usage().get(v(s))?.usedBytes??0;K(e,i.diskBytes,U)&&this.purgeApp(t,s,"all").catch(()=>{})}confirmStarted(t,s){t.cgroupDir!==null&&!this.cgroups.contains(t.cgroupDir,s)&&this.note({level:"warn",event:"limits.memory.not_joined",nodeId:t.nodeId,fields:{app:t.app,pid:s}})}finishRun(t){if(t.cgroupDir===null)return{oomKilled:!1};const s=t.cgroupDir;this.brokerRuns.delete(s);const i=this.cgroups.oomKilled(s);this.cgroups.kill(s),i&&this.note({level:"warn",event:"run.oom_killed",nodeId:t.nodeId,fields:{app:t.app,memory_bytes:t.budget.memoryBytes}});let e=0;const r=d(()=>{this.cgroups.remove(s)||++e>=20||setTimeout(r,100).unref()},"retry");return r(),{oomKilled:i}}killRun(t){t?.cgroupDir&&this.cgroups.kill(t.cgroupDir)}protectDaemon(t,s){try{this.setOom(t,s)}catch(i){this.note({level:"warn",event:"limits.oom_score.failed",fields:{pid:t,error:String(i)}})}}show(){const t=this.quota.unavailable===null?this.quota.usage():new Map;return{memory:{available:this.cgroups.unavailable===null,reason:this.cgroups.unavailable},disk:{available:this.quota.unavailable===null,reason:this.quota.unavailable},apps:this.options.users.entries().map(({app:s,uid:i})=>{const e=this.budget(s);return{app:s,uid:i,memory_bytes:e.memoryBytes,disk_bytes:e.diskBytes,memory_source:e.memorySource,disk_source:e.diskSource,disk_used_bytes:t.get(v(i))?.usedBytes??null}})}}async diskUsageReport(){const t=this.diskUsage;return t&&this.now()-t.at<W?t.report:(this.diskUsageRun??=this.measureDisk().finally(()=>{this.diskUsageRun=null}),this.diskUsageRun)}async measureDisk(){const t=_(this.options.appsRoot),s=Number(t.bsize),i=Number(t.blocks)*s,e=i-Number(t.bfree)*s,r=[...new Set(this.options.users.entries().map(l=>l.app))],[o,n,a]=await Promise.all([Promise.all(r.map(async l=>({app:l,bytes:await this.walk(b(this.options.appsRoot,g(l)))??0}))),this.walk(b(B(this.options.appsRoot),"user")),this.walk(this.options.runtimeRoot)]),h={total_bytes:i,used_bytes:e,apps:o.sort((l,u)=>u.bytes-l.bytes||l.app.localeCompare(u.app)),user_bytes:n,runtime_bytes:a,measured_at:this.now()};return this.diskUsage={at:this.now(),report:h},h}async purgeApp(t,s,i){const e=g(t),r=b(this.options.appsRoot,e,"cache");if(this.purging.has(e)||!S(r))return null;this.purging.add(e);try{const o=await this.purge(r,i,s,this.options.dataGid,this.now());return this.note({level:"info",event:"app.cache.purged",fields:{app:e,mode:i,files:o.files,bytes:o.bytes,dirs:o.dirs}}),{app:e,mode:i,files:o.files,bytes:o.bytes}}catch(o){return this.note({level:"warn",event:"app.cache.purge_failed",fields:{app:e,mode:i,error:String(o)}}),null}finally{this.purging.delete(e)}}async cachePass(){const t=this.quota.unavailable===null?this.quota.usage():new Map;let s=0;try{s=this.diskUsedFraction()}catch{}const i=this.options.users.entries(),e=Q(i.map(({app:n,uid:a})=>({app:n,usedBytes:t.get(v(a))?.usedBytes??0,limitBytes:this.budget(n).diskBytes})),s),r=new Map(i.map(n=>[n.app,n.uid]));return(await Promise.all(e.map(({app:n,mode:a})=>this.purgeApp(n,r.get(n),a)))).filter(n=>n!==null)}}export{at as LimitsManager,J as spawnPurge,V as walkBytes};
|
|
@@ -70,6 +70,49 @@ export declare function wasOomKilled(events: MemoryEvents): boolean;
|
|
|
70
70
|
/** The terminal reason a node ends with when its run was OOM-killed. */
|
|
71
71
|
export declare const OUT_OF_MEMORY_REASON: "out_of_memory";
|
|
72
72
|
export declare function exitReasonFor(oomKilled: boolean, ordinary: 'crashed' | 'boot_failed' | 'crash_looped'): 'out_of_memory' | typeof ordinary;
|
|
73
|
+
/** Largest kernel `memory.peak` of any broker run group in its first 20 s (498 MiB measured), rounded up. */
|
|
74
|
+
export declare const BOOT_RESERVATION_BYTES: number;
|
|
75
|
+
/** How long a launched broker still counts as booting; no measured group exceeded 261 MiB after it. */
|
|
76
|
+
export declare const BOOT_WINDOW_MS = 20000;
|
|
77
|
+
/** Growth the app's running nodes can still add: the largest measured (833 MiB), rounded up. */
|
|
78
|
+
export declare const GROWTH_HEADROOM_BYTES: number;
|
|
79
|
+
/** How long an admission holds its boot reservation while the launch it admitted has not reached the launcher. */
|
|
80
|
+
export declare const ADMISSION_TICKET_MS = 30000;
|
|
81
|
+
/** What a broker that started `BOOT_WINDOW_MS` ago or less still has to claim: its reservation less what it holds now, so it is not counted twice. */
|
|
82
|
+
export declare function bootPendingBytes(currentBytes: number): number;
|
|
83
|
+
/** One cgroup level's memory as the launcher measured it. */
|
|
84
|
+
export interface MemoryLevel {
|
|
85
|
+
/** null means no cap at this level. */
|
|
86
|
+
max: number | null;
|
|
87
|
+
current: number;
|
|
88
|
+
/** Boot reservations not yet visible in `current`. */
|
|
89
|
+
pending: number;
|
|
90
|
+
/** A live broker run or an unexpired ticket exists at this level. */
|
|
91
|
+
hasBroker: boolean;
|
|
92
|
+
}
|
|
93
|
+
export type MemoryAdmissionLevel = 'app' | 'apps';
|
|
94
|
+
export type MemoryAdmission = {
|
|
95
|
+
verdict: 'admitted';
|
|
96
|
+
} | {
|
|
97
|
+
verdict: 'denied';
|
|
98
|
+
level: MemoryAdmissionLevel;
|
|
99
|
+
free_bytes: number;
|
|
100
|
+
needed_bytes: number;
|
|
101
|
+
} | {
|
|
102
|
+
verdict: 'too_small';
|
|
103
|
+
level: MemoryAdmissionLevel;
|
|
104
|
+
max_bytes: number;
|
|
105
|
+
needed_bytes: number;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Whether one more broker may start. Each capped level must cover the boot reservation, plus the growth headroom
|
|
109
|
+
* once a broker already runs there: with none, the broker's own boot is the only risk, and waiting would never help.
|
|
110
|
+
* A cap below the reservation can never admit a broker. `app` is checked before `apps`.
|
|
111
|
+
*/
|
|
112
|
+
export declare function memoryAdmission(levels: {
|
|
113
|
+
app: MemoryLevel;
|
|
114
|
+
apps: MemoryLevel;
|
|
115
|
+
}): MemoryAdmission;
|
|
73
116
|
export interface TmpfsSizes {
|
|
74
117
|
root: number;
|
|
75
118
|
tmp: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
var
|
|
1
|
+
var x=Object.defineProperty;var e=(t,o)=>x(t,"name",{value:o,configurable:!0});const P=1073741824,R=1048576,E="terminal",y=1073741824,N=2147483648,D=1073741824,C=2048,F=536870912,Y=67108864,h=536870912,U=-900,K=300,L=1e6,g=67108864,$=16777216;function a(t){return t.startsWith("app:")?t.slice(4):t}e(a,"bareApp");function b(t,o){const n=a(t),r=o.apps[n]??{},s=n===E,_=e((p,m)=>p===void 0?{bytes:s?null:m,source:"default"}:{bytes:p==="unlimited"?null:p,source:"override"},"resolve"),u=_(r.memory_bytes,2147483648),i=_(r.disk_bytes,1073741824);return{memoryBytes:u.bytes,diskBytes:i.bytes,pids:s?null:2048,memorySource:u.source,diskSource:i.source}}e(b,"budgetFor");function k(t,o=1073741824){return t>o?Math.floor(t-o):Math.max(67108864,Math.floor(t/2))}e(k,"appsMemoryMax");function G(t){return{memoryMax:t.memoryBytes===null?"max":String(t.memoryBytes),memoryHigh:"max",swapMax:t.memoryBytes===null?"max":"0",pidsMax:t.pids===null?"max":String(t.pids)}}e(G,"appCgroupValues");function S(t,o){return`${t}/apps/${a(o)}`}e(S,"appCgroupPath");function V(t,o,n){return`${S(t,o)}/${n}`}e(V,"runCgroupPath");const M=/^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;function w(t,o){const n=`${t}.${o}`;if(!M.test(n))throw new Error(`invalid run group name: ${n}`);return n}e(w,"runGroupName");function H(t){return t}e(H,"appProjectId");function X(t){return t+1e6}e(X,"conversationsProjectId");function v(t){return t===null?0:Math.ceil(t/1024)}e(v,"quotaBlocks");function l(t,o,n){return o!==null&&o>0&&t>o*n}e(l,"passesFraction");const T=.8,O=.85;function J(t){const o=e(n=>{const r=new RegExp(`^${n}\\s+(\\d+)\\s*$`,"m").exec(t);return r?Number(r[1]):0},"read");return{oomKill:o("oom_kill"),oomGroupKill:o("oom_group_kill")}}e(J,"parseMemoryEvents");function W(t){return t.oomKill>0||t.oomGroupKill>0}e(W,"wasOomKilled");const B="out_of_memory";function z(t,o){return t?B:o}e(z,"exitReasonFor");const c=512*1048576,j=2e4,A=850*1048576,Z=3e4;function q(t){return Math.max(0,c-t)}e(q,"bootPendingBytes");function Q(t){const o=["app","apps"].flatMap(r=>t[r].max===null?[]:[{level:r,...t[r],max:t[r].max}]),n=o.find(r=>r.max<c);if(n)return{verdict:"too_small",level:n.level,max_bytes:n.max,needed_bytes:c};for(const r of o){const s=c+(r.hasBroker?A:0),_=r.max-r.current-r.pending;if(_<s)return{verdict:"denied",level:r.level,free_bytes:_,needed_bytes:s}}return{verdict:"admitted"}}e(Q,"memoryAdmission");function tt(t){return{root:67108864,tmp:t.memoryBytes===null?1073741824:Math.max(67108864,Math.min(536870912,Math.floor(t.memoryBytes/4)))}}e(tt,"tmpfsSizes");const I=4320*6e4,d=60*6e4;function ot(t,o){return o-Math.max(t.atimeMs,t.mtimeMs)>=I}e(ot,"isStale");function et(t,o){const n=o>O;return t.map(({app:r,usedBytes:s,limitBytes:_})=>({app:r,mode:n||l(s,_,T)?"all":"stale"}))}e(et,"selectCachePurges");function rt(t,o){return t===null||o-t>=d}e(rt,"cachePassDue");export{Z as ADMISSION_TICKET_MS,T as APP_QUOTA_CLEANUP_FRACTION,c as BOOT_RESERVATION_BYTES,j as BOOT_WINDOW_MS,d as CACHE_PASS_INTERVAL_MS,I as CACHE_STALE_MS,F as CONVERSATIONS_DISK_BYTES,L as CONVERSATIONS_PROJECT_OFFSET,U as CRTRD_OOM_SCORE_ADJ,D as DEFAULT_APP_DISK_BYTES,N as DEFAULT_APP_MEMORY_BYTES,C as DEFAULT_APP_PIDS,O as DISK_CLEANUP_FRACTION,P as GIB,A as GROWTH_HEADROOM_BYTES,R as MIB,$ as MIN_DISK_BYTES,g as MIN_MEMORY_BYTES,B as OUT_OF_MEMORY_REASON,y as RUNTIME_RESERVE_BYTES,K as SANDBOX_OOM_SCORE_ADJ,Y as SANDBOX_ROOT_TMPFS_BYTES,h as SANDBOX_TMP_TMPFS_BYTES,E as UNCAPPED_BY_DEFAULT_APP,S as appCgroupPath,G as appCgroupValues,H as appProjectId,k as appsMemoryMax,a as bareApp,q as bootPendingBytes,b as budgetFor,rt as cachePassDue,X as conversationsProjectId,z as exitReasonFor,ot as isStale,Q as memoryAdmission,J as parseMemoryEvents,l as passesFraction,v as quotaBlocks,V as runCgroupPath,w as runGroupName,et as selectCachePurges,tt as tmpfsSizes,W as wasOomKilled};
|
package/dist/launcher/main.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
var V=Object.defineProperty;var w=(
|
|
1
|
+
var V=Object.defineProperty;var w=(m,l)=>V(m,"name",{value:l,configurable:!0});import{spawn as z}from"node:child_process";import{createHash as Q}from"node:crypto";import{chownSync as O,chmodSync as A,closeSync as W,existsSync as X,mkdirSync as H,readFileSync as Y,rmSync as Z,statSync as ee}from"node:fs";import{createServer as te}from"node:net";import{dirname as U,join as _}from"node:path";import{fileURLToPath as re}from"node:url";import{loadLinuxNative as ne}from"../native/linux.js";import{envHomeOverride as oe}from"../shared/env.js";import{launchSandbox as se}from"./bwrap.js";import{LaunchGate as ie}from"./launch-gate.js";import{serveAccepted as ae}from"./accepted-socket.js";import{installNetworkRule as de}from"./netrule.js";import{LAUNCHER_SOCKET as S,LaunchRefused as p}from"./spec.js";import{systemIdentity as ce,AppUsers as ue}from"./users.js";import{SourceValidator as pe}from"./validate.js";import{prepareDaemonScopeDirectories as le}from"./scope-directories.js";import{LimitsManager as fe}from"./limits/manager.js";import{CRTRD_OOM_SCORE_ADJ as me}from"./limits/policy.js";import{writeLimitsLog as he}from"./limits/log.js";const D=ne();function $(m){const l=m._handle?.fd;if(l===void 0||l<0)throw new Error("launcher socket fd unavailable");return l}w($,"socketFd");async function we(){if(process.platform!=="linux"||process.getuid?.()!==0||!D)throw new Error("launcher requires Linux root and its native addon");const{daemonUid:m,daemonGid:l,dataGid:L}=ce(),R=U(S);if(H(R,{recursive:!0,mode:493}),(ee(R).mode&511)!==493)throw new Error(`${R} must be mode 0755`);O(R,m,l),process.umask(7);const B=_(R,"nodes");H(B,{recursive:!0,mode:504}),O(B,0,l),A(B,504);const k=new pe(oe()||"/",l);le(_(k.root,"runtime"),m,l);const b=new ue(L);de();const d=new fe({runtimeRoot:k.runtime,appsRoot:_(k.root,"apps"),stateDir:_(k.runtime,"limits-state"),dataGid:L,users:b});d.init();const T=new Map,F=w(r=>{const f=d.takeEvents();for(const o of f)he(o);return f.length===0?r:{...r,events:f}},"reply_"),E=new ie,x=new Map,I=new Map,J=w((r,f)=>{const o=f.capture,a=Q("sha256"),e=w((c,i)=>{let u=0,h=!1;const g=[];return{take(C){i?.update(C);const G=Math.max(0,Math.min(C.length,c-u));G&&(g.push(C.subarray(0,G)),u+=G),G!==C.length&&(h=!0)},result:w(()=>({text:Buffer.concat(g).toString("utf8"),truncated:h}),"result")}},"chunks"),s=e(o.stdoutBytes,a),t=e(o.stderrBytes);let n=Buffer.alloc(0),v=0;return r.stdout?.on("data",c=>s.take(c)),r.stderr?.on("data",c=>{const i=c;o.stderrTail?(v+=i.length,n=i.length>=o.stderrBytes?i.subarray(i.length-o.stderrBytes):Buffer.concat([n,i]).subarray(Math.max(0,n.length+i.length-o.stderrBytes))):t.take(i)}),new Promise(c=>r.once("close",(i,u)=>{const h=s.result(),g=o.stderrTail?{text:n.toString("utf8"),truncated:v>o.stderrBytes}:t.result();c({exitCode:i,signal:u,stdout:h.text,stderr:g.text,stdoutTruncated:h.truncated,stderrTruncated:g.truncated,stdoutSha256:a.digest("hex")})}))},"captureExit"),j=w(r=>{const f=new Set;for(;r>1&&!f.has(r);){const o=x.get(r);if(o)return o;f.add(r);try{const a=Y(`/proc/${r}/status`,"utf8");r=Number(/^PPid:\s*(\d+)/m.exec(a)?.[1]??0)}catch{return null}}return null},"sandboxForPid");let y,M=!1;const q=_(U(re(import.meta.url)),"../daemon/crtrd-cli.js"),K=w(()=>{M||(y=z("/bin/setpriv",["--reuid",String(m),"--regid",String(l),"--groups",String(L),"--",process.execPath,q,...process.argv.slice(2)],{stdio:"inherit",env:{...process.env,CRTR_LAUNCHER_SOCKET:S,CRTR_HOME:k.root}}),y.pid&&d.protectDaemon(y.pid,me),y.once("exit",()=>{y=void 0,M||K()}),y.once("error",r=>{console.error("crtrd spawn failed:",r),process.exitCode=1,M=!0}))},"startDaemon");X(S)&&Z(S);const N=te({pauseOnConnect:!0},r=>ae(r,f=>{const o=[];return(async()=>{try{if(D.peerCred($(r)).uid!==m)throw new p("invalid_spec","launcher peer is not crtrd");let a;for(;;)try{a=D.recvFds($(r));break}catch(t){if(t.code!=="EINTR")throw t}f(),o.push(...a.fds);const e=JSON.parse(a.data);let s;switch(e.operation){case"launch":{if(o.length>1)throw new p("invalid_uid","app uid is not registered");let t,n,v;try{n=await E.launch(async()=>{if(!b.has(e.spec.uid))throw new p("invalid_uid","app uid is not registered");const c=v=d.prepareRun(e.spec.uid,e.spec.nodeId,e.spec.broker===!0);return se(e.spec,o[0],k,L,i=>{if(!i.pid)throw new p("launch_failed","bwrap has no pid");t=i.pid;const u=i.pid;if(x.set(u,{child:i,uid:e.spec.uid,nodeId:e.spec.nodeId,mounts:e.spec.mounts,plan:c}),i.once("exit",()=>{x.delete(u);const h=d.finishRun(c);T.set(u,h.oomKilled),setTimeout(()=>T.delete(u),5*6e4).unref()}),e.spec.capture){const h=J(i,e.spec).then(g=>({...g,oomKilled:T.get(u)===!0}));I.set(u,h),h.then(()=>{setTimeout(()=>{I.get(u)===h&&I.delete(u)},3e5).unref()})}},c)})}catch(c){throw t!==void 0?I.delete(t):v&&d.finishRun(v),c}if(!n.pid)throw new p("launch_failed","bwrap has no pid");d.confirmStarted(v,n.pid),s={pid:n.pid};break}case"wait":{const t=I.get(e.pid);if(!t)throw new p("invalid_spec","unknown captured sandbox pid");const n=await t;I.delete(e.pid),s={result:n};break}case"stop":{const t=Number.isInteger(e.pid)?j(e.pid):null;if(!t||e.nodeId!==void 0&&t.nodeId!==e.nodeId)throw new p("invalid_spec","unknown sandbox pid");if(e.signal!==void 0&&e.signal!=="SIGTERM"&&e.signal!=="SIGKILL")throw new p("invalid_spec","invalid stop signal");const n=e.signal??"SIGKILL";t.child.kill(n),n==="SIGKILL"&&d.killRun(t.plan),s={ok:!0};break}case"user.create":s={uid:await E.write(()=>b.create(e.app,e.uid))};break;case"user.delete":{await E.write(()=>{if([...x.values()].some(t=>t.uid===e.uid))throw new p("invalid_spec","stop this app\u2019s sandboxes before deleting its user");b.delete(e.uid)}),s={ok:!0};break}case"users.sync":await E.write(()=>b.sync(e.users)),s={ok:!0};break;case"list":s={sandboxes:[...x].map(([t,n])=>({pid:t,uid:n.uid,nodeId:n.nodeId,mounts:n.mounts}))};break;case"limits.apply":{const t=b.entries().find(n=>n.app===e.app.replace(/^app:/,""));if(!t)throw new p("invalid_uid","app is not registered");d.applyApp(t.app,t.uid,!0),s={ok:!0};break}case"memory.admit":{const t=b.entries().find(n=>n.app===e.app.replace(/^app:/,""));if(!t)throw new p("invalid_uid","app is not registered");s=d.admitBroker(t.app,e.nodeId);break}case"limits.show":s={limits:d.show()};break;case"disk.usage":s={disk:await d.diskUsageReport()};break;case"cache.purge":s={purged:await d.cachePass()};break;case"exit.status":s={oomKilled:T.get(e.pid)===!0};break;case"daemon.handover":y?.kill("SIGTERM"),s={ok:!0};break;default:throw new p("invalid_spec","unknown launcher operation")}return`${JSON.stringify(F(s))}
|
|
2
2
|
`}catch(a){const e=a instanceof p?a.refusal:"launch_failed";return console.error(`launcher refusal ${e}:`,a),`${JSON.stringify({refusal:e})}
|
|
3
|
-
`}finally{for(const a of o)W(a)}})()}));await new Promise((r,
|
|
3
|
+
`}finally{for(const a of o)W(a)}})()}));await new Promise((r,f)=>{N.once("error",f),N.listen(S,r)}),O(S,m,l),A(S,384);const P=w(()=>{M=!0,y?.kill("SIGTERM");for(const{child:r}of x.values())r.kill("SIGKILL");N.close(),k.close()},"shutdown");process.once("SIGTERM",P),process.once("SIGINT",P),K()}w(we,"runLauncher"),await we().catch(m=>{console.error(m),process.exitCode=1});export{we as runLauncher};
|
package/dist/launcher/spec.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { MemoryAdmission } from './limits/policy.js';
|
|
1
2
|
export declare const APP_UID_MIN = 200000;
|
|
2
3
|
export declare const APP_UID_MAX = 299999;
|
|
3
4
|
export declare const LAUNCHER_SOCKET = "/run/crtr/launcher.sock";
|
|
@@ -22,6 +23,8 @@ export interface LaunchSpec {
|
|
|
22
23
|
stdin?: string;
|
|
23
24
|
};
|
|
24
25
|
pluginRoot?: string;
|
|
26
|
+
/** The launch is a node's broker engine. Only the daemon's broker host sets it; the launcher counts such runs for memory admission. */
|
|
27
|
+
broker?: true;
|
|
25
28
|
token?: never;
|
|
26
29
|
}
|
|
27
30
|
export type LauncherRequest = {
|
|
@@ -58,6 +61,16 @@ export type LauncherRequest = {
|
|
|
58
61
|
} | {
|
|
59
62
|
operation: 'limits.show';
|
|
60
63
|
}
|
|
64
|
+
/** Whether one more broker of this app fits in its memory now; an admit holds the boot reservation until the node's broker launch. */
|
|
65
|
+
| {
|
|
66
|
+
operation: 'memory.admit';
|
|
67
|
+
app: string;
|
|
68
|
+
nodeId: string;
|
|
69
|
+
}
|
|
70
|
+
/** Bytes the data volume holds, per app and for the person's files and the runtime directory (a directory walk, cached briefly). */
|
|
71
|
+
| {
|
|
72
|
+
operation: 'disk.usage';
|
|
73
|
+
}
|
|
61
74
|
/** One cache pass over every app (stale files; whole cache for an app past 80 % of its quota or a disk past 85 %). */
|
|
62
75
|
| {
|
|
63
76
|
operation: 'cache.purge';
|
|
@@ -98,6 +111,21 @@ export interface LimitsReport {
|
|
|
98
111
|
disk_used_bytes: number | null;
|
|
99
112
|
}>;
|
|
100
113
|
}
|
|
114
|
+
export type MemoryAdmitReply = MemoryAdmission;
|
|
115
|
+
export interface DiskUsageReport {
|
|
116
|
+
/** The data volume the apps live on, from statfs. */
|
|
117
|
+
total_bytes: number;
|
|
118
|
+
used_bytes: number;
|
|
119
|
+
/** Bytes under each registered app's folder, largest first. */
|
|
120
|
+
apps: Array<{
|
|
121
|
+
app: string;
|
|
122
|
+
bytes: number;
|
|
123
|
+
}>;
|
|
124
|
+
user_bytes: number | null;
|
|
125
|
+
runtime_bytes: number | null;
|
|
126
|
+
/** When the walk ran, in epoch milliseconds. */
|
|
127
|
+
measured_at: number;
|
|
128
|
+
}
|
|
101
129
|
export interface CachePurged {
|
|
102
130
|
app: string;
|
|
103
131
|
mode: 'stale' | 'all';
|
|
@@ -131,8 +159,10 @@ export type LauncherReply = ({
|
|
|
131
159
|
uid: number;
|
|
132
160
|
} | {
|
|
133
161
|
ok: true;
|
|
134
|
-
} | {
|
|
162
|
+
} | MemoryAdmitReply | {
|
|
135
163
|
limits: LimitsReport;
|
|
164
|
+
} | {
|
|
165
|
+
disk: DiskUsageReport;
|
|
136
166
|
} | {
|
|
137
167
|
purged: CachePurged[];
|
|
138
168
|
} | {
|
|
@@ -7,7 +7,15 @@ description: Behavior changes in @north-light/sdk that an app must act on, newes
|
|
|
7
7
|
|
|
8
8
|
Changes to `@north-light/sdk` that can change what an existing app sees. Newest first.
|
|
9
9
|
|
|
10
|
-
## 0.3.
|
|
10
|
+
## 0.3.494
|
|
11
|
+
|
|
12
|
+
`GET /v1/limits` reports how much of the data disk each app uses on a host without disk quota, and the disk as a whole. Additive: no request or response an existing app sends or reads changes.
|
|
13
|
+
|
|
14
|
+
### `disk_used_bytes` and `data_disk` on `LimitsDTO` (additive)
|
|
15
|
+
|
|
16
|
+
- `AppLimitsDTO.disk_used_bytes` was `null` whenever the host could not enforce project quota (every hosted runtime on Blaxel). It now holds the bytes under the app's folder, measured by a directory walk at most a minute old. It stays `null` when the host has no launcher, the first walk has not finished (a read waits at most 5 s for it), or the walk failed.
|
|
17
|
+
- `LimitsDTO.data_disk` is new and optional: `{total_bytes, used_bytes, user_bytes, runtime_bytes}` for the data volume. `crtr sys limits show` prints it.
|
|
18
|
+
|
|
11
19
|
|
|
12
20
|
An app can act on a person's hosted runtime with no sign-in of theirs, and `@north-light/plugin` can declare canvas object kinds. Everything below is additive: no request or response an existing app sends or reads changes.
|
|
13
21
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@north-light/cli",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.496",
|
|
4
4
|
"description": "crtr — agent runtime with memory, plugins, and marketplaces",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
},
|
|
139
139
|
"license": "UNLICENSED",
|
|
140
140
|
"dependencies": {
|
|
141
|
-
"@north-light/identity": "0.3.
|
|
141
|
+
"@north-light/identity": "0.3.496",
|
|
142
142
|
"@earendil-works/pi-agent-core": "1.1.0",
|
|
143
143
|
"@earendil-works/pi-ai": "1.1.0",
|
|
144
144
|
"@earendil-works/pi-coding-agent": "1.1.0",
|