@lunora/agent 1.0.0-alpha.57 → 1.0.0-alpha.58

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.
@@ -1,2 +1,2 @@
1
- import{LunoraError as I}from"@lunora/errors";import{defineTable as v,initLunora as S,defineSchemaExtension as B}from"@lunora/server";import{v as e}from"@lunora/values";import{a as f,A as k,d as j}from"./packem_shared/component-shared-Cn6E5d_q.mjs";import{graphTables as P,graphComponent as F}from"./packem_shared/normalizeEntityName-bz1Thnmp.mjs";import{normalizeEntityName as ce}from"./packem_shared/normalizeEntityName-bz1Thnmp.mjs";import{sandboxComponent as pe}from"./packem_shared/sandboxComponent-DcvkZjNW.mjs";const K="agent_episodes",G=5,H=20,X=500,x=200,Q=/\s+/gu,Y=u=>u===void 0||!Number.isFinite(u)?G:Math.min(H,Math.max(1,Math.trunc(u))),z={episodes:v({createdAt:e.number(),messageKey:e.string(),owner:e.string(),summary:e.string(),threadKey:e.optional(e.string())}).index("byOwnerCreatedAt",["owner","createdAt"]).index("byOwnerMessageKey",["owner","messageKey"],{unique:!0}).public()},{mutation:W,query:J}=S.dataModel().create(),V=()=>{const u=W.input({createdAt:e.optional(e.number()),messageKey:e.string(),owner:e.string(),summary:e.string(),threadKey:e.optional(e.string())}).mutation(async({args:d,ctx:c})=>{const m=d.summary.replaceAll(Q," ").trim().slice(0,X);if(m.length===0)return{recorded:!1};if(await c.db.query(K).withIndex("byOwnerMessageKey",w=>w.eq("owner",d.owner).eq("messageKey",d.messageKey)).first())return{recorded:!1};await c.db.insert(K,{createdAt:d.createdAt??Date.now(),messageKey:d.messageKey,owner:d.owner,summary:m,...d.threadKey===void 0?{}:{threadKey:d.threadKey}});const b=await c.db.query(K).withIndex("byOwnerCreatedAt",w=>w.eq("owner",d.owner)).order("asc").take(x+1);for(const w of b.slice(0,Math.max(0,b.length-x)))await c.db.delete(w._id);return{recorded:!0}}),o=J.input({limit:e.optional(e.number()),owner:e.string()}).query(async({args:d,ctx:c})=>{const m=await c.db.query(K).withIndex("byOwnerCreatedAt",y=>y.eq("owner",d.owner)).order("desc").take(Y(d.limit));return m.length===0?{context:""}:{context:m.toReversed().map(y=>`- ${y.summary}`).join(`
1
+ import{LunoraError as I}from"@lunora/errors";import{defineTable as v,initLunora as S,defineSchemaExtension as B}from"@lunora/server";import{v as e}from"@lunora/values";import{a as f,A as k,d as j}from"./packem_shared/component-shared-Cn6E5d_q.mjs";import{graphTables as P,graphComponent as F}from"./packem_shared/normalizeEntityName-bz1Thnmp.mjs";import{normalizeEntityName as ce}from"./packem_shared/normalizeEntityName-bz1Thnmp.mjs";import{sandboxComponent as pe}from"./packem_shared/sandboxComponent-BtOkUYpY.mjs";const K="agent_episodes",G=5,H=20,X=500,x=200,Q=/\s+/gu,Y=u=>u===void 0||!Number.isFinite(u)?G:Math.min(H,Math.max(1,Math.trunc(u))),z={episodes:v({createdAt:e.number(),messageKey:e.string(),owner:e.string(),summary:e.string(),threadKey:e.optional(e.string())}).index("byOwnerCreatedAt",["owner","createdAt"]).index("byOwnerMessageKey",["owner","messageKey"],{unique:!0}).public()},{mutation:W,query:J}=S.dataModel().create(),V=()=>{const u=W.input({createdAt:e.optional(e.number()),messageKey:e.string(),owner:e.string(),summary:e.string(),threadKey:e.optional(e.string())}).mutation(async({args:d,ctx:c})=>{const m=d.summary.replaceAll(Q," ").trim().slice(0,X);if(m.length===0)return{recorded:!1};if(await c.db.query(K).withIndex("byOwnerMessageKey",w=>w.eq("owner",d.owner).eq("messageKey",d.messageKey)).first())return{recorded:!1};await c.db.insert(K,{createdAt:d.createdAt??Date.now(),messageKey:d.messageKey,owner:d.owner,summary:m,...d.threadKey===void 0?{}:{threadKey:d.threadKey}});const b=await c.db.query(K).withIndex("byOwnerCreatedAt",w=>w.eq("owner",d.owner)).order("asc").take(x+1);for(const w of b.slice(0,Math.max(0,b.length-x)))await c.db.delete(w._id);return{recorded:!0}}),o=J.input({limit:e.optional(e.number()),owner:e.string()}).query(async({args:d,ctx:c})=>{const m=await c.db.query(K).withIndex("byOwnerCreatedAt",y=>y.eq("owner",d.owner)).order("desc").take(Y(d.limit));return m.length===0?{context:""}:{context:m.toReversed().map(y=>`- ${y.summary}`).join(`
2
2
  `)}});return{agentEpisodeRecall:f(o),agentEpisodeUpsert:f(u)}},$="threads",D="messages",M="run_queue",h=`${k}_${$}`,A=`${k}_${D}`,q=`${k}_${M}`,N=5,Z=780*60*1e3,ee=B(k,{tables:{[D]:v({content:e.string(),createdAt:e.number(),messageKey:e.string(),role:e.union(e.literal("user"),e.literal("assistant"),e.literal("tool"),e.literal("system")),seq:e.number(),status:e.optional(e.union(e.literal("awaiting_approval"),e.literal("approved"),e.literal("rejected"))),stepName:e.optional(e.string()),threadKey:e.string(),toolCallId:e.optional(e.string()),toolCalls:e.optional(e.array(e.object({id:e.string(),input:e.any(),name:e.string()}))),toolName:e.optional(e.string())}).index("byThread",["threadKey","seq"]).index("byMessageKey",["threadKey","messageKey"],{unique:!0}).public(),[$]:v({agent:e.string(),createdAt:e.number(),error:e.optional(e.string()),instanceId:e.optional(e.string()),key:e.string(),messageCount:e.number(),owner:e.optional(e.string()),state:e.optional(e.any()),status:e.union(e.literal("idle"),e.literal("running"),e.literal("error"),e.literal("cancelled"),e.literal("awaiting_input")),title:e.optional(e.string()),updatedAt:e.number(),usage:e.optional(e.object({inputTokens:e.optional(e.number()),outputTokens:e.optional(e.number()),totalTokens:e.optional(e.number())}))}).index("byKey",["key"],{unique:!0}).index("byAgent",["agent"]).index("byInstance",["instanceId"]).public(),[M]:v({agent:e.string(),enqueuedAt:e.number(),instanceId:e.string(),position:e.number(),threadKey:e.string()}).index("byThread",["threadKey","position"]).index("byThreadInstance",["threadKey","instanceId"],{unique:!0}).public(),...P,...z}}),{mutation:g,query:_}=S.dataModel().create(),te=async(u,o,d)=>{if(o.instanceId===void 0)throw new I("CONFLICT",`@lunora/agent: thread "${o.key}" already has a run in flight (instance "${o.priorInstanceId}") — cannot queue a dispatch with no instance id`);const{instanceId:c}=o,m=await u.query(q).withIndex("byThreadInstance",w=>w.eq("threadKey",o.key).eq("instanceId",c)).first();if(m)return{outcome:"queued",position:m.position};const y=await u.query(q).withIndex("byThread",w=>w.eq("threadKey",o.key)).collect();if(y.length>=N)throw new I("CONFLICT",`@lunora/agent: thread "${o.key}" run queue is full (depth ${String(N)}) — rejecting instance "${c}"`);const b=(y.at(-1)?.position??-1)+1;return await u.insert(q,{agent:o.agent,enqueuedAt:d,instanceId:c,position:b,threadKey:o.key}),{outcome:"queued",position:b}},ne=async(u,o,d)=>{if(o.policy==="queue")return te(u,{agent:o.agent,instanceId:o.instanceId,key:o.key,priorInstanceId:o.priorInstanceId},d);if(o.policy!=="replace")throw new I("CONFLICT",`@lunora/agent: thread "${o.key}" already has a run in flight (instance "${o.priorInstanceId}") — onConcurrentRun="${o.policy}"`);return await u.patch(o.existingId,{error:void 0,status:"running",updatedAt:d,...o.instanceId===void 0?{}:{instanceId:o.instanceId}}),{outcome:"replaced",priorInstanceId:o.priorInstanceId}},se=()=>{const u=g.input({agent:e.string(),initialState:e.optional(e.any()),instanceId:e.optional(e.string()),key:e.string(),onConcurrentRun:e.optional(e.union(e.literal("reject"),e.literal("queue"),e.literal("replace"))),owner:e.optional(e.string()),title:e.optional(e.string())}).mutation(async({args:t,ctx:n})=>{const a=Date.now(),i=await n.db.query(h).withIndex("byKey",r=>r.eq("key",t.key)).first();if(i){if(i.owner!==t.owner&&t.owner!==void 0)throw new Error(`@lunora/agent: thread "${t.key}" belongs to another owner`);const r=i.instanceId,s=typeof i.updatedAt=="number"?i.updatedAt:0;return!(a-s>Z)&&(i.status==="running"||i.status==="awaiting_input")&&r!==void 0&&(t.instanceId===void 0||t.instanceId!==r)?ne(n.db,{agent:t.agent,existingId:i._id,instanceId:t.instanceId,key:t.key,policy:t.onConcurrentRun??"reject",priorInstanceId:r},a):(await n.db.patch(i._id,{error:void 0,status:"running",updatedAt:a,...t.instanceId===void 0?{}:{instanceId:t.instanceId}}),{outcome:"continued"})}return await n.db.insert(h,{agent:t.agent,createdAt:a,key:t.key,messageCount:0,status:"running",updatedAt:a,...j({instanceId:t.instanceId,owner:t.owner,state:t.initialState,title:t.title})}),{outcome:"created"}}),o=g.input({content:e.string(),messageKey:e.string(),role:e.union(e.literal("user"),e.literal("assistant"),e.literal("tool"),e.literal("system")),status:e.optional(e.union(e.literal("awaiting_approval"),e.literal("approved"),e.literal("rejected"))),stepName:e.optional(e.string()),threadKey:e.string(),toolCallId:e.optional(e.string()),toolCalls:e.optional(e.array(e.object({id:e.string(),input:e.any(),name:e.string()}))),toolName:e.optional(e.string())}).mutation(async({args:t,ctx:n})=>{const a=await n.db.query(A).withIndex("byMessageKey",p=>p.eq("threadKey",t.threadKey).eq("messageKey",t.messageKey)).first();if(a)return{seq:a.seq};const i=await n.db.query(h).withIndex("byKey",p=>p.eq("key",t.threadKey)).first();if(!i)throw new Error(`@lunora/agent: cannot append to unknown thread "${t.threadKey}" — run agentEnsureThread first`);const r=i.messageCount,s=Date.now();return await n.db.insert(A,{content:t.content,createdAt:s,messageKey:t.messageKey,role:t.role,seq:r,threadKey:t.threadKey,...t.status===void 0?{}:{status:t.status},...t.stepName===void 0?{}:{stepName:t.stepName},...t.toolCallId===void 0?{}:{toolCallId:t.toolCallId},...t.toolCalls===void 0?{}:{toolCalls:t.toolCalls},...t.toolName===void 0?{}:{toolName:t.toolName}}),await n.db.patch(i._id,{messageCount:r+1,updatedAt:s}),{seq:r}}),d=g.input({error:e.optional(e.string()),instanceId:e.optional(e.string()),key:e.optional(e.string()),status:e.optional(e.union(e.literal("idle"),e.literal("running"),e.literal("error"),e.literal("cancelled"),e.literal("awaiting_input"))),title:e.optional(e.string()),usage:e.optional(e.object({inputTokens:e.optional(e.number()),outputTokens:e.optional(e.number()),totalTokens:e.optional(e.number())}))}).mutation(async({args:t,ctx:n})=>{const{instanceId:a,key:i}=t;let r;i!==void 0?r=await n.db.query(h).withIndex("byKey",s=>s.eq("key",i)).first():a!==void 0&&(r=await n.db.query(h).withIndex("byInstance",s=>s.eq("instanceId",a)).first()),r&&await n.db.patch(r._id,{updatedAt:Date.now(),...t.error===void 0?{}:{error:t.error},...t.status===void 0?{}:{status:t.status},...t.title===void 0?{}:{title:t.title},...t.usage===void 0?{}:{usage:t.usage}})}),c=g.input({error:e.optional(e.string()),instanceId:e.string(),key:e.string(),status:e.union(e.literal("idle"),e.literal("error"),e.literal("cancelled")),usage:e.optional(e.object({inputTokens:e.optional(e.number()),outputTokens:e.optional(e.number()),totalTokens:e.optional(e.number())}))}).mutation(async({args:t,ctx:n})=>{const a=Date.now(),i=await n.db.query(h).withIndex("byKey",l=>l.eq("key",t.key)).first();if(i?.instanceId!==t.instanceId){const l=await n.db.query(q).withIndex("byThreadInstance",E=>E.eq("threadKey",t.key).eq("instanceId",t.instanceId)).first();return l&&await n.db.delete(l._id),{}}const s=(await n.db.query(q).withIndex("byThread",l=>l.eq("threadKey",t.key)).collect())[0];if(!s)return await n.db.patch(i._id,{status:t.status,updatedAt:a,...t.error===void 0?{}:{error:t.error},...t.usage===void 0?{}:{usage:t.usage}}),{};const p=s.instanceId;return await n.db.delete(s._id),await n.db.patch(i._id,{instanceId:p,status:"running",updatedAt:a,...t.error===void 0?{}:{error:t.error},...t.usage===void 0?{}:{usage:t.usage}}),{dequeued:p}}),m=g.input({key:e.string(),state:e.any()}).mutation(async({args:t,ctx:n})=>{const a=await n.db.query(h).withIndex("byKey",i=>i.eq("key",t.key)).first();a&&await n.db.patch(a._id,{state:t.state,updatedAt:Date.now()})}),y=(t,n)=>{if(!t)return;const{owner:a}=t;if(!(a!==void 0&&a!==(n.userId??void 0)))return t},b=_.input({key:e.string()}).query(async({args:t,ctx:n})=>{const a=await n.db.query(h).withIndex("byKey",i=>i.eq("key",t.key)).first();return y(a,n.auth)}),w=_.input({key:e.string()}).query(async({args:t,ctx:n})=>{const a=await n.db.query(h).withIndex("byKey",i=>i.eq("key",t.key)).first();return y(a,n.auth)?.state}),O=_.input({key:e.string(),limit:e.optional(e.number())}).query(async({args:t,ctx:n})=>{const a=await n.db.query(h).withIndex("byKey",i=>i.eq("key",t.key)).first();return y(a,n.auth)===void 0?[]:t.limit!==void 0?(await n.db.query(A).withIndex("byThread",r=>r.eq("threadKey",t.key)).order("desc").take(t.limit)).toReversed():n.db.query(A).withIndex("byThread",i=>i.eq("threadKey",t.key)).collect()}),L=g.input({decision:e.union(e.literal("approve"),e.literal("reject")),instanceId:e.string(),note:e.optional(e.string()),threadKey:e.string(),toolCallId:e.string()}).mutation(async({args:t,ctx:n})=>{const a=await n.db.query(h).withIndex("byKey",l=>l.eq("key",t.threadKey)).first(),i=y(a,n.auth);if(i===void 0)throw new I("FORBIDDEN",`@lunora/agent: not allowed to resolve approvals on thread "${t.threadKey}"`);if(i.instanceId!==t.instanceId)throw new I("FORBIDDEN",`@lunora/agent: instance "${t.instanceId}" does not own thread "${t.threadKey}"`);const r=i.agent,{agents:s}=n,p=s?.[r];if(typeof p?.sendEvent!="function")throw new I("INTERNAL",`@lunora/agent: no ctx.agents["${r}"] producer to resolve the approval — run codegen/dev so the agent binding is wired`);return await p.sendEvent(t.instanceId,{payload:{decision:t.decision,toolCallId:t.toolCallId,...t.note===void 0?{}:{note:t.note}},type:`agent-approval:${t.toolCallId}`}),{resolved:!0}}),U=g.input({agent:e.string(),input:e.string(),threadKey:e.string(),title:e.optional(e.string())}).mutation(async({args:t,ctx:n})=>{const{agents:a}=n,i=a?.[t.agent];if(typeof i?.run!="function")throw new I("INTERNAL",`@lunora/agent: no ctx.agents["${t.agent}"] producer to start a run — run codegen/dev so the agent binding is wired, and check the agent name`);if(i.publicRun!==!0)throw new I("FORBIDDEN",`@lunora/agent: agent "${t.agent}" is not enabled for public runs — set defineAgent({ publicRun: true }) to allow an external client (e.g. the @lunora/mcp server) to start it`);const r=n.auth.userId??void 0,s=await n.db.query(h).withIndex("byKey",l=>l.eq("key",t.threadKey)).first();if(s){const l=s.status,E=s.instanceId,T=s.owner;if((l==="running"||l==="awaiting_input")&&E!==void 0&&(T===void 0||T===r))return{id:E,threadKey:t.threadKey};if(T!==r&&r!==void 0)throw new I("FORBIDDEN",`@lunora/agent: thread "${t.threadKey}" belongs to another owner`)}const{id:p}=await i.run({input:t.input,threadKey:t.threadKey,...r===void 0?{}:{owner:r},...t.title===void 0?{}:{title:t.title}});return{id:p,threadKey:t.threadKey}}),C=F(),R=V();return{extension:ee,functions:{agentAppendMessage:f(o),agentCompleteRun:f(c),agentEnsureThread:f(u),agentEpisodeRecall:R.agentEpisodeRecall,agentEpisodeUpsert:R.agentEpisodeUpsert,agentGraphTraverse:C.agentGraphTraverse,agentGraphUpsert:C.agentGraphUpsert,agentMessages:O,agentPatchThread:f(d),agentResolveApproval:L,agentRun:U,agentSetState:f(m),agentState:w,agentThread:b}}};export{se as agentComponent,ee as agentExtension,ce as normalizeEntityName,pe as sandboxComponent};
@@ -0,0 +1,5 @@
1
+ import{LunoraError as c}from"@lunora/errors";import{initLunora as h}from"@lunora/server";import{v as t}from"@lunora/values";import{t as g}from"./base64-5eyBfWO3.mjs";const{internalAction:w}=h.dataModel().create(),m=()=>globalThis.document?.documentElement?.outerHTML??"",l=1e6,b=20,x=new TextEncoder,y=async(e,n)=>{const o=n.url??"";switch(n.op){case"content":return e.content(o);case"pdf":return{data:g(await e.pdf(o)),encoding:"base64",mediaType:"application/pdf"};case"scrape":return e.scrape(o,m);case"screenshot":{const s=await e.screenshot(o,{...n.fullPage===void 0?{}:{fullPage:n.fullPage},...n.type===void 0?{}:{type:n.type}});return{data:g(s),encoding:"base64",mediaType:n.type==="jpeg"?"image/jpeg":"image/png"}}default:throw new c("INTERNAL",`@lunora/agent: sandbox browser op "${n.op}" is not supported`)}},E=e=>{const n=[`exit code: ${String(e.code)}`];return e.stdout.length>0&&n.push(`stdout:
2
+ ${e.stdout}`),e.stderr.length>0&&n.push(`stderr:
3
+ ${e.stderr}`),n.join(`
4
+
5
+ `)},$=async(e,n)=>{const o=e.any();if(n.op==="exec")try{return E(await o.exec(n.command??"",{args:n.args??[]}))}catch(s){return`exec failed: ${s instanceof Error?s.message:String(s)}`}if(n.op==="fetch")return(await o.fetch(n.path??"/",{...n.body===void 0?{}:{body:n.body},method:n.method??"GET"})).text();throw new c("INTERNAL",`@lunora/agent: sandbox container op "${n.op}" is not supported`)},u=e=>{let n=0,o=e.length;for(;n<o&&e[n]==="/";)n+=1;for(;o>n&&e[o-1]==="/";)o-=1;return e.slice(n,o)},T=(e,n)=>{const o=u(e),s=[];for(const r of n.split("/"))if(!(r===""||r===".")){if(r===".."){if(s.length===0)throw new c("BAD_REQUEST","@lunora/agent: fs path escapes the sandbox root");s.pop();continue}s.push(r)}const a=s.join("/");return o&&a?`${o}/${a}`:o||a},S=async(e,n,o)=>{const s=n.length>0?`${n}/`:"",a=o.length>0?o.length+1:0,r=[];let i;for(let d=0;d<b;d+=1){const p=await e.list({prefix:s,...i===void 0?{}:{cursor:i}});for(const f of p.objects)r.push(f.key.slice(a));if(!p.truncated||p.cursor===void 0)return{entries:r,truncated:!1};i=p.cursor}return{entries:r,truncated:!0}},N=async(e,n,o)=>{const s=u(n),a=T(n,o.path??"");switch(o.op){case"ls":{const{entries:r,truncated:i}=await S(e,a,s);return i?{entries:r,truncated:!0}:{entries:r}}case"read":{const r=await e.head(a);if(r&&r.size>l)throw new c("BAD_REQUEST",`@lunora/agent: fs read: "${o.path??""}" is ${String(r.size)} bytes (max ${String(l)})`);const i=await e.get(a);if(!i)throw new c("NOT_FOUND",`@lunora/agent: fs read: no file at "${o.path??""}"`);return i.text()}case"rm":return await e.delete(a),{path:o.path??"",removed:!0};case"stat":{const r=await e.head(a);return r?{exists:!0,size:r.size}:{exists:!1}}case"write":{const r=o.content??"",i=x.encode(r).length;if(i>l)throw new c("BAD_REQUEST",`@lunora/agent: fs write: ${String(i)} bytes exceeds the max (${String(l)})`);return await e.put(a,r),{bytes:i,path:o.path??"",wrote:!0}}default:throw new c("INTERNAL",`@lunora/agent: sandbox fs op "${o.op}" is not supported`)}},_=()=>({invoke:w.input({args:t.optional(t.array(t.string())),body:t.optional(t.string()),bucket:t.optional(t.string()),command:t.optional(t.string()),content:t.optional(t.string()),fullPage:t.optional(t.boolean()),kind:t.union(t.literal("browser"),t.literal("container"),t.literal("fs")),method:t.optional(t.string()),name:t.optional(t.string()),op:t.string(),path:t.optional(t.string()),root:t.optional(t.string()),selector:t.optional(t.string()),type:t.optional(t.string()),url:t.optional(t.string())}).action(async({args:n,ctx:o})=>{const s=o,a=n;if(a.kind==="browser"){if(!s.browser)throw new c("INTERNAL","@lunora/agent: sandbox browser op needs `ctx.browser` — install @lunora/browser and run codegen");return y(s.browser,a)}if(a.kind==="fs"){const d=s.env?.[a.bucket??""];if(!d||typeof d.get!="function")throw new c("INTERNAL",`@lunora/agent: sandbox fs op found no R2 bucket "${a.bucket??""}" on env — declare the r2_bucket binding in wrangler.jsonc and run codegen`);return N(d,a.root??"",a)}const r=a.name??"",i=s.containers?.[r];if(!i)throw new c("INTERNAL",`@lunora/agent: sandbox container op found no ctx.containers["${r}"] — declare the container in lunora/containers.ts and run codegen`);return $(i,a)})});export{T as resolveFsKey,N as runFsOp,_ as sandboxComponent};
@@ -25,8 +25,8 @@ type BrowserToolInput = {
25
25
  /**
26
26
  * The model-provided input to a {@link containerTool} call — a discriminated
27
27
  * union on `op`. `fetch` sends an HTTP request to the container; `exec` asks it
28
- * to run a command (routed as a POST to `/exec`, since the container surface
29
- * exposes no first-class exec RPC — the container app must serve that route).
28
+ * to run a command (delegated to `ctx.containers.<name>.exec`, the first-class
29
+ * exec contract in `@lunora/container`; the container app serves its route).
30
30
  * @experimental
31
31
  */
32
32
  type ContainerToolInput = {
@@ -112,12 +112,13 @@ interface ContainerToolOptions {
112
112
  description?: string;
113
113
  /**
114
114
  * Gate a call behind a human approval. Defaults to gating any command
115
- * execution: an `exec`, AND a `fetch` whose path resolves to the privileged
116
- * `/exec` route (both reach the same command-execution path in the container,
117
- * so gating on the `op` name alone would let a `fetch` to `/exec` run a
118
- * command unattended). A plain `fetch` to any other route runs unattended.
119
- * Pass a boolean or your own predicate to change that. Evaluated from
120
- * replay-stable input, so keep it deterministic.
115
+ * execution an `exec` plus any `fetch` using a non-idempotent method
116
+ * (POST/PUT/PATCH/DELETE), so a prompt-injected model cannot reach some
117
+ * other mutating route unattended. A read-only `fetch` runs unattended. The
118
+ * exec route itself needs no special case: `@lunora/container` reserves
119
+ * `/__lunora/*` and refuses a `fetch` into it. Pass a boolean or your own
120
+ * predicate to change that. Evaluated from replay-stable input, so keep it
121
+ * deterministic.
121
122
  */
122
123
  needsApproval?: ((input: ContainerToolInput) => boolean) | boolean;
123
124
  }
@@ -156,12 +157,12 @@ declare const browserTool: (options?: BrowserToolOptions) => AgentToolDefinition
156
157
  *
157
158
  * By default a read-only (GET/HEAD/OPTIONS, or method-omitted) `fetch` runs
158
159
  * unattended while everything else is gated behind a human approval — an
159
- * `exec`; a `fetch` whose path resolves to the privileged `/exec` route (both
160
+ * `exec`; a `fetch` whose path resolves to the privileged exec route (both
160
161
  * reach the same command-execution path in the container, so gating on the
161
- * `op` name alone would let a `fetch` to `/exec` run a command unattended);
162
+ * `op` name alone would let a `fetch` to it run a command unattended);
162
163
  * and a `fetch` using a non-idempotent method (POST/PUT/PATCH/DELETE) to ANY
163
164
  * route, since a prompt-injected model could otherwise mutate container state
164
- * through some other privileged route just by avoiding the literal `/exec`
165
+ * through some other privileged route just by avoiding the literal exec
165
166
  * path. A `fetch` can still reach any *other read* route unattended, so scope
166
167
  * the container's GET routes accordingly. Pass `opts.needsApproval` to widen
167
168
  * or disable the gate.
package/dist/sandbox.d.ts CHANGED
@@ -25,8 +25,8 @@ type BrowserToolInput = {
25
25
  /**
26
26
  * The model-provided input to a {@link containerTool} call — a discriminated
27
27
  * union on `op`. `fetch` sends an HTTP request to the container; `exec` asks it
28
- * to run a command (routed as a POST to `/exec`, since the container surface
29
- * exposes no first-class exec RPC — the container app must serve that route).
28
+ * to run a command (delegated to `ctx.containers.<name>.exec`, the first-class
29
+ * exec contract in `@lunora/container`; the container app serves its route).
30
30
  * @experimental
31
31
  */
32
32
  type ContainerToolInput = {
@@ -112,12 +112,13 @@ interface ContainerToolOptions {
112
112
  description?: string;
113
113
  /**
114
114
  * Gate a call behind a human approval. Defaults to gating any command
115
- * execution: an `exec`, AND a `fetch` whose path resolves to the privileged
116
- * `/exec` route (both reach the same command-execution path in the container,
117
- * so gating on the `op` name alone would let a `fetch` to `/exec` run a
118
- * command unattended). A plain `fetch` to any other route runs unattended.
119
- * Pass a boolean or your own predicate to change that. Evaluated from
120
- * replay-stable input, so keep it deterministic.
115
+ * execution an `exec` plus any `fetch` using a non-idempotent method
116
+ * (POST/PUT/PATCH/DELETE), so a prompt-injected model cannot reach some
117
+ * other mutating route unattended. A read-only `fetch` runs unattended. The
118
+ * exec route itself needs no special case: `@lunora/container` reserves
119
+ * `/__lunora/*` and refuses a `fetch` into it. Pass a boolean or your own
120
+ * predicate to change that. Evaluated from replay-stable input, so keep it
121
+ * deterministic.
121
122
  */
122
123
  needsApproval?: ((input: ContainerToolInput) => boolean) | boolean;
123
124
  }
@@ -156,12 +157,12 @@ declare const browserTool: (options?: BrowserToolOptions) => AgentToolDefinition
156
157
  *
157
158
  * By default a read-only (GET/HEAD/OPTIONS, or method-omitted) `fetch` runs
158
159
  * unattended while everything else is gated behind a human approval — an
159
- * `exec`; a `fetch` whose path resolves to the privileged `/exec` route (both
160
+ * `exec`; a `fetch` whose path resolves to the privileged exec route (both
160
161
  * reach the same command-execution path in the container, so gating on the
161
- * `op` name alone would let a `fetch` to `/exec` run a command unattended);
162
+ * `op` name alone would let a `fetch` to it run a command unattended);
162
163
  * and a `fetch` using a non-idempotent method (POST/PUT/PATCH/DELETE) to ANY
163
164
  * route, since a prompt-injected model could otherwise mutate container state
164
- * through some other privileged route just by avoiding the literal `/exec`
165
+ * through some other privileged route just by avoiding the literal exec
165
166
  * path. A `fetch` can still reach any *other read* route unattended, so scope
166
167
  * the container's GET routes accordingly. Pass `opts.needsApproval` to widen
167
168
  * or disable the gate.
package/dist/sandbox.mjs CHANGED
@@ -1 +1 @@
1
- import{LunoraError as a}from"@lunora/errors";import{jsonSchema as s}from"ai";import{toFunctionReference as c,SANDBOX_INVOKE_PATH as p}from"./packem_shared/AGENT_MODULE-CLaSQy3P.mjs";const i=c(p),d='Drive a headless browser: screenshot a page, render it to PDF, read its HTML content, or scrape it. Set `op` to "screenshot" | "pdf" | "content" | "scrape" and pass the target `url`.',u='Talk to a sandboxed container: `fetch` an HTTP path on it, or `exec` a command inside it. Set `op` to "fetch" (with `path`) or "exec" (with `command`).',h='Read and write files in a persistent sandbox filesystem. Set `op` to "ls" (list a directory), "read", "write" (with `content`), "rm", or "stat", and pass `path`.',l=e=>e.op==="write"||e.op==="rm",T=s({properties:{content:{description:"write: the file contents.",type:"string"},op:{description:"The filesystem operation to run.",enum:["ls","read","write","rm","stat"],type:"string"},path:{description:"The file or directory path, relative to the sandbox root.",type:"string"}},required:["op"],type:"object"}),m=s({properties:{fullPage:{description:"screenshot: capture the full scrollable page rather than just the viewport.",type:"boolean"},op:{description:"The browser operation to run.",enum:["screenshot","scrape","pdf","content"],type:"string"},selector:{description:"scrape: optional CSS selector hint to narrow the extraction.",type:"string"},type:{description:"screenshot: image encoding (default png).",enum:["png","jpeg"],type:"string"},url:{description:"The URL to operate on.",type:"string"}},required:["op","url"],type:"object"}),f="/exec",g=/[?#]/u,E=e=>{const[t=""]=(e??"").split(g),r=[];for(const o of t.split("/"))if(!(o===""||o===".")){if(o===".."){r.pop();continue}r.push(o)}return`/${r.join("/")}`},y=new Set(["DELETE","PATCH","POST","PUT"]),A=e=>e.op==="exec"||E(e.path)===f?!0:y.has((e.method??"GET").toUpperCase()),S=s({properties:{args:{description:"exec: command arguments.",items:{type:"string"},type:"array"},body:{description:"fetch: request body.",type:"string"},command:{description:"exec: the command to run.",type:"string"},method:{description:"fetch: HTTP method (default GET).",type:"string"},op:{description:"The container operation to run.",enum:["fetch","exec"],type:"string"},path:{description:"fetch: request path on the container (e.g. /health).",type:"string"}},required:["op"],type:"object"}),N=(e={})=>({description:e.description??d,execute:(t,r)=>r.run(i,{...t,kind:"browser"}),inputSchema:m,isLunoraAgentTool:!0,...e.needsApproval===void 0?{}:{needsApproval:e.needsApproval}}),R=(e,t={})=>{if(typeof e!="string"||e.length===0)throw new a("INTERNAL","@lunora/agent: containerTool requires a container `name` (the ctx.containers.<name> key from lunora/containers.ts)");const r=t.needsApproval??A;return{description:t.description??u,execute:(o,n)=>n.run(i,{...o,kind:"container",name:e}),inputSchema:S,isLunoraAgentTool:!0,needsApproval:r}},C=(e,t={})=>{if(typeof e!="string"||e.length===0)throw new a("INTERNAL","@lunora/agent: fsTool requires an R2 `bucket` binding name (the r2_bucket declared in wrangler.jsonc)");return{description:t.description??h,execute:(r,o)=>{const n=typeof t.root=="function"?t.root(o):t.root??"";return o.run(i,{...r,bucket:e,kind:"fs",root:n})},inputSchema:T,isLunoraAgentTool:!0,needsApproval:t.needsApproval??l}};export{N as browserTool,R as containerTool,C as fsTool};
1
+ import{LunoraError as a}from"@lunora/errors";import{jsonSchema as i}from"ai";import{toFunctionReference as p,SANDBOX_INVOKE_PATH as c}from"./packem_shared/AGENT_MODULE-CLaSQy3P.mjs";const s=p(c),d='Drive a headless browser: screenshot a page, render it to PDF, read its HTML content, or scrape it. Set `op` to "screenshot" | "pdf" | "content" | "scrape" and pass the target `url`.',h='Talk to a sandboxed container: `fetch` an HTTP path on it, or `exec` a command inside it. Set `op` to "fetch" (with `path`) or "exec" (with `command`).',l='Read and write files in a persistent sandbox filesystem. Set `op` to "ls" (list a directory), "read", "write" (with `content`), "rm", or "stat", and pass `path`.',u=e=>e.op==="write"||e.op==="rm",T=i({properties:{content:{description:"write: the file contents.",type:"string"},op:{description:"The filesystem operation to run.",enum:["ls","read","write","rm","stat"],type:"string"},path:{description:"The file or directory path, relative to the sandbox root.",type:"string"}},required:["op"],type:"object"}),m=i({properties:{fullPage:{description:"screenshot: capture the full scrollable page rather than just the viewport.",type:"boolean"},op:{description:"The browser operation to run.",enum:["screenshot","scrape","pdf","content"],type:"string"},selector:{description:"scrape: optional CSS selector hint to narrow the extraction.",type:"string"},type:{description:"screenshot: image encoding (default png).",enum:["png","jpeg"],type:"string"},url:{description:"The URL to operate on.",type:"string"}},required:["op","url"],type:"object"}),g=new Set(["DELETE","PATCH","POST","PUT"]),f=e=>e.op==="exec"||g.has((e.method??"GET").toUpperCase()),y=i({properties:{args:{description:"exec: command arguments.",items:{type:"string"},type:"array"},body:{description:"fetch: request body.",type:"string"},command:{description:"exec: the command to run.",type:"string"},method:{description:"fetch: HTTP method (default GET).",type:"string"},op:{description:"The container operation to run.",enum:["fetch","exec"],type:"string"},path:{description:"fetch: request path on the container (e.g. /health).",type:"string"}},required:["op"],type:"object"}),w=(e={})=>({description:e.description??d,execute:(t,r)=>r.run(s,{...t,kind:"browser"}),inputSchema:m,isLunoraAgentTool:!0,...e.needsApproval===void 0?{}:{needsApproval:e.needsApproval}}),O=(e,t={})=>{if(typeof e!="string"||e.length===0)throw new a("INTERNAL","@lunora/agent: containerTool requires a container `name` (the ctx.containers.<name> key from lunora/containers.ts)");const r=t.needsApproval??f;return{description:t.description??h,execute:(o,n)=>n.run(s,{...o,kind:"container",name:e}),inputSchema:y,isLunoraAgentTool:!0,needsApproval:r}},_=(e,t={})=>{if(typeof e!="string"||e.length===0)throw new a("INTERNAL","@lunora/agent: fsTool requires an R2 `bucket` binding name (the r2_bucket declared in wrangler.jsonc)");return{description:t.description??l,execute:(r,o)=>{const n=typeof t.root=="function"?t.root(o):t.root??"";return o.run(s,{...r,bucket:e,kind:"fs",root:n})},inputSchema:T,isLunoraAgentTool:!0,needsApproval:t.needsApproval??u}};export{w as browserTool,O as containerTool,_ as fsTool};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/agent",
3
- "version": "1.0.0-alpha.57",
3
+ "version": "1.0.0-alpha.58",
4
4
  "description": "Durable AI agents for Lunora: defineAgent compiles a replay-safe tool-loop onto Cloudflare Workflows, with DO SQLite threads and live message subscriptions",
5
5
  "keywords": [
6
6
  "agent",
@@ -84,10 +84,10 @@
84
84
  "access": "public"
85
85
  },
86
86
  "dependencies": {
87
- "@lunora/ai": "1.0.0-alpha.59",
87
+ "@lunora/ai": "1.0.0-alpha.60",
88
88
  "@lunora/errors": "1.0.0-alpha.22",
89
89
  "@lunora/mail": "1.0.0-alpha.50",
90
- "@lunora/server": "1.0.0-alpha.76",
90
+ "@lunora/server": "1.0.0-alpha.77",
91
91
  "@lunora/values": "1.0.0-alpha.27",
92
92
  "@lunora/workflow": "1.0.0-alpha.30",
93
93
  "@modelcontextprotocol/sdk": "^1.30.0",
@@ -1 +0,0 @@
1
- import{LunoraError as c}from"@lunora/errors";import{initLunora as h}from"@lunora/server";import{v as t}from"@lunora/values";import{t as g}from"./base64-5eyBfWO3.mjs";const{internalAction:w}=h.dataModel().create(),m=()=>globalThis.document?.documentElement?.outerHTML??"",d=1e6,b=20,y=new TextEncoder,x=async(a,n)=>{const o=n.url??"";switch(n.op){case"content":return a.content(o);case"pdf":return{data:g(await a.pdf(o)),encoding:"base64",mediaType:"application/pdf"};case"scrape":return a.scrape(o,m);case"screenshot":{const s=await a.screenshot(o,{...n.fullPage===void 0?{}:{fullPage:n.fullPage},...n.type===void 0?{}:{type:n.type}});return{data:g(s),encoding:"base64",mediaType:n.type==="jpeg"?"image/jpeg":"image/png"}}default:throw new c("INTERNAL",`@lunora/agent: sandbox browser op "${n.op}" is not supported`)}},E=async(a,n)=>{const o=a.any();if(n.op==="exec")return(await o.fetch("/exec",{body:JSON.stringify({args:n.args??[],command:n.command??""}),headers:{"content-type":"application/json"},method:"POST"})).text();if(n.op==="fetch")return(await o.fetch(n.path??"/",{...n.body===void 0?{}:{body:n.body},method:n.method??"GET"})).text();throw new c("INTERNAL",`@lunora/agent: sandbox container op "${n.op}" is not supported`)},u=a=>{let n=0,o=a.length;for(;n<o&&a[n]==="/";)n+=1;for(;o>n&&a[o-1]==="/";)o-=1;return a.slice(n,o)},T=(a,n)=>{const o=u(a),s=[];for(const e of n.split("/"))if(!(e===""||e===".")){if(e===".."){if(s.length===0)throw new c("BAD_REQUEST","@lunora/agent: fs path escapes the sandbox root");s.pop();continue}s.push(e)}const r=s.join("/");return o&&r?`${o}/${r}`:o||r},N=async(a,n,o)=>{const s=n.length>0?`${n}/`:"",r=o.length>0?o.length+1:0,e=[];let i;for(let p=0;p<b;p+=1){const l=await a.list({prefix:s,...i===void 0?{}:{cursor:i}});for(const f of l.objects)e.push(f.key.slice(r));if(!l.truncated||l.cursor===void 0)return{entries:e,truncated:!1};i=l.cursor}return{entries:e,truncated:!0}},S=async(a,n,o)=>{const s=u(n),r=T(n,o.path??"");switch(o.op){case"ls":{const{entries:e,truncated:i}=await N(a,r,s);return i?{entries:e,truncated:!0}:{entries:e}}case"read":{const e=await a.head(r);if(e&&e.size>d)throw new c("BAD_REQUEST",`@lunora/agent: fs read: "${o.path??""}" is ${String(e.size)} bytes (max ${String(d)})`);const i=await a.get(r);if(!i)throw new c("NOT_FOUND",`@lunora/agent: fs read: no file at "${o.path??""}"`);return i.text()}case"rm":return await a.delete(r),{path:o.path??"",removed:!0};case"stat":{const e=await a.head(r);return e?{exists:!0,size:e.size}:{exists:!1}}case"write":{const e=o.content??"",i=y.encode(e).length;if(i>d)throw new c("BAD_REQUEST",`@lunora/agent: fs write: ${String(i)} bytes exceeds the max (${String(d)})`);return await a.put(r,e),{bytes:i,path:o.path??"",wrote:!0}}default:throw new c("INTERNAL",`@lunora/agent: sandbox fs op "${o.op}" is not supported`)}},R=()=>({invoke:w.input({args:t.optional(t.array(t.string())),body:t.optional(t.string()),bucket:t.optional(t.string()),command:t.optional(t.string()),content:t.optional(t.string()),fullPage:t.optional(t.boolean()),kind:t.union(t.literal("browser"),t.literal("container"),t.literal("fs")),method:t.optional(t.string()),name:t.optional(t.string()),op:t.string(),path:t.optional(t.string()),root:t.optional(t.string()),selector:t.optional(t.string()),type:t.optional(t.string()),url:t.optional(t.string())}).action(async({args:n,ctx:o})=>{const s=o,r=n;if(r.kind==="browser"){if(!s.browser)throw new c("INTERNAL","@lunora/agent: sandbox browser op needs `ctx.browser` — install @lunora/browser and run codegen");return x(s.browser,r)}if(r.kind==="fs"){const p=s.env?.[r.bucket??""];if(!p||typeof p.get!="function")throw new c("INTERNAL",`@lunora/agent: sandbox fs op found no R2 bucket "${r.bucket??""}" on env — declare the r2_bucket binding in wrangler.jsonc and run codegen`);return S(p,r.root??"",r)}const e=r.name??"",i=s.containers?.[e];if(!i)throw new c("INTERNAL",`@lunora/agent: sandbox container op found no ctx.containers["${e}"] — declare the container in lunora/containers.ts and run codegen`);return E(i,r)})});export{T as resolveFsKey,S as runFsOp,R as sandboxComponent};