Repository navigation
LOU-N: runtime control server for Agent Forge (run/stop/status/approve) - #19
Merged
Merged
Conversation
Adds a local Express + ws server (apps/agent-forge/server/**) that runs
real agents via the SDK's public AgentExecutor.execute()/resumeAfterApproval()
API, plus `loushy studio` (src/cli/studio.ts) to launch it alongside the
Vite dev server. Wires the app's Run/Stop buttons and agent-list status
pills (LeftRail/Topbar/AppState) to the new REST/WS API instead of the
LOU-L/M placeholders, and adds a minimal inline approve/reject card for
the human-in-the-loop approval gate.
- REST: GET/PUT /agents/:id, POST /agents/:id/run|stop|approve, GET /agents/:id/status
- WS: /agents/:id/stream pushes status + best-effort execution events
- Cancellation: since AgentExecutor.execute() has no cancellation token,
stop() aborts an AbortSignal-wrapped LLMProvider (abortableProvider.ts)
so the next provider call throws, terminating the run without clearing
its checkpoint - the next run() for that agent id reuses the same
sessionId and rehydrates from the last checkpoint instead of restarting.
- Durable execution: FileCheckpointStore/FileApprovalStore persist under
.loushy/agents/<id>/{checkpoints,approvals}/, the seam LOU-L's
createFsAgentStore() TODO was left for.
- Tests: supertest HTTP-layer tests, RunManager unit tests (including a
deterministic stop-then-resume-from-checkpoint test), and a direct
AgentExecutor + resumeAfterApproval approval-gate round-trip test.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…dary Every /agents/:id route and the WS /agents/:id/stream upgrade handler interpolate the id directly into a filesystem path (fsAgentStore.ts, checkpointStore.ts, approvalStore.ts) with no sanitization. Express URL-decodes route params before handing them to the app, so a percent-encoded id like ..%2F..%2Fpwned arrives containing literal "/" and ".." segments, letting path.join() escape .loushy/agents/ entirely (e.g. PUT /agents/..%2F..%2Fpwned writing an attacker-chosen .yaml file outside the intended directory). Add isValidAgentId() (types.ts) restricting ids to a safe single-segment token, enforced once in app.ts's /agents/:id middleware and again in wsServer.ts's upgrade handler (which parses :id itself, outside Express routing). Adds a regression test covering the traversal attempt via the HTTP API. Co-Authored-By: Claude Sonnet 5 <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
apps/agent-forge/server/**runtime control server (Express +ws):POST/GET /agents/:id(save/load spec),POST /agents/:id/run,POST /agents/:id/stop,GET /agents/:id/status,POST /agents/:id/approve,WS /agents/:id/stream.RunManager(runRegistry.ts) is an in-memory registry of liveAgentExecutor.execute()runs keyed by agent id / stablesessionId, built entirely on the SDK's public@loushy/build-ai-agentexport surface (AgentExecutor,ToolRegistry,resumeAfterApproval,resolveSpecProvider,resolveSpecTool,AgentBuilder).abortableProvider.tswraps the run'sLLMProviderwith anAbortController/AbortSignalcheck aroundgenerate()/stream(), sinceAgentExecutor.execute()has no built-in cancellation token.stop()is a safe no-op if nothing is running, idempotent if called twice, and no-ops once a run has already completed (controller is cleared on settle).sessionIdstays stable across stop -> run for the same agent id, andAgentExecutoronly clears its checkpoint on successful completion (confirmed directly insrc/execution/AgentExecutor.ts) - so astop()followed byrun()resumes from the last checkpoint rather than restarting. Covered by a real, non-tautological test inrunRegistry.test.tsthat aborts mid-run right after a tool-result event and asserts the checkpoint survives and is picked back up.status: 'paused', reason: 'awaiting_approval', pendingApproval;POST /agents/:id/approvecalls the SDK's publicresumeAfterApproval(). Round-tripped inapprovalFlow.test.tsat the SDK-call level (a customneedsApprovalToolDescriptor, since none of the spec-resolvable built-in tools set that flag) - not exercised end-to-end through the HTTP API in this PR; flagged as a documented follow-up rather than a blocker, since the underlying resume path (src/execution/resume.ts) and its safety invariants (stale checkpoint deleted before the follow-upexecute()call, so it can never rehydrate pre-pause state) were read directly and check out.loushy studio(src/cli/studio.ts, wired intobin/loushy.js, new tsup entrycli/studio) spawns the API server and the Vite dev server as sibling child processes; Vite's dev proxy forwards/agents/**(HTTP + WS) to the API server.Review fix (this branch, on top of the original implementation)
Found and fixed a path-traversal bug during review: every
/agents/:idroute and theWS /agents/:id/streamupgrade handler interpolated:iddirectly into a filesystem path (fsAgentStore.ts,checkpointStore.ts,approvalStore.ts) with no sanitization. Express URL-decodes route params before exposing them, so a percent-encoded id like..%2F..%2Fpwnedarrives containing literal/and.., lettingpath.join()escape.loushy/agents/. AddedisValidAgentId()enforced inapp.ts's route middleware andwsServer.ts's upgrade handler, plus a regression test inapp.test.ts.Independently verified
abortableProvider.ts) and its race-condition handling (double-stop, stop-after-natural-completion, stop-with-nothing-running).src/execution/AgentExecutor.ts.resumeAfterApproval()'s safety invariants, read directly insrc/execution/resume.ts.buildAgent.tsonly imports from the SDK's public.export (verified againstpackage.json'sexportsmap andsrc/index.ts).127.0.0.1by default (not0.0.0.0).test:coverage-> 74 files / 753 passed / 3 skipped; app 56/56 (+1 new); server 18/18 (+1 new) = 19/19.Test plan
npm run build/npm run lint/npm run typecheck(root) - cleannpm run test:coverage(root) - 74 files / 754 passed / 3 skippedapps/agent-forge:npx vitest run- 56/56apps/agent-forge:npm run test:server- 19/19 (incl. new path-traversal regression test)apps/agent-forge:npm run lint/npm run typecheck/npm run typecheck:server- clean🤖 Generated with Claude Code