@north-light/cli 0.3.525 → 0.3.527
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/builtin-memory/crouter-sdk/release-notes.md +6 -0
- package/dist/commands/node/bash.js +2 -2
- package/dist/commands/node.js +1 -1
- package/dist/core/bash-guard.js +4 -3
- package/dist/core/canvas/migrations.js +53 -53
- package/dist/core/canvas/recovery.d.ts +13 -0
- package/dist/core/canvas/recovery.js +2 -2
- package/dist/core/runtime/broker/bash-job-launch.d.ts +10 -0
- package/dist/core/runtime/broker/bash-job-launch.js +1 -0
- package/dist/daemon/api/handlers/bash-jobs.js +1 -1
- package/dist/daemon/crtrd.js +8 -8
- package/dist/daemon/reconcilers/broker-supervision.d.ts +6 -2
- package/dist/daemon/reconcilers/broker-supervision.js +4 -4
- package/dist/pi-extensions/canvas-bash-valve.js +7 -7
- package/docs/sdk/release-notes.md +6 -0
- package/package.json +2 -2
- package/packages/crouter-identity/package.json +1 -1
- package/runtime.lock.json +12 -12
|
@@ -9,6 +9,12 @@ when-and-why-to-read: When you need Release notes, this knowledge should be read
|
|
|
9
9
|
|
|
10
10
|
Changes to `@north-light/sdk` that can change what an existing app sees. Newest first.
|
|
11
11
|
|
|
12
|
+
## 0.3.527
|
|
13
|
+
|
|
14
|
+
`POST /v1/nodes/:id/jobs/start` accepts a job prepared as already backgrounded. Additive: no request or response an existing app sends or reads changes.
|
|
15
|
+
|
|
16
|
+
- The route used to refuse a job directory without `job.run` (404 `job_not_found`). It now also accepts one with `job.bg`, which `crtr node bash run` writes before launching. A job directory with neither marker is still refused.
|
|
17
|
+
|
|
12
18
|
## 0.3.521
|
|
13
19
|
|
|
14
20
|
`GET /v1/status` names the runtime generation the daemon is serving. Additive: no request or response an existing app sends or reads changes.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
var
|
|
1
|
+
var v=Object.defineProperty;var d=(e,a)=>v(e,"name",{value:a,configurable:!0});import{envNodeId as w}from"../../shared/env.js";import{defineBranch as $,defineLeaf as h}from"../../core/command.js";import{InputError as r}from"../../core/io.js";import{ExitCode as T}from"../../types.js";import{existsSync as g,mkdirSync as I,rmSync as D,writeFileSync as f}from"node:fs";import{activeBackgroundBashJobs as E,backgroundRunningBashJobs as R,bashJobCommandLine as q,bashJobPaths as j,clearJobDeadline as N,formatBashElapsed as _,isSafeBashJobId as y,jobStartedAtMs as S,newBashJobId as M,readJobPgid as B,tailJobLog as L,writeJobDeadlineMs as J}from"../../core/bash-jobs.js";import{parseDurationMs as U}from"../../core/wake.js";import{cliClient as A,getNodeOrNull as u}from"../api-client.js";import{PREVIEW_RESULT_PATH_ENV as C}from"../../core/preview-result-path.js";import{nodeInPane as P}from"../surface/node/placement.js";function b(e,a){const n=e.pane??process.env.TMUX_PANE;let t=e.node;if((t===void 0||t==="")&&(t=P(n)),(t===void 0||t==="")&&(t=w()),t===void 0||t==="")throw new r({error:"no_node",message:a,next:"Run from inside a node, pass --node <id>, or --pane <pane>."});return{id:t,pane:n}}d(b,"resolveBashTarget");const O=h({name:"run",description:"start a shell command as a background job and return at once",whenToUse:"a command needs longer than one tool call \u2014 a test lane, a build, a soak \u2014 and you should be woken with its exit code instead of waiting, backgrounding with `&`, or polling a log",tier:"normal",help:{name:"node bash run",summary:"start one shell command as a background job owned by the calling node and return before it finishes",params:[{kind:"positional",name:"command",required:!0,constraint:"The shell command, run by bash from the current directory with this process\u2019s environment. The same refusals apply as for a bash tool call, judged on this command."}],output:[{name:"job_id",type:"string",required:!0,constraint:"The job object\u2019s id; read or watch it with `crtr canvas`, stop it with `node bash kill`."},{name:"log_path",type:"string",required:!0,constraint:"File receiving the command\u2019s combined stdout and stderr, capped at 10 MiB."}],outputKind:"object",effects:["Works only inside a node\u2019s bash tool call; the job belongs to the calling node.","Starts the command detached from this call, so it keeps running after the call and the turn end. A `timeout` around this leaf bounds only the launch, not the job.","Records the job as a canvas object; when the command exits, its watchers (the calling node by default) get an urgent `exited` push with the exit code and log path.","Sets no cancel deadline: the job runs to completion unless stopped with `node bash kill`.","Fails, with the job id and log path, if the job could not be recorded; the command is then running but will not message the node when it exits."]},run:d(async e=>{const a=e.command,n=w(),t=process.env.CRTR_CONTEXT_DIR;if(n===void 0||n===""||t===void 0||t==="")throw new r({error:"no_node",message:"`node bash run` starts a job for the calling node and works only inside a node\u2019s bash tool",next:"Run it from a node\u2019s bash tool call."});const s=process.cwd(),{bashCommandRefusal:i}=await import("../../core/bash-guard.js"),l=i(a,s);if(l!==null)throw new r({error:"refused",message:l,next:"Run a different command."});const o=j(t,M());I(o.dir,{recursive:!0}),f(o.cmdSh,a),f(o.jobLog,""),f(o.jobBg,"");const{launchBashJobSupervisor:c}=await import("../../core/runtime/broker/bash-job-launch.js"),{[C]:m,...k}=process.env;try{await c(n,o,t,s,k)}catch(p){throw D(o.dir,{recursive:!0,force:!0}),p}const{recordBackgroundedJob:x}=await import("../../core/runtime/broker/daemon-ops.js");try{await x(n,o.jobId,{command:a,job_dir:o.dir,log_path:o.jobLog})}catch(p){throw new r({error:"job_not_recorded",message:`job ${o.jobId} is running but could not be recorded (${p.message}); it will NOT message you when it exits. Log: ${o.jobLog}`,next:`Watch it with \`tail -f ${o.jobLog}\`, or stop it with \`crtr node bash kill ${o.jobId}\`.`},T.GENERAL)}return{job_id:o.jobId,log_path:o.jobLog}},"run"),render:d(e=>`[backgrounded as job ${e.job_id}. It will interrupt you with a message when it exits. Watch: tail -f ${e.log_path} ; stop: crtr node bash kill ${e.job_id}]`,"render")}),K=h({name:"background",description:"hand the focused node\u2019s running bash command to its background job system",whenToUse:"the tmux prefix menu invokes this when a foreground bash command should stop blocking the agent immediately but keep running as a file-backed background job",tier:"hidden",help:{name:"node bash background",summary:"hand every running foreground bash command for one node to its file-backed background job system",params:[{kind:"flag",name:"node",type:"string",required:!1,constraint:"Node whose running bash command to background. Defaults to the node in --pane, else the caller (CRTR_NODE_ID)."},{kind:"flag",name:"pane",type:"string",required:!1,constraint:"tmux pane showing the target node, when --node is omitted. Defaults to $TMUX_PANE. The prefix menu passes this for you."}],output:[{name:"node_id",type:"string",required:!0,constraint:"The target node."},{name:"jobs",type:"object[]",required:!0,constraint:"Per handed-off job {job_id, elapsed_ms} \u2014 the foreground elapsed time in milliseconds at the moment of handoff."}],outputKind:"object",effects:["Signals every foreground bash command currently running for the node to return from its tool call immediately while its detached supervisor keeps the command running.","Each handed-off command retains its log under the node context jobs directory and becomes a job object; when it exits, the job\u2019s watchers (its node by default) get an urgent `exited` push carrying the exit code and log path."]},run:d(async e=>{const{id:a}=b(e,"no node found for the bash handoff"),n=await u(a);if(n===null)throw new r({error:"not_found",message:`no node: ${a}`,next:"List nodes with `crtr node inspect list`."});const t=R(n.paths.context_dir),s=Date.now(),i=t.map(l=>({job_id:l.jobId,elapsed_ms:s-(S(l.jobId)??s)}));return{node_id:a,jobs:i}},"run"),render:d(e=>{const a=e.jobs;if(a.length===0)return`No foreground bash command is running for ${e.node_id}.`;const n=Math.max(...a.map(t=>t.elapsed_ms));return`Handed ${a.length} bash command(s) to the background job system for ${e.node_id} \xB7 ran ${_(n)} in the foreground.`},"render")}),X=h({name:"list",description:"list a node\u2019s live background bash jobs",whenToUse:"you need the ids, commands, and ages of your jobs that are still running",help:{name:"node bash list",summary:"read every still-running background bash job owned by one node",params:[{kind:"flag",name:"node",type:"string",required:!1,constraint:"Node whose background jobs to list. Defaults to the node in --pane, else the caller (CRTR_NODE_ID)."},{kind:"flag",name:"pane",type:"string",required:!1,constraint:"tmux pane showing the target node, when --node is omitted. Defaults to $TMUX_PANE."},{kind:"flag",name:"tail",type:"int",required:!1,default:0,constraint:"Include this many trailing log lines per job (0 = none)."}],output:[{name:"node_id",type:"string",required:!0,constraint:"The target node."},{name:"jobs",type:"object[]",required:!0,constraint:"Oldest-first {job_id, command, purpose, started_at, elapsed_ms, deadline_at, log_path, pgid, log_tail} for every live background job; purpose is a human-readable label or null; deadline_at is the auto-cancel time or null for a job that runs to completion."}],outputKind:"object",effects:["Reads the node context jobs directory. Changes nothing."]},run:d(async e=>{const{id:a}=b(e,"no node found for the bash job list"),n=await u(a);if(n===null)throw new r({error:"not_found",message:`no node: ${a}`,next:"List nodes with `crtr node inspect list`."});const t=Date.now(),s=Math.max(0,Math.trunc(Number(e.tail??0))),i=n.paths.context_dir,l=E(i).map(o=>({job_id:o.jobId,command:o.command,purpose:o.purpose,started_at:new Date(o.startedAtMs).toISOString(),elapsed_ms:t-o.startedAtMs,deadline_at:o.deadlineAtMs===null?null:new Date(o.deadlineAtMs).toISOString(),log_path:o.logPath,pgid:B(i,o.jobId)??null,log_tail:L(o.logPath,s)}));return{node_id:n.node_id,jobs:l}},"run"),render:d(e=>{const a=e.jobs;return a.length===0?`No background bash jobs for ${e.node_id}.`:a.map(n=>`${n.job_id} \xB7 ${_(n.elapsed_ms)} \xB7 ${q(n.command)}${n.purpose===null?"":`
|
|
2
2
|
purpose: ${n.purpose}`}${n.deadline_at===null?"":`
|
|
3
3
|
cancels at: ${n.deadline_at}`}
|
|
4
4
|
log: ${n.log_path}`).join(`
|
|
5
|
-
`)},"render")}),
|
|
5
|
+
`)},"render")}),G=h({name:"kill",description:"stop one of a node\u2019s background bash jobs",whenToUse:"a job you started is no longer wanted or is stuck, and it should stop now",help:{name:"node bash kill",summary:"best-effort stop a background bash job and tell the owning node it was canceled",params:[{kind:"positional",name:"job",required:!0,constraint:"Job id, as reported by node bash list."},{kind:"flag",name:"node",type:"string",required:!1,constraint:"Node owning the job. Defaults to the node in --pane, else the caller (CRTR_NODE_ID)."},{kind:"flag",name:"pane",type:"string",required:!1,constraint:"tmux pane showing the target node, when --node is omitted. Defaults to $TMUX_PANE."}],output:[{name:"node_id",type:"string",required:!0,constraint:"The owning node."},{name:"signaled",type:"boolean",required:!0,constraint:"True when at least one observed supervisor-tree process received SIGTERM."}],outputKind:"object",effects:["Snapshots the supervisor\u2019s observable process tree, sends SIGTERM then SIGKILL to those processes, and cannot stop a descendant that detached and reparented before the snapshot.","Records the job\u2019s exit so it stops showing as live, and ends its job object as killed; the job\u2019s watchers (its node by default) get an urgent `killed` push."]},run:d(async e=>{const{id:a}=b(e,"no node found for the bash job"),n=await u(a);if(n===null)throw new r({error:"not_found",message:`no node: ${a}`,next:"List nodes with `crtr node inspect list`."});const t=e.job;if(!y(t))throw new r({error:"invalid_job_id",message:`invalid background job id: ${t}`,next:"Use a job id reported by `crtr node bash list`."});const s=await A().stopBashJob(n.node_id,t);return{node_id:n.node_id,signaled:s.signaled}},"run"),render:d(e=>`Canceled a background bash job for ${e.node_id}${e.signaled===!0?"":" (no observed job process was still running)"}.`,"render")}),W=h({name:"extend",description:"claim an auto-backgrounded bash job so the daemon does not cancel it at its deadline",whenToUse:"a command that was moved to a job automatically is worth keeping past its cancel deadline \u2014 give it more time with --for, or let it run to completion with --keep",help:{name:"node bash extend",summary:"move or clear the auto-cancel deadline on one of a node\u2019s live background bash jobs",params:[{kind:"positional",name:"job",required:!0,constraint:"Job id, as reported by node bash list or the auto-background notice."},{kind:"flag",name:"for",type:"string",required:!1,constraint:"New window from now before the job is canceled, as a duration (e.g. 90s, 10m, 1h30m). Mutually exclusive with --keep; give exactly one."},{kind:"flag",name:"keep",type:"bool",required:!1,constraint:"Remove the deadline entirely so the job runs to completion. Mutually exclusive with --for; give exactly one."},{kind:"flag",name:"node",type:"string",required:!1,constraint:"Node owning the job. Defaults to the node in --pane, else the caller (CRTR_NODE_ID)."},{kind:"flag",name:"pane",type:"string",required:!1,constraint:"tmux pane showing the target node, when --node is omitted. Defaults to $TMUX_PANE."}],output:[{name:"node_id",type:"string",required:!0,constraint:"The owning node."},{name:"deadline_at",type:"string",required:!1,constraint:"The job\u2019s new auto-cancel time (ISO), or absent when --keep cleared the deadline so the job runs to completion."}],outputKind:"object",effects:["Rewrites or removes the job\u2019s deadline file. The daemon\u2019s deadline lane reads it on its next scan. Signals nothing."]},run:d(async e=>{const{id:a}=b(e,"no node found for the bash job"),n=await u(a);if(n===null)throw new r({error:"not_found",message:`no node: ${a}`,next:"List nodes with `crtr node inspect list`."});const t=e.job;if(!y(t))throw new r({error:"invalid_job_id",message:`invalid background job id: ${t}`,next:"Use a job id reported by `crtr node bash list`."});const s=e.for,i=e.keep===!0;if(i===(s!==void 0&&s!==""))throw new r({error:"invalid_args",message:"give exactly one of --for <duration> or --keep",next:"Extend by a window with --for 10m, or drop the deadline with --keep."});const l=n.paths.context_dir,o=j(l,t);if(!g(o.dir))throw new r({error:"not_found",message:`no background job ${t} for ${n.node_id}`,next:"List live jobs with `crtr node bash list`."});if(!g(o.jobBg)||g(o.jobExit))throw new r({error:"not_running",message:`background job ${t} for ${n.node_id} is no longer running`,next:`It finished or was stopped, so there is no deadline to change. Log: ${o.jobLog}`});if(i)return N(o),{node_id:n.node_id};const c=U(s);if(c===null||c<=0)throw new r({error:"invalid_duration",message:`not a duration: ${s}`,next:"Use a value like 90s, 10m, or 1h30m."});const m=Date.now()+c;return J(o,m),{node_id:n.node_id,deadline_at:new Date(m).toISOString()}},"run"),render:d(e=>e.deadline_at===void 0?`Cleared the deadline \u2014 the background job will run to completion for ${e.node_id}.`:`Background job for ${e.node_id} will be canceled at ${e.deadline_at} unless extended again.`,"render")}),ae=$({name:"bash",description:"start, list, stop, and extend your background bash jobs",whenToUse:"a command needs longer than one tool call \u2014 start it as a job and be woken with its exit code instead of using `&`, `nohup`, or a cron that polls a log \u2014 or you need to list, stop, or extend the deadline of a job you already have",help:{name:"node bash",summary:"start, list, stop, and extend background bash jobs owned by a node",model:"Every bash call runs under a detached supervisor with its output in a log file, so a command that outlives its call keeps running as a job: a canvas object its node owns, readable and watchable with `crtr canvas`. `run` starts a job on purpose and it is never auto-canceled. A foreground command still running after 5 minutes is moved to a job automatically, with a cancel deadline unless `extend` claims it."},children:[O,K,X,G,W]});export{ae as nodeBash};
|
package/dist/commands/node.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
var t=Object.defineProperty;var e=(o
|
|
1
|
+
var t=Object.defineProperty;var e=(n,o)=>t(n,"name",{value:o,configurable:!0});import{defineBranch as a}from"../core/command.js";import{nodeNew as i}from"./node/create.js";import{nodeInspect as r,nodeFork as s}from"./node/inspect.js";import{nodeLifecycle as d,nodeConfig as c,nodePromote as h,nodeYield as m}from"./node/lifecycle.js";import{nodeMessage as p}from"./node/message.js";import{nodeWaitBranch as l}from"./node/wait.js";import{nodeSubscription as f}from"./node/subscription.js";import{nodeBash as g}from"./node/bash.js";import{pyramidSlash as u}from"./node/pyramid.js";import{nodeWorktreeBranch as w}from"./node-worktree.js";import{nodeOutcome as y}from"./node/outcome.js";function A(){return a({name:"node",slash:u,rootEntry:{concept:"a unit of the agent runtime \u2014 an agent on the canvas with its own context dir and pi vehicle. Spawning nodes is how work gets done here, not an optimization you reach for late",desc:"spawn, inspect, and navigate nodes on the canvas",useWhen:"delegating a genuinely independent subtask, or walking the subscription graph. Base nodes work hands-on; orchestrators fan out. Give each child a bounded outcome distinct from your whole assignment so delegation moves work toward completion instead of recursively passing it down. Once delegated, do not duplicate the child\u2019s work; its finish wakes you. Use the help surface for the exact node contract before invoking it. For a long-running shell command, `crtr node bash -h`."},help:{name:"node",summary:"create, inspect, message, and change canvas nodes",model:"A node is one agent on the canvas. Immediate actions, durable waits, subscriptions, lifecycle changes, and scheduled crons are separate intent families; choose the family by when the effect happens and what persistent state it changes."},children:[i,s,p,l,f,y,r,d,w,c,h,m,g]})}e(A,"registerNode");export{A as registerNode};
|
package/dist/core/bash-guard.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
var $=Object.defineProperty;var a=(n,t)=>$(n,"name",{value:t,configurable:!0});import{homedir as
|
|
1
|
+
var $=Object.defineProperty;var a=(n,t)=>$(n,"name",{value:t,configurable:!0});import{homedir as k,userInfo as I}from"node:os";import{isAbsolute as R,resolve as x,sep as v}from"node:path";import{commandSegments as L,stripHeredocs as O}from"./shell-segments.js";const h=10,W={s:1,m:60,h:3600,d:86400};function N(n){const t=/^\s*sleep(\s+[^;&|\n]+)/.exec(n);if(!t)return null;let e=0;for(const s of t[1].trim().split(/\s+/)){const r=/^([0-9]*\.?[0-9]+)([smhd]?)$/.exec(s);if(!r)return null;e+=Number(r[1])*W[r[2]||"s"]}return e}a(N,"leadingSleepSeconds");function z(n){const t=N(n);return t===null||t<=h?null:`Refused: this command starts by sleeping ${t}s, over the ${h}s inline limit.
|
|
2
2
|
Holding a live window on a clock is never how you wait here. Instead:
|
|
3
3
|
- schedule the wake and end your turn: crtr cron -h (one-shot at a time, or recurring)
|
|
4
4
|
- race an inbox message against a deadline: crtr node wait deadline -h
|
|
5
|
+
- waiting on a command: crtr node bash run '<command>' starts it as a job and wakes you with its exit code
|
|
5
6
|
- waiting on a child, a reply from the user, or any spine event: just stop \u2014 the runtime wakes you.
|
|
6
|
-
If you truly need a short pause inline, sleep ${h}s or less.`}a(z,"longSleepRefusal");const D=["/","/Applications","/bin","/dev","/etc","/home","/Library","/opt","/private","/private/etc","/private/tmp","/private/var","/sbin","/System","/tmp","/usr","/Users","/var","/Volumes"];function M(n,t,e){return D.includes(n)?"a system directory":n===t?"your home directory":n===`${t}/.crouter`?"the canvas home \u2014 every node\u2019s conversation and state":e===n||!e.startsWith(n.endsWith(v)?n:n+v)?null:`a directory containing your working directory (${e})`}a(M,"protectedReason");function T(n,t){const e=
|
|
7
|
+
If you truly need a short pause inline, sleep ${h}s or less.`}a(z,"longSleepRefusal");const D=["/","/Applications","/bin","/dev","/etc","/home","/Library","/opt","/private","/private/etc","/private/tmp","/private/var","/sbin","/System","/tmp","/usr","/Users","/var","/Volumes"];function M(n,t,e){return D.includes(n)?"a system directory":n===t?"your home directory":n===`${t}/.crouter`?"the canvas home \u2014 every node\u2019s conversation and state":e===n||!e.startsWith(n.endsWith(v)?n:n+v)?null:`a directory containing your working directory (${e})`}a(M,"protectedReason");function T(n,t){const e=I().username;return new Map([["HOME",n],["PWD",t],["USER",e],["LOGNAME",e]])}a(T,"initialVariables");function y(n,t){let e=n;if(e==="~"||e.startsWith("~/")){const s=t.get("HOME");if(s===void 0)return null;e=s+e.slice(1)}else if(e.startsWith("~"))return null;return e=e.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)\}|\$([A-Za-z_][A-Za-z0-9_]*)/g,(s,r,i)=>t.get(r??i)??s),/[$`*?[]/.test(e)?null:e}a(y,"expandText");function w(n,t,e){const s=y(n.replace(/\/+\*?\.?$/,"")||"/",t);return s===null?null:R(s)?x(s):e===null?null:x(e,s)}a(w,"resolveTarget");function d(n,t){for(const e of t){const s=e.indexOf("=");if(s<=0)continue;const r=e.slice(0,s);if(!/^[A-Za-z_][A-Za-z0-9_]*$/.test(r))continue;const i=y(e.slice(s+1),n);i===null?n.delete(r):n.set(r,i)}}a(d,"applyAssignments");const H=/^[0-9]*(&?>>?|<<?|>&|<&)/;function m(n){const t=[];let e=!1,s=!1;for(const r of n.slice(1)){if(s){s=!1;continue}const i=H.exec(r.text);if(i!==null&&!r.opaque){s=r.text===i[0];continue}if(!e&&r.text==="--"){e=!0;continue}!e&&r.text.startsWith("-")&&r.text!=="-"||t.push(r.text)}return t}a(m,"operands");function P(n){const t=new Set,e=new Set;for(const s of n.slice(1)){if(s.text==="--")break;if(s.text.startsWith("--"))e.add(s.text.slice(2));else if(s.text.startsWith("-")&&s.text.length>1)for(const r of s.text.slice(1))t.add(r)}return{letters:t,long:e}}a(P,"flagLetters");function E(n){const{letters:t,long:e}=P(n);return t.has("r")||t.has("R")||e.has("recursive")}a(E,"isRecursive");const C=/^\/dev\/r?(disk|sd[a-z]|nvme|hd[a-z]|vd[a-z]|mmcblk|md)/;function q(n){const t=[];let e=1;for(;e<n.length;){const s=n[e].text;if(s==="--"||/^-[HLPEdsx]$/.test(s))e+=1;else if(s==="-D"||s==="-O")e+=2;else if(s==="-f")e+1<n.length&&t.push(n[e+1].text),e+=2;else break}for(;e<n.length&&!n[e].text.startsWith("-");e+=1)t.push(n[e].text);return t}a(q,"findPaths");const Z=new Set(["sudo","doas","command","env","nice","nohup","time","stdbuf","xargs","exec","builtin"]);function V(n){let t=n;for(;t.length>0&&!t[0].opaque&&Z.has(t[0].text);){let e=1;for(;e<t.length&&t[e].text.startsWith("-");){const s=/^-[nu]$/.test(t[e].text);e+=s?2:1}for(;e<t.length&&/^[A-Za-z_][A-Za-z0-9_]*=/.test(t[e].text);)e+=1;if(e>=t.length)return[];t=t.slice(e)}return t}a(V,"stripCommandPrefixes");function U(n){const t={targets:[],named:null},e=n[0];if(e===void 0||e.opaque)return t;const s=e.text.slice(e.text.lastIndexOf("/")+1),r=m(n);switch(s){case"rm":return E(n)?{targets:r,named:null}:t;case"mv":return r.length>=2?{targets:r.slice(0,-1),named:null}:t;case"chmod":case"chown":case"chgrp":return E(n)&&r.length>=2?{targets:r.slice(1),named:null}:t;case"find":return n.some(o=>!o.opaque&&(o.text==="-delete"||o.text==="-exec"||o.text==="-execdir"))?{targets:q(n),named:null}:t;case"dd":{const i=n.find(l=>l.text.startsWith("of="));if(i===void 0)return t;const o=i.text.slice(3);return C.test(o)?{targets:[],named:o}:o.startsWith("/dev/")?t:{targets:[o],named:null}}default:if(/^(mkfs|newfs)([.-].+)?$/.test(s))return{targets:[],named:s};if(s==="diskutil"){const i=r[0];if(i!==void 0&&/^(erase|reformat|partition|zeroDisk|randomDisk|secureErase)/i.test(i))return{targets:[],named:`diskutil ${i}`}}return t}}a(U,"destructiveTargets");function j(n,t,e){const s=T(e,t);let r=[t];for(const i of L(O(n))){const o=V(i.tokens),l=o[0];if(l!==void 0){const u=new Map(s);d(u,i.assignments);const c=r[r.length-1];c!==void 0&&u.set("PWD",c);const{targets:S,named:p}=U(o);if(p!==null)return b(i.text,p,"a whole disk or volume \u2014 never a task boundary");for(const A of S)for(const _ of r.length===0?[null]:r){const f=w(A,u,_);if(f===null)continue;const g=M(f,e,t);if(g!==null)return b(i.text,f,g)}}if(!i.background){if(l===void 0)d(s,i.assignments);else if(!l.opaque&&B.has(l.text))d(s,i.assignments),d(s,m(o));else if(!l.opaque&&l.text==="cd"){const u=m(o)[0],c=u===void 0?s.get("HOME")??null:w(u,s,r[r.length-1]??null);r=c===null?[]:[...r,c]}}}return null}a(j,"destructiveCommandRefusal");const B=new Set(["export","declare","typeset","readonly"]);function b(n,t,e){return`Refused: \`${n}\` targets ${t}, which is ${e}.
|
|
7
8
|
Nothing an agent is asked to do requires destroying that path, and it cannot be undone.
|
|
8
9
|
If you meant somewhere else, name that path explicitly and run the command again. Two traps to check first:
|
|
9
10
|
- a \`VAR=value cmd\` prefix applies ONLY to that one command, so a later \`$VAR\` in the same line is the SHELL's value \u2014 this is exactly how a node deleted a home directory.
|
|
10
11
|
- \`~\` and \`$HOME\` are the real home directory, never a scratch dir; build scratch paths with mktemp -d and hold them in a variable of your own.
|
|
11
|
-
If you believe this path genuinely must be destroyed, ask the user \u2014 do not work around this refusal.`}a(b,"refusalMessage");function K(n,t,e=
|
|
12
|
+
If you believe this path genuinely must be destroyed, ask the user \u2014 do not work around this refusal.`}a(b,"refusalMessage");function K(n,t,e=k()){return z(n)??j(n,t,e)}a(K,"bashCommandRefusal");export{K as bashCommandRefusal,N as leadingSleepSeconds};
|