AW-11 — Agent computers · Task breakdown
Ordered, executable tasks derived from
plan.md. Each carries explicit file paths and a definition of done. Every task ships with its tests (Constitution VI). Work top to bottom; tasks marked(parallel)may run alongside the task immediately above them.
Feature ID: aw-11-agent-computers
Spec: ./spec.md · Plan: ./plan.md
Status: Draft
Last updated: 2026-09-06
How to use
- All paths are repo-relative to the monorepo root.
- Run everything with
pnpmfrom the root unless a task says otherwise. - Migrations are authored from
apps/api/; nothing is run by hand on deploy — the API self-applies pending migrations on boot. - Test runners differ per package:
packages/agentandapps/apiuse Jest;apps/node,packages/contracts,packages/plugins/*andapps/webunit use Vitest;apps/webe2e uses Playwright. - Add new tasks at the bottom; never renumber.
- Commit style:
feat(computer): …,feat(fleet): …,test(computer): …,chore(i18n): ….
PHASE 1 — Watch
P1.A — Contracts
-
T1. Extract the secret scanner into the zero-dependency contracts package. - Create
packages/contracts/src/secret/secret-patterns.tsholding, moved verbatim frompackages/agent/src/utils/secret-scan.ts: the pattern table,SecretMatch,scanForSecrets,containsSecret,redactSecrets. Keep every length floor and every comment explaining why a pattern is conservative — they are the reason it has a low false positive rate. - Createpackages/contracts/src/secret/index.tsand export it frompackages/contracts/src/index.ts. - Rewritepackages/agent/src/utils/secret-scan.tstoexport { ... } from '@ever-works/contracts'for the four moved symbols and keep onlyassertNoSecrets(the one function that importsBadRequestException). No caller changes — the agent package's export surface is identical (Constitution X). - Test: createpackages/contracts/src/secret/__tests__/secret-patterns.spec.ts(Vitest) porting the existing agent-package cases; keep the agent-package spec passing untouched. - Done when:pnpm --filter @ever-works/contracts testandcd packages/agent && pnpm test -- secret-scanare both green, andgrep -rn "scanForSecrets" packages/agent/src | wc -lis unchanged. -
T2. Add the computer frame protocol to contracts.
- Create
packages/contracts/src/computer/computer-frame.types.ts. Mirror the invariants documented at the top ofpackages/contracts/src/terminal/terminal-frame.types.ts(size-capped before parse, null-never-throw, direction-mapped kinds, normalized field-by-field construction) and state them in the same header form.- Constants:
COMPUTER_MAX_FRAME_BYTES = 512 * 1024,COMPUTER_MAX_BATCH_FRAMES = 8,COMPUTER_MAX_BATCH_BYTES = 512 * 1024,COMPUTER_MAX_ERROR_MESSAGE_LENGTH = 8192,COMPUTER_MAX_AUTH_TOKEN_LENGTH = 4096,COMPUTER_MAX_TEXT_LENGTH = 4096. COMPUTER_QUALITY_PRESETS— frozen record ofsharp {width:1280,maxFps:8,keyframeMs:5000,q:70},smooth {width:960,maxFps:15,keyframeMs:5000,q:55},steady {width:800,maxFps:2,keyframeMs:2000,q:45}.COMPUTER_CLOSE_REASONS— the closed set from spec §5.2.COMPUTER_CONTROL_RELEASE_REASONS—given-backidledisconnectedceilinghanded-overrevokedsession-ended.- Frame interfaces:
ComputerScreenFrame(kind:'frame',seq,keyframe,width,height,mime,database64),ComputerTerminalFrame(kind:'terminal', wrapping aTerminalFramefrom the sibling module — do not redefine the terminal protocol),ComputerModeFrame,ComputerStatsFrame,ComputerErrorFrame,ComputerEndFrame,ComputerAuthFrame,ComputerPointerFrame,ComputerKeyFrame,ComputerTextFrame,ComputerScrollFrame,ComputerQualityFrame,ComputerRefreshFrame,ComputerControlFrame. - Direction maps:
COMPUTER_CLIENT_TO_SERVER_KINDS,COMPUTER_SERVER_TO_CLIENT_KINDS,COMPUTER_NODE_TO_SERVER_KINDS.
- Constants:
- Create
packages/contracts/src/computer/computer-frame.codec.ts— hand-rolled encode/decode/normalize with the same base64 canonicality gate and the sameisRecord/isBoundedInthelper style as the terminal codec. Never throws. - Create
packages/contracts/src/computer/index.ts; export frompackages/contracts/src/index.ts. - Test:
packages/contracts/src/computer/__tests__/computer-frame.codec.spec.tsand.../quality-presets.spec.tsperplan.md§10.1. - Done when:
pnpm --filter @ever-works/contracts testis green and the codec has a case proving an inboundframeand an outboundpointerare both rejected by the direction map.
- Create
-
T3. Add the session view types to contracts. (parallel)
- Create
packages/contracts/src/computer/computer-session.types.ts:ComputerSessionStatus,ComputerChannel,ComputerQuality,ComputerSessionView,ComputerControlSpan,ComputerNodeOption(withwatchable: booleanandunwatchableReason: ComputerUnwatchableReason | null),COMPUTER_UNWATCHABLE_REASONS(offlinepauseddisableddrainingno-displayno-browserno-terminalnot-attendedcluster),servableChannels: ComputerChannel[]onComputerNodeOption,NodeAgentProfileView. - Done when: every string union has a
readonlyarray constant beside it and aisX(value: unknown)guard, matching the house pattern inpackages/contracts/src/fleet/fleet-jobs.types.ts.
- Create
-
T4. Extend the fleet contracts — job kind, audit actions, capability tags.
packages/contracts/src/fleet/fleet-jobs.types.ts: add'computer-session'toFleetJobKind(line ~104) andFLEET_JOB_KINDS(line ~107).FLEET_JOB_DEFAULT_QUEUED_MAX_AGE_SEC(line ~191) is aReadonly<Record<FleetJobKind, number>>and will not compile without a new entry — set it to40, matching spec FR-8.packages/contracts/src/fleet/fleet-panic.types.ts: add the eightcomputer.*members toFleetAuditAction(line ~60) andFLEET_AUDIT_ACTIONS(line ~96), in the same order asplan.md§9.1.packages/contracts/src/fleet/fleet-node.types.ts: addscreen,input,attendedto the known capability tags; addcontrolPolicy,recordWatchSessions,recordingRetentionDaysand an optionalcontrolHolder { userId, since, expiresAt }toFleetNodeView.- Test: extend
packages/contracts/src/fleet/__tests__/fleet-jobs.spec.tsand.../fleet-panic.spec.tsperplan.md§10.1. - Done when:
pnpm --filter @ever-works/contracts testandpnpm --filter @ever-works/contracts type-checkare green, and nothing else in the repo fails to compile because of the new record entry.
P1.B — Schema and domain model
-
T5. Add the
ComputerSessionentity.- Create
packages/agent/src/entities/computer-session.entity.tswith the columns inplan.md§3.1. UsePortableDateColumnfrom./_types(aspackages/agent/src/entities/fleet-node.entity.tsdoes), rawuuidcolumns rather than@ManyToOneforagentId/nodeId/runId(FKs are added by the migration, not the decorator — the same ruleagent-action-proposal.entity.tsdocuments), and plainvarcharfor every string union. - Class-level indexes:
idx_computer_sessions_user_status(userId,status),idx_computer_sessions_node_status(nodeId,status),idx_computer_sessions_agent(agentId,createdAt),idx_computer_sessions_run(runId). - Register the entity wherever
FleetNodeis registered (packages/agent/src/entities/index.tsand the fleet/agent TypeORM feature arrays). - Done when:
pnpm --filter @ever-works/agent buildis clean and every column carries a doc comment naming what writes it.
- Create
-
T6. Add the
NodeAgentProfileentity. (parallel)- Create
packages/agent/src/entities/node-agent-profile.entity.tsperplan.md§3.1.profileKeyis an opaque id the Node maps to a directory — document explicitly that the platform never stores a filesystem path. - Unique index
(nodeId, agentId); index(userId, agentId). - Done when: the package builds and the doc comment states the isolation guarantee.
- Create
-
T7. Add the control-lock and policy columns to
FleetNode.- Modify
packages/agent/src/entities/fleet-node.entity.ts: addcontrolPolicyvarchar(24)default'owner',recordWatchSessionsboolean defaultfalse,recordingRetentionDaysint default14,controlHolderUserIduuid null,controlHolderSessionIduuid null,controlHeldSince/controlExpiresAtPortableDateColumn({ nullable: true }). - Export
FleetNodeControlPolicyas a string union with itsreadonlyarray beside the existingFleetNodeKind/FleetNodeStatusunions in the same file. - Done when: the package builds and the header comment explains that the four
controlHolder*columns are a CAS lock, not a cache.
- Modify
-
T8. Author and hand-review migration A.
- From
apps/api/:pnpm typeorm migration:generate -d typeorm.config.ts src/migrations/CreateComputerSessions - Land it as
apps/api/src/migrations/1791110000000-CreateComputerSessions.ts(AW-11 slot 00, README §5 rule 10), followingapps/api/src/migrations/1789000000000-AddFleetCredentialRotation.tsfor shape. - Hand-check: two
CREATE TABLE, sevenADD COLUMNonfleet_nodes, sixCREATE INDEX, the FKs onagentId/nodeId/runId/userId. NoDROP, noALTER … TYPE, noNOT NULLwithout a default.downdrops only whatupadded, in reverse. - Done when: it applies on a fresh database and again on a database with data, and a
re-run of
migration:generateafterwards produces an empty diff.
- From
P1.C — The session domain service
-
T9. Create the computer domain module skeleton.
- Create
packages/agent/src/computer/withindex.ts,computer.module.ts,computer-session.repository.ts,node-agent-profile.repository.ts. - Export the sub-path
@ever-works/agent/computerfrom the package'sexportsmap, matching how@ever-works/agent/agentsis exported. - Done when:
pnpm --filter @ever-works/agent buildemits the new sub-path's declarations.
- Create
-
T10. Write the pure session-policy functions.
- Create
packages/agent/src/computer/computer-session.policy.tsexporting, with no TypeORM / NestJS imports:resolveWatchability(node, opts): { watchable: boolean; reason: ComputerUnwatchableReason | null; servableChannels: ComputerChannel[] }— precedence exactly as spec §4.10: cluster → disabled → paused → draining → offline → not-attended → per channel (no-browser→no-displayremove onlyscreen;no-terminalremoves onlyterminal) → watchable when at least one channel remains (spec FR-4a, FR-70).requiredCapabilitiesForChannels(channels)—attendedalways,screenonly for the screen channel,terminalonly for the terminal channel (plan §2.5). The single source of the job'srequiredCapabilities; nothing else builds that list.resolveQuality(requested, stored): ComputerQuality(defaultsharp).shouldDegrade(stats, sinceMs)/shouldRecover(stats, sinceMs)— the 3-frame backlog and 1500 ms ack thresholds over a 5 s window, recovery after 30 s.stallState(lastFrameAt, now)→ok|stalled|auto-refresh|deadat the 6 s / 20 s / 45 s boundaries.SESSION_LIMITS—perNode: 2,perOrganization: 5,maxDurationMs: 4 h,noViewerMs: 30 min,lastViewerGraceMs: 15 s,claimTimeoutMs: 40 s, each with a documented clamp.
- Test:
packages/agent/src/computer/__tests__/computer-session.policy.spec.ts— a truth table over every watchability branch and every threshold at ±1 ms, and every channel combination ofrequiredCapabilitiesForChannels(a terminal-only result never containsscreen). - Done when: the file imports nothing but types from
@ever-works/contracts.
- Create
-
T11. Write
ComputerSessionService.- Create
packages/agent/src/computer/computer-session.service.ts.open(userId, agentId, input)— verify Agent ownership, resolve the Node (affinity binding first, viaFleetAgentNodeAffinityService), refuse on kill switch (FleetKillSwitchService, already exported bypackages/agent/src/fleet/fleet.module.ts), refuse on node state, enforceSESSION_LIMITS, insert the rowrequested, then enqueue the fleet job (T13).markLive/markStalled/close(reason)— the state machine from spec §5.2.bindRun(sessionId, runId)— set once, never re-bound.listForNode/getForUser— owner-scoped; a foreign id resolves tonullso the controller can 404 identically to unknown.
- Every refusal is a typed return value or a named error, never a bare throw, following
TerminalSessionLauncher's refusal-value posture (packages/agent/src/agents/terminal-session-launcher.service.ts). - Test:
packages/agent/src/computer/__tests__/computer-session.service.spec.tsperplan.md§10.2. - Done when: every close reason in
COMPUTER_CLOSE_REASONShas a test that produces it.
- Create
-
T12. Write
NodeAgentProfileService.- Create
packages/agent/src/computer/node-agent-profile.service.ts:ensure(nodeId, agentId)(lazy create, returns theprofileKey),recordSelfReport(...),get(...),reset(userId, nodeId, agentId, confirmAgentName)— refuses with a named value when afleet_jobsrow for that Agent on that Node isleasedorrunning. - Test:
packages/agent/src/computer/__tests__/node-agent-profile.service.spec.ts— lazy creation, one profile per pair, reset refusal under a live lease, name-confirmation mismatch refused. - Done when: the reset path writes an audit row (T14) and never touches another pair.
- Create
-
T13. Enqueue the
computer-sessionfleet job.- Create
packages/agent/src/computer/computer-session.dispatcher.ts— a thin wrapper that builds the payload (sessionId,agentId,userId,nodeId,profileKey,channels,quality,internalBaseUrl) and enqueues through the node job-runtime plugin'sNodeDispatcherFactory, exactly asapps/api/src/fleet/fleet-agent-task.dispatcher.tsdoes foragent-task— not by callingFleetJobServicedirectly, so idempotency, capability tags and lease-TTL mapping follow the sameJobEnqueueOptionssemantics. requiredCapabilities: requiredCapabilitiesForChannels(session.channels)(T10) — derived from the requested channels, never a fixed list: a screen session requires['attended','screen'], a terminal-only session['attended','terminal'], both channels all three. The lease matcher requires every listed tag, so a fixedscreenwould lock display-less Nodes out of terminal-only sessions (plan §2.5). Lease TTL 120 s.- Test:
packages/agent/src/computer/__tests__/computer-session.dispatcher.spec.ts— payload shape, required capabilities per channel combination (terminal-only has noscreen),targetNodeIdalways set (a session must never be claimed by a different machine). - Done when: a session job can only ever be leased by the node it named.
- Create
-
T14. Extend the fleet audit writer with the eight computer actions.
- Modify
packages/agent/src/fleet/fleet-audit.service.tsonly where a new helper is genuinely needed; the eight actions are new values, not a new writer. - Naming rule (this bites):
REDACTED_KEY_REin that file drops any value whose key containssecret|token|credential|password|passphrase|hash|apikey|api_key. Name the audit fields for what they mean —profileRef, notprofileKeyHash. - Test: extend
packages/agent/src/fleet/__tests__/fleet-audit.service.spec.ts— each new action writes a row; adetailsblob carrying a frame, a typed value or a selector is rejected by the test's own assertion set. - Done when: all eight actions round-trip and none carries a value that could be a secret.
- Modify
P1.D — API
-
T15. Create the computer API module.
- Create
apps/api/src/computer/computer.module.ts, import it inapps/api/src/api.module.tsbesideTerminalModule(imported at line 61, mounted at line 217), with a comment in the same house style as its neighbours saying what it is. - Apply
FleetEnabledGuardfromapps/api/src/fleet/guards/at the controller level so the whole surface disappears withFLEET_ENABLED=false, matchingFleetController. - Done when:
pnpm --filter ever-works-api buildis clean and the API boots with the module mounted.
- Create
-
T16. Write
ComputerController(owner-facing).- Create
apps/api/src/computer/computer.controller.ts—@Controller('api/agents/:id/computer'), every route inplan.md§4.1. - Authorization: copy the
authorizeRunshape fromapps/api/src/terminal/terminal-attach.controller.ts—AgentsService.getOnethen a user-scoped Node lookup; a cross-user Agent or Node 404s identically to an unknown one. - Throttles:
@Throttleat 10/min on session open, 20/min on control, 30/min on refresh. - DTOs under
apps/api/src/computer/dto/withclass-validatordecorators; response DTOs use@Exclude()/@Expose()so no entity leaks. - Test:
apps/api/src/computer/computer.controller.spec.tsperplan.md§10.3. - Done when: every refusal in spec §4 has a controller-spec case asserting its status code and that its message names the reason.
- Create
-
T17. Write
ComputerInternalController(node-facing).- Create
apps/api/src/computer/computer-internal.controller.ts—@Controller('api/internal/computer'),@Public(), guarded by the existingFleetNodeAuthGuard(apps/api/src/fleet/guards/fleet-node-auth.guard.ts) sodisabledandenrollingnodes are refused at the edge with the same undifferentiated 401. - Routes per
plan.md§4.3. Enforce the batch caps before decoding. - The
stepsroute re-runsredactSecretson every string server-side even though the Node already scanned — belt and braces, and the only line of defence against a modified Node. - Test:
apps/api/src/computer/computer-internal.controller.spec.tsper §10.3. - Done when: a node authenticated for node A cannot publish into a session belonging to node B, and the refusal is the same 401 as an unknown credential.
- Create
-
T18. Write the relay registry.
- Create
apps/api/src/computer/computer-relay.registry.ts, modelled onapps/api/src/terminal/terminal-relay.registry.ts: per-session registry, one retained keyframe (not a rolling scrollback), retained pre-attacherrorbanners, a pinnedendframe replayed last, seq dedupe, role-checked inbound fan-out, and the same reclaim rule (no clients AND ended AND at least one attach saw the keyframe). - Test:
apps/api/src/computer/computer-relay.registry.spec.tsper §10.3. - Done when: a re-attaching viewer always gets a picture or an explicit banner, never a blank stage.
- Create
-
T19. Write the attach service and the WebSocket gateway.
- Create
apps/api/src/computer/computer-attach.service.ts— short-lived signed tokens with a role (viewer|controller|node); a request may downgrade itself, never upgrade (copy the rule and its comment fromapps/api/src/terminal/terminal-attach.service.ts). - Create
apps/api/src/computer/computer-ws.service.ts— rawwson the API HTTP server'supgradeevent at/ws/computer/:sessionId. Refuse any query string on the upgrade (the token rides the first frame, never the URL). Close4001after 5 s unauthenticated; 30 s ping, two missed pongs reaps. No socket.io. - Test:
apps/api/src/computer/computer-ws.service.spec.tsper §10.3. - Done when: a
pointerframe from aviewersocket is answered with anerrorframe and never forwarded to the node leg.
- Create
-
T20. Extend the fleet heartbeat response.
- Modify
apps/api/src/fleet/fleet.controller.ts(and the service behind it) so the heartbeat response carries an optionalpendingComputerSessions: string[]. Older nodes ignore it. - Test: extend
apps/api/src/fleet/fleet.controller.spec.ts— the field is present when a session is pending for that node and absent otherwise; the response shape is otherwise byte-identical. - Done when:
apps/web/e2e/flow-fleet-enrollment-contract.spec.tsstill passes unchanged.
- Modify
P1.E — Node
-
T21. Add the
--attendverb and the attended fast poll.- Modify
apps/node/src/cli/program.ts: add--attendtostart, documented as "allow live viewing of this machine from the dashboard", independent of--work. - Modify
apps/node/src/core/worker-loop.ts: when attended, run a dedicated interactive lease poll at 2000 ms (clamp 500–10000) withkinds:['computer-session']and batch 1, backing off to 15000 ms after 10 consecutive empty polls with no session in the previous 10 minutes and returning to fast on any heartbeat that carriespendingComputerSessions. - Modify
apps/node/src/core/heartbeat.tsto surface that hint to the loop. - Test: extend
apps/node/src/core/worker-loop.spec.tsandapps/node/src/core/heartbeat.spec.tsperplan.md§10.4. - Done when: a node started without
--attendnever polls the interactive lane, and one started without--workstill does.
- Modify
-
T22. Advertise the new capability tags.
- Modify
apps/node/src/core/capabilities.ts: addscreen(only whenapps/node/src/core/browser-probe.tsresolves a binary — the same probe the capture will launch, per that file's own rule),input(screen + a display or a headed browser), andattended(only under--attend). - Test: extend
apps/node/src/core/capabilities.spec.ts—screenis never advertised on a machine where the probe finds nothing. - Done when: no tag is advertised that has no executor behind it.
- Modify
-
T23. Write the per-Agent profile manager.
- Create
apps/node/src/core/screen/agent-profile.ts: resolves a directory per(nodeId, agentId)under the node's data root, creates it lazily, returns the opaqueprofileKey, reportssignedInSiteCountanddiskBytes, and implementsresetas a delete-and-recreate of exactly one directory. Owner-only ACL on Windows via the existingicaclshelper inapps/node/src/node-io.ts. - All IO injected, as every other module in
apps/node/src/core/is. - Test:
apps/node/src/core/screen/agent-profile.spec.tsper §10.4. - Done when: resetting one Agent's profile provably leaves a sibling's untouched.
- Create
-
T24. Write the capture pump.
- Create
apps/node/src/core/screen/capture-pump.ts: launches (or attaches to) the Agent's own browser with its profile directory, starts a CDP screencast, encodes frames per the resolved quality preset, batches them at ≤ 8 frames / 512 KiB, publishes outbound toPOST /api/internal/computer/:sessionId/frames, and honoursrefresh(force keyframe),setQuality, andstop. - Degrade/recover logic reads the thresholds from
COMPUTER_QUALITY_PRESETSand the policy inpackages/agent/src/computer/computer-session.policy.ts's exported constants — do not re-declare the numbers here. - Test:
apps/node/src/core/screen/capture-pump.spec.tsper §10.4. - Done when: three consecutive failed keyframes restart the pump without ending the session, and a test proves the batch caps are never exceeded.
- Create
-
T25. Write the
computer-sessionexecutor.- Create
apps/node/src/core/executors/computer-session.ts: claims the job, ensures the profile, starts the capture pump, opens the node's own inbound WebSocket leg using aworker-tokenbrokered by session id, keeps the lease alive at 1/3 TTL, drains gracefully on stop/pause honouringLEASE_TERMINATION_SAFETY_MS, and completes with a verdict. - Register it in
apps/node/src/core/runtime.tsbeside thebrowser-checkregistration (line ~493) and only when the machine advertisesscreen— the same conditional registration patternbrowser-checkuses. - Export it from
apps/node/src/core/index.tsalongside the other executors. - Test:
apps/node/src/core/executors/computer-session.spec.tsper §10.4. - Done when: a drain during a live session ends it cleanly with
node-unavailablerather than letting the lease lapse.
- Create
P1.F — Web
-
T26. Add routes, tab and hero action.
- Add
DASHBOARD_AGENT_COMPUTER,DASHBOARD_AGENT_COMPUTER_RECORDINGandDASHBOARD_AGENT_DEMONSTRATIONtoapps/web/src/lib/constants.ts, besideDASHBOARD_AGENT_TERMINAL(line 189). - Add a
computerentry to the tab array inapps/web/src/components/agents/AgentDetailTabs.tsx, immediately afterterminal. - Add the Watch computer button to the hero action row in
apps/web/src/app/[locale]/(dashboard)/agents/[id]/page.tsx, second after Message. - Gate both behind
isFleetEnabled()fromapps/web/src/lib/fleet-flags.ts. - Test:
apps/web/src/components/agents/AgentDetailTabs.unit.spec.tsx(new) — the tab is present when fleet is on and absent when off. - Done when: the tab and the button both reach the new route.
- Add
-
T27. Write the shared client policy module.
- Create
apps/web/src/components/computer/computer-session.shared.ts— pure functions only, for the same reasonapps/web/src/components/agents/agent-fleet.shared.tsexists: node ordering, watchability + its reason string key, quality resolution and persistence key, control eligibility, stall thresholds, bandwidth formatting. - Reuse
runnerDotClassfromapps/web/src/components/dashboard/runner-status.shared.tsand the node-status strings from thedashboard.runner.nodeStatenamespace so this surface and the sidebar pill can never disagree about what "Paused" means. - Test:
apps/web/src/components/computer/computer-session.shared.unit.spec.tsper §10.5. - Done when: no layout file contains a threshold or an ordering rule.
- Create
-
T28. Write the page and the client shell.
- Create
apps/web/src/app/[locale]/(dashboard)/agents/[id]/computer/page.tsx— a server component that fetches the Agent, the Node list, the affinity binding and the profile in parallel and hands them to the client as props. Defensive.catch()per read so a stale environment cannot 500 the page, following the pattern in the Agent dashboard page. - Create
apps/web/src/components/computer/AgentComputerClient.tsx. - Done when: the page renders every empty/offline/error state from spec §6 without opening a session.
- Create
-
T29. Write the stage, strip, overlay and controls. (parallel)
- Create, under
apps/web/src/components/computer/:ComputerStage.tsx,ComputerIdentityStrip.tsx,ComputerStatusLine.tsx,ComputerBriefOverlay.tsx,ComputerWatermark.tsx,ComputerControls.tsx,ComputerNodePicker.tsx,ComputerProfilePanel.tsx,use-computer-attach.ts. ComputerStagerenders to<canvas>viacreateImageBitmap+drawImage, is a labelled focusable region, and announces its mode through a polite live region.use-computer-attach.tsmirrorsapps/web/src/components/terminal/use-terminal-attach.ts: token from the BFF, first-frame auth, reconnect with backoff, never a token in a URL.- Test: the unit specs listed in
plan.md§10.5 for the picker and the status line. - Done when: keyboard-only operation reaches every control and
?opens the shortcut sheet.
- Create, under
-
T30. Add the BFF routes.
- Create
apps/web/src/app/api/agents/[id]/computer/sessions/route.ts,.../sessions/[sessionId]/attach-token/route.ts,.../sessions/[sessionId]/control/route.ts,.../sessions/[sessionId]/refresh/route.ts, mirroring the existingapps/web/src/app/api/agents/[id]/runs/[runId]/terminal/{attach-token,start,transcript}/route.tsso the attach token is minted server-side and never round-trips through client code. - Done when: no client component holds an API base URL or an attach token before the socket opens.
- Create
P1.G — i18n, background work, tests
-
T31. Add the i18n namespace.
- Add the whole
dashboard.computerblock fromplan.md§8 toapps/web/messages/en.json, plusdashboard.agentsPage.tabs.computer. - Leaf key names are camelCase and must never contain a literal
.—next-intlrejects those at runtime and the hydration spec turns one into a multi-shard e2e failure. - Only
en.jsonis authored; the other 20 locale files fall back to English. - Done when:
grep -c '"[a-zA-Z]*\.[a-zA-Z]*":' apps/web/messages/en.jsonis unchanged from before this task, and no component inapps/web/src/components/computer/contains a user-visible literal string.
- Add the whole
-
T32. Add the session reaper task.
- Create
packages/tasks/src/tasks/trigger/computer-session-reaper.task.ts— cron2/2 * * * *(off the minute boundary, likepackages/tasks/src/tasks/trigger/fleet-job-lease-sweeper.task.ts's3/5). Ends abandoned (40 s unclaimed), stalled (45 s), ceiling-exceeded and viewerless sessions; releases expired control locks. - Register it wherever
fleet-job-lease-sweeperis registered. That task's header warns it must be run withTriggerInternalModuleor it fails silently on every fire — check the same for this one. - Test:
packages/tasks/src/__tests__/computer-session-reaper.task.spec.ts— each sweep branch, and idempotence when two ticks overlap. - Done when: killing the API mid-session still leaves the session
endedwithin 4 minutes.
- Create
-
T33. Write the P1 end-to-end spec.
- Create
apps/web/e2e/flow-agent-computer-watch.spec.tsandapps/web/e2e/flow-agent-computer-contract.spec.ts, named to match the existingapps/web/e2e/flow-fleet-*.spec.tsandflow-terminal-attach-contract.spec.ts. - Cover: the entry button, the surface, the identity strip, the status sentence, the watermark, the node picker with each reason, and the empty / offline / not-attended states.
- Prefer role-and-name locators; avoid
*ByRolechains that are known to be load-sensitive in this suite. - Done when: both specs are green locally and in CI on a cold database.
- Create
-
T34. Update the docs.
- Add a short section to
docs/specs/features/agent-workspace/TRACKER.mdmarking AW-11 P1 spec'd and implemented. - Done when: the tracker row exists and links to this folder.
- Add a short section to
PHASE 2 — Take over and teach
P2.A — Control
-
T35. Write the control arbiter. - Create
packages/agent/src/computer/control-arbiter.service.tsimplementing the CAS lock inplan.md§2.4 as a single owner-scopedUPDATE … WHERE (holder IS NULL OR expires < now()), plusrelease(scoped bycontrolHolderSessionIdso a stale releaser can never evict a newer holder),requestControl,answerRequest,extendOnce, and the idle / ceiling / disconnect sweeps. - Createpackages/agent/src/computer/control-policy.ts— a purecanControl(policy, viewerRole)truth table. - Test:packages/agent/src/computer/__tests__/control-arbiter.spec.tsand.../control-policy.spec.tsperplan.md§10.2. - Done when: two concurrent take-overs produce exactly one winner and the loser reads the real holder. -
T36. Wire control into the controller, relay and WS.
- Add the four control routes from
plan.md§4.1 toapps/api/src/computer/computer.controller.ts. - Teach
apps/api/src/computer/computer-relay.registry.tsto acceptpointer/key/text/scrollframes only from the socket holding control, and to answer any other inbound input with anerrorframe. - Re-check
controlExpiresAtandlastInputAton every inbound input frame and every 30 s ping so the countdown a user sees is second-accurate; the T32 cron is only the floor. - Test: extend
apps/api/src/computer/computer.controller.spec.tsandcomputer-relay.registry.spec.ts. - Done when: a viewer socket can never inject input, verified by a test.
- Add the four control routes from
-
T37. Write the input injector on the Node.
- Create
apps/node/src/core/screen/input-injector.ts— dispatches pointer, key, wheel and text events into the captured browser context. Refuses clipboard payloads, file drops and anything not in the allowed frame set, and refuses everything while control is not held. - Suspend the Agent's own synthetic input to that surface while control is held, and tell the Agent it is paused rather than letting its actions fail silently.
- Test:
apps/node/src/core/screen/input-injector.spec.tsper §10.4. - Done when: a fuzz case of unexpected frame kinds injects nothing and throws nothing.
- Create
-
T38. Build the control UI.
- Add to
apps/web/src/components/computer/: the amber control state onComputerStage, the✋ YOUbadge onComputerIdentityStrip, the take-over / give-back controls, the countdown andKeep controlaffordance, andComputerControlRequestDialog.tsx(both sides, with the 60 s auto-decline). - Keyboard:
Ttakes over;Escape Escapegives back and is the only key intercepted while in control. - Test:
apps/web/src/components/computer/ComputerControlRequestDialog.unit.spec.tsx. - Done when: the mode is legible from the status line alone, with colour disabled.
- Add to
P2.B — Teach
-
T39. Add the demonstration entities.
- Create
packages/agent/src/entities/agent-demonstration.entity.tsandpackages/agent/src/entities/agent-demonstration-step.entity.tsperplan.md§3.1, registered like the P1 entities. - Done when:
pnpm --filter @ever-works/agent buildis clean.
- Create
-
T40. Author and hand-review migration B.
- From
apps/api/:pnpm typeorm migration:generate -d typeorm.config.ts src/migrations/CreateAgentDemonstrations - Land it as
apps/api/src/migrations/1791110100000-CreateAgentDemonstrations.ts(AW-11 slot 01). - Hand-check: two
CREATE TABLE, fourCREATE INDEX, one unique index on(demonstrationId, seq), FKs onagentId/nodeId/sessionId/userId. No drops. - Done when: it applies twice cleanly and re-generation produces an empty diff.
- From
-
T41. Write the demonstration recorder on the Node.
- Create
apps/node/src/core/screen/demonstration-recorder.ts, capturing exactly the step fields in spec FR-57 and enforcing every redaction rule in FR-58:- password fields,
autocompletein{current-password, new-password, one-time-code}, and any field whose accessible name / label / placeholder / name matches the credential pattern → recorded as a named secret requirement withredacted:true, value never read; - clipboard contents never captured;
- query values stripped from recorded paths;
- typed values truncated at 200 characters;
- every remaining string run through
redactSecretsfrom@ever-works/contracts(T1) before it leaves the machine.
- password fields,
- Caps: 200 steps, 15 minutes, 60 screenshots at ≤ 200 KB each.
- Test:
apps/node/src/core/screen/demonstration-recorder.spec.ts— one case per redaction rule, plus a case proving a scanner-tripping value never appears in the outbound payload. - Done when: no test can construct an input that gets a credential off the machine.
- Create
-
T42. Write
DemonstrationService.- Create
packages/agent/src/computer/demonstration.service.ts—start(requires control),appendSteps(server-side re-scan, cap enforcement),finish,cancel,removeStep,redraft,fromRun, and the 30-day expiry. - Test:
packages/agent/src/computer/__tests__/demonstration.service.spec.tsper §10.2. - Done when: every lifecycle transition in spec §5.3 has a test.
- Create
-
T43. Write the synthesis dispatcher and task.
- Create
packages/agent/src/computer/demonstration-synthesis.dispatcher.tsdeclaringDEMONSTRATION_SYNTHESIS_DISPATCHER, modelled exactly onpackages/agent/src/agents/terminal-session-dispatcher.ts: aconststring token, a payload interface, anenqueuereturning{ jobRunId }, and@Optional()at the injection site so an install with no job runtime reports a no-op instead of crashing. - Bind it in
apps/api/src/agents/agents.module.tsbesideTERMINAL_SESSION_DISPATCHER(line 293) with the same@Global()token posture. packages/agent/src/tasks/_tasks-symbols.tspins the runtime-symbol set of the@ever-works/agent/tasksbarrel and its spec fails CI when a new symbol appears there. This token is exported from@ever-works/agent/computer, not/tasks, so no entry is needed — but if it is ever re-exported through/tasks, add it alphabetically or CI goes red one merge late.- Create
packages/tasks/src/tasks/trigger/demonstration-synthesis.task.ts— loads the demonstration and its steps, builds the prompt, calls the Agent's model through the AI facade, writesdraft, creates the approval and the conversation message. 3 attempts with 30 s / 2 min / 10 min backoff; on exhaustion setsynthesis-failedand post the plain failure message. - Test:
packages/tasks/src/__tests__/demonstration-synthesis.task.spec.ts— the retry ladder, that a failure never creates a partial Skill, and that no secret value reaches the prompt. - Done when: the task is registered and a stubbed dispatcher drives it synchronously in tests.
- Create
-
T44. Write the prompt builder as a pure function.
- Create
packages/agent/src/computer/draft-skill-prompt.ts— turns steps into the prompt and parses the model's reply into theDraftSkillViewshape, refusing (rather than guessing) when the reply is malformed. - Test:
packages/agent/src/computer/__tests__/draft-skill-prompt.spec.ts— redacted steps become named secret requirements; a malformed reply is refused; the title is clamped at 80 and the description at 200 characters. - Done when: the builder has no NestJS or TypeORM import.
- Create
-
T45. Add the
adopt_skillapproval kind and the adoption path.- Modify
packages/agent/src/entities/agent-action-proposal.entity.ts: add'adopt_skill'toAgentActionProposalActionTypeandAGENT_ACTION_PROPOSAL_ACTION_TYPES. No migration is needed —actionTypeis avarchar(32)with no check constraint (verified inapps/api/src/migrations/*-CreateAgentActionProposals.ts). - Create
packages/agent/src/computer/draft-skill-adoption.service.ts— on approve, create aSkillatagentowner scope and aSkillBindingto that Agent; on a slug collision, suffix and report the name used; on reject, create nothing and keep the demonstration 30 days. - Test:
packages/agent/src/computer/__tests__/draft-skill.spec.tsper §10.2. - Done when: an existing approval of any other action type is provably unaffected.
- Modify
-
T46. Add the demonstration API routes.
- Create
apps/api/src/computer/demonstrations.controller.tswith every route inplan.md§4.2, plus thestepsroute on the internal controller (T17). - Test:
apps/api/src/computer/demonstrations.controller.spec.tsper §10.3. - Done when: starting without control returns 422 and names the reason.
- Create
-
T47. Build the teach UI.
- Create
apps/web/src/components/computer/TeachTaskDialog.tsx,TeachRecordingStrip.tsx,DemonstrationReview.tsx,DraftSkillCard.tsx, and the demonstration review page atapps/web/src/app/[locale]/(dashboard)/agents/[id]/demonstrations/[demoId]/page.tsx. - The recording strip is
role="status"; the secret-skipped flash isrole="alert". DraftSkillCardrenders inside the conversation and in the approvals queue — one component, two mounts, so the two can never drift.- Test:
TeachTaskDialog.unit.spec.tsxandDraftSkillCard.unit.spec.tsxper §10.5. - Done when: the guard sentence appears verbatim and
Start recordingis disabled without control.
- Create
-
T48. Extend the i18n namespace for P2.
- Add the
control,teach,draftandreviewsub-blocks fromplan.md§8 toapps/web/messages/en.json. - Done when: no P2 component holds a literal user-visible string and no leaf key contains a dot.
- Add the
-
T49. Write the P2 end-to-end specs.
- Create
apps/web/e2e/flow-agent-computer-takeover.spec.tsandapps/web/e2e/flow-agent-computer-teach.spec.tsperplan.md§10.7. - Done when: the teach spec proves a password typed during a demonstration never appears in the resulting draft.
- Create
PHASE 3 — Re-watch and the terminal channel
-
T50. Add the recording segment entity and migration C.
- Create
packages/agent/src/entities/computer-recording-segment.entity.tsperplan.md§3.1 (bytes live in storage; the row holds astorageKey, never bytes). - Add
computerRecordedAtPortableDateColumn({ nullable: true })topackages/agent/src/entities/agent-run.entity.ts, documented as "set once when the first recording segment for a session bound to this run is written; a cheap flag so the receipt needs no join". - From
apps/api/:pnpm typeorm migration:generate -d typeorm.config.ts src/migrations/CreateComputerRecordings - Land it as
apps/api/src/migrations/1791110200000-CreateComputerRecordings.ts(AW-11 slot 02). - Hand-check: one
CREATE TABLE, twoCREATE INDEX, oneADD COLUMNonagent_runs, no drops. - Done when: it applies twice cleanly.
- Create
-
T51. Write the recording service.
- Create
packages/agent/src/computer/recording.service.ts— samples at ≤ 1 fps, writes bytes through the storage backend (getActiveStorageBackendinapps/api/src/uploads/storage-backend.factory.ts, reached the wayapps/api/src/uploads/uploads.service.tsdoes withgetBackend()), enforces 200 MB / 4 h, stampsexpiresAtfrom the Node's clampedrecordingRetentionDays, and degrades without ending the session when storage is unreachable, stampingrecordingSkippedReason. - Test:
packages/agent/src/computer/__tests__/recording.service.spec.tsper §10.2. - Done when: a storage outage never fails a session and always shows up on the receipt.
- Create
-
T52. Add the recording GC task.
- Create
packages/tasks/src/tasks/trigger/computer-recording-gc.task.ts, mirroringpackages/tasks/src/tasks/trigger/terminal-transcript-gc.task.ts. Delete the storage object first, then the row — the reverse order orphans bytes forever. - Test:
packages/tasks/src/__tests__/computer-recording-gc.task.spec.ts— an object whose delete fails leaves its row for the next sweep rather than being lost. - Done when: nothing older than the retention window survives a sweep.
- Create
-
T53. Build the player and wire it into the Run receipt.
- Create
apps/web/src/components/computer/ComputerRecordingPlayer.tsxand the route atapps/web/src/app/[locale]/(dashboard)/agents/[id]/computer/recordings/[sessionId]/page.tsx. - Add the
COMPUTERblock from spec §6.24 to the AW-09 receipt component, gated onagent_runs.computerRecordedAt, with the not recorded variants and their reasons. - The scrubber is a slider with a 1 s keyboard step and a 10 s page step; control spans are marked on the timeline.
- Test: extend the AW-09 receipt unit spec — the block renders, and its three not-recorded variants render their reasons.
- Done when: the receipt renders unchanged for runs with no recording.
- Create
-
T54. Add "Make a Skill from this run".
- Wire
POST /api/agents/:id/demonstrations/from-run/:runId(T46) to the receipt button;409when the run has no stored steps, rendered as a disabled button with a reason. - Done when: the same synthesis path runs with no new demonstration.
- Wire
-
T55. Add the Node-hosted terminal channel.
- Extend the
computer-sessionjob payload withchannelsincludingterminal; inapps/node/src/core/executors/computer-session.tsspawn a local PTY and publishTerminalFrames wrapped in theComputerTerminalFrameenvelope from T2 — reusing the existing protocol inpackages/contracts/src/terminal/terminal-frame.types.ts, not a second one. - In the web stage, delegate the terminal channel to the existing renderer factory at
apps/web/src/components/terminal/create-terminal-renderer.ts. - Add the channel switch to
ComputerControls.tsxand theCshortcut. - Test: extend
apps/node/src/core/executors/computer-session.spec.ts; add a web unit case that the read-only badge is present in watching mode. - Done when: the existing
/agents/[id]/terminaltab andapps/web/e2e/flow-terminal-attach-contract.spec.tsare provably unchanged, and the Node's long-advertisedterminalcapability finally has an executor behind it.
- Extend the
-
T55a. Prove a display-less Node leases a terminal-only session.
- Test (agent, Jest): add
packages/agent/src/fleet/__tests__/fleet-job.computer-session-capabilities.spec.ts— a lease case that enqueues acomputer-sessionjob through the T13 dispatcher withchannels: ['terminal']and leases it as a Node advertising['terminal','workspace','attended'](noscreen, noinput): the job is leased. The same Node leasing a['screen']session gets nothing, and a Node advertising['terminal','workspace'](not attended) gets neither. - Test (node): extend
apps/node/src/core/executors/computer-session.spec.ts— with the browser probe resolving nothing and no display, a terminal-only session spawns the PTY, publishesComputerTerminalFrames and never starts a capture. - Test (web unit): extend
computer-session.shared.unit.spec.ts— that Node is listed as watchable withservableChannels: ['terminal']and the screen channel's no display reason beside it. - Done when: spec FR-4a and its acceptance criterion hold end to end on a headless
machine started with
--attend.
- Test (agent, Jest): add
-
T56. Add the
screen-streamplugin capability.- Create
packages/plugin/src/contracts/capabilities/screen-stream.interface.ts, mirroringterminal-stream.interface.ts:IScreenStreamPluginwithproviderNameandcapture(input, transport), aScreenSessionHandle(setQuality,refresh,sendInput,stop,ended), aScreenTransport, anIScreenStreamFacade, aScreenNotProvisionedErrormatched by name across package boundaries, and anisScreenStreamPluginguard. - Export it from
packages/plugin/src/contracts/capabilities/index.ts(beside line 33) and addSCREEN_STREAM: 'screen-stream'topackages/plugin/src/contracts/facade-capabilities.ts(besideTERMINAL_STREAM, line 86). - Done when:
pnpm --filter @ever-works/plugin buildis clean.
- Create
-
T57. Create the
screen-cdpplugin.- Create
packages/plugins/screen-cdp/— ESM,tsup, Vitest,everworks.pluginblock withcapabilities: ['screen-stream']andplaywright-coreas an optionalDependency, exactly aspackages/plugins/browser-automation/package.jsondoes. - Implement capture over CDP screencast and input over
Input.dispatch*Event. - Test:
packages/plugins/screen-cdp/src/__tests__/screen-cdp.plugin.spec.tsper §10.6. - Add the plugin and the new capability to
docs/plugin-system/built-in-plugins.md— the only doc that carries plugin counts (Constitution VIII). - Done when: the plugin builds, tests pass, and no other doc's plugin count was edited.
- Create
-
T58. Add the screen facade and route the cloud path through it.
- Create
packages/agent/src/facades/screen-stream.facade.tsbesidebrowser-automation.facade.ts, resolving a provider from the settings cascade and forwarding the resolved id as aproviderOverridethe wayTerminalSessionDispatchPayload.providerIdalready does. - Test:
packages/agent/src/facades/__tests__/screen-stream.facade.spec.ts— resolution against a mock plugin, and a case asserting the stringscreen-cdpappears nowhere outside the plugin package (grep -rn "screen-cdp" packages/agent appsmust return nothing). - Done when: Constitution II holds by test, not by convention.
- Create
-
T59. Add per-Node control-policy and recording controls to the Fleet drawer.
- Extend
apps/web/src/components/settings/FleetNodeDrawer.tsxwith the control policy selector, the watch-recording opt-in and the retention-days field, under thedashboard.settings.fleet.controlsnamespace. - Test: extend the fleet settings unit spec; add a case that the retention field clamps 1–90 client-side and that the server clamps independently.
- Done when: a policy change takes effect on the next control attempt with no restart.
- Extend
-
T60. Widen access to Organization members.
- Extend the authorization in
apps/api/src/computer/computer.controller.tsso an Organization member may watch a Node whosecontrolPolicyisorg-adminsororg-members, and may control only when the policy allows their role. Revocation closes the session within 30 s via the reaper (T32). - Test: extend
computer.controller.spec.tswith the full role × policy matrix. - Done when: the default (
owner) behaves exactly as it did in P1 and P2.
- Extend the authorization in
-
T61. Write the P3 end-to-end and accessibility specs.
- Create
apps/web/e2e/flow-agent-computer-a11y.spec.ts— keyboard-only operation of every control plus an axe pass on the surface, the teach dialog and the player. - Extend
flow-agent-computer-watch.spec.tswith the channel switch and the recording player. - Done when: the accessibility sweep is clean and the whole suite is green in CI.
- Create
-
T62. Close out the epic.
- Update
docs/specs/features/agent-workspace/TRACKER.mdto mark AW-11 implemented across all three phases. - Add a short user-facing page under
docs/covering watch, take over and teach, and list it inapps/docs/sidebarsPlatform.tsso it is not an orphan page. - Done when: the tracker and the docs sidebar both reflect the shipped feature.
- Update