diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7de4b2c0..24467594 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/README.md b/README.md index 9bb0801c..2e74a9e8 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,7 @@ 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). - **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. diff --git a/app/src/components/agents/agent-profile.tsx b/app/src/components/agents/agent-profile.tsx index f74555d6..0c52d89e 100644 --- a/app/src/components/agents/agent-profile.tsx +++ b/app/src/components/agents/agent-profile.tsx @@ -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"; @@ -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 ( @@ -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), @@ -107,6 +117,12 @@ export function AgentProfile({ agentId }: { agentId: string }) {
{profile.visibility === "private" ? "Private" : "Public"} {profile.systemOwned ? System owned : 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 ? Imported : null}
@@ -162,6 +178,8 @@ export function AgentProfile({ agentId }: { agentId: string }) { */} {isEditing ? null : } + {isEditing ? null : } + {actionError ? (

{actionError.message} @@ -194,6 +212,14 @@ export function AgentProfile({ agentId }: { agentId: string }) { {duplicateAgent.isPending ? "Duplicating…" : "Duplicate"} + {/* + * 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. + */} + + + {/* + * The packer refuses rather than truncating, so this sentence is usually actionable: a + * skill slug the format will not admit, prose past a ceiling, or something in the Bot's own + * text shaped like a key. None of the three is a fault in the export; each is a thing to + * fix on the coworker. + */} + {exportTemplate.error ? ( +

+ {exportTemplate.error.message} +

+ ) : null} + + ); + } + + return ( +
+

+ Template draft +

+ +

+ Read it before you send it. Widen the boundary to what this coworker + actually needs, and cut anything in the requests it does not. +

+ +