@lunora/agent 1.0.0-alpha.56 → 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.
package/dist/component.mjs
CHANGED
|
@@ -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-
|
|
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};
|
package/dist/sandbox.d.mts
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 (
|
|
29
|
-
*
|
|
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
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* Pass a boolean or your own
|
|
120
|
-
* replay-stable input, so keep it
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
29
|
-
*
|
|
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
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
* Pass a boolean or your own
|
|
120
|
-
* replay-stable input, so keep it
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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,12 +84,12 @@
|
|
|
84
84
|
"access": "public"
|
|
85
85
|
},
|
|
86
86
|
"dependencies": {
|
|
87
|
-
"@lunora/ai": "1.0.0-alpha.
|
|
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.
|
|
90
|
+
"@lunora/server": "1.0.0-alpha.77",
|
|
91
91
|
"@lunora/values": "1.0.0-alpha.27",
|
|
92
|
-
"@lunora/workflow": "1.0.0-alpha.
|
|
92
|
+
"@lunora/workflow": "1.0.0-alpha.30",
|
|
93
93
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
94
94
|
"ai": "7.0.59",
|
|
95
95
|
"yaml": "^2.9.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};
|