Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
a077609
chore: adding eval workspace to gitignore
bfirestone Mar 28, 2026
cfcbce9
feat(paste): add foundation types, storage, and crypto helpers
bfirestone Apr 29, 2026
4d53727
fix(paste): coerce Uint8Array to BufferSource for Web Crypto API
bfirestone Apr 29, 2026
1dabc29
feat(paste): add HTTP handlers for /api/paste/*
bfirestone Apr 29, 2026
28b75e5
feat(web): add paste client, anchor, events, identity utilities
bfirestone Apr 29, 2026
cc4923a
test(paste): add cross-language crypto roundtrip fixtures
bfirestone Apr 29, 2026
9a936ac
feat(web): add /share/[id] route with full annotation markup
bfirestone Apr 29, 2026
85cd69d
feat(api): mount paste handlers in arc-server
bfirestone Apr 29, 2026
862e0dc
feat(arc-paste): add standalone paste service binary
bfirestone Apr 29, 2026
69edd9f
feat(cli): add arc share commands and shares.json registry
bfirestone Apr 29, 2026
aca67c5
feat(web): rebuild /share/[id] with editorial review UX
bfirestone Apr 29, 2026
4450aa2
chore(web): scope vitest, refine paste crypto helper, format pass
bfirestone Apr 29, 2026
a240c86
fix(make): pass -tags webui to build-paste so the SPA is embedded
bfirestone Apr 29, 2026
a9beb46
docs: add paste-server manual test runbook
bfirestone Apr 29, 2026
649d47e
feat(share): annotation editing, layout polish, config precedence
bfirestone Apr 30, 2026
9039fa6
build(arc-paste): scratch image with Go 1.26, compose deployment
bfirestone Apr 30, 2026
ed425b6
feat(share): JSON bundle for agents + author can edit reviewer comments
bfirestone Apr 30, 2026
7b60505
feat(share): proper markdown rendering, planner-aligned theme, merged…
bfirestone Apr 30, 2026
0f8c129
feat(share): fluid doc width with clamp() for breathing room on wide …
bfirestone Apr 30, 2026
145beb1
chore(lint): clear all golangci-lint issues across paste & share pack…
bfirestone Apr 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,8 @@ coverage.html
# Worktrees
.worktrees/

# eval workspaces
plan-*

# Config (contains user-specific settings)
# Note: ~/.arc/cli-config.json is user config
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,11 @@ build-bin: ## Build arc binary with embedded web UI (requires frontend built fir
build-quick: ## Build CLI-only binary (no embedded web UI)
$(BUILD_SCRIPT)

.PHONY: build-paste
build-paste: web-build ## Build arc-paste standalone binary (with embedded SPA)
@echo "==> Building arc-paste binary..."
$(GO) build -tags webui -o $(BIN_DIR)/arc-paste ./arc-paste

.PHONY: release
release: ## Build release with goreleaser (requires git tag)
goreleaser release --clean
Expand Down
59 changes: 59 additions & 0 deletions arc-paste/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# syntax=docker/dockerfile:1.7
#
# Three-stage build for arc-paste:
# 1. `web` builds the SvelteKit SPA with bun (matches local toolchain)
# 2. `go-build` compiles a fully-static Go binary with the embedded SPA.
# `-tags webui` is REQUIRED — without it `web.RegisterSPA` is a no-op
# stub and `/share/<id>` returns Echo's default JSON 404.
# 3. Final image is `scratch` — no shell, no libc, just the binary.
#
# Build from the repo root, not arc-paste/, since stage 2 needs the Go module.
# docker build -f arc-paste/Dockerfile -t arc-paste:latest .

# ─── Stage 1: SvelteKit SPA build ────────────────────────────────────────────
FROM oven/bun:1-alpine AS web
WORKDIR /web

# Install deps in their own layer so source-only changes don't bust the cache.
COPY web/package.json web/bun.lock* ./
RUN bun install --frozen-lockfile

COPY web/ ./
RUN bun run build

# ─── Stage 2: static Go binary ───────────────────────────────────────────────
FROM golang:1.26-alpine AS go-build
WORKDIR /build

# Module download is its own cache layer too.
COPY go.mod go.sum ./
RUN go mod download

# Bring in source + the SPA bundle the build tag will embed.
COPY . .
COPY --from=web /web/build ./web/build

# CGO_ENABLED=0 → fully static, no glibc dependency, runs on scratch.
# -tags webui → embed the SPA (without it, the SPA returns 404).
# -trimpath → strip absolute paths from the binary (smaller + reproducible).
# -ldflags '-s -w' → drop DWARF + symbol tables (~20% smaller binary).
RUN CGO_ENABLED=0 GOOS=linux go build \
-tags webui \
-trimpath \
-ldflags='-s -w' \
-o /out/arc-paste \
./arc-paste

# ─── Stage 3: minimal runtime ────────────────────────────────────────────────
FROM scratch

COPY --from=go-build /out/arc-paste /arc-paste

# Default DB lives under /data; mount a volume here to persist across restarts.
# arc-paste creates the directory itself, so this works even without a volume.
ENV ARC_PASTE_DB=/data/arc-paste.db
ENV ARC_PASTE_ADDR=:7433

EXPOSE 7433

ENTRYPOINT ["/arc-paste"]
43 changes: 43 additions & 0 deletions arc-paste/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# arc-paste

A tiny standalone binary that exposes the arc paste API and serves the embedded SvelteKit SPA. Designed for public deployment as a zero-knowledge paste service for sharing arc plan reviews.

## Building

```bash
make build-paste
```

Produces `./bin/arc-paste`.

## Running

```bash
./bin/arc-paste
```

Starts the server on port 7433 by default.

## Configuration

- `ARC_PASTE_ADDR`: Listen address (default: `:7433`)
- `ARC_PASTE_DB`: SQLite database path (default: `./arc-paste.db`)

## API

The binary serves:
- `/api/paste/*` — Paste HTTP handlers (create, retrieve, update, delete pastes)
- `/` — Embedded SPA (with index.html fallback for SPA routing)

CORS is enabled for all origins.

## Docker

```bash
make docker-build
docker run -p 7433:7433 arc-paste:latest
```

## License

See the main arc repository.
40 changes: 40 additions & 0 deletions arc-paste/compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# arc-paste compose — standalone deployment of the encrypted paste service.
#
# Build the image and start the service:
# docker compose -f arc-paste/compose.yaml up -d --build
# Tail logs:
# docker compose -f arc-paste/compose.yaml logs -f
# Stop:
# docker compose -f arc-paste/compose.yaml down
#
# The Dockerfile lives in arc-paste/ but builds from the repo root because it
# needs the parent Go module — that's what `context: ..` below selects.

services:
arc-paste:
build:
context: ..
dockerfile: arc-paste/Dockerfile
image: arc-paste:latest
container_name: arc-paste
ports:
- "7433:7433"
volumes:
# Named volume keeps the SQLite db across container rebuilds.
# arc-paste creates the directory itself on startup, so a fresh
# volume works without any init step.
- arc-paste-data:/data
environment:
- TZ=UTC
# Defaults set in the Dockerfile; uncomment to override per deployment:
# - ARC_PASTE_ADDR=:7433
# - ARC_PASTE_DB=/data/arc-paste.db
restart: unless-stopped
# No healthcheck: the runtime image is `scratch`, which has no shell,
# wget, curl, or anything else compose's `healthcheck.test` could exec.
# For production monitoring, run an external probe (Cloudflare health
# check, Uptime Kuma, etc.) against the host port instead.

volumes:
arc-paste-data:
name: arc-paste-data
90 changes: 90 additions & 0 deletions arc-paste/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Package main is arc-paste, a tiny standalone binary that exposes only the paste API
// and serves the SvelteKit SPA. Designed for public deployment as a
// zero-knowledge paste service for arc plan reviews.
package main

import (
"context"
"database/sql"
"fmt"
"log"
"os"
"path/filepath"

"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
_ "modernc.org/sqlite" // match arc's driver

"github.com/sentiolabs/arc/internal/paste"
pastesqlite "github.com/sentiolabs/arc/internal/paste/sqlite"
"github.com/sentiolabs/arc/web"
)

// dbDirMode is the permission used when creating the parent directory of the
// SQLite database. World-readable on purpose — the file mode itself (which
// SQLite controls) is what protects the data.
const dbDirMode = 0o755

func main() {
if err := run(); err != nil {
log.Fatal(err)
}
}

func run() error {
addr := envOr("ARC_PASTE_ADDR", ":7433")
dbPath := envOr("ARC_PASTE_DB", "./arc-paste.db")

// Ensure the parent directory exists. SQLite will create the db file but
// not the directory, which matters when running under scratch / distroless
// images that mount a fresh volume at a path the binary has never seen.
if dir := filepath.Dir(dbPath); dir != "." && dir != "/" {
if err := os.MkdirAll(dir, dbDirMode); err != nil {
return fmt.Errorf("create db dir %q: %w", dir, err)
}
}

db, err := sql.Open("sqlite", dbPath)
if err != nil {
return fmt.Errorf("open db: %w", err)
}
defer db.Close()

if err := pastesqlite.Apply(context.Background(), db); err != nil {
return fmt.Errorf("apply migrations: %w", err)
}

store := pastesqlite.New(db)
handlers := paste.NewHandlers(store)

e := echo.New()
e.HideBanner = true
e.Use(middleware.RequestLoggerWithConfig(middleware.RequestLoggerConfig{
LogStatus: true,
LogURI: true,
LogMethod: true,
LogError: true,
LogValuesFunc: func(_ echo.Context, v middleware.RequestLoggerValues) error {
log.Printf("%s %s -> %d (err=%v)", v.Method, v.URI, v.Status, v.Error)
return nil
},
}))
e.Use(middleware.Recover())
e.Use(middleware.CORS())

// Mount paste handlers at /api/paste
handlers.Register(e.Group("/api/paste"))

// Serve embedded SPA with fallback to index.html for routing
web.RegisterSPA(e)

return e.Start(addr)
}

// envOr returns the value of env variable key, or defaultVal if not set.
func envOr(key, defaultVal string) string {
if v, ok := os.LookupEnv(key); ok {
return v
}
return defaultVal
}
37 changes: 37 additions & 0 deletions arc-paste/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package main

import (
"bytes"
"context"
"database/sql"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"

"github.com/labstack/echo/v4"
"github.com/sentiolabs/arc/internal/paste"
pastesqlite "github.com/sentiolabs/arc/internal/paste/sqlite"
_ "modernc.org/sqlite"
)

func TestArcPasteCreate(t *testing.T) {
db, _ := sql.Open("sqlite", ":memory:")
defer db.Close()
_ = pastesqlite.Apply(context.Background(), db)
e := echo.New()
paste.NewHandlers(pastesqlite.New(db)).Register(e.Group("/api/paste"))

body, _ := json.Marshal(map[string]any{
"plan_blob": []byte{1, 2, 3},
"plan_iv": []byte{4, 5, 6},
"schema_ver": 1,
})
req := httptest.NewRequest("POST", "/api/paste", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
e.ServeHTTP(rec, req)
if rec.Code != http.StatusCreated {
t.Fatalf("expected 201, got %d", rec.Code)
}
}
19 changes: 19 additions & 0 deletions cmd/arc/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,25 @@ func main() {
type Config struct {
ServerURL string `json:"server_url"`
Channel string `json:"channel,omitempty"`
// ShareAuthor is the default author name embedded in `arc share create`
// plans. It's the canonical reviewer identity used by the share UI to
// gate Accept / Resolve / Reject controls — only visitors who type this
// exact name in the SPA's prompt are recognized as the plan owner.
// Resolution precedence in `arc share create`:
// 1. --author flag (highest)
// 2. this config field
// 3. $ARC_SHARE_AUTHOR
// 4. `git config user.name`
ShareAuthor string `json:"share_author,omitempty"`
// ShareServer is the default URL for the remote paste server used by
// `arc share create --share`. Lets users persistently target a private
// arc-paste deployment instead of the public default.
// Resolution precedence in `arc share create --share`:
// 1. --server flag (highest)
// 2. this config field
// 3. $ARC_SHARE_SERVER
// 4. https://arcplanner.sentiolabs.io (built-in default)
ShareServer string `json:"share_server,omitempty"`
}

// ProjectSource indicates how the project was resolved
Expand Down
Loading
Loading