@agentdeploymentco/bishop 0.5.2 → 0.5.3
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/cli.js +19 -19
- package/dist/config.js +4 -4
- package/dist/exec.js +1 -1
- package/dist/review.js +1 -0
- package/dist/sandbox.js +9 -9
- package/package.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{stat as
|
|
3
|
-
`),process.exit(1)}async function
|
|
2
|
+
import{stat as me}from"node:fs/promises";import{join as le,resolve as he}from"node:path";import{Command as ue}from"commander";import{agentSnapshots as fe,fixedAgentDir as we}from"./agent-dir.js";import{looksRemote as j,parseRepoSpec as Y}from"./agent-source.js";import{everyStore as W,storePath as J}from"./agent-store.js";import{attachmentStore as ge}from"./attachments.js";import{HARNESSES as ye,loadConfig as $,nonEmptyFlag as Q,parseHarnessName as ke,positiveIntFlag as Se,resolveAgent as be,sandboxConfig as ve,writeConfig as Te}from"./config.js";import{dbPath as V,openDb as X}from"./db.js";import{describeMissing as Ie,loadEnv as b,readCredentials as Ee}from"./env.js";import{BishopError as v}from"./errors.js";import{fileStore as xe}from"./files.js";import{DEFAULT_MAX_AGE_DAYS as Ae,makeCollector as _e,sweep as Re}from"./gc.js";import{ghAuthLogin as Ne,ghCliIn as Oe,gitAuthenticatesToGithub as Ge,withTemporaryGhConfig as $e}from"./github/gh.js";import{AGENT_SCOPES as Le,checkGithubSetup as Ce}from"./github/setup.js";import{createGmail as He}from"./gmail/create.js";import{checkGmailSetup as Pe}from"./gmail/setup.js";import{createHarness as Me}from"./harness/create.js";import{sharedFileFetcher as De}from"./interface/attachments.js";import{principalCheck as Z}from"./interface/authorize.js";import{deliveryRouter as Be}from"./interface/delivery.js";import{conversationMatch as Ue,destinationResolver as Fe}from"./interface/destination.js";import{messageReader as Ke}from"./interface/messages.js";import{log as d,requestedLevel as ze}from"./log.js";import{DEFAULT_PROFILE as qe,setupNono as je}from"./nono-setup.js";import{principalStore as Ye}from"./principals.js";import{startReviewer as We}from"./review.js";import{checkSandbox as Je,clearedStaging as Qe,sandboxPolicy as Ve,writeLauncher as Xe}from"./sandbox.js";import{addSchedule as Ze,listSchedules as et,removeSchedule as tt,setScheduleEnabled as ee}from"./schedule/cli.js";import{authorizedToRun as ot,makeScheduler as nt}from"./schedule/runner.js";import{scheduleStore as st}from"./schedule/store.js";import{threadQueue as it}from"./sessions/queue.js";import{interfaceState as rt,retentionStore as te,sessionStore as at,threadContexts as ct,threadHandover as pt,threadMeta as dt}from"./sessions/store.js";import{ensureGitignore as mt,openBrowser as lt,resolveAppDetails as ht,SetupError as oe,setupSlack as ut,setupSlackManually as ft,withPrompt as ne,writeEnv as se}from"./setup.js";import{createSlackInterface as wt}from"./slack/interface.js";import{buildManifest as gt}from"./slack/manifest.js";import{createTeams as yt}from"./teams/create.js";import{administratorInstructions as kt,repointTeamsEndpoint as St,setupTeams as bt,setupTeamsManually as vt}from"./teams/setup.js";import{threadModel as Tt}from"./thread-model.js";import{fileTools as It}from"./tools/files.js";import{messageTools as Et}from"./tools/messages.js";import{scheduleTools as xt}from"./tools/schedules.js";import{startToolServer as At}from"./tools/server.js";import{transcriptStore as _t}from"./transcript.js";import{makeTurnRunner as Rt}from"./turn.js";import{version as Nt}from"./version.js";import{hasGitIdentity as Ot,isGitRepo as Gt,sharedDirNotice as $t,WORKTREE_DIR as Lt}from"./worktrees.js";function f(e){process.stderr.write(`${e}
|
|
3
|
+
`),process.exit(1)}async function Ct(e){const t=he(e??".");let n;try{n=(await me(t)).isDirectory()}catch{throw new v(`--agent directory doesn't exist: ${t}`)}if(!n)throw new v(`--agent is not a directory: ${t}`);return t}async function Ht(e){const t=await Gt(e),n={enabled:!0,isRepo:t};return t?(d.info({agentDir:e,worktrees:le(e,Lt)},"worktree mode is on"),await Ot(e)||d.warn({agentDir:e},"git has no user.name or user.email here, so the agent cannot commit what it does in a worktree"),n):(d.warn({agentDir:e},"worktree mode is on but the agent directory is not a git repository; threads share it"),n)}function Pt(e){if(e.kind==="options-replaced"){d.warn({harness:e.harness},`agent.harnesses.${e.harness} replaces agent.options, which is ignored for this run`);return}d.warn({harness:e.harness,configured:e.configured},`--harness overrides the configured harness, but agent.options is still written for that one; move it to agent.harnesses.${e.configured} and give ${e.harness} its own block`)}async function Mt(e=[],t={}){e.length>0&&f(`bishop: unknown command "${e[0]}"
|
|
4
4
|
|
|
5
|
-
Run \`bishop --help\` to see what's available.`);const n=process.cwd(),c=t.agent&&j(t.agent)?Y(t.agent):void 0;t.autoUpdate&&!c&&
|
|
5
|
+
Run \`bishop --help\` to see what's available.`);const n=process.cwd(),c=t.agent&&j(t.agent)?Y(t.agent):void 0;t.autoUpdate&&!c&&f("--auto-update needs --agent to be a git URL. Bishop never updates a directory it was pointed at; that state is yours to manage."),b(n);const s=await $(n),i=Ee();i.ok||f(Ie(i));const r=be(s,t);for(const o of r.warnings)Pt(o);const l=ve(s);if(l){const o=await Je(l);d.info({nono:o,...l.profile?{profile:l.profile}:{}},"sandbox is on"),l.profile||d.warn("sandbox is enabled but agent.sandbox.profile is missing; run `bishop nono setup`")}const u=s.review?await We(s.review,{home:n}):void 0;u&&d.warn("review is configured, but this version only checks the reviewer at startup; no message is reviewed yet (#254)");const m=l?(o,p)=>Ve({home:n,cwd:o,...p?{dirKey:p}:{},...l.profile?{profile:l.profile}:{},...u?.unwritable.length?{unwritable:u.unwritable}:{}}):void 0,a=X(V(n)),h=at(a,r.harness);let A;const re=async o=>await A?.check(o,await m?.(o))??{ok:!0},_=c?fe({spec:c,store:J(n,c.slug),dirs:h,autoUpdate:!!t.autoUpdate,recheck:re}):void 0,y=_?await _.start():await Ct(t.agent),ae=_?_.forTurn:we(y,h);d.info({bishopDir:n,agentDir:y,...y===n?{}:{separate:!0},...c?{source:c.cloneUrl,autoUpdate:!!t.autoUpdate}:{},app:s.slack?.app?.name},"bishop starting");const R=s.agent?.worktrees?await Ht(y):void 0,T=Me(r.harness,{...r.options?{options:r.options}:{},...r.model?{model:r.model}:{},...r.effort?{effort:r.effort}:{},...R?{worktrees:R}:{},...l?{launcher:await Xe(n)}:{},credentials:i.credentials}),N=await T.check(y,await m?.(y));N.ok||f(T.describeStartupFailure(y,N.error));const L=N.model??r.model;d.info({harness:T.name,credentials:N.credentialSource??"unreported",model:L,...r.effort?{effort:r.effort}:{},...r.options?{agentOptions:Object.keys(r.options)}:{}},"harness ready"),A=T,d.info({sessions:h.count()},"opened session store");const P=_e({retention:te(a),stores:()=>W(n),home:n,...s.gc?.maxAgeDays===void 0?{}:{maxAgeDays:s.gc.maxAgeDays}});P.start();let w;const ce=o=>w?Z(w,S)(o):ot(o),pe=async(o,p)=>{if(!w)throw new v("no interface is running, so there is nowhere to send");return Fe(w,{optedInOnly:!0})(o,p)},de=(o,p)=>w?Ue(w)(o,p):!1,M=xe({home:n,...s.files?.maxMb===void 0?{}:{maxMb:s.files.maxMb}}),D=ge(a),S=Ye(a),B=st(a),U=await At({tools:[...xt({schedules:B,authorized:ce,resolve:pe,sameConversation:de,principals:S}),...It({fetch:De({interfaces:()=>w??[],store:D,files:M})}),...i.credentials.slack?Et({read:Ke({interfaces:()=>w??[]})}):[]]}),O=dt(a),C=Tt(O),F={...L?{model:L}:{},...r.effort?{effort:r.effort}:{}},K=Rt({harness:T,cwd:ae,sessions:h,model:C,tools:U,contexts:ct(a,O),transcript:_t(a),principals:S,...R&&!R.isRepo?{sharedDirNotice:$t(y)}:{},...m?{sandbox:{policy:(o,p)=>m(o,p.dirKey),staging:await Qe(n)}}:{}}),z=it(),I={threads:h,events:h,files:M,attachments:D,principals:S,run:o=>z.run(o.thread.threadKey,()=>K.run(o)),stop:o=>K.stop(o)},g=[];if(i.credentials.slack&&g.push(await wt({...I,credentials:i.credentials.slack,meta:O,model:C,configuredModel:F,...s.slack?.verbosity?{verbosity:s.slack.verbosity}:{},...s.slack?.allow?{allow:s.slack.allow}:{},...s.slack?.maxBotTurns===void 0?{}:{maxBotTurns:s.slack.maxBotTurns}})),i.credentials.gmail)try{g.push(await He({...I,credentials:i.credentials.gmail,...s.gmail?{config:s.gmail}:{},state:rt(a)}))}catch(o){o instanceof v&&f(o.message);const p=i.credentials.gmail,G=p.kind==="oauth"?"the authorized Gmail account":`${p.user}`;f(`Couldn't reach the mailbox for ${G}:
|
|
6
6
|
${o instanceof Error?o.message:String(o)}
|
|
7
7
|
|
|
8
|
-
Run \`bishop gmail setup\` to check the credentials.`)}if(
|
|
8
|
+
Run \`bishop gmail setup\` to check the credentials.`)}if(i.credentials.teams)try{g.push(await yt({...I,credentials:i.credentials.teams,meta:O,model:C,configuredModel:F,...s.teams?{config:s.teams}:{}}))}catch(o){o instanceof v&&f(o.message),f(`Couldn't start the Teams app for client ${i.credentials.teams.clientId}:
|
|
9
9
|
${o instanceof Error?o.message:String(o)}
|
|
10
10
|
|
|
11
|
-
Check the credentials in .env and the \`teams\` block in .bishop/config.json.`)}
|
|
11
|
+
Check the credentials in .env and the \`teams\` block in .bishop/config.json.`)}w=g;for(const o of g)await o.start();d.info({interfaces:g.map(o=>o.name)},"bishop is listening");const q=nt({schedules:B,run:I.run,threads:h,cancel:I.stop,deliver:Be(g),authorized:Z(g,S),principals:S,attach:pt(h)});q.start();for(const o of["SIGINT","SIGTERM"])process.once(o,()=>{d.info({signal:o,inFlight:z.size},"shutting down"),q.stop().catch(p=>d.error({err:p},"the scheduler failed to stop cleanly")).then(()=>Promise.allSettled(g.map(p=>p.stop()))).then(async p=>{const G=p.filter(E=>E.status==="rejected");for(const E of G)d.error({err:E.reason},"an interface failed to stop cleanly");await U.stop().catch(E=>{d.error({err:E},"the tool endpoint failed to stop cleanly")}),await P.stop(),a.close(),process.exit(G.length>0?1:0)})})}function Dt(e){for(const t of e.snapshots)process.stdout.write(`removed snapshot ${t.commit} (unused for ${t.ageDays} days)
|
|
12
12
|
`);for(const t of e.worktrees){const n=t.branch?`, branch ${t.branch}`:"";process.stdout.write(`released ${t.path}${n}
|
|
13
13
|
`)}process.stdout.write(`snapshots: removed ${e.snapshots.length}, kept ${e.keptSnapshots}
|
|
14
14
|
worktrees: released ${e.worktrees.length}
|
|
15
15
|
files: removed ${e.files} download directories
|
|
16
16
|
rows: forgot ${e.threads} threads, ${e.events} events
|
|
17
|
-
`)}async function
|
|
17
|
+
`)}async function Bt(e){const t=process.cwd();b(t),e.agent&&!j(e.agent)&&f("gc --agent takes the git URL Bishop cloned, not a path. It narrows the pass to the files Bishop owns for one agent, and it owns none for a directory agent. Run `bishop gc` with no --agent to collect what its threads left behind.");const n=e.agent?[J(t,Y(e.agent).slug)]:await W(t),c=await $(t),s=e.maxAgeDays??c.gc?.maxAgeDays;ze()||(d.level="error");const i=X(V(t));try{Dt(await Re({retention:te(i),stores:n,home:t,...e.agent?{only:n}:{},...s===void 0?{}:{maxAgeDays:s}}))}finally{i.close()}}function Ut(){return!(process.env.SSH_CONNECTION||process.env.SSH_TTY||process.env.SSH_CLIENT||process.platform==="linux"&&!process.env.DISPLAY&&!process.env.WAYLAND_DISPLAY)}async function Ft(e={}){const t=process.cwd();b(t);const n=process.env.BISHOP_GMAIL_CLIENT_ID?.trim(),c=process.env.BISHOP_GMAIL_CLIENT_SECRET?.trim(),s=process.env.BISHOP_GMAIL_REFRESH_TOKEN?.trim(),i=process.env.BISHOP_GMAIL_USER?.trim(),r=process.env.BISHOP_GMAIL_SERVICE_ACCOUNT?.trim();n&&!c&&f("BISHOP_GMAIL_CLIENT_ID is set but BISHOP_GMAIL_CLIENT_SECRET is not.");const l=e.browser===!1||!Ut()?"paste":"browser",u=await ne(!0,async m=>Pe({...n&&c?{client:{clientId:n,clientSecret:c}}:{},...s?{refreshToken:s}:{},...i?{user:i}:{},...r?{serviceAccount:r}:{},mode:l,openBrowser:lt,readPaste:async a=>(process.stdout.write(`Open this in a browser and approve it:
|
|
18
18
|
|
|
19
|
-
${
|
|
19
|
+
${a}
|
|
20
20
|
|
|
21
21
|
It will finish on a http://127.0.0.1 address that fails to load. That is expected.
|
|
22
22
|
Copy the whole address out of the browser and paste it here.
|
|
23
23
|
|
|
24
|
-
`),
|
|
25
|
-
`),
|
|
26
|
-
Saved BISHOP_GMAIL_REFRESH_TOKEN to ${
|
|
27
|
-
`)}
|
|
28
|
-
`),e.print)
|
|
29
|
-
`);else if(
|
|
30
|
-
Saved GH_TOKEN to ${
|
|
31
|
-
`)}const
|
|
32
|
-
`)}
|
|
33
|
-
`),n.config&&process.stdout.write(`Saved ${await
|
|
34
|
-
`),n.ok||process.exit(1)}async function
|
|
35
|
-
`)}const
|
|
24
|
+
`),m("Redirect URL",""))}));if(process.stdout.write(`${u.report}
|
|
25
|
+
`),u.refreshToken){const m=await se(t,{BISHOP_GMAIL_REFRESH_TOKEN:u.refreshToken});process.stdout.write(`
|
|
26
|
+
Saved BISHOP_GMAIL_REFRESH_TOKEN to ${m}
|
|
27
|
+
`)}u.ok||process.exit(1)}async function Kt(e){const t=process.cwd(),n=process.env.GH_TOKEN?.trim();process.env.GH_TOKEN!==void 0&&!n&&delete process.env.GH_TOKEN,b(t);const c=process.env.GH_TOKEN?.trim(),s=!!c&&!n,i=e.prompt&&process.stdin.isTTY===!0,r=e.print?process.stderr:process.stdout,l=a=>r.write(a),u=a=>Ce({...c?{token:c,tokenIsSaved:s}:{},...i?{login:()=>Ne(Le.map(h=>h.scope),a),confirm:async h=>/^y/i.test(await ne(!0,A=>A(h,"y"),r))}:{},gitConfigured:Ge,gh:Oe(a),...e.temporary?{temporary:!0}:{}}),m=e.temporary&&!c?await $e(u):await u();if(l(`${m.report}
|
|
28
|
+
`),e.print)m.token&&process.stdout.write(`${JSON.stringify({GH_TOKEN:m.token})}
|
|
29
|
+
`);else if(m.token){if(m.source==="gh"){const h=await se(t,{GH_TOKEN:m.token});l(`
|
|
30
|
+
Saved GH_TOKEN to ${h}
|
|
31
|
+
`)}const a=await mt(t,[".env"]);a.length>0&&l(`Added ${a.join(", ")} to .gitignore
|
|
32
|
+
`)}m.ok||process.exit(1)}async function zt(e){const t=process.cwd(),n=await je({config:await $(t),name:e.name});process.stdout.write(`${n.report}
|
|
33
|
+
`),n.config&&process.stdout.write(`Saved ${await Te(n.config,t)}
|
|
34
|
+
`),n.ok||process.exit(1)}async function qt(e){b(process.cwd());const t={...e.name===void 0?{}:{name:e.name},...e.displayName===void 0?{}:{displayName:e.displayName},...e.description===void 0?{}:{description:e.description},prompt:e.prompt};e.manual?await ft(t):await ut(t)}async function jt(e){if(e.appId!==void 0&&!e.manual)throw new oe("--app-id adopts an app somebody else created, so it needs --manual too.");if(e.endpoint!==void 0&&e.manual)throw new oe("--endpoint sets the URL on an app Bishop is creating, so it cannot be used\nwith --manual. Point an adopted app with `bishop teams endpoint <url>`,\nor ask whoever created it to.");b(process.cwd());const t={...e.name===void 0?{}:{name:e.name},...e.displayName===void 0?{}:{displayName:e.displayName},...e.description===void 0?{}:{description:e.description},...e.endpoint===void 0?{}:{endpoint:e.endpoint},...e.appId===void 0?{}:{appId:e.appId},prompt:e.prompt};e.manual?await vt(t):await bt(t)}async function Yt(e){const t=process.cwd(),n=await $(t),c=await ht({name:e.name??n.slack?.app?.name,displayName:e.displayName??n.slack?.app?.displayName,description:e.description??n.slack?.app?.description,prompt:!1},t,async(s,i)=>i);process.stdout.write(`${JSON.stringify(gt(c),null,2)}
|
|
35
|
+
`)}const k=new ue().name("bishop").description("An agent gateway. Runs an agent and connects it to Slack, email, and Teams.").version(Nt,"-v, --version").enablePositionalOptions().option("--agent <path-or-url>","the agent's directory, or a git URL to clone; defaults to the current directory").option(`--harness <${ye.join("|")}>`,"which SDK runs the agent",ke).option("--model <model>","the model to run, in the harness's own naming",Q("--model")).option("--effort <effort>","how hard the model should think, in the harness's own naming",Q("--effort")).option("--auto-update","for a git-URL --agent, check for a new commit at the start of every new thread").allowExcessArguments(!0).action((e,t)=>Mt(t.args,e));k.command("gc").description("Collect the snapshots, worktrees, and rows old conversations left behind").option("--max-age-days <days>",`how long a quiet thread's remains are kept; defaults to gc.maxAgeDays, then ${Ae}`,Se("--max-age-days")).option("--agent <url>","only this agent's snapshots; defaults to every agent Bishop has cloned here").action(Bt);const x=k.command("schedules").description("Prompts the agent runs on a clock, and where their answers go");x.command("list").description("Everything scheduled here, in your own time zone").action(()=>et(process.cwd())),x.command("add").description("Add a schedule").requiredOption("--name <name>","what a person sees in a list, and the subject of an email").requiredOption("--cron <expression>","five-field cron, or @daily and friends").requiredOption("--prompt <text>","what the agent is asked, including when to stay quiet").requiredOption("--to <destination>","#channel or @person for Slack, an address for Gmail").option("--tz <zone>","IANA zone the expression is read in; defaults to this machine").option("--interface <name>","which interface delivers, when --to is ambiguous").action(e=>Ze(process.cwd(),e)),x.command("rm <id>").description("Remove a schedule").action(e=>tt(process.cwd(),e)),x.command("disable <id>").description("Stop a schedule firing, keeping it").action(e=>ee(process.cwd(),e,!1)),x.command("enable <id>").description("Start a disabled schedule firing again, from now").action(e=>ee(process.cwd(),e,!0));const Wt=k.command("gmail").description("Gmail interface commands");Wt.command("setup").description("Authorize a Gmail mailbox, or check the one already configured").option("--no-browser","print a URL to open elsewhere instead of opening one here").action(Ft);const Jt=k.command("github").description("GitHub credentials for the agent");Jt.command("setup").description("Give the agent a GitHub token, or check the one it's using").option("--no-prompt","don't offer to run `gh auth login`").option("--print","write the credential to stdout as JSON instead of to .env").option("--temporary","log in without touching this machine's own gh login").action(Kt);const Qt=k.command("nono").description("The nono sandbox the agent runs under");Qt.command("setup").description("Write a nono profile that lets a sandboxed agent push").option("--name <name>","the profile name",qe).action(zt);const ie=k.command("slack").description("Slack interface commands");ie.command("setup").description("Create and install a Slack app, and save its credentials here").option("--name <name>","app name; defaults to the current directory name").option("--display-name <name>","bot display name in Slack; defaults to --name").option("--description <text>","short description shown in Slack").option("--no-prompt","don't prompt for anything left off").option("--manual","adopt an app you created in Slack yourself, using its tokens").action(qt),ie.command("manifest").description("Print the app manifest, for creating an app in Slack by hand").option("--name <name>","app name; defaults to config or the directory name").option("--display-name <name>","bot display name; defaults to --name").option("--description <text>","short description shown in Slack").action(Yt);const H=k.command("teams").description("Microsoft Teams interface commands");H.command("setup").description("Create a Teams app and save its credentials here, or check the one configured").option("--name <name>","app name; defaults to the current directory name").option("--display-name <name>","app name shown in Teams; defaults to --name").option("--description <text>","short description shown in Teams").option("--endpoint <url>","the URL Teams POSTs to; can be set later").option("--app-id <id>","with --manual, the Teams app ID you were given").option("--no-prompt","don't prompt for anything left off").option("--manual","adopt an app somebody else created, using its credentials").action(jt),H.command("endpoint <url>").description("Point the Teams app at a different URL, changing nothing else").option("--app-id <id>","the Teams app to point; defaults to the one in config").action((e,t)=>St(e,t.appId===void 0?{}:{appId:t.appId})),H.command("instructions").description("Print what to send whoever creates the Teams app, when that is an administrator").option("--endpoint <url>","the URL Teams should POST to, if you already know it").action(e=>{process.stdout.write(kt(e.endpoint))});try{await k.parseAsync()}catch(e){throw e instanceof v&&f(e.message),e}
|
package/dist/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{mkdir as
|
|
2
|
-
`)}async function
|
|
3
|
-
${
|
|
4
|
-
`,"utf8"),
|
|
1
|
+
import{mkdir as b,readFile as w,writeFile as x}from"node:fs/promises";import{dirname as O,join as y}from"node:path";import{z as o}from"zod";import{BishopError as p}from"./errors.js";const v=".bishop",j="config.json",E=o.strictObject({id:o.string().optional(),name:o.string().min(1),displayName:o.string().optional(),description:o.string().optional()}),k=["low","medium","high"],J="high",f=o.string().transform(n=>n.trim().toLowerCase()).pipe(o.enum(k)),$=o.strictObject({workspace:o.string().optional(),app:E.optional(),verbosity:f.optional(),allow:o.array(o.string().min(1)).optional(),maxBotTurns:o.number().int().nonnegative().optional()}),R=o.strictObject({allow:o.array(o.string().min(1)).optional(),pollSeconds:o.number().int().positive().optional(),labelPrefix:o.string().trim().min(1).optional(),markRead:o.boolean().optional(),internalDomains:o.array(o.string().trim().toLowerCase().regex(/^[^\s@]+\.[^\s@]+$/,"must be a domain like example.com")).optional()}),N=o.strictObject({cert:o.string().trim().min(1),key:o.string().trim().min(1)}),I=o.strictObject({id:o.string().optional(),name:o.string().min(1),displayName:o.string().optional(),description:o.string().optional()}),S=o.strictObject({app:I.optional(),allow:o.array(o.string().min(1)).optional(),verbosity:f.optional(),port:o.number().int().min(1).max(65535).optional(),host:o.string().trim().min(1).optional(),tls:N.optional()}),a=["claude","codex"],C=o.strictObject({model:o.string().trim().min(1).optional(),effort:o.string().trim().min(1).optional(),options:o.record(o.string(),o.unknown()).optional()}),A=o.strictObject({harness:o.enum(a).optional(),model:o.string().trim().min(1).optional(),effort:o.string().trim().min(1).optional(),harnesses:o.partialRecord(o.enum(a),C,{error:()=>`not a harness Bishop runs; supported: ${a.join(", ")}`}).optional(),worktrees:o.boolean().optional(),sandbox:o.union([o.boolean(),o.strictObject({profile:o.string().trim().min(1).optional()})]).optional(),options:o.record(o.string(),o.unknown()).optional()}),F=o.strictObject({maxAgeDays:o.number().int().positive().optional()}),T=o.strictObject({maxMb:o.number().positive().optional()}),_=["all","external"],L=["reject","approve"],g=o.strictObject({scope:o.enum(_).optional(),onError:o.enum(L).optional()});function B(n){let t;try{t=new URL(n)}catch{return!1}return t.protocol==="https:"?!0:t.protocol!=="http:"?!1:t.hostname==="localhost"||t.hostname==="[::1]"||/^127(\.\d{1,3}){3}$/.test(t.hostname)}const D=o.strictObject({command:o.array(o.string().min(1)).min(1).optional(),url:o.string().trim().refine(B,"must be an https URL, or http on loopback").optional(),inbound:g.optional(),outbound:g.optional(),timeoutSeconds:o.number().positive().max(300).optional(),env:o.array(o.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/,"must be an environment variable name")).optional()}).superRefine((n,t)=>{n.command&&n.url?t.addIssue({code:"custom",message:"names both command and url; pick one"}):!n.command&&!n.url&&t.addIssue({code:"custom",message:"needs a command or a url"}),n.url&&n.env&&t.addIssue({code:"custom",path:["env"],message:"is only for a command reviewer"}),!n.inbound&&!n.outbound&&t.addIssue({code:"custom",message:"reviews nothing without inbound or outbound"})}),V=o.strictObject({slack:$.optional(),gmail:R.optional(),teams:S.optional(),agent:A.optional(),files:T.optional(),gc:F.optional(),review:D.optional()}).superRefine((n,t)=>{n.review&&!n.agent?.sandbox&&t.addIssue({code:"custom",path:["review"],message:"needs agent.sandbox on"})});class l extends p{path;constructor(t,e){super(t,"ConfigError"),this.path=e}}function W(n,t={}){const e=n.agent,s=e?.harness??"claude",i=t.harness??s,r=e?.harnesses?.[i],c=[];e?.options&&(r&&i===s?c.push({kind:"options-replaced",harness:i}):!r&&i!==s&&c.push({kind:"options-foreign",harness:i,configured:s}));const m=t.model??r?.model??e?.model,u=t.effort??r?.effort??e?.effort,d=r?r.options:e?.options;return{harness:i,...m?{model:m}:{},...u?{effort:u}:{},...d?{options:d}:{},warnings:c}}function Y(n){const t=n.agent?.sandbox;if(t)return t===!0||!t.profile?{}:{profile:t.profile}}function Z(n){return t=>{const e=t.trim();if(!e)throw new p(`${n} needs a value`);return e}}function M(n){return t=>{const e=Number(t.trim());if(!Number.isInteger(e)||e<=0)throw new p(`${n} needs a positive whole number, got "${t}"`);return e}}function q(n){const t=n.trim();if(a.includes(t))return t;throw new p(`unknown harness "${n}"; supported: ${a.join(", ")}`)}function h(n=process.cwd()){return y(n,v,j)}function G(n){return n.issues.map(t=>`${t.path.length>0?t.path.join("."):"(root)"}: ${t.message}`).join(`
|
|
2
|
+
`)}async function K(n=process.cwd()){const t=h(n);let e;try{e=await w(t,"utf8")}catch(r){if(r.code==="ENOENT")return{};throw new l(`Couldn't read ${t}: ${r.message}`,t)}let s;try{s=JSON.parse(e)}catch(r){throw new l(`${t} isn't valid JSON: ${r.message}`,t)}const i=V.safeParse(s);if(!i.success)throw new l(`${t} has invalid values:
|
|
3
|
+
${G(i.error)}`,t);return i.data}async function Q(n,t=process.cwd()){const e=h(t);return await b(O(e),{recursive:!0}),await x(e,`${JSON.stringify(n,null,2)}
|
|
4
|
+
`,"utf8"),e}function X(n){return n.displayName?.trim()||n.name}export{v as CONFIG_DIR,j as CONFIG_FILE,l as ConfigError,V as ConfigSchema,J as DEFAULT_VERBOSITY,a as HARNESSES,L as REVIEW_ON_ERROR,_ as REVIEW_SCOPES,k as VERBOSITY,h as configPath,X as displayNameOf,K as loadConfig,Z as nonEmptyFlag,q as parseHarnessName,M as positiveIntFlag,W as resolveAgent,Y as sandboxConfig,Q as writeConfig};
|
package/dist/exec.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{spawn as
|
|
1
|
+
import{spawn as g}from"node:child_process";const m=1e3;function w(a,c,r={}){return new Promise(i=>{const t=g(a,c,{stdio:"pipe",...r.cwd?{cwd:r.cwd}:{},...r.env?{env:r.env}:{},...r.argv0?{argv0:r.argv0}:{},...r.signal?{signal:r.signal,killSignal:"SIGKILL"}:{}});t.stdin.on("error",()=>{}),t.stdin.end(r.input);let o="",u="",n="";t.stdout.setEncoding("utf8"),t.stderr.setEncoding("utf8"),t.stdout.on("data",e=>{o+=e,n+=e}),t.stderr.on("data",e=>{u+=e,n+=e});let l;const s=(e,d)=>{clearTimeout(l),i({code:e,stdout:o,stderr:u,output:n,...d?{signal:d}:{}})};t.on("error",()=>{t.stdout.destroy(),t.stderr.destroy(),s(null)}),t.on("exit",(e,d)=>{l=setTimeout(()=>{t.stdout.destroy(),t.stderr.destroy(),s(e,d)},m)}),t.on("close",(e,d)=>s(e,d))})}export{m as DRAIN_MS,w as exec};
|
package/dist/review.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{constants as $}from"node:fs";import{access as x,realpath as P,stat as E}from"node:fs/promises";import{delimiter as S,join as O,resolve as b}from"node:path";import{z as d}from"zod";import{BishopError as f}from"./errors.js";import{exec as R}from"./exec.js";import{log as v}from"./log.js";import{retry as _}from"./retry.js";import{unwritablePaths as j}from"./sandbox.js";const y="BISHOP_REVIEWER_TOKEN",A=1,I=5,N=d.discriminatedUnion("verdict",[d.object({verdict:d.literal("approve")}),d.object({verdict:d.literal("reject"),reason:d.string().trim().min(1)}),d.object({verdict:d.literal("rewrite"),text:d.string().min(1)})]);class p extends Error{}async function g(e,r){const a=r.env??process.env,i=(e.timeoutSeconds??I)*1e3;let n,o=[];if(e.url){const t=a[y]?.trim();if(!t)throw new f(`review.url needs ${y} in .env, sent as a bearer token`);n=U(e.url,t)}else{const[t,...s]=e.command??[];if(!t)throw new f("review needs a command or a url");const c=await k(t,r.home,a.PATH),l=a.PATH?{PATH:a.PATH}:{};for(const w of e.env??[]){const m=a[w];if(m===void 0)throw new f(`review.env names ${w}, which isn't set`);l[w]=m}const h=await Promise.all(s.map(async w=>{const m=b(r.home,w);return await E(m).catch(()=>{})?m:void 0}));o=[c,...h.filter(w=>w!==void 0)].flatMap(j),n=M(c,t,s,r.home,l)}const u={covers:(t,s)=>{const c=e[t];return c?(c.scope??"all")==="all"||s!=="internal":!1},review:async t=>{const s=e[t.direction],c=JSON.stringify({version:A,...t});try{const l=await n(c,AbortSignal.timeout(i)),h=N.safeParse(l);if(!h.success)throw new Error("answered something that isn't a verdict");return h.data}catch(l){return H(t,s,V(l,i))}}};return v.info({reviewer:e.url?new URL(e.url).origin:e.command?.[0],...e.inbound?{inbound:e.inbound.scope??"all"}:{},...e.outbound?{outbound:e.outbound.scope??"all"}:{},timeoutMs:i},"message reviewer found"),{reviewer:u,unwritable:o}}function H(e,r,a){const i={direction:e.direction,thread:e.thread,error:a};return r?.onError==="approve"?(v.warn(i,"the reviewer failed, and onError approves the message unreviewed"),{verdict:"approve",error:a}):(v.warn(i,"the reviewer failed, so the message is rejected"),{verdict:"reject",reason:"The reviewer didn't answer.",error:a})}function V(e,r){return e instanceof Error&&(e.name==="TimeoutError"||e.name==="AbortError")?`no answer within ${r/1e3}s`:e instanceof Error?e.message:String(e)}async function k(e,r,a){const i=e.includes("/")?[b(r,e)]:(a??"").split(S).filter(Boolean).map(n=>O(n,e));for(const n of i){const o=await B(n);if(o)return o}throw new f(`review.command names ${e}, which isn't an executable program`)}async function B(e){try{const r=await P(e);return(await E(r)).isFile()?(await x(r,$.X_OK),r):void 0}catch{return}}function M(e,r,a,i,n){return async(o,u)=>{const t=await R(e,a,{cwd:i,env:n,input:o,signal:u,argv0:r});u.throwIfAborted();const s=t.stderr.trim().slice(-500),c=s?`: ${s}`:"";if(t.signal)throw new Error(`${e} was killed by ${t.signal}${c}`);if(t.code===null)throw new Error(`${e} couldn't be started`);if(t.code!==0)throw new Error(`${e} exited with code ${t.code}${c}`);return T(t.stdout)}}function U(e,r){const a=async(i,n)=>{let o;try{o=await fetch(e,{method:"POST",headers:{authorization:`Bearer ${r}`,"content-type":"application/json"},body:i,signal:n,redirect:"error"})}catch(t){if(n.aborted)throw n.reason;const s=t.cause,c=s instanceof Error?s.message:t.message;throw new p(`couldn't reach the reviewer: ${c}`)}const u=await o.text();if(o.status>=500)throw new p(`the reviewer answered HTTP ${o.status}`);if(!o.ok)throw new Error(`the reviewer answered HTTP ${o.status}`);return T(u)};return(i,n)=>_(()=>a(i,n),{what:"message review",attempts:2,baseDelayMs:100,retryable:o=>o instanceof p&&!n.aborted})}function T(e){try{return JSON.parse(e)}catch{throw new Error("answered something that isn't JSON")}}export{I as DEFAULT_REVIEW_TIMEOUT_SECONDS,y as REVIEWER_TOKEN_ENV,A as REVIEW_VERSION,g as startReviewer};
|
package/dist/sandbox.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import{randomBytes as
|
|
2
|
-
${n.output.trim()}`);if(t.profile){const r=await
|
|
3
|
-
${r.output.trim()}`)}return o.stdout.trim()}function
|
|
4
|
-
# Written by Bishop at startup. Runs the command in ${
|
|
1
|
+
import{randomBytes as R}from"node:crypto";import{constants as N,createWriteStream as C,existsSync as _,realpathSync as M}from"node:fs";import{chmod as H,mkdir as b,open as T,realpath as I,rm as L,stat as P,writeFile as k}from"node:fs/promises";import{homedir as E,platform as W,tmpdir as U}from"node:os";import{basename as $,dirname as y,isAbsolute as A,join as d,resolve as f,sep as h}from"node:path";import{pipeline as z}from"node:stream/promises";import{CONFIG_DIR as S}from"./config.js";import{BishopError as l}from"./errors.js";import{exec as v}from"./exec.js";import{ensureThreadFilesDir as K}from"./files.js";import{MAX_SHARE_BYTES as X,ShareRefused as m}from"./outgoing.js";import{gitDirs as j}from"./worktrees.js";const p="nono",V=["linux","darwin"];async function dt(t,e=W()){if(!V.includes(e))throw new l(`agent.sandbox is on, but nono can't sandbox on ${e}. It runs on Linux and macOS.`);const o=await v(p,["--version"]);if(o.code!==0)throw new l("agent.sandbox is on, but nono is not installed or not on PATH. See https://nono.sh/docs/cli/getting_started/installation for how to install it.");const n=await v(p,["setup","--check-only"]);if(n.code!==0)throw new l(`agent.sandbox is on, but nono says this system can't enforce its sandbox:
|
|
2
|
+
${n.output.trim()}`);if(t.profile){const r=await v(p,["profile","show",t.profile]);if(r.code!==0)throw new l(`agent.sandbox.profile names "${t.profile}", which nono can't load:
|
|
3
|
+
${r.output.trim()}`)}return o.stdout.trim()}function Y(t){return[f(t,S),f(t,".env")]}function u(t){const e=f(t);try{return M(e)}catch{const o=y(e);return o===e?e:d(u(o),$(e))}}function F(t,e){const o=u(t),n=u(e);return n===o||n.startsWith(o.endsWith(h)?o:o+h)}function ft(t){const e=f(t);return w([u(e),d(u(y(e)),$(e))])}function B(t,e){for(const o of t){const n=u(o),r=e.find(a=>{const i=(c,s)=>c.startsWith(s.endsWith(h)?s:s+h);return a===n||i(a,n)||i(n,a)});if(r)throw new l(`The sandbox would let the agent write ${o}, which contains ${r}. An agent that can change its message reviewer can approve itself: move the reviewer, and anything it reads, outside every directory the agent can write.`)}}function D(t,e){for(const o of t){const n=e.find(r=>F(o,r));if(n)throw new l(`The sandbox would have to grant ${o}, which contains ${n}. Bishop's own state can't sit inside the agent's directory with agent.sandbox on: run Bishop from a directory of its own and point --agent at the agent.`)}}async function lt(t){const e=t.userHome??E(),o=[f(t.cwd),...await j(t.cwd),t.tmp??U()],n=[d(e,".config","git")].filter(_);t.dirKey&&n.push(await K(t.home,t.dirKey));const r=[d(e,".gitconfig")].filter(_),a=Y(t.home);D([...o,...n,...r],a);const i=t.unwritable??[];return B(o,i),{allow:o,read:n,readFiles:r,...t.profile?{profile:t.profile}:{},protect:a,...i.length>0?{unwritable:i}:{}}}function ut(t,e,o,n){const r=w([...t.allow,...e.allow??[]]),a=w([...t.read,...e.read??[]]),i=w(e.allowFiles??[]),c=w(t.readFiles);return D([...r,...a,...i,...c],t.protect),B([...r,...i],t.unwritable??[]),{command:p,args:["-s","wrap",...t.profile?["--profile",t.profile]:[],...r.flatMap(s=>["--allow",s]),...a.flatMap(s=>["--read",s]),...i.flatMap(s=>["--allow-file",s]),...c.flatMap(s=>["--read-file",s]),"--",o,...n]}}function w(t){return[...new Set(t)]}function q(t,e){return[...t.allow,...t.read].some(o=>F(o,e))}async function wt(t){const e=f(t,S,"outgoing");return await L(e,{recursive:!0,force:!0}),await b(e,{recursive:!0,mode:448}),e}async function ht(t,e,o){if(!A(e))return e;const n=new m(`${e} is outside the directories you can work in, so nothing was sent. Copy it into your working directory first.`),r=await T(e,N.O_RDONLY|N.O_NONBLOCK).catch(()=>{throw new m(`There is no file at ${e}.`)});try{const a=await r.stat();if(!a.isFile())throw new m(`There is no file at ${e}.`);const i=await I(e).catch(()=>{}),c=i?await P(i).catch(()=>{}):void 0;if(!i||!c||c.ino!==a.ino||c.dev!==a.dev||!q(t,i))throw n;if(a.size>X)throw new m(`${e} is too large to send.`);const s=d(o,R(6).toString("hex"));await b(s,{recursive:!0,mode:448});const O=d(s,$(e));return await z(r.createReadStream({autoClose:!1}),C(O,{mode:384})),O}finally{await r.close()}}function mt(t){return A(t)?y(u(t)):void 0}const g="BISHOP_SANDBOX_COMMAND",x="BISHOP_SANDBOX_DIR",G=`#!/bin/sh
|
|
4
|
+
# Written by Bishop at startup. Runs the command in ${g}, one
|
|
5
5
|
# argument per line, from ${x}, followed by this script's arguments.
|
|
6
|
-
command=$${
|
|
6
|
+
command=$${g}
|
|
7
7
|
dir=$${x}
|
|
8
|
-
unset ${
|
|
8
|
+
unset ${g} ${x}
|
|
9
9
|
if [ -z "$command" ] || [ -z "$dir" ]; then
|
|
10
10
|
echo "bishop: the sandbox launcher was started without a command" >&2
|
|
11
11
|
exit 1
|
|
@@ -15,6 +15,6 @@ set -f
|
|
|
15
15
|
IFS='
|
|
16
16
|
'
|
|
17
17
|
exec $command "$@"
|
|
18
|
-
`;async function
|
|
19
|
-
`));if(n!==void 0)throw new l(`The sandbox can't pass ${JSON.stringify(n)} to its launcher, which takes one argument per line.`);return{[
|
|
20
|
-
`),[x]:e}}export{
|
|
18
|
+
`;async function pt(t){const e=f(t,S,"sandbox");await b(e,{recursive:!0,mode:448});const o=d(e,"launch");return await k(o,G,{mode:448}),await H(o,448),o}function gt(t,e){const o=[t.command,...t.args],n=[...o,e].find(r=>r===""||r.includes(`
|
|
19
|
+
`));if(n!==void 0)throw new l(`The sandbox can't pass ${JSON.stringify(n)} to its launcher, which takes one argument per line.`);return{[g]:o.join(`
|
|
20
|
+
`),[x]:e}}export{g as LAUNCH_COMMAND_ENV,x as LAUNCH_DIR_ENV,p as NONO,dt as checkSandbox,wt as clearedStaging,mt as executableDir,gt as launchEnv,ut as nonoCommand,Y as protectedPaths,lt as sandboxPolicy,q as sandboxReadable,ht as stageFile,ft as unwritablePaths,pt as writeLauncher};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@agentdeploymentco/bishop",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "An agent gateway. Connects an agent to Slack, email, and Microsoft Teams.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -45,10 +45,10 @@
|
|
|
45
45
|
"docs:preview": "npm --prefix docs run preview --"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@anthropic-ai/claude-agent-sdk": "0.3.
|
|
48
|
+
"@anthropic-ai/claude-agent-sdk": "0.3.292",
|
|
49
49
|
"@microsoft/teams.apps": "^2.0.16",
|
|
50
50
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
51
|
-
"@openai/codex-sdk": "0.160.
|
|
51
|
+
"@openai/codex-sdk": "0.160.1",
|
|
52
52
|
"@slack/bolt": "^5.0.0",
|
|
53
53
|
"@slack/web-api": "^8",
|
|
54
54
|
"commander": "^13",
|