Send somebody a coworker, and grant it nothing on arrival - #299
Draft
jerelvelarde wants to merge 15 commits into
Draft
Send somebody a coworker, and grant it nothing on arrival#299jerelvelarde wants to merge 15 commits into
jerelvelarde wants to merge 15 commits into
Conversation
…te does on the trail
… and install it granting nothing
…tes never mount The routes were wired into createApp behind an optional dependency and the composition root never constructed one, so /api/templates answered 404 on a real server while every route test passed: the tests build the app directly and hand it the dependency the process never had. Assembled here rather than inside createApp because the installer needs the vault, the plugin store, the trail and this deployment's endpoint policy, and each of those already exists exactly once in this file. The endpoint policy is deliberately the same pair agentFetch is given: an address registered by an import must be held to exactly what an address registered through /api/agents is.
A connector id was checked for length and nothing else, so a template could file a real tool ref as one. With no tools under it the per-tool check that a ref belongs to its connector never runs, so `id: google-drive/read_file_content` parsed cleanly and arrived where the server and the profile screen both re-derive connector-from-tool by looking for a slash. The invisible-character refusal is now the Unicode property classes rather than an enumerated list, which had drifted nine blocks narrower than the sentence it defends and let the first sixteen variation selectors through. Lengths are measured in the units the edit form measures them in, so a template can no longer install a Bot its owner cannot then save.
The grant route read the ref and never the ledger row's status, so an ask the consent screen had described as "nothing will be granted and nothing will be written", and the profile as "there is nothing yet to grant", sat beside a button that wrote a live MCP grant. Two guards, and the overlap is load-bearing. The status guard refuses an ask that was unavailable when the person read it, because somebody connecting that server afterwards is not their consent. The live re-check refuses a ref whose server or tool is absent right now, because the stored status is a snapshot from resolve time and a grant for a server that does not exist is invisible on every screen until the day one appears under that id.
The endpoint check and the byte-identical guard behind `reuse` both had no test, so either could have been deleted and the suite would have stayed green. Two skills in one template can no longer plan into the same slug, an imported Bot keeps the avatar its template named, and reuse of a skill somebody else owns falls through to a suffix rather than quietly attaching theirs.
The audit write went to the pool while the caller held a transaction on a pooled connection, so every import needed two at once and enough concurrent ones would wait on each other.
…be refused Name and title were truncated on the consent screen and both are concatenated into the standing role the model is given, so a stranger could write two hundred characters into a field the reviewer saw forty of. Nothing model-visible is clipped now. A deployment in dry-run enforces nothing, so it gets the amber warning rather than the reassuring block. A key typed for one template is no longer carried to the next one's address. The audit page knows the template events, so a refused import reads as refused rather than as allowed.
bun test runs every file in one process, so a pool left open is held for the rest of the suite. Six new files opened one and never closed it, and past a certain point PostgreSQL stops handing out connections — which does not read as a connection limit, it reads as the run dying at whatever file happened to be next. On CI it died at a different one each time and printed no summary at all. Every other integration test here already closes. These now do too, after their cleanup rather than before it.
jerelvelarde
force-pushed
the
feat/bot-templates
branch
from
August 30, 2026 20:16
67a0d55 to
3c4993f
Compare
Happy DOM defaults to about:blank, whose origin is the string "null". Better Auth builds its base URL from window.location.origin when it is not given one, so the first file to pull in the auth client under a bare registration threw Invalid base URL: null while it was still being imported, taking its tests with it and reporting an unhandled error rather than a failure anybody could place. Locally auth-client.test.ts stubs a window with a real origin and, when it runs first, the client is already cached by the time a rendered test needs it. That is an ordering accident. On CI the order differs, eight tests never ran, and the suite reported a count nobody could reconcile.
The sign-in test assigns a window with a location and nothing else. That was harmless while nothing in the suite installed a DOM; the rendered tests here do, and overwriting a registered window leaves document defined and window.history undefined. app/src/router.tsx builds its history at module scope from exactly that pair, so it threw while being imported by whichever file reached it next, and took that file's tests with it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem
A coworker is the most valuable thing a person builds here, and it cannot leave the deployment it was
built in. Somebody spends a week getting a Bot's role right, pairing it with the skills that narrow
its tools, working out which connectors it needs — and the only way to give that to a colleague is to
describe it and have them do the week again. Across a fleet the same Bot is rebuilt by hand in
staging and in production, and the two drift.
Two things almost do this and neither fits. The tenant package is the whole deployment — brand,
channels, model, roster — read from a directory at boot by whoever runs the process; it cannot
describe one coworker, and it is unsafe on a stranger's file, since
expandEnvironmentsubstitutes${NAME}out of the server's own environment before the YAML is parsed.duplicatecopies fourpresentation fields, drops the endpoint and every grant, and throws
ManagedAgentUnavailableErroroutright when the deployment has no Bot of its own — so on the one-container image the README
recommends, that button is dead.
The design is already written down in the code that needs it:
tenant-package.ts:137concludes askill is safe to seed because "a skill is an instruction and confers nothing", while an MCP grant is
not because "those reach a person's own account". Nothing used it.
The approach
A Bot template is one YAML file describing one coworker — identity and prose, the skills it
depends on, the capabilities it asks for, and a ceiling on what it may do. The vocabulary is the
tenant package's, so anyone who has read
examples/fintech/can read a template.Configuration travels; capability does not. No id, endpoint URL, credential, MCP grant, component
source or policy rule can travel, because none are fields: a document carrying one fails to parse and
is told which key and why. That beats stripping them silently — an author who tried to ship a key
learns, and a reviewer is not reading a redacted copy of something larger. Import writes exactly one
grant,
plugin_grantskind'skill', markedtemplate:<digest12>so a retraction takes back whatthe import gave and never an administrator's own. Everything else lands in a ledger as requested and
not granted, satisfied afterwards on the screens that already decide those things.
Three divergences from the package loader, each a security decision. Parsing is strict, because
an ignored key is one the reviewer's eye slid over and the parser agreed to. There is no environment
interpolation at all, and the check reads raw bytes so a comment cannot carry one either. Slugs use
the API's rule rather than the package's looser one, which admits
xandfind-— both installcleanly and are then permanently uneditable through the product.
The consent screen is the control, so it is built as one. The role description and every skill's
instructions render verbatim and unabridged, under the sentence that a stranger wrote them. Invisible
codepoints are a parse refusal rather than a rendering problem, because a review control that can be
made invisible is not a control.
An unmet ask never blocks the install, which is the decision most likely to be questioned.
Blocking would make "grant everything" the fastest route to a working Bot. The Bot arrives cold and
does not lie about it:
grantedToolGuidancealready builds a Bot's self-description from the toolsit was actually offered, so it says it has no source rather than claiming what the template
promised.
The boundary is disclosed, not enforced. The
boundary:block is parsed and shown in plainEnglish; compiling it into scoped policy is the next change. Until then the consent screen says so —
that this deployment currently allows every action, and an imported Bot has exactly the computer
reach of any other Bot here. That cost is real, and stating it beats a ceiling nobody enforces.
What is not covered
template_boundariesships empty. The compiler and thepolicyStore.get()composition follow.Generated clauses will live in that table rather than in
action_policy.deny, becausepolicyStore.setreplaces the whole array from a screen snapshot with no version column, so aclause stored there is erased by an unrelated save.
examples/templates/ships three templates and aCI check, but nothing in the product reads that directory yet.
template.versionis a string nothing reads. Security fixes do notpropagate either — that is the trade, and auto-update has no safe form without publisher identity.
type: built_incannot travel, so exporting one of the shipped fintech Bots is not a faithfulround trip.
any other Bot.
screens: the ledger stores the ask and a decision, never a second source of truth for a permission.
Verification
format:check,lintandtypecheckclean.bun run test:cireports 2318 pass, 0 fail againsta baseline of 2149 on
main— 169 new tests.Driven end to end against a running deployment, which is how the first two of these were found: the
routes were mounted behind an optional dependency the composition root never built, so
/api/templatesanswered 404 on a real server while every route test passed; and the packer's ownsecret-scanner fixtures were shaped enough like credentials to be blocked by push protection.
Three more came from CI failing where a laptop could not. The suite is order-dependent in ways that
only bite once a test renders a real component tree, and these are the first that do. Six new
integration files held their connection pool open for the rest of the run. Happy DOM registers at
about:blank, whose origin is the string"null", so the first file to reach@/lib/auth/clientunder a bare registration threw while being imported. And
auth-client.test.tsassigns a windowcarrying a
locationand nothing else, which was harmless while nothing installed a DOM and, oncesomething did, left
documentdefined andwindow.historyundefined forapp/src/router.tsxtobuild its history from. That last fix touches a file this feature does not otherwise go near; it is
here because these tests are what made it reachable.
shared/bot-template.test.tsserver/tests/template-pack.test.tsserver/tests/template-resolve.integration.test.tsserver/tests/template-install.integration.test.tsserver/tests/template-routes.integration.test.tsserver/tests/plugin-store-transaction*.test.tsallowUnknownToolsapp/tests/import-template.test.tsxapp/tests/audit-template-rows.test.tsxA hostile review over the diff produced 27 candidate defects; 24 survived independent verification and
are fixed in the last five commits — including one where an ask both consent surfaces described as
inert sat beside a button that wrote a live MCP grant.