Commit Graph

8 Commits

Author SHA1 Message Date
Claude 882a071521 fix(login): capture the full claude auth URL on a wide tmux window
Testing the web login surfaced a truncated authorization URL
(…client_id=9d1c250a-e61b-44d9-88) and a "missing redirect_uri" error: the
login session ran at the default 80 columns, so claude clipped the long URL and
capture-pane read it back cut off.

- Launch the login session with a very wide, tall window (500x50) via new
  optional width/height on tmux.new_session, so claude prints the URL on one
  unclipped line.
- Harden URL extraction to stop at box-drawing glyphs (U+2500–U+257F) in case
  the TUI renders the link flush against a border.

Tests: assert the wide window is requested, and that extraction keeps a full
redirect_uri/PKCE URL intact and strips a trailing box border. Suite green (197).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YKVyBmKvWDVgrFC9WER2f2
2026-07-13 19:19:19 +00:00
Claude fa2e97130d feat: bundle agent executables + web-driven claude login
Two changes so an operator can stand up and authenticate Handler entirely
from the browser, with a self-contained control image.

Bundle executables in the control image (Dockerfile.control)
- Node.js (NodeSource) + the Claude Code CLI, mise (official apt repo), and
  forge (git-pkgs/forge, built in a Go stage) join the existing git/tmux/ssh.
  No more bring-your-own binaries: live agent spawning, the verification gate,
  CI resolution, and the login flow all work out of the box. Installed under
  /usr so the /var/lib/handler VOLUME never masks them; mise apt source pinned
  to $TARGETARCH for the multi-arch (amd64/arm64) build.

Claude login from the web UI
- New login_start / login_submit command types (migration 0005) drive the
  interactive `claude /login` through the same enqueue→worker handoff every
  other control action uses — the API container has no claude binary.
- control/login.py opens `claude` in a dedicated tmux session, sends /login,
  selects the subscription account, and scrapes the claude.com authorization
  URL (tmux.capture_pane, -pJ so a wrapped URL rejoins); a second command feeds
  back the pasted code. Fully mockable via the tmux seam.
- API: POST /login/start, POST /login/submit (admin-gated).
- Dashboard: a "Claude Login" pane — a button that starts the flow, embeds the
  URL in an iframe (with a new-tab fallback, since claude.com may refuse
  framing), and takes the code to finish.

Also un-ignores frontend/lib/ (a broad Python `lib/` rule was swallowing the
UI's own api client + formatters, breaking rebuilds from a fresh clone) and
reconstructs those two source files; rebuilt static export committed.

Tests: control/login unit tests (tmux faked), worker dispatch, and API route
tests. Full suite green (195 tests), ruff clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YKVyBmKvWDVgrFC9WER2f2
2026-07-13 17:45:58 +00:00
Claude 71a7550f48 feat: git servers own credentials, one-line project adds with auto-clone, and scheduled agents
Git servers (forge_hosts) become full credential owners:
- an encrypted forge token (Fernet, HANDLER_SECRET_KEY) stored per server and
  never returned by the API (has_token flag only); used automatically by every
  project on that host and addressable as db:host:<hostname> — the reserved
  db: credential scheme is now live
- a per-server ed25519 SSH deploy key: generated server-side, public half
  shown in the dashboard to paste into the forge, private half encrypted at
  rest and materialized 0600 only in the control container (GIT_SSH_COMMAND /
  core.sshCommand)

Project registration gets a git-server mode: pick a registered server, type
owner/name, and the API derives the remote (ssh when the server has a deploy
key, https otherwise), computes root_dir under PROJECTS_ROOT, and enqueues a
new 'sync' command the worker executes (clone, or ff-only pull). Spawn always
pulls first, so runs start from the remote's latest state; POST /projects/:p/sync
and 'handler sync' re-pull on demand.

Schedules: recurring agent spawns (prefix, prompt, interval, role). The worker
fires due schedules as ordinary queued spawn commands with timestamped agent
names, so runs are fresh stateless agents and appear in the Activity audit
trail; missed intervals collapse into one catch-up run.

Dashboard: Git Servers pane shows the SSH public key (copy button) and takes a
write-only token; Repositories gains the server-first add form and a Pull now
button; new Schedules pane. Rebuilt static export. Also restores the missing
frontend/lib (api client + format helpers) the components import.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XY1tEhQZXHZ5wci7dLc7rM
2026-07-10 19:17:12 +00:00
Wyatt 7399315185 Replace Alpine frontend with Next.js Claude Activity Dashboard (#7)
Rebuild the bundled web UI as a Next.js (React + TypeScript) static export
implementing the Claude Activity Dashboard design: a left-nav "Control Center"
hub over Runs, Repositories, Agents, Approvals, Git Servers, Activity, and
Shared, styled with the Leeworks design-system tokens (flat, dark, border-led).

The dashboard is a pure client of the existing API (same contract as curl):
the browser prompts for the token once, stores it in localStorage, attaches it
to every call, and renders all API values as React text so agent-authored
strings stay inert. Control actions enqueue a command and poll it to a terminal
state, matching the worker model.

The build output is committed to src/handler/api/static/ so the wheel ships it
and FastAPI serves it same-origin. app.py now mounts the export at "/" after the
API routers (a non-shadowing fallback: unmatched paths 404, no SPA rewrite).
UI-serving tests updated for the export; frontend source lives in frontend/.


Claude-Session: https://claude.ai/code/session_01ATgVWRjFzG8nHEnwgZpJWD

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-10 14:34:43 -04:00
Claude 4f05d09c2b feat(web): fully web-managed control plane via a DB command queue
Make credentials/hosts, projects, agents, and approvals manageable from the
dashboard. The API and control layer are separate containers, so the API can't
run control actions directly (no git/tmux/claude, doesn't own the tmux
sessions). Instead the API enqueues a command and a worker in the control
container executes it and writes the result back.

Data model (migration 0003):
- `commands` queue/audit table; `forge_hosts` registry; `approvals` gains a
  nullable approver id + `actor` so operator verdicts are first-class.

Control worker:
- `control/worker.py` claims commands and dispatches to the existing control
  functions (spawn/kill/resume/record_approval/write_skills/poller.sweep),
  plus a periodic CI sweep. New `handler worker` CLI subcommand; it becomes the
  control image's default command (subsumes `poll-ci --watch`).

API:
- `require_admin` gate + `ADMIN_TOKEN`; project GET/PATCH/DELETE; agent
  spawn/kill/delete; resume now enqueues (fixes a cross-container bug where the
  API tried to send tmux keys to a session in the control container); new
  approvals/commands/hosts routes; forge-init and poll-ci enqueue endpoints.

Credentials/hosts:
- host->token-env lookup consults the `forge_hosts` registry first (built-in
  map is the fallback); `resolve()` refactored to a scheme dispatch reserving
  `db:` for a future encrypted store. Web input restricts credential_ref to
  env:/file:/db: (cmd: stays CLI-only — it would run arbitrary commands).

Dashboard:
- New tabs for projects, agents (spawn/kill with live command-status polling),
  approvals, hosts, and an activity/audit view; shared context is now writable.

Tests: +33 (queue atomicity, worker dispatch, CRUD, hosts, admin gating,
cmd: rejection, host-aware credentials, and an API->queue->worker->spawn
end-to-end). README gains a Web management section.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CrhrBToauu4L2qG6jdnuFP
2026-07-10 16:32:45 +00:00
0xWheatyz bbb01d0882 feat(phase-3): web UI served same-origin by the API
Add a no-build, same-origin web frontend so an operator can open a URL,
see every agent's state, and answer a paused question with no terminal
(Phase 3 DoD). The UI is a client of the existing API — no endpoint,
schema, or auth change — so the 106 existing tests pass unchanged.

- app.py serves the bundled UI from / and /static, gated on UI_ENABLED
  (default on); optional CORS_ORIGINS (default empty => no middleware)
  for hosting the UI on a separate origin. Dedicated /static prefix +
  explicit / route so API routes are never shadowed. Zero new runtime
  deps (StaticFiles/CORSMiddleware ship with Starlette).
- static/: vanilla fetch + plain CSS + vendored alpine.min.js (v3.14.8,
  no CDN). Token captured once into localStorage; all API values render
  via x-text (never x-html) to block agent-authored markup injection.
  Project switcher, agent list, checkmark panel, paginated log, shared
  feed, and Answer / Answer & Resume. Polling scoped to the selected
  agent to avoid an N+1 over the fleet.
- config.py: ui_enabled, cors_origins (+ cors_origin_list); documented
  in .env.example.
- tests/test_api_ui.py: serving, unauthenticated shell, non-shadowing
  401 regression, CORS toggle, UI_ENABLED=false. 114 tests, ruff clean.

The static assets ship in the wheel by default (they live inside the
packaged src/handler tree) — no force-include needed.
2026-07-09 20:43:23 -04:00
0xWheatyz 6fb26115ce feat(phase-2): forge integration — credentials, role skills, approval gate, CI poller
Phase 2 configures forge for the agents (operator only sets a credential_ref +
optional version pin) and lets them drive a junior→senior→deploy workflow:

- Credential resolution/injection (control/credentials.py): credential_ref pointers
  (env:/file:/cmd:) resolved only at spawn, injected as FORGE_TOKEN + host var, with a
  forge-host-scoped git credential helper reading the token from env (never on disk / in
  the DB). Resolution is a fail-fast spawn gate.
- Role-based forge skills committed into the managed repo (control/skills_gen.py,
  `handler forge-init`): forge-junior/senior/deploy + a workflow overview.
- Hard approval gate (hooks/gate.py, approvals table, migration 0002): merge/deploy —
  and direct pushes to protected branches — are denied unless a DIFFERENT agent has an
  `approved` record for the branch, pinned to the reviewed commit (approved_sha). Senior
  records verdicts via `handler approve`/`reject`.
- forge/git seams (control/forge.py, control/gitops.py) matching the Phase 1 seam pattern.
- CI status poller (control/poller.py, `handler poll-ci [--watch]`) backfilling
  ci_status/ci_checked_at via `forge ci list`.
- Fix: migrations/env.py commits explicitly after run_migrations — pysqlite on Py 3.12+
  was rolling back the final migration's DDL + alembic_version stamp (latent in Phase 1).

Reviewed via a separate code-reviewer pass; gate-bypass and credential-scoping findings
addressed. 106 tests, ruff clean, verified end-to-end against real git + migrations.
2026-07-08 22:05:49 -04:00
Claude eba0e19ec9 feat(mvp): Phase 1 control layer + API vertical slice
Implements the Phase 1 MVP from the README: a stateless control layer + HTTP
API over a centralized database, with hook-enforced test/push gates.

- DB layer: SQLAlchemy Core, one schema rendering both Postgres (BIGSERIAL /
  TIMESTAMPTZ / JSONB) and SQLite (INTEGER PK / TEXT / JSON) via portable types;
  native ON CONFLICT DO UPDATE checkmark upsert on both dialects.
- Alembic dual-dialect migrations (render_as_batch for SQLite); tests run a real
  `alembic upgrade head`.
- FastAPI: projects/agents/checkmark/log/answer/resume + shared log/context
  routes, single global bearer token, higher-trust token gating shared-context
  writes, project isolation on every route.
- Hooks (`python -m handler.hooks <event>`): Stop test gate (block on red),
  PreToolUse AskUserQuestion defer + `git push` gate (tests then throwaway
  build), Notification generic webhook (no-op without WEBHOOK_URL). Identity via
  env injected at spawn; verify is the mock seam.
- Control CLI: spawn/list/attach/kill, hard `.mise.toml [tasks.test]` gate,
  generated per-agent settings.json, identity + DATABASE_URL injected via tmux;
  tmux is the mock seam.
- 45 tests (SQLite), ruff clean. Live claude/tmux/mise spawning deferred behind
  the mocked seams.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W5ZuS5pV1NS6eKsRZHXonY
2026-07-07 18:16:54 +00:00