Skip to content

AppForge-3 — the Agent learns propose-code-file (staged CodeFile/v1 drafts) - #365

Merged
th3-br41n merged 1 commit into
mainfrom
feat/appforge-3-propose-code-file
Jul 29, 2026
Merged

AppForge-3 — the Agent learns propose-code-file (staged CodeFile/v1 drafts)#365
th3-br41n merged 1 commit into
mainfrom
feat/appforge-3-propose-code-file

Conversation

@th3-br41n

Copy link
Copy Markdown
Contributor

AppForge-3 — the Agent learns propose-code-file

The agent's curated tool set gains an eighth artifact kind: it can DRAFT a brainstorm/CodeFile/v1 (filename + language + code) as a staged card the user approves — the same propose → approve → one-entities.create contract as the other seven kinds. This unblocks the agent act of the "build an app inside Brainstorm" video (env-gated demo script included) and is genuinely useful on its own: the agent can draft a script/snippet file straight into the vault, opened by the Code editor.

⚠️ Flagged for security review — new agent capability surface: the manifest gains intents.dispatch:propose-code-file + entities.write:brainstorm/CodeFile/v1 (consented at install like the other propose types), and the tray renders untrusted model output as a code preview.

What's in here

  • apps/agent/src/logic/propose-code-file.ts (new) — the pure staging module: PROPOSE_CODE_FILE_VERB, buildCodeFileProposal (path sanitized + clamped via the SDK sanitizeInlineText, exactly like the Code editor's rename validation; content refused beyond 256 KiB, never truncated; language validated against the CodeLanguage enum or inferred from extension/shebang via @brainstorm-os/sdk/language-detect, degrading to PlainText), the honest "staged, NOT saved" ack, and the fail-closed offer gate canProposeCodeFiles (requires entities.write:brainstorm/CodeFile/v1 in the conversation's frozen set).
  • Three-tier ceiling unchanged — the tool flows through curatedAgentTools (offer gated on the write cap, mirroring the row tool's hasDatabases gate) → intersectAgentTools (requires intents.dispatch:propose-code-file) → makeDispatchTool interception (stages a draft; never dispatches, never writes).
  • Persist — approve → exactly ONE entities.create("brainstorm/CodeFile/v1", { path, content, language, sizeBytes, lineCount, createdAt, updatedAt }), mirroring the Code editor's own new-file create ({path, content, language}; the editor seeds its Y.Doc buffer from the content property on first open — real-shell verified below). Provenance-stamped with the conversation id like every approved proposal; the created-object chip (its label now falls back to path) opens the file via the cap-checked open intent.
  • Tray card — filename (editable, drives the summary) + language chip + a read-only, scroll-capped, keyboard-focusable code preview. Highlighting reuses the shared @brainstorm-os/sdk/code-highlight Shiki tokenizer (dynamic-imported so shiki core stays out of the agent's initial chunk; plain-text fallback on any failure; skipped above 20 k chars). No new highlighter dependency. The LanguageKey → shiki id map was extracted from the code-editor to @brainstorm-os/sdk/language-detect (shikiIdForLanguage) at copy two; the code-editor now delegates to it.
  • Demo script (capture-only)BRAINSTORM_DEMO_AGENT=appforge selects a scripted sequence proposing hello-app/manifest.json + hello-app/index.html then finalizing; any other value keeps the existing follow-up reel. Never registered outside the env gate.
  • i18n — 4 new keys (propose.codeFile.label, propose.kind.codeFile, propose.field.path, propose.codeFile.preview) across all six catalogs (en/es/de/fr/it/pt). createT {name} interpolation only, no ICU.

Security posture (prompt-injection)

The staged-draft contract stays airtight: model output NEVER reaches entities.create — a propose-code-file call only stages a bounded draft; the write happens exclusively on the human approve gesture. Specifically tested:

  • injection-shaped content renders as inert text in the preview (React text nodes only; test asserts no <img>/<script> element is created and no handler runs);
  • oversized content (> 256 KiB) is refused with a reason fed back to the model;
  • the tool is absent without the write cap or the dispatch grant (offer-time gate + the loop's intersection, both tested);
  • approve creates exactly one entity; discard creates none;
  • path is a vault-resident organizing string — sanitized (control/zero-width/bidi strip + 200-char clamp), never handed to any filesystem API; traversal-shaped input stays inert text.

Real-shell verification (launch-orchestrator bug-class check)

Drove the worktree-built shell (packaged main, real broker/ledger/worker pipeline, BRAINSTORM_DEMO_AGENT=appforge) with Playwright:

  1. fresh vault + seeded first-party apps → Agent app opened;
  2. one user message → the demo provider drove the REAL agent loop → two propose-code-file cards staged in the tray (manifest.json → JSON chip, index.html → HTML chip, both with highlighted previews) — proving the tool is actually offered and dispatched in a real conversation, not just in unit tests;
  3. approved both → two CodeFile/v1 entities created, created-object chips listed both paths;
  4. opened the Code editor → "2 files" listed, index.html open with the exact proposed content, HTML highlighting live, "No problems" — the acceptance test (agent-approved file opens cleanly in Code) passes.

Also verified in the capture: the assistant message carries two "Used propose-code-file" chips, the approval notice reads Added "hello-app/index.html" to your vault, and the "Created in this chat" chips list both paths (the path label fallback) and open via the cap-checked open intent.

Four-path evidence (tray card)

  • Screenshot — captured in the real shell: two staged code-file cards (language chips + highlighted previews), post-approve created-chips, and the Code editor showing the created files (see PR comment / session capture appforge3-shots/).
  • Keyboard — Tab reaches the filename input → the preview region (tabIndex=0, scrollable with arrow keys — WCAG scrollable-region-focusable) → "Add to vault" → "Discard". Approve is disabled when the filename is cleared (never a blank write).
  • Screen reader — the card is a role="group" labelled "Proposed Code file: "; the filename input is label-associated ("Filename"); the preview is a role="region" named "Code preview for "; approval confirmation is a role="status" live region ("Added "" to your vault").
  • Discoverability — the tool is offered to the model only when usable (write cap held), the card appears in the same tray as every other proposal with the same approve/discard affordances, the language chip explains the draft at a glance, and the created chip links straight to the file in the Code editor.

Tests

  • apps/agent/src/logic/propose-code-file.test.ts (new, 15 tests) — validation, bounds, sanitization, language inference, ack contract, cap gate.
  • propose-persist.test.ts — create shape mirrors the code editor's; exactly one create; injection content persists as plain data.
  • turn.test.ts — staged never dispatched; refusal acks; unknown-tool fail-closed.
  • proposal-tray.test.tsx — inert rendering of injection content; single editable line; focusable preview; language chip.
  • agent-tools.test.ts / manifest.test.ts — offer gating; manifest covers the curated footprint.
  • demo-agent-provider.test.ts — mode selection; script shape; clamping.
  • Full affected suites green: 87 files / 942 tests (agent + code-editor + sdk + shell-ai), plus the in-process pipeline/entities suites (36 files / 418 tests). bun run typecheck (packages + apps) and bun run lint (incl. css-tokens, app-i18n, native-select gates) clean.

🤖 Generated with Claude Code

…ct kind

The agent can now DRAFT a brainstorm/CodeFile/v1 (filename + language +
code) as a staged tray card the user approves — same propose → approve →
one-entities.create contract as the other seven kinds. Approval mirrors
the Code editor's own new-file create ({path, content, language} +
sizeBytes/lineCount), so the created file opens cleanly in Code
(real-shell verified end-to-end with the demo provider).

- propose-code-file.ts: pure staging — path sanitized+clamped like the
  editor's rename (sanitizeInlineText), content REFUSED beyond 256 KiB,
  language validated against CodeLanguage or inferred (extension/shebang,
  PlainText fallback), fail-closed offer gate on
  entities.write:brainstorm/CodeFile/v1 (canProposeCodeFiles).
- three-tier ceiling unchanged: offer gate + intersectAgentTools
  (intents.dispatch:propose-code-file) + makeDispatchTool interception —
  model output never reaches entities.create.
- tray card: editable filename, language chip, read-only scroll-capped
  keyboard-focusable code preview; untrusted content renders as React
  text nodes only (injection test); Shiki via the shared
  @brainstorm-os/sdk/code-highlight tokenizer, dynamic-imported so shiki
  core stays out of the agent's initial chunk. No new highlighter dep.
- sdk: LanguageKey→shiki-id map extracted to language-detect
  (shikiIdForLanguage) at copy two; code-editor delegates to it.
- created-objects chip label falls back to `path` so approved code files
  link back (open intent) with a real name.
- manifest: + intents.dispatch:propose-code-file,
  + entities.write:brainstorm/CodeFile/v1 (install-consented).
- demo provider: BRAINSTORM_DEMO_AGENT=appforge selects a capture-only
  script proposing hello-app/manifest.json + index.html (the video act);
  any other value keeps the existing reel.
- i18n: 4 new keys across all six catalogs (en/es/de/fr/it/pt).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@th3-br41n
th3-br41n merged commit 7353341 into main Jul 29, 2026
3 checks passed
@th3-br41n
th3-br41n deleted the feat/appforge-3-propose-code-file branch July 29, 2026 14:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant