Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
d975052
Give a Bot template a format that refuses what it must not carry
jerelvelarde Aug 30, 2026
b2f0e8b
Keep a template's draft, its provenance and the asks it made in their…
jerelvelarde Aug 30, 2026
0b74717
Let a skill be installed inside a transaction, and name what a templa…
jerelvelarde Aug 30, 2026
823c520
Pack a coworker into a template, resolve one against this deployment,…
jerelvelarde Aug 30, 2026
cdf945c
Show a person every word a stranger wrote before any of it reaches a …
jerelvelarde Aug 30, 2026
94dff75
Ship three templates in the box, and a check that keeps them installable
jerelvelarde Aug 30, 2026
a82f5c4
Build the template store where every other store is built, or the rou…
jerelvelarde Aug 30, 2026
af89084
Hold a connector id to the rule an MCP server id is actually held to
jerelvelarde Aug 30, 2026
6dbdfc8
Refuse to grant an ask this deployment was told was inert
jerelvelarde Aug 30, 2026
be61c28
Cover the refusals the import path was trusting rather than testing
jerelvelarde Aug 30, 2026
3c69c04
Write a skill's trail on the connection its caller already holds
jerelvelarde Aug 30, 2026
9397257
Show a stranger's words in full, and stop offering a grant that will …
jerelvelarde Aug 30, 2026
3c4993f
Give the connection back when an integration test is done with it
jerelvelarde Aug 30, 2026
3b48185
Give the test DOM an address, so the auth client can be imported first
jerelvelarde Aug 30, 2026
b41156d
Stop the window stub replacing a DOM another file installed
jerelvelarde Aug 30, 2026
c353a86
Compile a template's ceiling into rules the policy engine already und…
jerelvelarde Aug 31, 2026
5d5cb64
Enforce an imported Bot's ceiling beside the deployment's own rules
jerelvelarde Aug 31, 2026
df12607
Say the ceiling is enforced, now that it is
jerelvelarde Aug 31, 2026
e13508c
Offer the templates this image ships, and the ones a repository is pi…
jerelvelarde Aug 31, 2026
e35dc31
Serve the gallery, and read the file from it rather than from the bro…
jerelvelarde Aug 31, 2026
97fb84e
Give the catalogue a screen, and the imports a place to be read
jerelvelarde Aug 31, 2026
8b68f1a
Install a DOM before this file loads the app, not after
jerelvelarde Aug 31, 2026
d8ac8cf
Write down the catalogue, the pins and the ceiling
jerelvelarde Aug 31, 2026
b2f892a
Emit the template directory only when a release sets one
jerelvelarde Aug 31, 2026
531c188
Serve the screen what an administrator wrote, not what is in force
jerelvelarde Aug 31, 2026
61008a7
Make the gallery a thing you can look at, not a list you have to read
jerelvelarde Aug 31, 2026
ad80991
Give a template a page you can read without importing it
jerelvelarde Aug 31, 2026
3c36477
Lead the export with what travels, not with the file
jerelvelarde Aug 31, 2026
dbbd412
Remember which repositories an administrator trusted
jerelvelarde Aug 31, 2026
bb9f90e
Hand back the draft rather than refusing the second export
jerelvelarde Aug 31, 2026
e3929e1
Draw the templates screens, and correct three sentences they outgrew
jerelvelarde Aug 31, 2026
ba40e83
Give the catalogue enough coworkers to need finding, and a way to fin…
jerelvelarde Aug 31, 2026
32f648d
Let the reading page be a document, and land "Use this template" wher…
jerelvelarde Aug 31, 2026
6025970
Give the runtime image the YAML parser this format needs
jerelvelarde Aug 31, 2026
f1efd52
List the catalogue the directory actually holds
jerelvelarde Aug 31, 2026
2ee9ef4
Let a page taller than the window be read
jerelvelarde Aug 31, 2026
77cd014
Answer the three questions a reader arrives with before the prose starts
jerelvelarde Aug 31, 2026
c153f36
Lead with the coworker, then what it may do, then what it was told
jerelvelarde Aug 31, 2026
da66553
Run an imported coworker here when there is nothing to send it to
jerelvelarde Aug 31, 2026
0ef11fc
Let the roster fill the row it was given
jerelvelarde Aug 31, 2026
424ea44
Make the optional endpoint optional
jerelvelarde Aug 31, 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
31 changes: 31 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,37 @@ KEY_ENCRYPTION_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
PORT=3001
SERVER_PORT=3001
TENANT_PACKAGE_DIR=../examples/fintech
# Where the Bot templates shipped in the box are, resolved from server/ as the line above is. The
# image carries examples/, so a deployment with no network at all still opens a populated gallery.
# Unset, it is the line below.
#
# A directory that is not there is a gallery with nothing in it rather than a deployment that will
# not start, and one file in it that does not parse is named in the log and passed over rather than
# taking the others with it. These are many authors' files, and one person's typo must not stop
# somebody else booting -- which is the opposite of how the tenant package above is read.
# OPENBOT_TEMPLATE_DIR=../examples/templates
#
# The repositories an administrator may register as a template source, comma separated as
# owner/repo, and there are none unless this names one. Nothing is fetched from the network until
# somebody has both named a repository here and registered a pinned commit in the product: a
# self-hosted product that reaches a third party on first boot because its vendor shipped a default
# has made that decision on its operator's behalf. The admin screen renders this list and cannot
# widen it, the way it renders INITIAL_ADMIN_EMAILS.
#
# owner/repo only. A URL, a branch or a pin here is refused at startup with the entry named: the pin
# is registered in the product, and it must be a commit sha rather than a branch somebody else can
# repoint after you have read the files. The reference catalogue is the line below.
# OPENBOT_TEMPLATE_SOURCES=jerelvelarde/awesome-openbot-templates
#
# Who may install a template: anyone, or admin. Unset means anyone, because everything an install
# writes -- the Bot, its skills, and the grants that pair the two -- is what the same person can
# already do one act at a time on /agents, /skills and the grant screens. There is no fourth call to
# put behind an administrator.
#
# What is set here is a floor. The product may raise it to admin and may never lower it back, so a
# deployment that restricts installs can rely on it holding rather than on nobody clicking it away.
# Anything other than these two words is refused at startup rather than quietly read as anyone.
# OPENBOT_TEMPLATE_INSTALLERS=anyone
# What this deployment calls itself, when more than one shares an Intelligence project. A copy of a
# deployment made for development uses the same project key, and threads are listed per Bot with
# nothing to say which deployment a conversation came from. The name goes into every thread id this
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ jobs:
- run: bun run format:check
- run: bun run lint
- run: bun run typecheck
# The templates that ship in the box, read with the same parser the server runs at preview and
# again at install. Nothing else in the build reads them: they are data copied into the image,
# so one that does not parse compiles, ships, and is refused for the first time on somebody
# else's deployment at the moment they try to install it. This is the only place that failure
# is ours rather than theirs.
- run: bun scripts/check-bot-templates.ts

# The two packages that are deployables in their own right rather than root workspaces. Root
# `typecheck` is `bun run --filter '*' typecheck`, and `--filter '*'` enumerates `workspaces`, which
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ Leave `EMBEDDED_POSTGRES` off and set `DATABASE_URL` to point at a database you
| -------------------- | ------------------------------------------------------------------ |
| `/` | Start and browse channels. |
| `/agents` | Create, edit, duplicate, hide, delete, and launch coworkers. |
| `/agents/gallery` | Browse the templates this deployment carries and import one. |
| `/channel/:id` | Converse with one coworker, watch its screen, and see what it ran. |
| `/bot` | Direct chat with a Bot; `?agent=<id>` selects one. |
| `/skills` | Create and enable personal skills. |
Expand All @@ -133,6 +134,7 @@ Leave `EMBEDDED_POSTGRES` off and set `DATABASE_URL` to point at a database you
| `/admin/components` | Publish components and govern which Bots may use them. |
| `/admin/playground` | Draft and publish sandboxed components in the browser. |
| `/admin/plugins` | Configure MCP servers, MCP grants, and deployment skills. |
| `/admin/templates` | Register a pinned template source and decide who may install. |
| `/admin/audit` | Review permitted, refused, and failed actions. |

## Features
Expand All @@ -148,6 +150,8 @@ Leave `EMBEDDED_POSTGRES` off and set `DATABASE_URL` to point at a database you
- **Components instead of prose**: compiled React components live in `app/src/components/gallery/`, sandboxed ones are authored in `/admin/playground` and published with no deployment. Every call asks the server whether the component exists, is published, and is not withheld from that Bot. Data functions are granted per component.
- **Governed MCP**: Google Drive and Notion ship in the catalogue, reached as the person asking. The catalogue carries only vendors this deployment stands behind, so adding one is a review of that vendor. Custom servers must pass URL checks; unknown tools and custom-server tools are treated as writes, and a catalogue tool the server advertises but does not name as a write classifies as a read. A Bot is told which connectors exist here and which it holds, so it says it has not been granted one rather than browsing to the vendor's website.
- **Skills are instructions, not capabilities**: personal skills attach only to Bots their author owns, deployment skills are admin-owned, and both are invoked with `/` in the composer.
- **A coworker as a portable file**: export a Bot to one YAML file — its role, its skills, and the connectors it asks for — and import it on another deployment. Configuration travels; capability does not: a template carries no id, no endpoint, no credential and no grant, and a document containing one fails to parse rather than being quietly stripped. What it wanted lands as a request an administrator decides on the screens that already decide it, so an imported Bot arrives cold and says so. The importer is shown every word a stranger wrote, verbatim, before any of it reaches a model. See [docs/bot-templates.md](docs/bot-templates.md).
- **A gallery in the box, and a catalogue that grows by a push**: `examples/templates/` is copied into the image, so a deployment with no network at all opens `/agents/gallery` and finds something in it. Each file is parsed on its own, and one that does not parse is named and passed over rather than stopping a boot. Beyond that seed an administrator registers a public repository as a source, and only one the deployment's own `OPENBOT_TEMPLATE_SOURCES` names, pinned to a commit sha and fetched server-side: nothing reaches the network until somebody asks for it, and moving the pin is the only update there is.
- **Sign in with what your company already has**: Google, Microsoft or Okta from the environment, or a company's own SAML or OpenID Connect provider registered while the deployment runs and routed by email domain. Any one turns sign-in on; several may be configured at once.
- **Decide who gets in**: `/admin/people` lists everybody who has signed in, promotes and demotes them, and removes access, which ends the session they are using and stops the next sign-in. Every change is on the audit trail.
- **An audit trail you can read**: `/admin/audit` lists what was permitted, what was refused and what failed, and every refusal carries the rule that caused it.
Expand Down
6 changes: 6 additions & 0 deletions app/src/components/admin/admin-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
IconKey,
IconLayoutGrid,
IconListDetails,
IconPackageImport,
IconPuzzle,
IconShieldCheck,
IconUsers,
Expand Down Expand Up @@ -61,6 +62,11 @@ const GROUPS: {
icon: IconDeviceDesktop,
linkOptions: { to: "/admin/computers" },
},
{
title: "Templates",
icon: IconPackageImport,
linkOptions: { to: "/admin/templates" },
},
],
},
{
Expand Down
26 changes: 26 additions & 0 deletions app/src/components/agents/agent-profile.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import { type ReactNode, useState } from "react";
import { AbstractAvatar } from "@/components/agents/abstract-avatar";
import { AgentFields } from "@/components/agents/agent-fields";
import { CallbackTokenPanel } from "@/components/agents/callback-token-panel";
import { ExportTemplate } from "@/components/agents/export-template";
import { HandoffPanel } from "@/components/agents/handoff-panel";
import { TemplateRequests } from "@/components/agents/template-requests";
import { Button } from "@/components/ui/button";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
Expand All @@ -16,6 +18,7 @@ import {
updateAgentMutationOptions,
} from "@/lib/agents/mutations";
import { agentQueryOptions } from "@/lib/agents/queries";
import { templateImportQueryOptions } from "@/lib/templates/queries";

function Tag({ children }: { children: ReactNode }) {
return (
Expand Down Expand Up @@ -65,6 +68,13 @@ export function AgentProfile({ agentId }: { agentId: string }) {
const isConfirmingDelete = confirmingDeleteId === agentId;

const agent = useQuery(agentQueryOptions(agentId));
/*
* Where this coworker came from, or nothing — most were made by hand and this answers null for
* them without an error. Read here as well as inside the panel below because the tag belongs in
* the header beside the other things that are true of the Bot rather than in a section further
* down; both reads are the same cache entry.
*/
const imported = useQuery(templateImportQueryOptions(agentId));
const updateAgent = useMutation(updateAgentMutationOptions(queryClient));
const duplicateAgent = useMutation(
duplicateAgentMutationOptions(queryClient),
Expand Down Expand Up @@ -107,6 +117,12 @@ export function AgentProfile({ agentId }: { agentId: string }) {
<div className="flex flex-wrap justify-center gap-1.5">
<Tag>{profile.visibility === "private" ? "Private" : "Public"}</Tag>
{profile.systemOwned ? <Tag>System owned</Tag> : null}
{/*
* Said out loud, permanently. An imported coworker is an ordinary Bot in every other
* respect — owned, editable, deletable — and the one fact that does not follow from
* looking at it is that its instructions were written somewhere else by somebody else.
*/}
{imported.data ? <Tag>Imported</Tag> : null}
</div>
</header>

Expand Down Expand Up @@ -162,6 +178,8 @@ export function AgentProfile({ agentId }: { agentId: string }) {
*/}
{isEditing ? null : <HandoffPanel agentId={agentId} />}

{isEditing ? null : <TemplateRequests agentId={agentId} />}

{actionError ? (
<p className="text-sm text-destructive" role="alert">
{actionError.message}
Expand Down Expand Up @@ -194,6 +212,14 @@ export function AgentProfile({ agentId }: { agentId: string }) {
{duplicateAgent.isPending ? "Duplicating…" : "Duplicate"}
</Button>

{/*
* Beside Duplicate, because it is the same verb pointed somewhere else: Duplicate makes
* another copy here, Export makes one that can leave. A system-owned Bot is offered it as
* well as an owned one — those are the most template-worthy things in the product, and
* Duplicate already lets anybody fork them.
*/}
<ExportTemplate agentId={agentId} />

<Button
className="w-full text-sm!"
disabled={setHidden.isPending}
Expand Down
Loading