Skip to content

Commit ee912e3

Browse files
authored
Merge branch 'main' into fix-video-sync
2 parents bc77548 + 2c4de09 commit ee912e3

File tree

6 files changed

+767
-144
lines changed

6 files changed

+767
-144
lines changed

AGENTS.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# AGENTS.md
2+
3+
## Cursor Cloud specific instructions
4+
5+
### Project overview
6+
7+
Kernel Images is a sandboxed cloud browser infrastructure platform. The Go server (under `server/`) provides a REST API for screen recording, process execution, file management, and a CDP (Chrome DevTools Protocol) WebSocket proxy. Everything runs inside a single Docker container orchestrated by supervisord.
8+
9+
### Development commands
10+
11+
- **Lint**: `cd server && go vet ./...`
12+
- **Unit tests** (skip e2e): `cd server && go test -v -race $(go list ./... | grep -v /e2e$)`
13+
- **Full tests** (requires Docker + pre-built images): `cd server && make test`
14+
- **Build server**: `cd server && make build`
15+
- **Build headless image**: `cd /workspace && DOCKER_BUILDKIT=1 docker build -f images/chromium-headless/image/Dockerfile -t kernel-headless-test .`
16+
- **Run headless container**: `docker run -d --name kernel-headless -p 10001:10001 -p 9222:9222 --shm-size=2g kernel-headless-test`
17+
- See `server/README.md` and `server/Makefile` for additional commands and configuration.
18+
19+
### Docker in Cloud VM
20+
21+
The Cloud VM runs inside a Firecracker microVM. Docker requires:
22+
- `fuse-overlayfs` storage driver (configured in `/etc/docker/daemon.json`)
23+
- `iptables-legacy` (not nftables)
24+
- Start daemon with `sudo dockerd &>/tmp/dockerd.log &` then `sudo chmod 666 /var/run/docker.sock`
25+
26+
### Key gotchas
27+
28+
1. **`UKC_METRO` must be the full API URL** (e.g., `https://api.<region>.<domain>/v1`), not just the metro short name. The kraft CLI defaults to `*.kraft.cloud` but this org uses a custom domain — check the `UKC_METRO` environment variable for the correct value.
29+
30+
2. **Some kraft cloud subcommands need `--metro "<full-url>"` explicitly** even when the `UKC_METRO` env var is set.
31+
32+
3. **CDP proxy on port 9222 routes ALL WebSocket connections to the browser-level endpoint** (ignores request path). Use `Target.createTarget` + `Target.attachToTarget` with `flatten: true` for page-level interaction. Playwright/Puppeteer handle this automatically.
33+
34+
4. **The default recorder cannot be restarted after stop+delete within the same process lifetime.** Restart the container or use a custom `recorder_id`.
35+
36+
5. **The server (`make dev`) only runs inside the Docker container** — it exits on bare host because it waits for Chromium devtools upstream on port 9223.
37+
38+
6. **Image naming convention**: Cursor Cloud agents use `onkernel/cursor-agent-<type>:latest` for test images pushed to KraftCloud. Always check quota with `kraft cloud quota` before pushing. Never auto-delete images — present them to the user for approval.
39+
40+
7. **Image storage quota is tight** (~80 GiB limit). Old `kernel-cu` and `chromium-headless` versions consume most of it.
41+
42+
8. **E2e tests** use `testcontainers-go` and require Docker + pre-built images. Set `E2E_CHROMIUM_HEADFUL_IMAGE` and `E2E_CHROMIUM_HEADLESS_IMAGE` env vars to point to the correct image tags.
43+
44+
9. **Go version**: The project requires Go 1.25.0 (per `server/go.mod`). The system Go may be older — ensure `/usr/local/go/bin` is on PATH.

images/chromium-headless/image/Dockerfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=$CACHEIDPREFIX-ap
136136
dbus-x11 \
137137
xvfb \
138138
x11-utils \
139+
xclip \
139140
xdotool \
140141
fontconfig \
141142
fonts-noto-cjk \

server/cmd/api/api/computer.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1136,6 +1136,54 @@ func clampPoints(points [][2]int, screenWidth, screenHeight int) {
11361136
}
11371137
}
11381138

1139+
func (s *ApiService) ReadClipboard(ctx context.Context, request oapi.ReadClipboardRequestObject) (oapi.ReadClipboardResponseObject, error) {
1140+
log := logger.FromContext(ctx)
1141+
1142+
s.inputMu.Lock()
1143+
defer s.inputMu.Unlock()
1144+
1145+
display := s.resolveDisplayFromEnv()
1146+
cmd := exec.CommandContext(ctx, "xclip", "-selection", "clipboard", "-o")
1147+
cmd.Env = append(os.Environ(), fmt.Sprintf("DISPLAY=%s", display))
1148+
output, err := cmd.Output()
1149+
if err != nil {
1150+
var exitErr *exec.ExitError
1151+
if errors.As(err, &exitErr) && exitErr.ExitCode() == 1 {
1152+
return oapi.ReadClipboard200JSONResponse{Text: ""}, nil
1153+
}
1154+
log.Error("xclip read failed", "err", err)
1155+
return oapi.ReadClipboard500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{
1156+
Message: fmt.Sprintf("failed to read clipboard: %v", err)},
1157+
}, nil
1158+
}
1159+
return oapi.ReadClipboard200JSONResponse{Text: string(output)}, nil
1160+
}
1161+
1162+
func (s *ApiService) WriteClipboard(ctx context.Context, request oapi.WriteClipboardRequestObject) (oapi.WriteClipboardResponseObject, error) {
1163+
log := logger.FromContext(ctx)
1164+
1165+
s.inputMu.Lock()
1166+
defer s.inputMu.Unlock()
1167+
1168+
if request.Body == nil {
1169+
return oapi.WriteClipboard400JSONResponse{BadRequestErrorJSONResponse: oapi.BadRequestErrorJSONResponse{
1170+
Message: "request body is required"},
1171+
}, nil
1172+
}
1173+
1174+
display := s.resolveDisplayFromEnv()
1175+
cmd := exec.CommandContext(ctx, "xclip", "-selection", "clipboard")
1176+
cmd.Env = append(os.Environ(), fmt.Sprintf("DISPLAY=%s", display))
1177+
cmd.Stdin = strings.NewReader(request.Body.Text)
1178+
if err := cmd.Run(); err != nil {
1179+
log.Error("xclip write failed", "err", err)
1180+
return oapi.WriteClipboard500JSONResponse{InternalErrorJSONResponse: oapi.InternalErrorJSONResponse{
1181+
Message: fmt.Sprintf("failed to write to clipboard: %v", err)},
1182+
}, nil
1183+
}
1184+
return oapi.WriteClipboard200Response{}, nil
1185+
}
1186+
11391187
// generateRelativeSteps produces a sequence of relative steps that approximate a
11401188
// straight line from (0,0) to (dx,dy) using at most the provided number of
11411189
// steps. Each returned element is a pair {stepX, stepY}. The steps are

server/e2e/e2e_chromium_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,43 @@ func TestDisplayResolutionChange(t *testing.T) {
169169
t.Log("all resolution changes verified successfully")
170170
}
171171

172+
func TestClipboardHeadless(t *testing.T) {
173+
t.Parallel()
174+
175+
if _, err := exec.LookPath("docker"); err != nil {
176+
t.Skipf("docker not available: %v", err)
177+
}
178+
179+
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute)
180+
defer cancel()
181+
182+
c := NewTestContainer(t, headlessImage)
183+
require.NoError(t, c.Start(ctx, ContainerConfig{}), "failed to start container")
184+
defer c.Stop(ctx)
185+
186+
require.NoError(t, c.WaitReady(ctx), "api not ready")
187+
188+
specResp, err := http.Get(c.APIBaseURL() + "/spec.yaml")
189+
require.NoError(t, err)
190+
specBody, _ := io.ReadAll(specResp.Body)
191+
specResp.Body.Close()
192+
require.True(t, strings.Contains(string(specBody), "/computer/clipboard/write"),
193+
"API spec does not include clipboard routes - rebuild the image with: cd kernel-images/images/chromium-headless && docker build --no-cache -f image/Dockerfile -t %s ../..", headlessImage)
194+
195+
client, err := c.APIClient()
196+
require.NoError(t, err, "failed to create API client")
197+
198+
writeResp, err := client.WriteClipboardWithResponse(ctx, instanceoapi.WriteClipboardRequest{Text: "e2e-clipboard-test"})
199+
require.NoError(t, err, "WriteClipboard request failed")
200+
require.Equal(t, http.StatusOK, writeResp.StatusCode(), "unexpected write status: %s body=%s", writeResp.Status(), string(writeResp.Body))
201+
202+
readResp, err := client.ReadClipboardWithResponse(ctx)
203+
require.NoError(t, err, "ReadClipboard request failed")
204+
require.Equal(t, http.StatusOK, readResp.StatusCode(), "unexpected read status: %s body=%s", readResp.Status(), string(readResp.Body))
205+
require.NotNil(t, readResp.JSON200, "expected JSON200 response")
206+
require.Equal(t, "e2e-clipboard-test", readResp.JSON200.Text, "clipboard content mismatch")
207+
}
208+
172209
func TestExtensionUploadAndActivation(t *testing.T) {
173210
t.Parallel()
174211
ensurePlaywrightDeps(t)

0 commit comments

Comments
 (0)