Everything is a plugin

Decision 0017 · panel P: Opus, Fable, Sol, Astra · P2, never blocks the release
✓ Approved by Will 17:03 ✓ Astra and Fable reviews applied ● Ready to build slice 0

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.

Your three answers, and what each changed

17:03, with "Looks good but you should have fable and astra review this one extra time".

Question 1 · React

May plugins draw with React?

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
ChangedEvery plugin, not only built-ins, can ship an arbitrary React app, inline in chat or full screen. A plugin that isn't compiled in runs it in a sandboxed frame with a typed bridge to flows. The agent gets the same ability through ui/app cards. This overrides the panel's "no plugin JavaScript in a frame".
Question 2 · Sandboxes

Where may repository and personal code run?

The question makes no sense to me quickjs is a sandbox
ChangedThe trust tiers are now written as sandboxes. Plugin and flow code from a repository or a person runs in the QuickJS realm, hand-edited personal flows included (no more FlowNeedsSandbox for them). Containers are only for code that needs a machine. Frames are for React apps.
Question 3 · The queue

Product feature or maintainer tool?

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
Changed"A flow that runs over other flows" is now first-class: it observes other runs, reads them, and asks you. The product queue becomes one, running over coding flows, after dogfooding. Slice 1 stays small: a view of what waits on you.

The decision in five rules

Thirteen rules in the decision; these five carry it.

  1. A small core; everything else is a plugin.16 named primitives. Our features use the same interface as anyone's, and the core names no plugin outside its built-in list.
  2. One manifest, in the SDK we have.plugin.json, decoded by Plugin.Manifest in @smthrs/plugin, with the host contract PluginHost beside it. No new package.
  3. Plugins act only through flows, and every dispatch carries who asked.The server mints a credential per run naming the person, the agent or plugin acting for them, and its ceiling. Flows can run over other flows.
  4. Everything not compiled in runs in a sandbox.Flow code in the QuickJS realm; React apps in a sandboxed frame; machine work in a container. Every plugin and the agent can ship a React app.
  5. People act; plugins and agents never act as them.Person-only routes refuse any agent or plugin credential, and the host draws every person-only button and key itself. A merge activates a plugin; an Admin grants each secret to each plugin.

Core versus plugins

Plugins sit on one interface. The interface sits on the core. Nothing reaches past it.

Deletion test
CI deletes each whole built-in plugin and its one line: every other test passes. CI boots with no plugins: shell, sign-in, sign-out and plugin settings still work. Slice 1 proves the skeleton; slice 3's parity gate proves the public interface.

Where code runs

Three sandboxes, and the app's realm for code compiled into the build. Placement stays local first, remote by need.

The app's realm

Runs
the core and built-in plugins
Who
only code compiled into the build
Boundary
code review and the build: it is trusted application code

QuickJS realm

sandbox Runs
today: the agent's cells and views; from slice 5: flow code from repositories and people, hand-edited flows too
Can't
network, files, a clock, randomness; imports only the host's set; 64 MiB, 1 s a step
On
the browser engine, or the Worker by need

Sandboxed frame

sandbox Runs
React apps from any plugin and the agent, inline or full screen
Can't
network or navigate away; data only through the typed bridge; never acts as you
On
a views-origin wrapper, under a label the host draws

Container

sandbox Runs
code that needs a machine: files, processes, a checkout
Secrets
only through the egress, per plugin grant
On
Cloudflare Containers

The manifest, for real: the decision queue as a plugin

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" }
    ]
  }
}
Data, not codeThe host decodes and checks it before any plugin code loads. Schemas resolve only to this file's $defs; a bad field fails the build, naming it.
No trust fieldNothing here says "trusted". component renders only because this plugin is compiled in; elsewhere the same view ships as a framed app.
The host draws your buttonsrun/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.
Keys fill payloads2 sets index to 1 through $key.index; args picks $item.options[1]. No queue-specific code in the host.
One sourcecore/waits lists every wait addressed to you, from runs. No queue table. Members and up, as run/answer requires.
Your keys1–9 option, a approve, r changes, i the strip's text box, ⌘↵ send, j/k. Later and undo come in slice 2.

Contribution points

Each lands with its first consumer. Until then the host refuses it by name, so nothing half-loads.

views

A view in the chat shell, inline or full screen, with the queries it reads and the events it binds to flows.

any · slice 1
slots

An entry in a named place: shell.nav, shell.header, repo.tabs, branch.pane, item.pane.

any · slice 1
commands

A flow, with fixed args, as a slash command and a palette entry.

any · slice 1
keybindings

A key and a when: exactly one command or one declared view event.

any · slice 1
triggers

An event, cron, webhook, or other runs' events with over: flows over flows.

built-in · repository · slice 2
hooks · taps

A seam a plugin owns, and a flow or value on it. Each acting tap is a durable child run.

as the hook allows · slice 4
records

Small JSON records only the plugin's flows write, synced by an authorized shape.

any · slice 4
flows · owns

The flows a plugin owns: its own folder, or .smithers/flows/<name> for a repository plugin. One owner per flow.

slice 5 for repositories
settings

A person or repository setting with a JSON Schema. A secret field is refused.

any · slice 5
modelRoles

A named model use; Admins map it; calls go through ModelStep within budgets.

built-in · repository · slice 5
cards

A card kind in the conversation. Replaces VIEW_TEMPLATES.

any · slice 7
collectEvery tap adds a result, kept in order.item kinds, a PR's check flows
selectOne provider, chosen explicitly. A failed decision falls back to the default.triage decider, repo home, TODO flow
narrowOnly removes; mandatory members are restored.who gets notified
guardAllow only if every tap allows. A failure or timeout refuses.issue transitions, merge rules
observeSees the event; can't change it.telemetry, audit

Trust tiers

Set by where the plugin came from, never by its manifest. The interface is identical; the tier decides the sandbox and the caps.

Built-in compiled in

Draws with
blocksviewsappscomponents
Flow code runs in
the app's realm
Can do
any act that isn't person-only
Hooks
owns hooks; taps any hook
Secrets
by name, through the egress
Activated by
the build

Repository .smithers/plugins

Draws with
blocksviewsapps (frame)components
Flow code runs in
the QuickJS realm; a container for machine work
Can do
Member-level repo:* acts, its own plugin:<id>:*, run:observe
Hooks
taps hooks that allow repository taps
Secrets
by name, once an Admin grants each one to this plugin
Text you type in its UI
its data, going only where its events send it
Activated by
a person's merge, then an installation snapshot; a bad one stays Merged · not active

Personal saved by you

Draws with
blocksviewsapps (frame)components
Flow code runs in
the same as a repository plugin
Can do
only what you may do
Hooks
taps nothing shared; starts no unattended team work
Secrets
none
Activated by
Save; each run pins its revision
Never, for any plugin, app or agent
  • act with your session: they hold a run credential, and person-only routes refuse it
  • answer, approve, merge or pre-approve for you
  • change roles, settings, protection or secrets
  • start or override a system flow
  • read a secret's value, or use another plugin's grant
  • run in the app's realm without being compiled in
  • ask you for a password or token inline
  • draw a person-only button: the host draws those

Slices

P2, beside the release. No conversion touches a file a release lane is editing.

  1. 0

    Run credentials core prerequisite · build next

    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.

  2. 1

    Walking skeleton: needs-you compiled-plugin skeleton

    You answer the agent's questions full screen, by keyboard, in the hosted app. Five points, one view, no new table.

    Built

    • Plugin.Manifest, check, Load and PluginHost in @smthrs/plugin
    • core/waits over runs; run/answer takes a note with an approval
    • The app's adapters: built-in lists, host contract with select and propose, the action strip, <Slot>, view host, keymap, key door
    • needs-you: the queue view and nav count; the host's strip draws the options, Approve, Changes and the text box
    • Load spans and counters, content-free

    Acceptance

    • e2e: 3 asks; gn; 2 answers from the strip; the run resumes; empty state; GIF
    • The Queue dispatching run/answer itself is refused
    • Held admission: chat stays usable; Sending settles only on the real result
    • Two presses, one answer; a stale answer shows not_open
    • Reload mid-send, offline answer, two devices
    • Delete the plugin, boot with none: all green
    • The printed manifest loads through the real loader
    • You answer one real question in production
  3. 2

    Durable asks and flows over flows

    smthrs 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.

  4. 3

    React apps in sandboxed frames, and the parity gate

    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.

  5. 4

    Review plugins, hooks and records

    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.

  6. 5

    Repository plugins and the realm flow host

    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.

  7. 6

    Seams repositories can tap, and secret grants (M-47)

    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.

  8. 7

    The rest

    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.

Review fixes

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.

SevFindingFix
Astra P1The server can't tell the agent from you: sign-in never sets Actor.actSlice 0's run credential; no cookie fallback; person-only routes refuse any act; receipts from host UI
Astra P1The authority intersection had no enforcement; compiled code was called boundedAn invocation context in every dispatch, narrowed per hop; uses checked against the caller; compiled code stated as trusted
Astra P1Secret approval was repository-wideGrants per plugin installation; egress from manifest ∩ ceiling ∩ grants; off until tested
Astra P1A timed-out merge policy fell back to "allow"Five semantics with failure rules; guard fails closed
Astra P2 ×9No 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 broadPluginHost 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 P1A framed queue couldn't answer: run/answer is person-only and a frame acts as appThe 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 P1A plugin or the agent could imitate host UI and ask for your tokenA host-drawn label on every frame; no inline credential prompts; password inputs refused; gated before slice 3
Fable P1A frame could leak by navigating itselfA views-origin wrapper with frame-src 'self' refuses it before the request; pwa.ts:34 narrowed; a zero-request test
Fable P2 ×6Which runs get a credential; a hand-written person-only list; realm determinism; triggers with no person; confirm spoofing; citation driftsThe 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)