feat: phase 3 — workspace capability + pluggable backends (sub-project 4)#170
Merged
Conversation
Design for sub-project 4 of the Dawn opinionated agent harness. The workspace tools (readFile, writeFile, listDir, runBash) become a built- in capability auto-wired by the convention of having a workspace/ directory under a route. Filesystem and exec implementations become pluggable via a new @dawn-ai/workspace package shipping the type interfaces, localFilesystem/localExec defaults, a compose() helper, and one demonstration middleware (withLogging). dawn.config.ts switches from the existing hand-rolled string-only parser to tsx-evaluated import so callable backend values can be expressed naturally. Default behavior is unchanged: apps that don't touch dawn.config.ts keep working. Path-jail enforcement lives in the capability; backends receive already-resolved absolute paths. Human-in-the-loop permission gating (interrupt to ask the user about jail escapes) is deferred to a separate sub-project (4.5) with its own brainstorm + spec + plan. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bite-sized, TDD-structured plan covering: @dawn-ai/workspace package (types, localFilesystem, localExec, compose, withLogging), the createWorkspaceMarker capability, dawn.config.ts loader switch from hand-rolled parser to tsx import, tool-name uniqueness check inversion for overridable tools, runtime wiring, typegen, chat example migration, and the smoke + PR steps. 15 tasks; each commits independently. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds the package skeleton (manifest, tsconfig, vitest config) for the upcoming pluggable workspace backends. No exports yet — types, defaults, and helpers land in subsequent commits. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…sx import
The hand-rolled parser supported only string-literal property values and
const string bindings. The upcoming workspace capability needs to express
callable backend values in dawn.config.ts, which strings can't express.
Switch to a tsx-evaluated dynamic import (same loader Dawn already uses
for route discovery and tool execution).
Existing dawn.config.ts files (just { appDir }) remain valid TS modules
and continue to load without modification.
Side-effects of the loader swap:
- Two CLI integration tests assumed the old parser's specific error
message or its fresh-read-from-disk behavior. The verify test's
expected error string is updated to match the runtime ReferenceError
that the tsx import now surfaces, and the dev test that mutated
dawn.config.ts mid-session is rewritten to start the dev process with
the invalid config in place (Node's ESM cache prevents a re-import of
the same module URL within one process — mid-session config edits
will become a per-task concern as backends land).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Type-only edge: @dawn-ai/core now imports FilesystemBackend/ExecBackend types from @dawn-ai/workspace via 'import type'. No runtime weight yet (workspace stays in devDependencies until the marker lands). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Auto-detects a route's workspace/ directory and contributes four tools (readFile/writeFile/listDir/runBash) routed through configurable backends. Defaults to localFilesystem + localExec when no backends are configured in dawn.config.ts. Path-jail enforced in the capability; backends receive resolved absolute paths. Tools carry an `overridable: true` flag so a future uniqueness-check inversion can let user-authored tools/<name>.ts files supersede them. Promotes @dawn-ai/workspace to a runtime dependency of @dawn-ai/core, and extends the cli typegen harness to pack @dawn-ai/workspace alongside cli/core/langchain/langgraph/sdk so externally installed dawn bin tests resolve the new transitive dep. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Tools marked overridable on a capability contribution can be shadowed by a user-authored tool with the same name. Used by the workspace capability so authors can override readFile/writeFile/listDir/runBash by dropping a file in tools/. Non-overridable capability tools (writeTodos, readSkill, task) retain the collision error. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…config Registers createWorkspaceMarker in the capability registry. Loads dawn.config.ts at the start of prepareRouteExecution and threads config.backends into the CapabilityMarkerContext so the workspace marker uses the configured backends (defaulting to localFilesystem + localExec when none are configured). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Delete the hand-rolled readFile/writeFile/listDir/runBash tool files (and their workspace-path helpers) from both the /chat route and the research subagent. The workspace capability auto-contributes these tools when the route has a workspace/ directory, so add empty workspace/ dirs (with .gitkeep) under both routes to opt in. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…agents-md T13's migration of the chat example surfaced a mismatch: the workspace capability was resolving to <routeDir>/workspace/ while the agents-md capability (and the prior hand-rolled tools) used <process.cwd()>/workspace/. Result: post-migration, the chat agent's memory file and its workspace tools pointed at completely different directories. Align the workspace capability with the existing convention: process.cwd()/workspace/. Same trigger as agents-md; same root as the deleted hand-rolled tools. The chat example's pre-existing examples/chat/server/workspace/ directory (with AGENTS.md) now serves as the workspace for both /chat and the research subagent. Removes the empty per-route workspace/ stubs T13 created. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
- system-prompt: runBash signature is { command } now (no timeoutSeconds);
returns { stdout, stderr, exitCode } instead of a formatted string
- README: status reflects shipped subagents + workspace capabilities;
layout shows current file structure (no tools/, no workspace-path.ts);
deferred list updated to flag HITL permission gating (sub-project 4.5)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
3 tasks
* docs: phase 3 HITL permissions design spec (sub-project 4.5)
Designs the human-in-the-loop permission system that builds on sub-
project 4's workspace capability. Path-jail escapes and every first-
occurrence bash command trigger an interrupt prompt with three
approval scopes (Once / Always-for-pattern / Deny). Persisted "always"
decisions live in .dawn/permissions.json (project-local, gitignored)
using a tool-keyed flat-string format that's forward-compatible with
future tool categories.
Three operating modes: interactive (dev default), non-interactive
(production / CI), bypass (explicit "operator knows what they're
doing"). dawn.config.ts gains a permissions field with mode, allow,
deny — same shape as the runtime file. DAWN_PERMISSIONS_MODE env var
overrides config for the session.
SSE envelope shape is Agent-Protocol-compatible so sub-project 7 can
implement the spec on top of this without refactoring.
New @dawn-ai/permissions package ships types + pattern-matching engine
+ store. Workspace capability gains a permission check between the
path-jail / bash invocation and the actual backend call.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* docs: implementation plan for phase 3 HITL permissions
Bite-sized, TDD-structured plan for sub-project 4.5. 13 tasks across
five phases: @dawn-ai/permissions package (T1-T5), config + capability
changes (T6-T7), runtime interrupt + resume (T8-T9), chat demo updates
(T10-T11), smoke + PR (T12-T13).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* scaffold(permissions): empty @dawn-ai/permissions package
Adds the package skeleton for the upcoming HITL permissions system.
No exports yet — types, pattern matching, and store land in subsequent
commits.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(permissions): public types
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(permissions): suggested-pattern helpers
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(permissions): pattern-matching engine
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(permissions): PermissionsStore with file I/O, write queue, gitignore handling
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(core): extend DawnConfig + CapabilityMarkerContext with permissions
Type-only edge to @dawn-ai/permissions. Workspace capability will read
context.permissions in a subsequent commit.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(core): workspace capability gates through PermissionsStore
Each tool's run() consults the optional PermissionsStore in the
capability context:
- Path tools (readFile/writeFile/listDir): silent for paths inside the
workspace; consult the store for paths outside.
- runBash: gate every command regardless of path.
Three modes:
- interactive: unknown ops emit LangGraph interrupt() and pause the run
- non-interactive: unknown ops hard-refuse (fail-closed)
- bypass: all ops proceed (path-jail disabled), warn on capability load
The old pathJail() helper is removed — the gate now handles
out-of-workspace cases via the permission flow.
Also packs @dawn-ai/permissions in the CLI typegen install test so the
external install can resolve core's new runtime dep.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(langchain): propagate LangGraph interrupt events to the SSE stream
LangGraph 1.x's `interrupt()` does not emit a dedicated streamEvents v2
event; the parked state surfaces as `__interrupt__: [{value, ...}]` in
the graph's final `on_chain_end` output. Detect this and yield a
{type: "interrupt", data: payload} chunk so the SSE consumer (and the
soon-to-arrive resume endpoint) sees the workspace capability's
PermissionRequest envelope verbatim.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(cli): resume endpoint + PermissionsStore wiring
Adds POST /threads/:thread_id/resume to the dev runtime server, backed
by a module-level pending-interrupts map keyed by thread_id. The
endpoint validates interrupt_id (409 on stale, 400 on missing/invalid
decision) and invokes the registered resolve() callback before
clearing the entry.
execute-route.ts now loads the permissions config from dawn.config.ts,
honors the DAWN_PERMISSIONS_MODE env override, constructs a
PermissionsStore via createPermissionsStore, and threads it into
applyCapabilities so the workspace capability's gates have a store to
consult. streamResolvedRoute also bridges {type: "interrupt"} chunks
from the agent-adapter into the pending map when called with a
threadId.
Approach: two-call via checkpointer was the only viable option (the
in-process Deferred pattern would require the parked tool call to
yield back into Node's microtask queue, which LangGraph's
GraphInterrupt unwinds). However, Dawn does not yet wire a
LangGraph MemorySaver or propagate thread_id into createReactAgent —
that plumbing arrives with the Agent Protocol work in sub-project 7.
Until then resolve() is a no-op stub: the endpoint accepts decisions
and clears the entry, but cannot actually replay the parked graph.
The smoke test (T12) will exercise the loop end-to-end once
checkpointer support lands.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(langchain,cli): wire checkpointer + thread_id; complete resume mechanism
The original T8/T9 implementation propagated interrupts to the SSE stream
and registered them in pendingByThread, but the resume callback was a
no-op stub because LangGraph 1.x requires a MemorySaver checkpointer +
a stable thread_id to actually replay from the parked state.
This commit:
- Wires a process-level MemorySaver into createReactAgent.
- Propagates thread_id from the request body through streamResolvedRoute
to streamAgent to config.configurable.thread_id.
- When agent-adapter detects an interrupt, it registers a resolve
callback in pendingByThread and awaits the user's decision.
- On resume, the adapter re-invokes the graph with new Command({resume})
and yields the resulting events into the same SSE stream.
- Moves pending-interrupts.ts to @dawn-ai/langchain so the adapter
imports the same map the resume endpoint writes to.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(examples/chat): seed permissions allow/deny in dawn.config.ts
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* feat(examples/chat-web): inline permission panel + resume proxy
Detects 'event: interrupt' frames in the SSE stream; renders an inline
panel with Once / Always-for-pattern / Deny buttons; POSTs the decision
through /api/permission-resume to the Dawn server's resume endpoint.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(langchain): bind streamEvents to its Pregel instance to restore /runs/stream
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
* fix(langchain): detect LangGraph interrupts at correct event/data path
LangGraph 1.x surfaces a tool-invoked interrupt() via streamEvents v2 as an
on_tool_error whose data.error is a stringified GraphInterrupt — the leading
JSON array is the interrupts list. The top-level LangGraph on_chain_end does
not carry __interrupt__ on this path. Parse the error string in on_tool_error
to surface the interrupt SSE event; keep __interrupt__-on-chain-end and live
GraphInterrupt object detection as defensive fallbacks.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
CI lint failed on two issues: - Shadow of global `escape` in the interrupt-error JSON parser - Line-length formatting in a test helper signature Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ework verify Adds the two new workspace packages (introduced in sub-projects 4 and 4.5) to the framework-verification harness's pack list, override maps, and fixture snapshots so the generated-app contract tests can install them locally instead of trying the npm registry (404). Also extends create-dawn-ai-app's internal-mode overrides to point @dawn-ai/permissions and @dawn-ai/workspace at the in-repo packages so the contributor-local lifecycle resolves transitive workspace:* deps. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1203c6c to
a110279
Compare
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.
Summary
Sub-project 4 of the Dawn opinionated agent harness. The workspace tools (
readFile/writeFile/listDir/runBash) move from hand-rolled per-route files into a built-in capability auto-wired by theworkspace/directory convention (cwd-relative, matching the existingagents-mdcapability). Filesystem and exec implementations become pluggable via a new@dawn-ai/workspacepackage; defaults preserve existing behavior so apps that don't touchdawn.config.tskeep working unchanged.dawn.config.tsloader switches from a hand-rolled string-only parser to atsx-evaluated import so callable backend values can be expressed.Spec:
docs/superpowers/specs/2026-05-20-phase3-workspace-backends-design.mdPlan:
docs/superpowers/plans/2026-05-20-phase3-workspace-backends.mdChanges
@dawn-ai/workspacepackage:FilesystemBackend/ExecBackendtype interfaces,localFilesystem()andlocalExec()defaults,compose()middleware composition helper, andwithFilesystemLogging/withExecLoggingdemonstration middlewares.createWorkspaceMarker()capability in@dawn-ai/core. Detects<process.cwd()>/workspace/(matching the existingagents-mdcapability's resolution — fixed during implementation when the original<routeDir>/workspace/design broke the chat example's shared-workspace pattern); contributes the four workspace tools routed through configurable backends; enforces path-jail before calling the backend.DawnConfigandCapabilityMarkerContextgain an optionalbackends: { filesystem?, exec? }field. When omitted, the capability falls back tolocalFilesystem()+localExec().tools/readFile.ts(etc.) replaces the workspace capability's contribution; non-overridable tools (writeTodos,readSkill,task) retain the collision error.dawn.config.tsloader switches from hand-rolled parser totsximport. Existing configs (just{ appDir }) continue to work; richer configs (callable backends, imports) now possible.<process.cwd()>/workspace/exists./chat(4 files) and from/coordinator/subagents/research(2 files), plus their workspace-path helpers.Test plan
@dawn-ai/workspaceunit tests: types + localFilesystem (5) + localExec (4) + compose (3) + with-logging (3) = 15 casescreateWorkspaceMarkerunit tests: detect, load, tool wiring, path-jail, default backends, overridable flag — 9 casescheckToolNameUniquenessoverridable cases — 3 new cases/chatand/coordinatorboth produce clean SSE streams via the workspace capability's tools; 0 paired duplicates; 0 errors;doneevent fires. Research subagent'slistDir+readFilenow wired through the capability.Deferred / known limitations
interrupt()flow so the user can grant per-path permissions, with persistence to a yet-to-be-decided location.runBashreturn shape changed — was a formatted string ("<stdout>...[exit N]"), now an object ({stdout, stderr, exitCode}). LangChain JSON-stringifies tool results for the model anyway; should be readable, but a behavior delta worth flagging.listDirno longer sorts or marks directories with/— capability returns rawreaddiroutput. Cosmetic difference; agent can sort if needed.dawn.config.tswith a syntax error gets silently ignored, with the workspace capability falling back to defaults. Acknowledged as a follow-up: should narrow the catch to ENOENT only (or warn loudly on other errors).Architectural note — workspace root resolution
The original spec said the capability would resolve workspace as
<routeDir>/workspace/. During implementation, T13's chat-example migration surfaced that the existingagents-mdcapability and the prior hand-rolled tools both used<process.cwd()>/workspace/. The chat demo depends on a single shared workspace across routes (/chatand the research subagent both read/write the same files;AGENTS.mdlives there too). Per-route workspaces would have broken that pattern.Aligned the workspace capability with the existing convention rather than introduce a new inconsistent one. Documented in the marker source.
🤖 Generated with Claude Code