Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
91 commits
Select commit Hold shift + click to select a range
0b1957b
Make every chart say where its figures came from
guidovizoso Oct 26, 2026
d923fc8
Tell Bots to look figures up before charting them
guidovizoso Oct 26, 2026
2e54810
Let an admin turn on and give an app to a Bot in one press
guidovizoso Oct 26, 2026
4e75075
Say on the app card's marker whether the app was blocked when the Bot…
guidovizoso Oct 26, 2026
8c81ffd
Give an admin the fix itself on an app card, not a request to themselves
guidovizoso Oct 26, 2026
e84b84b
Carry the task on by itself once a Bot's app card says the app is ready
guidovizoso Oct 26, 2026
52763bb
Record who may give an app to a Bot, and whether its owner or an admi…
guidovizoso Oct 26, 2026
3d5a046
Tell a Bot its computer is still starting, rather than failing, when …
guidovizoso Oct 26, 2026
bc4fd98
Answer a screenshot of a computer that is not ready with its state, w…
guidovizoso Oct 26, 2026
745501f
Tell whether a Bot holds a search tool, from its refs or its tool names
guidovizoso Oct 26, 2026
c868c84
Send a search to the search tool, and never to the browser
guidovizoso Oct 26, 2026
494cf85
Say in computer_navigate's description that it is not a web search
guidovizoso Oct 26, 2026
93f4ab7
Format the not-ready screenshot route tests
guidovizoso Oct 26, 2026
d549fd3
Type the still-starting headless test's fetch double
guidovizoso Oct 26, 2026
dbb4b86
Report a live screen being retried as reconnecting, not as a problem
guidovizoso Oct 26, 2026
959a45b
Show a computer starting calmly, and a problem only when there is one
guidovizoso Oct 26, 2026
e95a2f8
Say what a conversation is waiting on, on reads and on every busy event
guidovizoso Oct 26, 2026
1608651
Tell the asking conversation when a hop is queued and when the queue …
guidovizoso Oct 26, 2026
a541d6f
Show a conversation as working while a routine fires into it
guidovizoso Oct 26, 2026
fef0aa0
Show a computer call turned away by a cold start as not done yet, and…
guidovizoso Oct 26, 2026
1d39656
Have a Bot say what it is about to do before its first computer action
guidovizoso Oct 26, 2026
dcfbd0b
Tell a Bot's owner when an app is theirs to give it, and let an admin…
guidovizoso Oct 26, 2026
919c7f0
Let a Bot's owner give it their own apps, and take back only what the…
guidovizoso Oct 26, 2026
b87271e
Keep a Team Bot on each teammate's own account for apps its owner gav…
guidovizoso Oct 26, 2026
d45122b
Restore the canvas and image prototypes the calm-start tests stub
guidovizoso Oct 26, 2026
c2c0543
Keep what a conversation waits on in the roster and in the open chann…
guidovizoso Oct 26, 2026
9a0f6c2
Show who a conversation is waiting on, with a running clock, where th…
guidovizoso Oct 26, 2026
c48fe9f
Let an admin choose on an app's page whether Bot owners can give it
guidovizoso Oct 26, 2026
8c1e8df
Ask a Bot's owner on the app card whether to let it use their app, in…
guidovizoso Oct 26, 2026
597ff8b
Give a new Bot your connected apps, and switch them on its page
guidovizoso Oct 26, 2026
dd55aae
Keep a chart's sources line readable when the model's arguments are odd
guidovizoso Oct 26, 2026
8dec0e8
Test the sources line with an unchecked note and malformed arguments
guidovizoso Oct 26, 2026
88f934c
Count a routine firing as the conversation moving on past an app card
guidovizoso Oct 26, 2026
f3367fd
Write the withdrawal into the trail when an admin's one press is undone
guidovizoso Oct 26, 2026
a55db65
Word the no-search guidance as a condition, not a claim that the Bot …
guidovizoso Oct 26, 2026
a370171
Name computerGuidance, not the removed COMPUTER_GUIDANCE, in a copilo…
guidovizoso Oct 26, 2026
cf88214
Tell an admin's one press to try again, not to approve a request
guidovizoso Oct 26, 2026
a40eda6
Refuse an owner a grant on an action their app does not advertise
guidovizoso Oct 26, 2026
0e83dd3
Record nothing when an owner's grant lands on one that already stands
guidovizoso Oct 26, 2026
d864e92
Say where an owner gives a write action, in the give route's note
guidovizoso Oct 26, 2026
e4eba71
Format the owner grant refusal
guidovizoso Oct 26, 2026
16e75b9
Tell a Bot its computer is still starting when the supervisor's own h…
guidovizoso Oct 26, 2026
5ff46b8
Merge branch 'guido/connect-card-resume' into guido/owner-app-grants
guidovizoso Oct 26, 2026
76117af
Report a docker computer whose browser is still booting as starting, …
guidovizoso Oct 26, 2026
10d4270
Answer a cold sandbox start within 45s, under a load balancer's idle …
guidovizoso Oct 26, 2026
c80d013
Keep the computer panel honest when a not-ready answer goes stale
guidovizoso Oct 26, 2026
87abd4a
Format the stale-state computer panel test
guidovizoso Oct 26, 2026
2982a03
Read a conversation's open hops through an index instead of scanning …
guidovizoso Oct 26, 2026
821597a
Keep what a conversation waits on to the person whose thread it is
guidovizoso Oct 26, 2026
758de81
Ignore a work list read before the one a conversation already holds
guidovizoso Oct 26, 2026
7444d79
Keep the assertion-shape guard green across the work-list tests
guidovizoso Oct 26, 2026
0b1f214
Merge branch 'guido/search-routing' into guido/ux-integration
guidovizoso Oct 26, 2026
ff12e96
Merge branch 'guido/calm-computer-start' into guido/ux-integration
guidovizoso Oct 26, 2026
bd3419f
Merge branch 'guido/handoff-progress-row' into guido/ux-integration
guidovizoso Oct 26, 2026
f7914da
Merge branch 'guido/owner-app-grants' into guido/ux-integration
guidovizoso Oct 26, 2026
4d5537f
Check the first-action line in both computer guidance variants
guidovizoso Oct 26, 2026
38a8cfb
Follow the moved headless-components lines in the assertion-shape all…
guidovizoso Oct 26, 2026
14e483d
Follow the moved computer-supervisor and computer-sandbox lines in th…
guidovizoso Oct 26, 2026
9e7424e
Hold the admin's approve call to an absent body, so the assertion can…
guidovizoso Oct 26, 2026
de41a3f
Merge branch 'guido/connect-card-resume' into guido/owner-app-grants
guidovizoso Oct 26, 2026
93070ca
Hold the owner's give call to an absent body, and follow the moved si…
guidovizoso Oct 26, 2026
efd8bb2
Add a changelog line for chart sources
guidovizoso Oct 26, 2026
6fad6e9
Add a changelog line for search routing
guidovizoso Oct 26, 2026
1c931c8
Add a changelog line for the app card carrying on
guidovizoso Oct 26, 2026
b980192
Add a changelog line for the handoff progress row
guidovizoso Oct 26, 2026
aabae85
Add a changelog line for the calm computer start
guidovizoso Oct 26, 2026
959312c
Merge branch 'guido/connect-card-resume' into guido/owner-app-grants
guidovizoso Oct 26, 2026
70d76ea
Add a changelog line for owner app grants
guidovizoso Oct 26, 2026
3848c8f
Mark a Shared-account audience an owner's grant set, so it can be wit…
guidovizoso Oct 26, 2026
50ab1e3
Let an owner's grant set a Shared app's audience, and read it only wh…
guidovizoso Oct 26, 2026
4b6577d
Set the audience when an owner gives a Shared app, and withdraw it on…
guidovizoso Oct 26, 2026
4333122
Test an owner's Shared-app grant: usable at once, scoped to the Bot, …
guidovizoso Oct 26, 2026
f15712e
Say on a Shared app's page that owners who give it let their Bots use…
guidovizoso Oct 26, 2026
dc9690a
Document owners giving Shared apps, and add the changelog line
guidovizoso Oct 26, 2026
cd006f4
Say where a chart's figures came from in Slack and Teams too
guidovizoso Oct 26, 2026
a7030b0
Give each Bot its own files on a computer many Bots share
guidovizoso Oct 26, 2026
010e41e
Merge remote-tracking branch 'origin/guido/chart-sources' into guido/…
guidovizoso Oct 26, 2026
e385f0a
Merge remote-tracking branch 'origin/guido/search-routing' into guido…
guidovizoso Oct 26, 2026
89bae9e
Merge remote-tracking branch 'origin/guido/calm-computer-start' into …
guidovizoso Oct 26, 2026
ebe8714
Merge remote-tracking branch 'origin/guido/handoff-progress-row' into…
guidovizoso Oct 26, 2026
7750f6f
Merge remote-tracking branch 'origin/guido/connect-card-resume' into …
guidovizoso Oct 26, 2026
a8da34a
Merge remote-tracking branch 'origin/guido/owner-app-grants' into gui…
guidovizoso Oct 26, 2026
b6c8123
Merge remote-tracking branch 'origin/guido/shared-computer-workspaces…
guidovizoso Oct 26, 2026
8fd3ff4
End a hop at once when the other Bot's address is one this deployment…
guidovizoso Oct 26, 2026
81d5d77
Merge remote-tracking branch 'origin/guido/handoff-progress-row' into…
guidovizoso Oct 26, 2026
a0070bb
Recognise a refused address by its message, since the AG-UI client ke…
guidovizoso Oct 26, 2026
57c019d
Merge remote-tracking branch 'origin/guido/handoff-progress-row' into…
guidovizoso Oct 26, 2026
8ad807e
Show the asked Bot's avatar on the waiting row, and drop the clock
guidovizoso Oct 26, 2026
4650be0
Merge remote-tracking branch 'origin/guido/handoff-progress-row' into…
guidovizoso Oct 26, 2026
7365713
Read container health in its own file, so its test does not need dock…
guidovizoso Oct 26, 2026
9e5e364
Merge remote-tracking branch 'origin/guido/calm-computer-start' into …
guidovizoso Oct 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 78 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged.

## Unreleased

**Before upgrading.** Eight things change for an existing deployment:
**Before upgrading.** Nine things change for an existing deployment:
- A hosted computer's browser is now headed by default. It uses a little more memory than headless
did; set `COMPUTER_BROWSER_MODE=headless` (Helm: `computers.browserMode: headless`) to keep the old
behaviour.
Expand Down Expand Up @@ -39,6 +39,83 @@ Newest first. `Unreleased` is what is on `main` and not yet tagged.
research run replaces them.
- Migration `0062_action_policy_labels` adds a `labels` column to `action_policies`, defaulting to
empty. Existing boundary rules keep working and show as "Custom rule" until somebody names them.
- On Helm's `shared` computer and the docker-compose `agent-computer`, each Bot's files now live in
`computers/<Bot id>` on the workspace volume (below). Files saved before the upgrade stay at the
root of the volume, where no Bot sees them; move them into a Bot's folder to give them back.

### Each Bot on a shared computer has its own files

A computer that serves many Bots kept one folder of files for all of them, so a note one Bot saved
was a file every other Bot could list and read. Each Bot now gets its own folder, and its commands
start there. Logins were already kept apart. This is on for Helm's `shared` computer and the
docker-compose `agent-computer`, and `COMPUTER_SEPARATE_WORKSPACES=on` turns it on for the
all-in-one image. A command can still read another Bot's folder, so a deployment that needs people
kept apart should give each Bot a computer of its own (`computers.mode: sandbox`).

### Every chart says where its figures came from

A chart a Bot draws now ends with a line naming what its figures were read from and when they are
from, such as "Sources: World Bank World Development Indicators · As of 2025". Figures a Bot charted
from its own knowledge, without looking them up, are marked "From the Bot's own knowledge. Not
checked against a source." Bots are told to look up figures that change over time before charting
them. In Slack and Teams the same line follows the chart in the reply. Charts already in a
conversation are drawn as before, with no such line.

### A Bot searches with its search tool, never its browser

Asked to search for something, a Bot could drive its browser to Google or another search engine
instead of using the search tool it held. With a search tool it now searches with that, and keeps
the browser for signing in, clicking through a site, filling in a form or opening a page the person
named. Without one it answers from what it knows, says it has not checked, and shows a card for a
search app (Parallel Search, Exa Search or Firecrawl) that the person can ask an administrator for.

### A computer that is starting says so, instead of showing a problem

Opening the Computer panel while a Bot's computer was asleep or starting showed "Can't show the
screen right now", and an action during a cold start was marked "didn't work". The panel now says
the computer is starting, or is off until the Bot next uses it, and shows a problem only when it
can't be reached or has gone two and a half minutes without a picture; a live screen being
reconnected shows a small "Reconnecting…" badge. An action turned away by a cold start reads "not
done yet" and the Bot is told to try again. Looking at the panel no longer wakes a sleeping computer,
a Bot says in one sentence what it is about to do before its first computer action, and a hosted
computer's cold start answers within 45 seconds, under a load balancer's usual 60-second idle
timeout. A Docker computer still booting its browser reads as starting once the supervisor is
upgraded too; an older supervisor works as before.

### A conversation shows who it is waiting on

When a Bot asks another Bot, the end of the conversation now shows that Bot's avatar and "Asking
Risk Analyst…" until the answer comes back; a routine firing into it reads "Market Scout is running a
routine…". A Bot whose address this deployment will not dial is reported at once, not after five
minutes of retries. The sidebar keeps the conversation lit while it waits, and both survive a
reload. In a shared conversation only the person whose Bot asked sees whom it asked; the others see
only that it is busy. Migration `0063_work_items_open_thread` adds one partial index to `work_items`.

### A Bot's owner can give it their own apps

The owner of a Bot can now let it use an app they connect for themselves, without an administrator:
from the app card ("Let Research Bot use your Gmail?"), under Your apps on the Bot's page, or on the
New Bot sheet, which offers their connected apps. Calls under such a grant always use the asking
person's own account, even on a published Team Bot, and an owner can take back only what they gave.
By default this covers apps each person connects for themselves and Routines; an administrator
decides per app on its page, and can switch it off for people with the "Give apps to their own Bots"
capability. Migration `0063_owner_app_grants` adds two nullable columns, `mcp_servers.owner_grants`
and `plugin_grants.granted_as`.

Where an administrator lets owners give a Shared app, an owner's grant works straight away: it lets
the people who can use the Bot (just the owner, or a Team Bot's audience) use the team account, with
no separate approval. Writes still ask first. Taking the app back, or the administrator switching the
app back to admins only, withdraws that. Migration `0064_owner_shared_audience` adds the nullable
`shared_use_approvals.approved_as`.

### An app card carries the task on once the app is ready

When a Bot stopped because it needed an app, and the person then connected it in another tab or an
administrator turned it on for the Bot, the card said "Ask again". It now sends "Gmail is ready now.
Please carry on." for the person, once, and the Bot picks the task back up; a card opened after the
change offers Continue instead. An administrator looking at the card gets the fix itself, "Turn on
and give it to Sales" or "Give it to Sales", which grants the app's read-only actions with the same
checks and trail as approving a request, or a link to the app's page where one press can't do it.

### Screens share one spacing, row and empty-state design

Expand Down
17 changes: 15 additions & 2 deletions agent-bot/src/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
*/
import type { RunAgentInput } from "@ag-ui/core";
import type OpenAI from "openai";
import { COMPUTER_GUIDANCE, NO_ANSWER_CAME } from "../../shared/bot-prompt";
import { computerGuidance, NO_ANSWER_CAME } from "../../shared/bot-prompt";
import { researchRoleOfToolName } from "../../shared/research";
import { userContent } from "../../shared/user-content";

export { NO_ANSWER_CAME };
Expand All @@ -17,7 +18,19 @@ export function toProviderMessages(
input: RunAgentInput,
): OpenAI.Chat.ChatCompletionMessageParam[] {
const messages: OpenAI.Chat.ChatCompletionMessageParam[] = [
{ role: "system", content: COMPUTER_GUIDANCE },
/*
* Worded for the tools this run offers, which is all this Bot can see of its grants: the names
* say whether one of them is a search tool, and that decides whether the browser is told to
* leave searching to it or never to search at all.
*/
{
role: "system",
content: computerGuidance({
search: (input.tools ?? []).some(
(tool) => researchRoleOfToolName(tool.name) === "search",
),
}),
},
// AG-UI application context is separate from history. The A2UI catalog and tool instructions
// arrive here; omitting them leaves the model guessing component names and action schemas.
...(input.context ?? []).map(({ description, value }) => ({
Expand Down
37 changes: 37 additions & 0 deletions agent-bot/tests/history.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,44 @@
import { describe, expect, test } from "bun:test";
import type { RunAgentInput } from "@ag-ui/core";
import { computerGuidance } from "../../shared/bot-prompt";
import { NO_ANSWER_CAME, toProviderMessages } from "../src/history";

/**
* Whether the browser is told to leave searching to a search tool is decided by the tools this run
* offers, since the names are all this Bot sees of its grants. "search for X" made a Bot holding
* a search tool drive its browser to a search engine instead.
*/
describe("the computer guidance this Bot is given", () => {
const offered = (...names: string[]) =>
({
messages: [{ id: "u1", role: "user", content: "Search for X." }],
tools: names.map((name) => ({ name, description: "", parameters: {} })),
}) as unknown as RunAgentInput;

test("leaves searching to a search tool the run offers", () => {
const [system] = toProviderMessages(
offered("show_app_access", "mcp__exa__web_search_exa"),
);
expect(system).toEqual({
role: "system",
content: computerGuidance({ search: true }),
});
});

test("never searches in the browser when no search tool is offered", () => {
for (const run of [
offered("show_app_access", "mcp__google-drive__search_files"),
// A reader alone does not find sources.
offered("mcp__exa__web_fetch_exa"),
{ messages: [] } as unknown as RunAgentInput,
])
expect(toProviderMessages(run)[0]).toEqual({
role: "system",
content: computerGuidance({ search: false }),
});
});
});

/**
* The Bot that ships in the box, and the conversation a declined handover used to end.
*
Expand Down
35 changes: 19 additions & 16 deletions agent-computer/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ import {
TAKE_CONTROL_FIRST,
} from "./control";
import { describeHumanGesture } from "./demonstration";
import { enterText, FieldValueError } from "./enter-text";
import { handleEgressPolicyRequest, startEgressFilter } from "./egress";
import { enterText, FieldValueError } from "./enter-text";
import { identity } from "./identity";
import {
assertPageAccess,
Expand All @@ -51,7 +51,6 @@ import {
sensitiveRefs,
} from "./secret-masking";
import { type BotSession, createSessions } from "./sessions";
import { createShell } from "./shell";
import {
fillSignIn,
fillSignInCode,
Expand All @@ -61,12 +60,12 @@ import {
import { startVirtualDisplay } from "./virtual-display";
import { applyWebBotAuthPush } from "./web-bot-auth";
import {
createWorkspace,
WorkspaceFileError,
WorkspaceFileNotFoundError,
WorkspaceFileTooLargeError,
WorkspacePathError,
} from "./workspace";
import { createWorkspaces } from "./workspaces";

/**
* The Bot's computer: one long-lived browser, reachable over HTTP.
Expand Down Expand Up @@ -197,9 +196,9 @@ function botIdOf(request: Request, fallback?: string | null): string {
* The Bot's durable files.
*
* Rooted at WORKSPACE_DIR, which the image creates and docker-compose mounts as a volume, so what a
* Bot saves outlives the container. Built once at boot: the root is fixed, and resolving it per request
* would only add a syscall to every call. Everything about why confinement is harder than it looks
* lives in workspace.ts.
* Bot saves outlives the container. Each folder is built the first time its Bot uses it and kept, so
* a request adds no syscall after that. Which folder a Bot gets lives in workspaces.ts; everything
* about why confinement is harder than it looks lives in workspace.ts.
*/
/**
* The two directories this process cannot do its job without, checked before it says it is ready.
Expand Down Expand Up @@ -245,8 +244,10 @@ await assertWritable(
process.env.PROFILES_DIR?.trim() || "/profiles",
);

const workspace = createWorkspace(
// One folder per Bot where one process serves many; see workspaces.ts.
const workspaces = createWorkspaces(
process.env.WORKSPACE_DIR?.trim() || "/workspace",
process.env.COMPUTER_SEPARATE_WORKSPACES?.trim().toLowerCase() === "on",
);

/**
Expand Down Expand Up @@ -286,9 +287,6 @@ const profiles = createProfiles(
await sessions.get(botId)?.viewer.releaseAll(COMPUTER_STOPPED);
},
);
// Rooted in the same workspace the file tools use, so a command and a written file see one
// directory rather than two.
const shell = createShell(process.env.WORKSPACE_DIR?.trim() || "/workspace");

/**
* The id normally arrives as a header on every request. This is the fallback for a caller that has no
Expand Down Expand Up @@ -1209,9 +1207,8 @@ serve<StreamData>({
// MAY touch a path: the gateway in front of this process does that.
if (url.pathname === "/files/download" && request.method === "GET") {
try {
const file = await workspace.download(
url.searchParams.get("path") ?? "",
);
const { files } = await workspaces.for(botId);
const file = await files.download(url.searchParams.get("path") ?? "");
return new Response(file.body, {
headers: downloadHeaders(file.name, file.bytes),
});
Expand All @@ -1228,7 +1225,8 @@ serve<StreamData>({
path?: unknown;
} | null;
try {
return json(await workspace.read(String(body?.path ?? "")));
const { files } = await workspaces.for(botId);
return json(await files.read(String(body?.path ?? "")));
} catch (error) {
return json(
{ error: describe(error, "The file could not be read.") },
Expand All @@ -1242,8 +1240,9 @@ serve<StreamData>({
path?: unknown;
} | null;
try {
const { files } = await workspaces.for(botId);
return json(
await workspace.list(
await files.list(
typeof body?.path === "string" ? body.path : undefined,
),
);
Expand Down Expand Up @@ -1283,6 +1282,9 @@ serve<StreamData>({
return json({ error: timeout.error }, 400);
}
try {
// Rooted in the same folder the file tools use, so a command and a written file see one
// directory rather than two.
const { shell } = await workspaces.for(botId);
return json(
await shell.run({
command: body.command,
Expand Down Expand Up @@ -1310,8 +1312,9 @@ serve<StreamData>({
return json({ error: "The contents to write are required." }, 400);
}
try {
const { files } = await workspaces.for(botId);
return json(
await workspace.write(String(body?.path ?? ""), body.contents, {
await files.write(String(body?.path ?? ""), body.contents, {
append: body.append === true,
}),
);
Expand Down
60 changes: 60 additions & 0 deletions agent-computer/src/workspaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/**
* Whose files a request reaches.
*
* WHY. One computer process can serve many Bots: Helm's default `computers.mode: shared` and the
* docker-compose `agent-computer` service with no supervisor both do. Browser profiles were already
* kept apart by the id each request names (`bot-id.ts`), but the workspace was one folder, so a note
* one Bot saved for one person was a file every other Bot could list and read. With
* `COMPUTER_SEPARATE_WORKSPACES=on` each id gets its own folder under the workspace root, and the
* shell starts there with HOME pointing at it.
*
* WHY NOT ALWAYS. A computer the supervisor or the sandbox starts serves one Bot, and the desktop's
* computer serves one person. Their files already sit at the root of the volume, and moving the
* root under them would hide every file they had. So only the deployments that share one process
* turn this on.
*
* WHAT IT IS NOT. A command is still a process in a shared container and can read any path the
* container can. Separate folders keep each Bot's file tools to its own files and keep commands from
* landing in another Bot's folder by default. Isolation between people who run commands needs a
* computer each, which `computers.mode: sandbox` gives.
*/
import { mkdir } from "node:fs/promises";
import { isPlainBotId, UnusableBotIdError } from "./bot-id";
import { createShell, type Shell } from "./shell";
import { createWorkspace } from "./workspace";

export type Workspace = ReturnType<typeof createWorkspace>;

/** The folder an id's files live in. The same allow-list as profiles, so an id is one segment. */
export function workspaceDirectoryFor(
root: string,
botId: string,
separate: boolean,
): string {
if (!separate) return root;
if (!isPlainBotId(botId)) throw new UnusableBotIdError(botId);
return `${root}/computers/${botId}`;
}

export function createWorkspaces(
root: string,
separate: boolean,
sourceEnv: NodeJS.ProcessEnv = process.env,
) {
const made = new Map<string, { files: Workspace; shell: Shell }>();
return {
/** The files and shell for this id, creating its folder the first time it is used. */
async for(botId: string): Promise<{ files: Workspace; shell: Shell }> {
const directory = workspaceDirectoryFor(root, botId, separate);
const known = made.get(directory);
if (known) return known;
await mkdir(directory, { recursive: true });
const created = {
files: createWorkspace(directory),
shell: createShell(directory, sourceEnv),
};
made.set(directory, created);
return created;
},
};
}
Loading
Loading