Rebuild OpenAI Dots
OpenAI's dots are always-on ChatGPT agents that get to know what matters to you and keep working on it. OpenAI launched them at DevDay on 29 September 2026, running on GPT-6 Astra. For now each person gets one dot, which they name and which gets a handle like @pip-dot; more dots and teams of dots are planned. A dot has its own cloud computer, researches in the background with tools that can only read, and checks sensitive actions against your rules before they run. You reach it in ChatGPT, in Slack, in Teams or on a call. This page builds the same shape in Foundry and deploys it on Kuiper.
An independent example built from OpenAI's announcement, its help center and early hands-on reviews (sources at the end). It is not affiliated with or endorsed by OpenAI, and it is not how dots are built. The code is in examples/foundry-standing-agents, and pnpm verify runs all of it offline.
What dots do
- A dot you name. You start with one primary dot, give it a name and make it your own. Its memory is the current chat, your ChatGPT memory and its own notes, shared across channels, and it works on several projects without separate threads.
- A coordinator. It splits work across background agents, hands coding to Codex and long deliverables to ChatGPT Work, checks the results and reports back.
- Proactive research. When you aren't working with it, it looks for ways to help using your connected apps, with tools restricted to read-only: they can't send messages, change app content, or control the browser or computer.
- Triggers. Follow-ups it schedules for itself, fixed schedules with a time zone and notification rules, and event triggers where the connected app supports them.
- Its own computer. A cloud computer and browser of its own, which you can open to inspect its work. Saved passwords are used without exposing them to the model.
- Custom Rules and auto-review. You set, per action, which it acts on without asking, which it acts on only when you ask, which it asks about first, and which it hands off to you. Auto-review checks sensitive actions, such as sending email, before they run, and you can't switch it off. Password changes and money transfers are always handed to you; permanent deletion, software installs and saved-card purchases always need your approval. Safety monitoring can pause or stop a dot, and stopping doesn't undo actions already taken.
- Where you work. ChatGPT on desktop, web and mobile, Slack (DMs and channel mentions) and Teams, and voice calls. Activity View shows its progress, files and pending requests, and lets you redirect or pause it.
- Specialist dots. In an enterprise preview, IT provisions dots with their own identity, credentials and system access for one role, such as invoicing or support.
How a dot works
- Sam's primary dot is an instance of one definition, and he named it Pip. Its ongoing responsibilities (what to keep true, which sources to read, what must not happen yet) are instance data, and its prompt is built from them.
- A schedule wakes it for proactive research, and new mail wakes it too. On these runs it gets only reads: #feedback, and the mailbox through the Gmail app in code. It keeps notes and may suggest one thing. Acting tools, Gmail's send, the builder and its computer are not there to misuse.
- The app, Slack and a call are conversations on the same instance, so each keeps its own history while memory is shared. Slack messages arrive through a transmission that only lets its owner in.
- Every acting tool is wrapped, and so is the Gmail app's send. The auto-review reads your Custom Rule for that action, checks built-in safety, and checks the input against your instructions, such as an email to someone who never wrote in to the mailbox.
- “Act without asking” runs. “Act when asked” runs when you started the run, and asks otherwise. “Ask first” becomes a Foundry approval. “Hand off to you” does not run and tells you what to do yourself. Password changes are always yours, whatever the rules say, and three hand-offs or denials in a row pause the dot.
- Routine progress can go to the app and decisions to Slack, set per dot.
- Pip works on its own computer, can split work across runs of its own, and starts bigger coding jobs on its builder, a durable subagent, the way a dot starts a task in Codex. It checks the result and reports. Research runs can't reach the builder.
Each feature, as a Foundry primitive
| Dots | Foundry | Where |
|---|---|---|
| A dot you name | An instance of one dot definition; its name, responsibilities, rules and routing are its data | lib/dot.ts |
| Ongoing responsibilities | A list on the instance; research reads every one's sources | lib/dot.ts |
| Proactive research, read-only | A research schedule every 2 hours. On read-only runs (research, new mail, work the dot started that isn't a follow-up, and every run of a paused dot), the tools resolver returns only read tools, the Gmail app mounts only its reads, and the builder's tools, the computer and the tools that start runs aren't mounted | agents/dot/ |
| Triggers | glove_foundry_schedule with every, at, or cron and a time zone. On a run that may act, what the dot schedules or spawns is stamped followUp: true and acts under your rules. New mail is an event trigger that here only reads | agents/dot/research.ts |
| Works in your apps | A Gmail app installed with the mailbox the dot works from: Google's MCP server for reads (in code), the Gmail transmission for sending, a connection for new mail | agents/dot/apps/gmail.app.ts |
| Memory: the chat, ChatGPT memory, its own notes | The conversation; the person's account memory (one namespace per workspace) under the dot's own (one per instance), shared by every channel; and keep_note, one file per dot | lib/memory.ts, lib/sources.ts |
| Custom Rules and auto-review | A wrapper around every acting tool and around Gmail's send: a hand-off, requiresPermission, or a plain run. “Act when asked” asks unless the person started the run | lib/auto-review.ts, agents/dot/tools.ts |
| Built-in safety | Two lists checked before the rules: change_password and transfer_money are always yours; delete_permanently, install_software and buy_with_saved_card always ask first | lib/auto-review.ts |
| Safety monitoring | A run hook that pauses the dot after three hand-offs or denials in a row | lib/monitor.ts |
| Its own computer | A working environment saved per dot with foundryDataEnvironmentPersistence | agents/dot/workspace.ts |
| ChatGPT, Slack, a call | Conversations on one instance, keyed app and slack; Slack through a transmission and a playbook; voiceCall for calls. Teams isn't built here | agents/dot/ |
| Coordinator (background agents, Codex) | Runs of the dot started with glove_foundry_spawn, and the dot's builder, a durable subagent it hands coding to | agents/dot/research.ts, agents/builder/ |
| Activity View | The inspector: runs, the builder's tasks, pending approvals, and steering to redirect a run; paused on the dot to pause it | built in, lib/dot.ts |
| Specialist dots (enterprise preview) | The same definition, an instance in the organisation's workspace with its own name, Slack account and connected mailbox | specialistFor in lib/dot.ts |
- Each person's dot: its name, responsibilities and rules
- Your account store and OAuth callback
- The acting tools and what each hands off
- The Slack events forwarder
1. A dot is an instance you name
There is one dot definition. Each dot is an instance, and its data is what makes it yours: the name you gave it, the responsibilities it keeps (what should stay true, which sources to read, what deserves your attention, what it must not do yet), your Custom Rules, and where to send updates. The prompt is built from it.
export const SAM = {
id: "sam",
dot: {
name: "Pip",
owner: "Sam",
responsibilities: [
{
job: "Track product feedback from #feedback and the support inbox, and group the reports.",
sources: ["#feedback", "support inbox"],
attention: "A problem several people report, or anything that blocks a customer.",
mustNot: "Reply to anyone without Sam's approval.",
},
{
job: "Make sure every customer who wrote to support hears back within a day.",
sources: ["support inbox"],
attention: "Anyone who has waited more than a day.",
mustNot: "Promise a ship date.",
},
],
rules: { send_email: "ask", open_draft_pr: "allow", merge_pr: "block" },
routing: { progress: "app", decisions: "slack" },
slackUserId: "U0SAM",
},
};The example seeds Sam's dot with defineAgentInstance (its builder comes on the first task), with the support mailbox he connected as a sample account; everyone else's is created when they sign up (scripts/onboard.ts, scripts/connect.ts):
export const samsDot = defineAgentInstance(dot, primaryDot(SAM.id, SAM.dot, [installationFor(samsGmail)]));
export const data = new FileFoundryDataAdapter({
file: dataPath("foundry.json"),
agents: [samsDot],
conversations: [createConversation(samsDot, { id: `${samsDot.id}-app`, title: "App", context: { conversationKey: "app" } })],
});export default defineAgent({
description: "An always-on agent that works on its person's responsibilities and acts within their rules",
components: composeAgent(gmailApp, slackApp, person),
model: (_agent, ctx) => (isCall(ctx) ? callModel() : dotModel(ctx.agentInstance.workspaceId)),
systemPrompt: async (_agent, ctx) => dotPrompt(await currentDot(ctx), ctx, isCall(ctx)),
store: ({ conversationId }) => FileStore.open(conversationId),
memory: [person],
tools: (_agent, ctx) => dotTools(ctx),
schedules: [research],
playbooks: (_agent, ctx) => dotPlaybooks(ctx), // the owner's Slack messages; new mail
// The mailbox's reads as functions: research reads it in code.
repl: () => defineRepl({
language: "javascript",
session: JsSession.create(),
mount: { discovery: "auto" },
programmaticTools: { select: ({ apps }) => apps.flatMap((app) => app.tools.filter((tool) => tool.readOnly)), keepDirect: false },
}),
// Gmail's send under auto-review, and follow-ups; gone on read-only runs.
configure: (agent, ctx) => configureDot(agent, ctx),
// Its own computer, but not on read-only runs.
workingEnvironment: async (_agent, ctx) => (readOnly(ctx.request, await currentDot(ctx)) ? undefined : dotComputer),
// Coding jobs go to the dot's own builder, a durable subagent Foundry
// creates on the first task. Read-only runs lose its tools in configureDot.
durableSubagents: {
builder: { agent: builder, description: "Coding and heavy build jobs on its own computer." },
},
// The Slack bot user is the app's; every other account is one a person connected.
accountSessions: {
identifier: "dot-accounts",
withSession: (request, use) => request.accountId === slackBot.id
? use({ token: process.env.SLACK_BOT_TOKEN || OFFLINE_SLACK_TOKEN })
: connectedAccounts.withSession(request, use),
},
// The monitor pauses the dot after hand-offs and denials in a row.
run: (agent, ctx) => (isCall(ctx) ? call(agent, ctx) : monitored(agent, ctx, reviewedTools(), () => ctx.defaultRun())),
});2. Research that only reads, follow-ups that act
The research schedule wakes the dot every two hours to read the sources of every responsibility. What makes the run safe is not an instruction in the prompt but what is mounted: tools, the Gmail app's tools, the builder's tools and the computer all resolve per run, and a read-only run gets only reads. Nothing that acts is there. New mail is a read-only run, because mail comes from anyone, and so is every run of a paused dot.
/** Runs the person started: from the app, or by messaging the dot in Slack (only its owner gets in). New mail does not count. */
export function startedByPerson(request: FoundryRequest): boolean {
if (request.source?.kind === "direct") return true;
return request.source?.kind === "transmission" && request.source.provider === slackMessages.id;
}
/** A run the dot started on a run that could act. Transmissions never count, whatever their payload says. */
export function followUp(request: FoundryRequest): boolean {
const kind = request.source?.kind;
return (kind === "activation" || kind === "spawn" || kind === "background")
&& (request.payload as { followUp?: unknown } | null | undefined)?.followUp === true;
}
/** Runs that only read: research, wake-ups, new mail, and every run of a paused dot. */
export function readOnly(request: FoundryRequest, dot?: { readonly paused?: unknown }): boolean {
if (dot?.paused === true) return true;
return !(startedByPerson(request) || followUp(request) || request.source?.kind === "delegation");
}export async function dotTools(ctx: Context): Promise<GloveFoldArgs<any>[]> {
const read = researchTools(ctx.agentId); // read_channel, read_notes, keep_note, suggest
const dot = await currentDot(ctx);
if (readOnly(ctx.request, dot)) return read;
const review = reviewOf(dot, [], ctx.request);
return [...read, ...actionsFor(ctx.agentId).map((action) => reviewed(action, review))];
}
// agents/dot/apps/gmail.app.ts: on read-only runs, only the mailbox's reads mount.
tools: (tool, ctx) => (readOnly(ctx.request) && !tool.readOnly && !reads.includes(tool.name) ? "hide" : policy(tool, ctx)),The dot also follows up. You ask it to check back tomorrow, or set a schedule for every Monday at 9:00 in your time zone, and it calls glove_foundry_schedule, which takes every, at, or cron with a time zone. To split work it starts runs of itself with glove_foundry_spawn. On a run that may act, the dot's configure wraps these tools so what they start carries followUp: true; Foundry passes the payload on, and readOnly lets those runs act under your Custom Rules. On a read-only run the tools are gone and schedules can only be listed, so research can't start a run that acts or change one. And no run can change what a schedule carries, so nothing can stamp the research schedule to make research act:
export function followUps(agent: IGloveRunnable, agentId: string, acts: boolean): void {
if (!acts) {
for (const name of STARTS_RUNS) aroundTool(agent, name, () => undefined);
// …and glove_foundry_schedules becomes a tool that only lists.
return;
}
for (const name of STARTS_RUNS) {
aroundTool(agent, name, ({ run, ...tool }) => ({
...tool,
do: (input: Record<string, unknown>, _display, _glove, signal) => run({
...input,
...(name === "glove_foundry_spawn" && !input.agentId && !input.definitionId ? { agentId } : {}),
payload: { followUp: true, ...(input.payload !== undefined ? { payload: input.payload } : {}) },
}, signal),
}));
}
// An update never changes what a schedule carries, so no run can stamp the
// research schedule and make research act.
aroundTool(agent, "glove_foundry_schedules", ({ run, ...tool }) => ({
...tool,
do: (input: Record<string, unknown>, _display, _glove, signal) => {
const { payload: _payload, ...kept } = input;
return run(kept, signal);
},
}));
}The check is on every source, not only on "activation": a spawned run's source is "spawn", and a transmission can't claim to be a follow-up. New mail is the event trigger here, and it only reads.
3. Custom Rules and auto-review
Every acting tool passes through one wrapper before it is mounted, so the dot never sees an unreviewed version. Built-in safety comes first, then your Custom Rule for that action, then who started the run, then a check of the input against your instructions:
/** Always handed to the person. */
export const ALWAYS_YOURS: ReadonlySet<string> = new Set(["change_password", "transfer_money"]);
/** Never below "ask first". */
export const ALWAYS_ASKS: ReadonlySet<string> = new Set(["delete_permanently", "install_software", "buy_with_saved_card"]);
export function levelFor(name: string, rules: Readonly<Record<string, Level>>): Level {
if (ALWAYS_YOURS.has(name)) return "block";
const level = rules[name] ?? "ask";
return ALWAYS_ASKS.has(name) && (level === "allow" || level === "when_asked") ? "ask" : level;
}
export function reviewed<I>(action: Action<I>, review: Review): GloveFoldArgs<I> {
const { recipient, handOff, rule = action.name, ...tool } = action;
const level = levelFor(rule, review.rules);
const described = { ...tool, description: `${tool.description} Rule: ${ruleLabel(rule, review.rules)}.` };
if (level === "block") {
return {
...described,
do: async (input) => notRun(rule, `It stays with ${review.owner}. ${handOff(input)} ${review.tellPerson}`),
};
}
const asks = level === "ask" || (level === "when_asked" && !review.askedByPerson);
// The instruction check: writing to someone the person never heard from always asks.
const unknownRecipient = (input: I) =>
recipient !== undefined && !review.contacts.includes(recipient(input).trim().toLowerCase());
return {
...described,
requiresPermission: asks ? true : unknownRecipient,
};
}| Rule | What happens |
|---|---|
allow (act without asking) | Runs on its own, unless it would write to an address that never wrote in; then it asks. |
when_asked (act when asked) | Runs on its own on a run the person started, from the app or Slack. On a follow-up, research or the builder's reply, it asks. The recipient check still applies. |
ask (ask first) | requiresPermission: true. Foundry records the exact input and holds the run for an approval. |
block (hand off to you) | Never runs. The dot is told what to hand the person, and where to tell them. |
| Built-in safety | ALWAYS_YOURS is handed off and ALWAYS_ASKS never goes below ask first, whatever the rule says. Of these, only change_password is an action here; the sets apply to any action registered under those names. |
An action with no rule asks. Each tool's description includes its rule, so the model knows before it tries.
Email goes out through the Gmail app's transmission, so Foundry delivers it from the mailbox in its own process. The dot puts the same review around that send in configure, under the rule send_email. The people who wrote in are read from the mailbox through the app, so “allow” still asks before writing to anyone else:
export async function reviewSends(agent: IGloveRunnable, ctx: Context, dot: ParsedDot): Promise<void> {
if (readOnly(ctx.request, dot)) return aroundTool(agent, sendTool(), () => undefined); // read-only runs can't send
const review = reviewOf(dot, await knownContacts(agent), ctx.request); // who wrote in, via gmail.search_threads
aroundTool(agent, sendTool(), (send) => reviewed<Send>({
name: send.name,
description: "Send an email from the mailbox you work from: a reply (pass threadId and inReplyTo) or a new message.",
jsonSchema: send.jsonSchema,
rule: "send_email",
recipient: (input) => input.payload.to,
handOff: (input) => `Send "${input.payload.subject}" to ${input.payload.to} yourself.`,
do: (input, _display, _glove, signal) => send.run(input, signal), // Foundry still delivers it
}, review));
}A monitor watches how reviewed actions end. It wraps every run in the definition's run hook, with a subscriber on the dot's tool results: an action that ran resets the count, and a hand-off or a denied approval adds one. After three in a row it sets paused on the dot. Every run of a paused dot only reads, its prompt says so, and the person resumes it with configureAgent. As with dots, pausing doesn't undo actions already taken.
for (const outcome of ended) streak = outcome === "ran" ? 0 : streak + 1;
if (streak >= PAUSE_AFTER) {
const instance = await storedInstance(ctx);
await Effect.runPromise(ctx.data.putAgent({
...instance,
context: { ...instance.context, paused: true },
updatedAt: new Date().toISOString(),
}));
streak = 0;
}4. Its own computer
The dot works on a computer of its own: a working environment with files, scripts and the public web, saved per dot by Foundry's data adapter. Read-only runs don't get it. Like a dot, it coordinates: it splits work across runs of its own with glove_foundry_spawn, which act under the same rules, hands coding jobs to its builder with glove_foundry_delegate, the way a dot hands them to Codex, and checks the builder's result, which comes back to the conversation that asked, before it reports. The builder is a durable subagent: Foundry creates it on the first task.
export const dotComputer = defineWorkingEnvironment({
options: { stdlib: [fetchFiles({ maxResponseBytes: 2_000_000 })] },
persistence: foundryDataEnvironmentPersistence({ scope: "agent" }),
});5. The app, Slack and a call, one memory
Each channel is a conversation on the same instance, created with a conversationKey of app or slack. Slack messages arrive through a transmission; a playbook per dot matches only its owner's Slack user id, and the forwarder fires the route with conversationKey: "slack". Teams isn't built here; it would be a second transmission and forwarder like Slack's. A call is voiceCall over the same assembled dot.
The transcripts stay separate, and memory is shared. As with dots, it has three parts: the current conversation, the person's account memory, and the dot's own. The account memory is one namespace per workspace, which the dot reads but can't change; its own memory is one namespace per instance, layered on top, so what Sam says in the app is known in Slack. Its notes file (keep_note) sits beside them.
export function instanceMemory(description: string) {
return defineMemory({
description,
context: {
adapter: ({ workspaceId, agentId }) =>
Effect.sync(() => layerContext([
{ name: "account", adapter: accountMemory(workspaceId), access: "read" },
{ name: "dot", adapter: pinned(`${workspaceId}/${agentId}`), access: "write" },
])),
},
});
}export function messagesFrom(slackUserId: string) {
return composePlaybook({
name: "slack-messages",
transmission: slackMessages,
match: { event: message, routes: [slackMessagesIn], predicate: { definition: fromOwner, parameters: { slackUserId } } },
directives: [{
action: answer,
instruction: "Answer with post_to_slack in the same channel. For a mention in a channel, answer in its thread. Use what you know from the app too.",
}],
applications: [slackApp],
});
}Routing, this example's choice, decides where updates go. With decisions routed to Slack, a hand-off reaches the person as a Slack DM, while routine progress stays in the app. Slack's Events API needs its url_verification challenge answered and every request's signature checked, which the web app's /webhooks/slackroute does before forwarding (lib/slack-events.ts). It passes only DMs and mentions, so, as with dots, adding the bot to a channel doesn't make it watch the channel. Reading a channel is a read-only tool, read_channel, through Slack read access.
6. Specialist dots
In the product, specialist dots are an enterprise preview: IT provisions them with their own identity, credentials and system access. Here a specialist dot is not a new definition. It is the same dot, created in the organisation's workspace for one well-defined responsibility, with its own name, its own Slack account and its own connected mailbox, so it acts under its own credentials rather than a person's:
export function specialistFor(org: string, specialist: Omit<Dot, "kind"> & { account?: AccountReference }) {
const { account, ...dot } = specialist;
const parsed = Dot.parse({ ...dot, kind: "specialist" });
return {
id: `${org}-${parsed.name.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`,
workspaceId: org,
name: parsed.name,
description: `Specialist dot for ${parsed.owner}: ${parsed.responsibilities.map((item) => item.job).join(" ")}`,
context: parsed,
installations: [install(slackApp, undefined, { account: account ?? slackBot })],
};
}7. Run it
pnpm --filter glove-foundry-standing-agents verify
1. Pip (sam-dot) and the app conversation are seeded data; research runs every 2h there; the mailbox watch is connected
2. research read #feedback and the mailbox in code, kept notes and suggested: "CSV export times out on large reports: …"
23 tools mounted, none that act (no send_email, open_draft_pr, merge_pr, change_password, Gmail send, post_to_slack), no builder, no computer, no schedule or spawn
runs from schedules, spawn and background are read-only unless they are follow-ups; new mail always is
3. "ask": the Gmail send waited for approval; approved: "Sent the reply to dana@acme.test."
4. "block": merge_pr handed to Sam in Slack: "Over to you: Review PR #42 and merge it yourself."
change_password with a rule of "allow" is still Sam's (built-in safety): …
5. "allow": open_draft_pr ran with no approval: "Opened draft PR #101: Stream large CSV exports."
send_email on "allow" still asks before writing to an address that never wrote in
6. Slack DM from Sam joined "transmission:agent:sam-dot:slack"; answered there: "From the app: Billing questions go to Priya."
a stranger's mention started nothing
7. new mail started a read-only run: "New mail from ken@umbrella.test about the CSV export; noted with the other reports."
8. on Pip's own computer: /workspace/feedback-this-week.md
9. handed to Pip's builder (dot/workers/builder, a durable subagent); back in the app: "The builder finished. Wrote /out/weekly-feedback.md: …"
10. a follow-up Sam set, every Monday at 9:00 in his time zone: "Set: every Monday at 9:00 New York time, I'll open a draft PR for the week's export fixes."
open_draft_pr on "when_asked": no approval when Sam asks; on the follow-up's run it asked first, and Sam denied it
11. a denial and two hand-offs in a row paused Pip; asked for a draft PR, the tool wasn't there
Sam resumes it with configureAgent; the draft PRs already opened stay openIn step 2 the scripted model deliberately tries Gmail's send during research. The run's trace shows the tool does not exist there, and the list of available tools holds nothing that acts and nothing that starts a run. glove_foundry_transmit stays mounted, but it only uses routes the dot's playbooks declare, and they declare none. Step 10 starts the follow-up's run the way its schedule would, with the schedule's payload.
Deploy on Kuiper
kuiper login
kuiper projects create dots && kuiper use dots
kuiper units create dots
kuiper providers add anthropic --key - # once per organisation
kuiper secrets set SLACK_BOT_TOKEN=- SLACK_SIGNING_SECRET=- FOUNDRY_TOKEN=- WEB_SESSION_SECRET=-
kuiper secrets set GOOGLE_CLIENT_SECRET=- GOOGLE_SERVICE_ACCOUNT_JSON=-
kuiper env set -u dots DOTS_DATA_DIR=/data \
'FOUNDRY_TOKEN=${{secrets.FOUNDRY_TOKEN}}' \
'WEB_SESSION_SECRET=${{secrets.WEB_SESSION_SECRET}}' WEB_URL=https://… \
'ANTHROPIC_API_KEY=${{models.API_KEY}}' \
'ANTHROPIC_BASE_URL=${{models.ANTHROPIC_BASE_URL}}' \
'SLACK_BOT_TOKEN=${{secrets.SLACK_BOT_TOKEN}}' \
'SLACK_SIGNING_SECRET=${{secrets.SLACK_SIGNING_SECRET}}' \
GOOGLE_CLIENT_ID=… 'GOOGLE_CLIENT_SECRET=${{secrets.GOOGLE_CLIENT_SECRET}}' \
GMAIL_PUBSUB_TOPIC=… GMAIL_PUBSUB_SUBSCRIPTION=… \
'GOOGLE_SERVICE_ACCOUNT_JSON=${{secrets.GOOGLE_SERVICE_ACCOUNT_JSON}}'
kuiper deploy --source . -u dots --wait
# Set <WEB_URL>/webhooks/slack as the Slack app's request URL.- One unit. The web app takes the public port and Foundry runs beside it on a private port named
foundry, which Kuiper's Inspect card reads. Slack's events come in through the web app at/webhooks/slack, so nothing else needs an address. - State (dots, transcripts, memory, notes, accounts and tokens, the computers, schedules) is on the
/datadisk. New mail needs nothing public: the Gmail watch pulls its subscription from inside the unit. - The API and inspector are locked by
FOUNDRY_TOKEN, which the web app sends. Set it before the first deploy. - Calls need a public port for the call room, declared in a committed
kuiper.json, andOPENAI_API_KEYas a secret, since the model gateway has no realtime route. The README has the steps. - A real computer. Mount a Kuiper sandbox and browser with
glove-execution/kuiperin place of the dot's and the builder's working environments.
Where it differs
- Dots run their own auto-review, which you can't switch off; here it is code over rules and recipients. You could put a classifier or a second model call inside
reviewed(). - The computer is a Glove working environment until you swap in a Kuiper sandbox and browser, and you can't connect your own laptop.
- Here the dot's notes are a file you can read and edit, and its memory changes entry by entry. A dot's notes can't be viewed or edited one by one, and deleting the dot is the only reset.
- The monitor here counts hand-offs and denials in a row and only pauses; you stop a single run from the inspector.
- Dots connect to thousands of apps through plugins. Here there is one, Gmail; any app with a hosted MCP server is another file like it, and the Slack channel history is still a stand-in in
lib/sources.ts.
Sources
- Introducing dots, OpenAI
- Dots privacy, security and safety FAQs, OpenAI Help Center
- Gmail push notifications and the Gmail MCP server, Google
- OpenAI Dots hands-on review: my first day with Herman, The Neuron
- A deep dive into OpenAI dots, Flavio Copes