Skip to content

Commit 68db926

Browse files
committed
feat: Lima Docker-in-VM backend, remote attach over tailnet, task reporting
Lima backend overhaul: - Boot Docker-ready VM (template:docker), pull OCI image, run workspace container - Stream output during VM creation instead of silent blocking - Prompt to install Lima when limactl is missing (brew on macOS, GitHub release on Linux) - Mount ~/.config/gh into VM for GHCR auth, docker login with gh token - Fix exec to route through docker exec inside VM with binary existence guard - Add -it flags for interactive TTY allocation (cw exec -it -- bash) Remote attach over tailnet: - Serve /ws (frame protocol) alongside /peer (RPC) on tailnet listener - Runtime credential auth for /ws on tailnet connections - cw attach node:session dials remote node over tailnet and attaches via /ws - Extract AttachConn/ResolveSessionArgConn for transport-agnostic attach - wrapLocalRuntimeRunCommand passes -it for PTY propagation through cw run Task reporting, relay improvements, and MCP tool additions.
1 parent edf1aa7 commit 68db926

60 files changed

Lines changed: 6091 additions & 313 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,6 +371,46 @@ Run a relay server. The relay provides SSH gateway access, node discovery, and s
371371
cw relay serve --base-url https://relay.example.com --data-dir /data/relay
372372
```
373373

374+
To enable durable task reporting and `cw tasks`, point the relay at NATS JetStream:
375+
376+
```bash
377+
cw relay serve \
378+
--base-url https://relay.example.com \
379+
--data-dir /data/relay \
380+
--auth-mode token \
381+
--auth-token dev-secret \
382+
--nats-url nats://127.0.0.1:4222
383+
```
384+
385+
Relevant flags:
386+
387+
- `--nats-url` enables JetStream-backed task events
388+
- `--nats-creds` uses a NATS user credentials file
389+
- `--nats-subject-root` defaults to `tasks`
390+
- `--task-events-stream` defaults to `TASK_EVENTS`
391+
- `--task-latest-bucket` defaults to `TASK_LATEST`
392+
393+
Nodes still talk only to the relay. NATS credentials stay private to the relay tier.
394+
395+
### `cw tasks`
396+
397+
List or watch the latest task reports aggregated at the relay.
398+
399+
```bash
400+
# Latest task per (node, session)
401+
cw tasks
402+
403+
# Stream live task events
404+
cw tasks --watch
405+
406+
# Speak new task events when a local TTS backend is available
407+
cw tasks --watch --speak
408+
409+
# Filter by node / session / state
410+
cw tasks --node builder --state working
411+
cw tasks --session 7
412+
```
413+
374414
### `cw mcp-server`
375415

376416
Start an MCP (Model Context Protocol) server for programmatic access.
@@ -856,7 +896,7 @@ claude mcp add --scope user codewire -- cw mcp-server
856896
claude mcp add codewire -- cw mcp-server
857897
```
858898

859-
This exposes 26 tools across sessions, environments, messaging, and shared state:
899+
This exposes tools across sessions, environments, messaging, network state, and task reporting:
860900

861901
**Sessions**
862902

@@ -871,6 +911,7 @@ This exposes 26 tools across sessions, environments, messaging, and shared state
871911
| `codewire_kill_session` | Terminate session (by ID or tags) |
872912
| `codewire_subscribe` | Subscribe to session events |
873913
| `codewire_wait_for` | Block until sessions complete |
914+
| `codewire_report_task` | Report current task status for a session |
874915

875916
**Messaging**
876917

@@ -908,6 +949,33 @@ This exposes 26 tools across sessions, environments, messaging, and shared state
908949
| `codewire_kv_list` | List keys by prefix |
909950
| `codewire_kv_delete` | Delete key |
910951

952+
### Task Reporting Via MCP
953+
954+
Agents can report what they are doing with:
955+
956+
```json
957+
{
958+
"session_id": 7,
959+
"summary": "implement relay task SSE replay",
960+
"state": "working"
961+
}
962+
```
963+
964+
The MCP tool is `codewire_report_task`. It requires:
965+
966+
- `session_id`: the local Codewire session ID
967+
- `summary`: a short, current description of the work
968+
- `state`: one of `working`, `complete`, `blocked`, or `failed`
969+
970+
Recommended agent behavior:
971+
972+
- emit `working` when starting a meaningful new chunk of work
973+
- emit a fresh `working` update when the focus changes materially
974+
- emit `blocked` when waiting on a missing dependency or decision
975+
- emit `complete` or `failed` when the task finishes
976+
977+
These reports stay local in the session event log and, when the node is connected to a JetStream-enabled relay, are also visible through `cw tasks` and `cw tasks --watch`.
978+
911979
## Contributing
912980

913981
```bash

cmd/cw/exec_cmd.go

Lines changed: 35 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ var execLocally = func(workDir string, command []string) error {
4343
return nil
4444
}
4545

46-
var execInLocalRuntimeTarget = func(instance *cwconfig.LocalInstance, workDir string, command []string) error {
46+
var execInLocalRuntimeTarget = func(instance *cwconfig.LocalInstance, workDir string, command []string, allocTTY ...bool) error {
47+
wantTTY := len(allocTTY) > 0 && allocTTY[0]
4748
if instance == nil {
4849
return fmt.Errorf("local instance is required")
4950
}
@@ -55,6 +56,9 @@ var execInLocalRuntimeTarget = func(instance *cwconfig.LocalInstance, workDir st
5556
switch instance.Backend {
5657
case "docker":
5758
args := []string{"exec", "-i"}
59+
if wantTTY {
60+
args = append(args, "-t")
61+
}
5862
if strings.TrimSpace(workDir) != "" {
5963
args = append(args, "-w", workDir)
6064
}
@@ -70,16 +74,34 @@ var execInLocalRuntimeTarget = func(instance *cwconfig.LocalInstance, workDir st
7074
args = append(args, command...)
7175
cmd = osExec.Command("incus", args...)
7276
case "lima":
73-
args := []string{"shell"}
77+
// Lima runs a Docker container inside the VM. Route exec through it.
78+
name := limaInstanceName(instance)
79+
80+
// Verify the command exists inside the container, not the VM.
81+
checkOut, checkErr := localRunCommand("limactl", "shell", "--workdir", "/", name,
82+
"docker", "exec", limaContainerName, "which", command[0])
83+
if checkErr != nil {
84+
return fmt.Errorf("%q not found inside Lima instance %q\n%s", command[0], name, strings.TrimSpace(string(checkOut)))
85+
}
86+
7487
wd := workDir
7588
if wd == "" {
7689
wd = instance.Workdir
7790
}
91+
92+
// limactl shell <vm> docker exec -i [-t] -w <wd> cw-workspace <command...>
93+
dockerArgs := []string{"docker", "exec", "-i"}
94+
if wantTTY {
95+
dockerArgs = append(dockerArgs, "-t")
96+
}
7897
if strings.TrimSpace(wd) != "" {
79-
args = append(args, "--workdir", wd)
98+
dockerArgs = append(dockerArgs, "-w", wd)
8099
}
81-
args = append(args, limaInstanceName(instance))
82-
args = append(args, command...)
100+
dockerArgs = append(dockerArgs, limaContainerName)
101+
dockerArgs = append(dockerArgs, command...)
102+
103+
args := []string{"shell", "--workdir", "/", name}
104+
args = append(args, dockerArgs...)
83105
cmd = osExec.Command("limactl", args...)
84106
case "firecracker":
85107
vsockPath := instance.FirecrackerSocket + ".vsock"
@@ -116,9 +138,11 @@ var execInLocalRuntimeTarget = func(instance *cwconfig.LocalInstance, workDir st
116138

117139
func execCmd() *cobra.Command {
118140
var (
119-
workDir string
120-
timeout int
121-
on string
141+
workDir string
142+
timeout int
143+
on string
144+
interactive bool
145+
tty bool
122146
)
123147

124148
cmd := &cobra.Command{
@@ -150,7 +174,7 @@ func execCmd() *cobra.Command {
150174
if workDir == "" {
151175
workDir = localWorkspacePath
152176
}
153-
return execInLocalRuntimeTarget(instance, workDir, cmdArgs)
177+
return execInLocalRuntimeTarget(instance, workDir, cmdArgs, interactive && tty)
154178
}
155179
if workDir == "" {
156180
workDir, _ = os.Getwd()
@@ -183,5 +207,7 @@ func execCmd() *cobra.Command {
183207
cmd.Flags().StringVar(&on, "on", "", "Override the current target for this command")
184208
cmd.Flags().StringVarP(&workDir, "workdir", "w", "", "Working directory (default: cwd for local, /workspace for env)")
185209
cmd.Flags().IntVar(&timeout, "timeout", 30, "Timeout in seconds for environment exec")
210+
cmd.Flags().BoolVarP(&interactive, "interactive", "i", false, "Keep stdin open")
211+
cmd.Flags().BoolVarP(&tty, "tty", "t", false, "Allocate a pseudo-TTY")
186212
return cmd
187213
}

cmd/cw/exec_cmd_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@ func TestExecCmdUsesCurrentLocalInstanceTarget(t *testing.T) {
153153
var gotInstance *cwconfig.LocalInstance
154154
var gotWorkDir string
155155
var gotCommand []string
156-
execInLocalRuntimeTarget = func(instance *cwconfig.LocalInstance, workDir string, command []string) error {
156+
execInLocalRuntimeTarget = func(instance *cwconfig.LocalInstance, workDir string, command []string, allocTTY ...bool) error {
157157
gotInstance = instance
158158
gotWorkDir = workDir
159159
gotCommand = append([]string(nil), command...)

0 commit comments

Comments
 (0)