The app is a short list of core primitives plus plugins, ours included. Every plugin is a plugin.json that binds Flow.make flows to contribution points. Anything not compiled into the app runs in a sandbox.
17:03, with "Looks good but you should have fable and astra review this one extra time".
they should be able to make arbitrary react based apps that are embedded in chat or can be full screened this is extremely important functionality just like how agent itself can directly do this
ui/app cards. This overrides the panel's "no plugin JavaScript in a frame".The question makes no sense to me quickjs is a sandbox
FlowNeedsSandbox for them). Containers are only for code that needs a machine. Frames are for React apps.Everything in the maintainer tool is dogfooded and we will be adding it to the product itself after it's really high quality but yes I like this feature it will very likely end up as a workflow that can be ran on top of other workflows specifically ones that are doing coding work or some other type
Thirteen rules in the decision; these five carry it.
plugin.json, decoded by Plugin.Manifest in @smthrs/plugin, with the host contract PluginHost beside it. No new package.Plugins sit on one interface. The interface sits on the core. Nothing reaches past it.
Flow.make flows only, each dispatch carrying a server-minted invocation contextThree sandboxes, and the app's realm for code compiled into the build. Placement stays local first, remote by need.
apps/mvp/plugins/needs-you/plugin.json, slice 1. It is also a test fixture that must load and dispatch through the real loader.
{
"apiVersion": 1,
"id": "needs-you",
"version": "0.1.0",
"title": "Needs you",
"description": "Everything that waits on you, one item at a time, full screen, by keyboard.",
"requires": { "api": "^1.0.0" },
"capabilities": [],
"uses": ["run/answer", "app/navigate"],
"$defs": {
"Option": { "type": "object", "properties": { "index": { "type": "integer", "minimum": 0, "maximum": 8 } },
"required": ["index"], "additionalProperties": false },
"Note": { "type": "object", "properties": { "note": { "type": "string", "maxLength": 4000 } },
"required": ["note"], "additionalProperties": false }
},
"contributes": {
"views": [{
"id": "needs-you/queue", "title": "Needs you", "scope": "app", "path": "",
"sizes": ["inline", "full"], "minimum": "member",
"reads": ["core/waits"],
"render": { "kind": "component", "export": "Queue" },
"events": [
{ "name": "option", "payload": { "$ref": "#/$defs/Option" }, "flow": "run/answer",
"args": { "runId": "$item.runId", "version": "$item.version", "execution": "$item.execution",
"value": "$item.options[$event.index]" } },
{ "name": "approve", "payload": { "$ref": "#/$defs/Note" }, "flow": "run/answer",
"args": { "runId": "$item.runId", "version": "$item.version", "execution": "$item.execution",
"value": { "approved": true, "note": "$event.note" } } },
{ "name": "changes", "payload": { "$ref": "#/$defs/Note" }, "flow": "run/answer",
"args": { "runId": "$item.runId", "version": "$item.version", "execution": "$item.execution",
"value": { "approved": false, "note": "$event.note" } } },
{ "name": "reply", "payload": { "$ref": "#/$defs/Note" }, "flow": "run/answer",
"args": { "runId": "$item.runId", "version": "$item.version", "execution": "$item.execution",
"value": "$event.note" } },
{ "name": "next", "local": true },
{ "name": "previous", "local": true }
]
}],
"slots": [{ "slot": "shell.nav", "id": "needs-you/nav", "order": 10, "minimum": "member",
"reads": ["core/waits"], "render": { "kind": "component", "export": "NavCount" } }],
"commands": [{ "id": "needs-you/open", "flow": "app/navigate", "args": { "to": "/app/needs-you" },
"title": "Needs you", "minimum": "member", "slash": { "name": "needs" }, "palette": true }],
"keybindings": [
{ "key": "g n", "command": "needs-you/open" },
{ "key": "digit", "event": "option", "fields": { "index": "$key.index" },
"when": "view == needs-you/queue && item.question && !focus.input" },
{ "key": "a", "event": "approve", "when": "view == needs-you/queue && item.approval && !focus.input" },
{ "key": "r", "event": "changes", "when": "view == needs-you/queue && item.approval && !focus.input" },
{ "key": "mod+enter", "event": "reply", "when": "view == needs-you/queue && item.question" },
{ "key": "j", "event": "next", "when": "view == needs-you/queue && !focus.input" },
{ "key": "k", "event": "previous", "when": "view == needs-you/queue && !focus.input" }
]
}
}
$defs; a bad field fails the build, naming it.component renders only because this plugin is compiled in; elsewhere the same view ships as a framed app.run/answer is person-only, so the host's action strip draws these four events and owns their keys. The view only shows the item, calls select, and may propose a note.index to 1 through $key.index; args picks $item.options[1]. No queue-specific code in the host.core/waits lists every wait addressed to you, from runs. No queue table. Members and up, as run/answer requires.Each lands with its first consumer. Until then the host refuses it by name, so nothing half-loads.
viewsA view in the chat shell, inline or full screen, with the queries it reads and the events it binds to flows.
slotsAn entry in a named place: shell.nav, shell.header, repo.tabs, branch.pane, item.pane.
commandsA flow, with fixed args, as a slash command and a palette entry.
keybindingsA key and a when: exactly one command or one declared view event.
triggersAn event, cron, webhook, or other runs' events with over: flows over flows.
hooks · tapsA seam a plugin owns, and a flow or value on it. Each acting tap is a durable child run.
recordsSmall JSON records only the plugin's flows write, synced by an authorized shape.
flows · ownsThe flows a plugin owns: its own folder, or .smithers/flows/<name> for a repository plugin. One owner per flow.
settingsA person or repository setting with a JSON Schema. A secret field is refused.
modelRolesA named model use; Admins map it; calls go through ModelStep within budgets.
cardsA card kind in the conversation. Replaces VIEW_TEMPLATES.
Set by where the plugin came from, never by its manifest. The interface is identical; the tier decides the sandbox and the caps.
repo:* acts, its own plugin:<id>:*, run:observeP2, beside the release. No conversion touches a file a release lane is editing.
Every run except your own root press gets a server-minted credential; the server sets act from the registry, never the request. Person-only routes come from the registry's userOnlyReason, one source, and refuse it. chat/Answer → run/answer nested is refused. Slice 1 ships to production only after it.
needs-you compiled-plugin skeletonYou answer the agent's questions full screen, by keyboard, in the hosted app. Five points, one view, no new table.
Plugin.Manifest, check, Load and PluginHost in @smthrs/plugincore/waits over runs; run/answer takes a note with an approvalselect and propose, the action strip, <Slot>, view host, keymap, key doorneeds-you: the queue view and nav count; the host's strip draws the options, Approve, Changes and the text boxrun/answer itself is refusednot_opensmthrs ask with its own deadline, so the lead and curator ask you through Smithers. Later and undo, never past a deadline. Priority. Run-event triggers, with the curator over todo runs as the first flow over flows.
Gated first: a host-drawn label on every frame; no inline credential prompts; password inputs refused; a views-origin wrapper with frame-src 'self' and a zero-foreign-requests test; src/pwa.ts:34 narrowed. Then the frame, its bridge and the agent's ui/app cards. Gate: the same queue ships as a framed app, and 2 on the host's strip still answers.
review-media (a chapter timeline) and review-code (slices pinned to a commit) add item kinds through the first hook. The maintainer queue and GIF pages retire.
Hand-edited personal flows run in QuickJS. Installation snapshots, owns, preview, and the work board: merge it and the Work tab appears with no deploy.
Triage, notifications, issue transitions and merge rules take repository taps with their failure rules. Per-plugin secret grants, switched on only when their tests pass.
Convert web, jev, runs, flows, repo, home, issues, triage, notifications, todos, branch, then agent. Personal plugins, vendored npm plugins, and the lossless pack.json migration.
Astra: 4 P1s and 9 P2s, all fixed. Fable: 3 P1s and 6 P2s, all fixed but one rejected sub-point. Every citation was checked against the code; the decision has the evidence.
| Sev | Finding | Fix |
|---|---|---|
| Astra P1 | The server can't tell the agent from you: sign-in never sets Actor.act | Slice 0's run credential; no cookie fallback; person-only routes refuse any act; receipts from host UI |
| Astra P1 | The authority intersection had no enforcement; compiled code was called bounded | An invocation context in every dispatch, narrowed per hop; uses checked against the caller; compiled code stated as trusted |
| Astra P1 | Secret approval was repository-wide | Grants per plugin installation; egress from manifest ∩ ceiling ∩ grants; off until tested |
| Astra P1 | A timed-out merge policy fell back to "allow" | Five semantics with failure rules; guard fails closed |
| Astra P2 ×9 | No public data contract; parallel drops results; snooze vs a 10-minute deadline; Pack loses skills; flow ownership; templates that don't exist; an unvalidatable example; activation pins; slice 1 too broad | PluginHost and core/waits; a collect kernel kind; Later moved to slice 2; Pack migration in slice 7; owns; realm code instead of templates; discriminated unions and a fixture; installation snapshots; slice 1 cut to five points |
| Fable P1 | A framed queue couldn't answer: run/answer is person-only and a frame acts as app | The host draws and keys person-only actions in an action strip; select and propose; the $key grammar; the slice 1 Queue built that way |
| Fable P1 | A plugin or the agent could imitate host UI and ask for your token | A host-drawn label on every frame; no inline credential prompts; password inputs refused; gated before slice 3 |
| Fable P1 | A frame could leak by navigating itself | A views-origin wrapper with frame-src 'self' refuses it before the request; pwa.ts:34 narrowed; a zero-request test |
| Fable P2 ×6 | Which runs get a credential; a hand-written person-only list; realm determinism; triggers with no person; confirm spoofing; citation drifts | The registry's acts mark, act set by the server, app added; the list generated from userOnlyReason; realm limits; the installation as principal; a hardened confirm; drifts fixed. Rejected: "the agent's cells don't use QuickJS today" (Agent.ts:53) |