Phase 7: machines, the machine ports, Windows, attachments as blobs #13
Loading…
Reference in a new issue
No description provided.
Delete branch "refactor/phase-7-machines"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Phase 7 of the DDD/CQRS refactor: Kanbot knows which machine it runs on and which machine each project lives on. Everything that touches a machine goes through ports, attachments become content-addressed blobs, and the machine-specific code has Windows versions. Stacked on #12.
What changes
Machines (
packages/core/src/machines)machinestable. Its id is a uuid kept in the data folder (machine-id), so it survives host renames and reinstalls.projects.machine_id); existing projects are put on this one.Machines.listandMachines.setCapacityare served over RPC.machines.max_concurrent, or the setting when that is empty. With one machine, scheduling is exactly as before, and the characterisation tests pass unchanged.Ports (all machine-touching code now goes through one of these)
HostOS, the machine the user sits at: open in editor or terminal, open a URL or folder, pick a folder or app, list launchers, show notifications.MachineWorkspaces.for(machineId), a project's folders: inspection, git state, worktrees, the setup command,.kanbot/settings.json, lessons, skills and MCP config. It answersMachineUnavailablefor a machine this server can't reach. It takes and returns plain data, so a node can implement it in Phase 10. It replaces the two ports that were both namedWorkspace.prepareno longer writes to the database. It reports what it did, and the hub records the timeline entry,tasks.workdirand the newtask_workspacesrow (machine, mode, branch). Readers that need a task's folder useworkdirOf, which derives the path on its machine.The local machine (
server/kanbot/machines/local)ProcessRunner, with an argument list and never a shell string./bin/sh -con macOS and Linux,cmd.exe /d /s /con Windows.platform/{darwin,win32,linux}.ts.launch.ts,server/agent/git.tsand the old notification helper are gone.Windows
C:\repo→C:\repo.worktrees\<branch>. The home directory comes fromos.homedir().cmd.exe, which is how npm scripts run. PowerShell's execution policy can refuse npm's shims.classifyCommand) now recognises the Windows forms of the same risks:del /s,rd /s,Remove-Item -Recurse,iwr | iex,-EncodedCommand,reg,schtasks,Set-ExecutionPolicy, secrets in%…%or$env:. These rules only match where a command starts, sonpm startis not caught asstart.Attachments are blobs
blobs/<aa>/<sha256>(task_attachments.blob_id), and every write goes throughsaveAttachment.attachments/<task>/, because that is where agents read them.Routes: 11 REST routes became the
Host.*andWorkspaces.*RPC groups.POST /api/projects/[id]/openstays, as a thin route over the port, because Electron calls it.LEGACY_API_ROUTESis down from 5 to 2.Checks
path.win32: worktree paths, the shell's argument list, launchers, picker scripts and the command guard.git initrepo, git state, and the.kanbot/settings.jsonround trip all worked. Acustomlauncher was refused by the contract.desktop-prepare --platform=win32pluselectron-builder --win --x64 --dirbuiltwin-unpacked/Kanbot.exe, with the win32 better-sqlite3 prebuild included.Not yet verified
<repo>.worktrees\<branch>.CLAUDE_CODE_PATHpointing at a.cmdshim would fail on Windows, because Node can't run a.cmdfile without a shell. Leave it unset so the SDK uses its own bundled CLI..kanbot/settings.jsonon the hub to label their buttons.makeCtxstops with an error, instead of marking the session unreachable, when a project's machine can't be reached. With one machine this can't happen.ctx.process.execstill spawns programs itself, with its permission check, instead of going throughProcessRunner.🤖 Generated with Claude Code
packages/{core,contracts,infra-sqlite,plugin-sdk} and plugins/* become workspaces (@kanbot/core, @kanbot/contracts, @kanbot/infra-sqlite, @kanbot/plugin-sdk, @kanbot/plugin-<id>), each with the dependencies it imports and nothing else: effect pinned at the repo's exact version, the workspace packages as "*" (npm links a workspace whose version satisfies the range; it has no workspace: protocol). Only the SDK is not private. The plugins take effect and the SDK as peers: the host supplies the one copy of each, which is also what a prebuilt plugin bundle leaves external. zod and the Claude Agent SDK leave the root for @kanbot/plugin-claude-code, the only code that imports them. A plugin's package version is its manifest's. Each package's exports point at its TypeScript sources: the app, Nitro, vitest and Storybook compile them as they did, through the workspace symlinks. Imports still use relative paths; the next commit switches them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Cookies are not scoped by port. The packaged app set its per-launch cookie (kanbot_local) on 127.0.0.1, and after the tray's "Open in browser" the system browser had it too: it then sent it to every server on 127.0.0.1, whatever the port, for instance a dev server an agent started from a cloned repository's code, which could log it (and drive Kanbot with it) or overwrite it with a Set-Cookie of its own. The shell now makes a name for this machine once per install, kanbot-<16 hex>.localhost (kept 0600 in the user-data folder), and the window, its cookie and the browser handoff all use it; the shell's own REST calls and boot poll stay on 127.0.0.1 with the Bearer header. The name goes to the server with the token over the message port (it is not secret, but must be known), and hostIsLoopback accepts exactly that name besides the loopback names; the server takes no name the shell could not have made (/^kanbot-[a-z0-9]{12,32}\.localhost$/). Nothing changes in how the server listens: it still binds 127.0.0.1. Browsers resolve *.localhost to this machine by themselves (Chromium, Firefox), and Safari asks macOS's resolver, which does too (getaddrinfo answers 127.0.0.1 and ::1 here), so the handoff needs no fallback to 127.0.0.1. Checked in headless Chrome against a server bound to 127.0.0.1: the name resolves, and a cookie set under it is not sent to 127.0.0.1 or localhost on another port. Remaining: the cookie still goes to the same name on another port, so a program that reads the name from the user-data folder and gets the browser to open it on its own port could still read the cookie; it is one more step than before, not a wall. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>vue-i18n reads @ as a linked message, so plugins.problem.noMetadata and undecorated failed to compile and took the dev page down with them. They are reworded rather than escaped as {'@'}, which the server's translate() would print verbatim. A test compiles every message, the plugins' included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Any code from Settings › Devices could pair a node, so a code offered for someone's phone could become a node the hub sends prompts to. An offer is now for a node or, by default, for a browser or a script (Devices.offerPairing takes an optional { kind }; no payload still works), and redeeming it as the other kind fails like a wrong code (it counts towards burning it; every failure is the same PairingRefused / 401). Settings › Devices' pairing card has a "For a node" switch: it asks for a node's code and shows the kanbot-node pair command to run there instead of the address and QR code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>A code offered for a desktop (Devices.offerPairing({ kind: 'desktop' })) pairs two devices in one transaction: the window's browser device (its token is the kanbot_device cookie, Secure over HTTPS) and the node it runs (its token in the answer: { deviceId, nodeDeviceId, nodeToken }). devices.companion_id (migration 0022) points each at the other. Revoking either revokes both and cuts both off. A code for another kind fails like a wrong one. Settings › Devices offers the three kinds and shows a desktop as one entry. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Over the utility process's parent port ({ type: 'kanbot-shell', action: 'open-connect' }), only from the desktop app's own window (a Local caller) and only when the shell started the server to talk over that port; anything else is ShellUnavailable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>