688 lines
31 KiB
Markdown
688 lines
31 KiB
Markdown
<p align="center">
|
|
<img src="apps/next/public/favicon.png" alt="Spoon logo" width="96" height="96" />
|
|
</p>
|
|
|
|
<h1 align="center">Spoon</h1>
|
|
|
|
<p align="center">
|
|
<strong>Fork freely & keep them all intimately close to upstream.</strong>
|
|
</p>
|
|
|
|
<p align="center">
|
|
Spoon is a self-hostable fork maintenance cockpit built around managed forks,
|
|
durable maintenance threads, and a persistent per-user dev box that agents,
|
|
terminals, and project commands all run inside.
|
|
</p>
|
|
|
|
<p align="center">
|
|
<a href="#what-this-is">What this is</a>
|
|
·
|
|
<a href="#product-model">Product model</a>
|
|
·
|
|
<a href="#architecture">Architecture</a>
|
|
·
|
|
<a href="#environment-reference">Environment</a>
|
|
</p>
|
|
|
|
---
|
|
|
|
## What This Is
|
|
|
|
Spoon is a private, actively evolving project for making forks less lonely to
|
|
maintain.
|
|
|
|
Forking a project is easy. Keeping that fork close to upstream after you add
|
|
custom changes is the hard part. Spoon treats a fork as an ongoing relationship:
|
|
it watches upstream, understands fork-only commits, automatically syncs clean
|
|
drift when it can, and opens a durable **Thread** when a decision needs context
|
|
or code.
|
|
|
|
The application is currently GitHub-first. Future provider-neutral fields exist
|
|
in the data model, but GitHub is the active automation surface today.
|
|
|
|
## Highlights
|
|
|
|
- **Managed forks, called Spoons**
|
|
Track upstream metadata, fork metadata, clone URLs, extra remotes, sync
|
|
cadence, production-ref strategy, fork-only commits, and pull requests.
|
|
|
|
- **Thread-first maintenance**
|
|
Upstream updates, conflict review, ignore decisions, user-requested work,
|
|
worker output, and draft PR handoff all live inside Threads.
|
|
|
|
- **Clean drift auto-sync**
|
|
If upstream moves and the fork has no custom commits, Spoon can fast-forward
|
|
the fork without creating busywork.
|
|
|
|
- **Custom forks get context**
|
|
If the fork has custom commits, Spoon creates a maintenance thread rather than
|
|
pretending the update is trivial.
|
|
|
|
- **Effective drift**
|
|
Spoon keeps raw GitHub drift visible while also tracking ignored upstream
|
|
changes so irrelevant commits do not keep a fork permanently actionable.
|
|
|
|
- **One persistent per-user box**
|
|
Every user owns a single long-running Fedora container `spoon-box-{username}`
|
|
with a persistent home. Every thread's agent turn, terminal session, and
|
|
project command `docker exec`s into that same box — there is no per-job
|
|
throwaway container.
|
|
|
|
- **Three agent runtimes**
|
|
Codex, OpenCode, and Claude Code all run inside the box behind one adapter
|
|
interface, selected per thread and validated at queue time against the AI
|
|
provider profile.
|
|
|
|
- **Workspaces**
|
|
Agent work happens in a workspace with a file tree, browser editor, diff
|
|
viewer, command panel, logs, artifacts, an interactive terminal, and draft PR
|
|
actions.
|
|
|
|
- **My Machine**
|
|
A `/machine` surface to start/stop/restart the box, open a `~`-rooted
|
|
terminal, and browse the persistent home.
|
|
|
|
- **GitHub webhooks**
|
|
A signature-verified webhook keeps drift fresh on `push` and flips connection
|
|
status on `installation` changes; the hourly cron is a fallback.
|
|
|
|
- **Notifications**
|
|
An in-app bell plus preference-gated transactional email for maintenance
|
|
threads, agent turns, needed input, sync failures, and connection re-auth.
|
|
|
|
- **User-owned providers and secrets**
|
|
AI provider profiles, Codex/OpenCode/Anthropic auth, and per-Spoon project
|
|
secrets are encrypted. Secrets are written `0600`, redacted from logs, and
|
|
refused from commits when materialized into env files.
|
|
|
|
- **Draft PR handoff**
|
|
Code changes become branches and draft pull requests. Spoon does not
|
|
auto-merge custom forks behind the user's back.
|
|
|
|
## Product Model
|
|
|
|
<details open>
|
|
<summary><strong>Spoons</strong></summary>
|
|
|
|
A **Spoon** is a managed fork. It records the upstream project, the fork
|
|
repository, default branches, sync policy, extra remotes, current drift, cached
|
|
commits, cached pull requests, secrets, and agent settings.
|
|
|
|
Spoons are the durable project-level objects. They answer:
|
|
|
|
- What did I fork?
|
|
- Where does my fork live?
|
|
- How far has it drifted?
|
|
- Which commits are mine?
|
|
- Which upstream changes matter?
|
|
- What threads or PRs are open?
|
|
|
|
</details>
|
|
|
|
<details open>
|
|
<summary><strong>Threads</strong></summary>
|
|
|
|
A **Thread** is the durable place where Spoon talks about maintenance work.
|
|
Threads can be created by a user or by the system.
|
|
|
|
Common thread sources:
|
|
|
|
- `user_request`: user asks Spoon to change a fork.
|
|
- `upstream_update`: upstream moved and the fork needs review.
|
|
- `merge_conflict`: a sync conflict needs context or code.
|
|
- `manual_review`: user explicitly asks for a review.
|
|
- `system`: internal maintenance coordination.
|
|
|
|
Threads hold messages, status, outcomes, related sync runs, related jobs,
|
|
workspace links, draft PR links, and ignored upstream decisions.
|
|
|
|
Opening a thread opens its workspace when a run exists. The workspace is the
|
|
primary surface for that thread: agent messages, tool activity, file edits,
|
|
manual edits, diffs, commands, and draft PR actions all happen there. Legacy
|
|
job URLs under `/spoons/[spoonId]/agent/[jobId]` are kept for compatibility,
|
|
but normal navigation targets `/threads/[threadId]`.
|
|
|
|
</details>
|
|
|
|
<details open>
|
|
<summary><strong>Maintenance decisions</strong></summary>
|
|
|
|
Spoon's maintenance policy is intentionally conservative:
|
|
|
|
| Situation | Default action |
|
|
| ------------------------------------------ | ------------------------------------- |
|
|
| No fork-only commits and upstream is ahead | Auto-sync |
|
|
| Fork-only commits and upstream is ahead | Create a maintenance thread |
|
|
| Merge conflicts | Open or continue a workspace thread |
|
|
| Irrelevant upstream changes | Record an intentional ignore decision |
|
|
| Agent/code changes | Open a draft PR |
|
|
|
|
The goal is to keep forks close without hiding risk or skipping review when
|
|
custom work exists.
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><strong>The box and workspaces</strong></summary>
|
|
|
|
Spoon's optional agent worker is designed to run outside Convex actions. The
|
|
worker claims queued jobs, clones the current GitHub fork into the owner's
|
|
persistent home, creates a branch, and exposes workspace operations to the Next
|
|
app through server-only API proxies.
|
|
|
|
Everything runs inside **one long-running container per user**,
|
|
`spoon-box-{username}`. The box is a Fedora image (`docker/agent-job.Dockerfile`)
|
|
started with `sleep infinity` and a `--memory 4g` / `--cpus 2` cap. Each
|
|
thread's agent turn, terminal, and project commands `docker exec` into that same
|
|
box; the box's `/home/{username}` is a bind-mounted persistent home so dotfiles,
|
|
installed tools, shell history, and thread checkouts under `~/Code/{spoon}/{branch}`
|
|
survive across sessions. The box is reference-counted by the worker and reaped
|
|
after `SPOON_AGENT_BOX_IDLE_MS` idle (default 30m). There is no per-job
|
|
`docker run --rm` container and no separate `opencode serve` container — that
|
|
path was removed.
|
|
|
|
Workspace capabilities:
|
|
|
|
- browse repository files
|
|
- edit files in a browser editor
|
|
- use optional Vim keybindings
|
|
- resize the agent thread panel on desktop
|
|
- inspect diffs
|
|
- send thread messages to the agent
|
|
- run configured commands
|
|
- use an interactive terminal (xterm.js → box PTY; see
|
|
[docs/agent-terminal.md](docs/agent-terminal.md))
|
|
- store logs and artifacts
|
|
- push a branch
|
|
- open a draft PR
|
|
|
|
The browser never receives worker tokens and never talks directly to the worker
|
|
or the box.
|
|
|
|
Worker cleanup is available in `Settings -> Worker`. It can delete stale
|
|
workspace records and ask the active worker to remove orphaned containers and
|
|
inactive work directories (persistent per-user homes are preserved).
|
|
|
|
Local worker development:
|
|
|
|
```sh
|
|
scripts/build-agent-images
|
|
bun smoke:agent-container
|
|
bun dev:next:worker
|
|
bun dev:next:worker:staging
|
|
```
|
|
|
|
Local host-run worker commands load env through Infisical, then
|
|
`scripts/dev-agent-worker` selects Podman when available and falls back to
|
|
Docker. Override the container CLI with:
|
|
|
|
```env
|
|
SPOON_AGENT_CONTAINER_RUNTIME=podman
|
|
```
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><strong>Agent runtimes</strong></summary>
|
|
|
|
Three agent CLIs run inside the box behind one `AgentRuntime` adapter interface
|
|
(`apps/agent-worker/src/runtime/*-adapter.ts`), selected by `job.runtime`:
|
|
|
|
| Runtime | CLI | Selected for |
|
|
| ---------- | ----------------------------------- | -------------------------------------------------- |
|
|
| `codex` | `@openai/codex` | OpenAI providers and Codex ChatGPT-login snapshots |
|
|
| `opencode` | `opencode-ai` | OpenAI-compatible API-key providers (default) |
|
|
| `claude` | `@anthropic-ai/claude-code@2.1.207` | Anthropic providers |
|
|
|
|
The runtime is validated at queue time (`runtimeSupport.ts` +
|
|
`agentJobs.insertJob`): a job is rejected unless the resolved runtime is one the
|
|
AI provider profile supports. `runtimesForProfile` maps profiles to runtimes —
|
|
ChatGPT-login snapshots → `codex` only; `openai` API keys → `opencode`/`codex`;
|
|
Anthropic API keys → `claude`/`opencode`; Anthropic `anthropic_oauth_json`
|
|
credential snapshots → `claude` only; every other OpenAI-compatible API key →
|
|
`opencode`.
|
|
|
|
Auth is materialized into the box before a turn:
|
|
|
|
- **Codex ChatGPT-login** profiles get the encrypted `auth.json` written to
|
|
`CODEX_HOME/.codex/auth.json`.
|
|
- **Anthropic API-key** profiles authenticate via `ANTHROPIC_API_KEY` in the
|
|
environment (no file needed).
|
|
- **Anthropic `anthropic_oauth_json`** credential-snapshot profiles get the
|
|
encrypted OAuth blob written to `~/.claude/.credentials.json`.
|
|
- **API-key** profiles run through OpenCode.
|
|
|
|
All materialized auth files are written `0600`. Treat those saved auth files
|
|
like a password and only use them on trusted workers.
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><strong>Production runtime images</strong></summary>
|
|
|
|
Gitea CI builds and pushes three production images:
|
|
|
|
```txt
|
|
git.gbrown.org/gib/spoon-next:latest
|
|
git.gbrown.org/gib/spoon-agent-worker:latest
|
|
git.gbrown.org/gib/spoon-agent-job:latest
|
|
```
|
|
|
|
The worker image is the long-running service that polls Convex. The
|
|
`spoon-agent-job` image is the **box** image — the per-user Fedora dev box the
|
|
worker `exec`s into (it is not launched fresh per job). Point the worker at it
|
|
with:
|
|
|
|
```env
|
|
SPOON_AGENT_JOB_IMAGE="git.gbrown.org/gib/spoon-agent-job:latest"
|
|
```
|
|
|
|
The box image is Fedora 41 with Node, Bun, pnpm and yarn, npm, git, ripgrep,
|
|
Python, build tools, plus interactive tooling (neovim, tmux, fzf, fd, bat, eza,
|
|
zoxide, gh, gum, oh-my-posh) and the pinned agent CLIs OpenCode, Codex, and
|
|
Claude Code. It is not the forked project's production runtime; it is the agent
|
|
execution environment.
|
|
|
|
Production worker runtime requirements:
|
|
|
|
- `spoon-agent-worker` must run as a separate service.
|
|
- The worker needs `/var/run/docker.sock` mounted so it can create and `exec`
|
|
into per-user boxes on the host daemon.
|
|
- Production should keep `SPOON_AGENT_CONTAINER_RUNTIME=docker`.
|
|
- `SPOON_AGENT_HOST_WORKDIR` must equal the absolute host path backing
|
|
`SPOON_AGENT_WORKDIR` (identical inside and outside the worker container) so
|
|
the host daemon can bind-mount per-user homes under `${WORKDIR}/homes/{username}`.
|
|
- The production Docker host must be logged into `git.gbrown.org` so the worker
|
|
can pull the private `spoon-agent-job` box image.
|
|
- `SPOON_WORKER_TOKEN` must match the value stored in Convex production env.
|
|
- `spoon-next` needs `SPOON_AGENT_WORKER_URL=http://spoon-agent-worker:3921` and
|
|
`SPOON_AGENT_WORKER_INTERNAL_TOKEN` so Next API routes can proxy workspace and
|
|
box file, diff, message, command, terminal, and draft PR actions.
|
|
- `spoon-agent-worker` also needs `GITHUB_APP_ID` and `GITHUB_APP_PRIVATE_KEY`.
|
|
If the private key is stored in a single-line dotenv value, encode newlines as
|
|
literal `\n` characters so the worker can restore the PEM before using it.
|
|
|
|
Useful production checks:
|
|
|
|
```sh
|
|
docker login git.gbrown.org
|
|
docker pull git.gbrown.org/gib/spoon-agent-worker:latest
|
|
docker pull git.gbrown.org/gib/spoon-agent-job:latest
|
|
docker logs --tail=200 spoon-agent-worker
|
|
curl -H "Authorization: Bearer $SPOON_AGENT_WORKER_INTERNAL_TOKEN" \
|
|
http://spoon-agent-worker:3921/health
|
|
```
|
|
|
|
Deployment readiness checklist:
|
|
|
|
1. Production Convex env has `SPOON_WORKER_TOKEN`, `SPOON_ENCRYPTION_KEY`,
|
|
GitHub App env (including `GITHUB_APP_WEBHOOK_SECRET`), and Convex Auth
|
|
signing keys.
|
|
2. Compose env has `SPOON_AGENT_WORKER_URL`,
|
|
`SPOON_AGENT_WORKER_INTERNAL_TOKEN`, `SPOON_AGENT_JOB_IMAGE`, and the GitHub
|
|
App private key; the `spoon-next` image is built with
|
|
`NEXT_PUBLIC_SPOON_AGENT_WORKER_WS_URL`.
|
|
3. The production Docker host can pull private images from `git.gbrown.org`.
|
|
4. `Settings -> Worker` reports the expected box image, runtime, network, and
|
|
active box count.
|
|
5. The first test thread uses a configured API-key provider or a trusted Codex
|
|
login profile.
|
|
6. If a worker restart leaves stale state, use the workspace recovery panel or
|
|
`Settings -> Worker` cleanup.
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><strong>GitHub webhooks</strong></summary>
|
|
|
|
Spoon exposes a signature-verified webhook as a Convex `httpAction` at
|
|
`POST /webhooks/github` (`packages/backend/convex/githubWebhooks.ts`,
|
|
`http.ts`). Configure the GitHub App webhook URL as
|
|
`<CONVEX_SITE_URL>/webhooks/github` and set `GITHUB_APP_WEBHOOK_SECRET`; the
|
|
endpoint fails closed (HTTP 503) when no secret is configured and rejects any
|
|
payload whose `x-hub-signature-256` does not verify.
|
|
|
|
- `push` → a targeted drift refresh for every Spoon tracking that repository.
|
|
- `installation` / `installation_repositories` → the matching connection's
|
|
status flips to `revoked` (deleted/suspended), `active` (created/unsuspended),
|
|
or otherwise `needs_reauth`, surfaced in `Settings -> Integrations`.
|
|
|
|
The hourly `refreshDueSpoons` cron remains a fallback, so drift still refreshes
|
|
even if a webhook is missed.
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><strong>Notifications</strong></summary>
|
|
|
|
Spoon delivers an in-app bell plus preference-gated transactional email
|
|
(`packages/backend/convex/notifications.ts`). Five kinds are emitted:
|
|
`maintenance_thread`, `agent_turn_finished`, `agent_needs_input`, `sync_failed`,
|
|
and `connection_needs_reauth`.
|
|
|
|
- Every event inserts an in-app notification (bell badge via `unreadCount`,
|
|
list via `listMine`, `markRead` / `markAllRead`).
|
|
- Email is sent through UseSend (`USESEND_*`) only when the user has an email
|
|
and the matching per-kind preference is not disabled. Unset preferences
|
|
default to enabled.
|
|
- Per-kind email toggles live in `Settings -> Notifications`. Web push is out of
|
|
scope.
|
|
|
|
</details>
|
|
|
|
## Architecture
|
|
|
|
<details open>
|
|
<summary><strong>Workspace layout</strong></summary>
|
|
|
|
```txt
|
|
.
|
|
├── apps
|
|
│ ├── next # Next.js 16 web app and primary Spoon UI
|
|
│ ├── agent-worker # Optional OpenCode workspace / draft PR worker
|
|
│ └── expo # Expo companion app scaffold
|
|
├── packages
|
|
│ ├── backend # Convex backend package
|
|
│ │ └── convex # Schema, functions, auth, HTTP routes
|
|
│ └── ui # Shared shadcn-based UI components
|
|
├── tools # Shared lint, format, Tailwind, TS, Vitest config
|
|
├── docker # Compose files and worker/job Dockerfiles
|
|
└── scripts # Env, Convex, codegen, database, and CI helpers
|
|
```
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><strong>Core tables</strong></summary>
|
|
|
|
| Table | Purpose |
|
|
| ------------------------- | ----------------------------------------------------------- |
|
|
| `spoons` | Managed fork records |
|
|
| `threads` | Durable maintenance and work conversations |
|
|
| `threadMessages` | Messages inside threads |
|
|
| `syncRuns` | Upstream checks, sync attempts, and maintenance decisions |
|
|
| `ignoredUpstreamChanges` | Intentional ignore records that affect effective drift |
|
|
| `gitConnections` | Git provider connection metadata |
|
|
| `spoonRepositoryStates` | Latest cached upstream/fork state |
|
|
| `spoonCommits` | Cached upstream and fork-only commits |
|
|
| `spoonPullRequests` | Cached fork/upstream pull requests |
|
|
| `spoonSecrets` | Encrypted per-Spoon environment variables |
|
|
| `spoonAgentSettings` | Per-Spoon runtime, branch, command, and env-file settings |
|
|
| `aiProviderProfiles` | Encrypted provider/auth profiles (Codex/OpenCode/Anthropic) |
|
|
| `agentJobs` | Worker-executed workspace jobs and PR lifecycle |
|
|
| `agentJobEvents` | Append-only worker event log |
|
|
| `agentJobArtifacts` | Diffs, summaries, command output, PR body drafts |
|
|
| `agentWorkspaceChanges` | Recorded user, agent, and command file changes |
|
|
| `userDotfiles` | Encrypted per-user dotfiles overlay + repo/setup config |
|
|
| `userEnvironment` | Per-user box/home environment config |
|
|
| `notifications` | In-app notification rows (bell) |
|
|
| `notificationPreferences` | Per-user email notification toggles |
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><strong>Important routes</strong></summary>
|
|
|
|
| Route | Purpose |
|
|
| --------------------------------- | ----------------------------------------------- |
|
|
| `/` | Public product landing page |
|
|
| `/dashboard` | Maintenance overview |
|
|
| `/spoons` | Managed fork list |
|
|
| `/spoons/new` | Manual/GitHub Spoon creation |
|
|
| `/spoons/[spoonId]` | Spoon detail dashboard |
|
|
| `/spoons/[spoonId]/agent/[jobId]` | Interactive workspace |
|
|
| `/threads` | Global thread queue |
|
|
| `/threads/[threadId]` | Thread detail |
|
|
| `/machine` | My Machine: box status, terminal, home |
|
|
| `/settings/profile` | User profile settings |
|
|
| `/settings/integrations` | GitHub and service integration settings |
|
|
| `/settings/ai-providers` | AI provider profiles (Codex/OpenCode/Anthropic) |
|
|
| `/settings/dotfiles` | Per-user dotfiles overlay + repo |
|
|
| `/settings/notifications` | Email notification preferences |
|
|
|
|
The Convex `httpAction` `POST /webhooks/github` (mounted at
|
|
`<CONVEX_SITE_URL>/webhooks/github`) handles GitHub App webhooks. Legacy
|
|
`/updates` and `/agents` routes redirect into `/threads`.
|
|
|
|
</details>
|
|
|
|
## Mobile App
|
|
|
|
<details open>
|
|
<summary><strong>Current Expo scope</strong></summary>
|
|
|
|
`apps/expo` is the mobile Spoon client. It is designed to mirror the core web
|
|
product without exposing worker internals or trying to turn a phone into the
|
|
primary code-editing surface.
|
|
|
|
The mobile app currently supports:
|
|
|
|
- password, GitHub, and Authentik sign-in
|
|
- Dashboard, Spoons, Threads, Workspace Review, and Settings tabs/screens
|
|
- manual Spoon creation and GitHub-assisted repository tracking
|
|
- Spoon detail views for overview, upstream commits, fork-only commits, PRs,
|
|
threads, settings, clone URLs, and additional remotes
|
|
- Spoon maintenance settings, agent settings, encrypted secrets, and bulk
|
|
`.env` paste import
|
|
- thread list/detail, message composer, resolve/cancel actions, and workspace
|
|
review links
|
|
- GitHub integration status and repository listing
|
|
- AI provider profile management, including Codex auth JSON and API-key
|
|
providers
|
|
- read-only workspace review for job status, messages, diffs, events,
|
|
artifacts, and draft PR links
|
|
|
|
The mobile app intentionally does not currently support:
|
|
|
|
- live workspace file browsing/editing
|
|
- mobile command execution
|
|
- direct mobile calls to the agent worker HTTP API
|
|
- mobile access to worker/container tokens
|
|
- long-running app preview stacks
|
|
- production app-store/EAS release flow
|
|
|
|
Mobile workspace editing is deferred until worker authorization and mobile
|
|
editor UX are designed explicitly. For now, the phone is a strong review and
|
|
control surface; the browser remains the code workspace.
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><strong>Expo validation</strong></summary>
|
|
|
|
Useful mobile checks:
|
|
|
|
```sh
|
|
bun --filter @spoon/expo lint
|
|
bun --filter @spoon/expo typecheck
|
|
bun --filter @spoon/expo test:unit
|
|
bun --filter @spoon/expo test:component
|
|
```
|
|
|
|
The Expo unit tests cover pure utilities such as `.env` parsing and formatting.
|
|
The component tests use a lightweight React Native mock layer to exercise shared
|
|
mobile controls, higher-value forms, and route smoke renders without booting a
|
|
native simulator.
|
|
|
|
</details>
|
|
|
|
## Environment Reference
|
|
|
|
This project is currently private, so this section is a reference for what the
|
|
application expects rather than public setup documentation.
|
|
|
|
<details>
|
|
<summary><strong>Local Infisical account selection</strong></summary>
|
|
|
|
Local `dev` and `staging` commands export secrets through Infisical. Spoon runs
|
|
`scripts/infisical-account ensure` from `scripts/export-env` before exporting so
|
|
machines logged into multiple Infisical accounts do not accidentally use the
|
|
wrong organization.
|
|
|
|
If your machine has only one local Infisical account, no extra setup is needed.
|
|
If it has multiple accounts, create this ignored local file:
|
|
|
|
```sh
|
|
mkdir -p .local
|
|
printf "INFISICAL_EMAIL=me@gbrown.org\n" > .local/infisical.env
|
|
```
|
|
|
|
Log into each needed account once with `infisical login`. You can inspect local
|
|
profiles without printing tokens:
|
|
|
|
```sh
|
|
jq '.loggedInUsers[] | {email, domain}' ~/.infisical/infisical-config.json
|
|
```
|
|
|
|
`.local/infisical.env` supports only `INFISICAL_EMAIL=...` and must not be
|
|
committed. CI is unchanged; it uses injected environment files/secrets and must
|
|
not call Infisical.
|
|
|
|
</details>
|
|
|
|
<details open>
|
|
<summary><strong>Public Next variables</strong></summary>
|
|
|
|
| Variable | Used for |
|
|
| --------------------------------------- | ------------------------------------------------------------------- |
|
|
| `NEXT_PUBLIC_SITE_URL` | Canonical Spoon web URL |
|
|
| `NEXT_PUBLIC_CONVEX_URL` | Convex client URL |
|
|
| `NEXT_PUBLIC_SPOON_AGENT_WORKER_WS_URL` | Browser-facing worker WS base for the terminal (**build-time** var) |
|
|
| `NEXT_PUBLIC_DEPLOYMENT_URL` | Convex dashboard/deployment URL when needed |
|
|
| `NEXT_PUBLIC_PLAUSIBLE_URL` | Plausible analytics endpoint |
|
|
| `NEXT_PUBLIC_SENTRY_DSN` | Browser Sentry DSN |
|
|
| `NEXT_PUBLIC_SENTRY_URL` | Sentry instance URL |
|
|
| `NEXT_PUBLIC_SENTRY_ORG` | Sentry organization |
|
|
| `NEXT_PUBLIC_SENTRY_PROJECT_NAME` | Sentry project name |
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><strong>Auth and email</strong></summary>
|
|
|
|
| Variable | Used for |
|
|
| ----------------------- | ----------------------------- |
|
|
| `SITE_URL` | Convex Auth site URL |
|
|
| `JWT_PRIVATE_KEY` | Convex Auth signing key |
|
|
| `JWKS` | Convex Auth JWKS |
|
|
| `AUTH_AUTHENTIK_ID` | Authentik OAuth client ID |
|
|
| `AUTH_AUTHENTIK_SECRET` | Authentik OAuth client secret |
|
|
| `AUTH_AUTHENTIK_ISSUER` | Authentik issuer URL |
|
|
| `AUTH_GITHUB_ID` | GitHub OAuth client ID |
|
|
| `AUTH_GITHUB_SECRET` | GitHub OAuth client secret |
|
|
| `USESEND_API_KEY` | UseSend API key |
|
|
| `USESEND_URL` | UseSend API URL |
|
|
| `USESEND_FROM_EMAIL` | Transactional email sender |
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><strong>GitHub App</strong></summary>
|
|
|
|
| Variable | Used for |
|
|
| ---------------------------- | ---------------------------------- |
|
|
| `GITHUB_APP_ID` | GitHub App ID |
|
|
| `GITHUB_APP_CLIENT_ID` | GitHub App OAuth client ID |
|
|
| `GITHUB_APP_CLIENT_SECRET` | GitHub App OAuth client secret |
|
|
| `GITHUB_APP_PRIVATE_KEY` | GitHub App PEM private key |
|
|
| `GITHUB_APP_WEBHOOK_SECRET` | GitHub webhook verification secret |
|
|
| `GITHUB_APP_SLUG` | GitHub App slug |
|
|
| `GITHUB_APP_INSTALLATION_ID` | Default/local installation ID |
|
|
| `GITHUB_APP_OWNER` | Default/local installation owner |
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><strong>Convex, storage, and runtime</strong></summary>
|
|
|
|
| Variable | Used for |
|
|
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------- |
|
|
| `CONVEX_SELF_HOSTED_URL` | Self-hosted Convex API URL |
|
|
| `CONVEX_SELF_HOSTED_ADMIN_KEY` | Admin key for deploying/syncing Convex |
|
|
| `CONVEX_CLOUD_ORIGIN` | Convex backend origin |
|
|
| `CONVEX_SITE_ORIGIN` | Convex site-function origin |
|
|
| `CONVEX_SITE_URL` | Site URL seen by Convex Auth |
|
|
| `POSTGRES_URL` | Convex storage database URL |
|
|
| `SPOON_ENCRYPTION_KEY` | Encryption key for stored secrets/provider auth |
|
|
| `SPOON_WORKER_TOKEN` | Worker token for Convex worker mutations |
|
|
| `SPOON_AGENT_WORKER_URL` | Internal worker HTTP URL used by Next |
|
|
| `SPOON_AGENT_WORKER_HTTP_PORT` | Worker HTTP port |
|
|
| `SPOON_AGENT_WORKER_INTERNAL_TOKEN` | Server-only token for Next-to-worker proxy |
|
|
| `SPOON_AGENT_TERMINAL_SECRET` | HMAC secret for terminal/box tokens (falls back to the internal/worker token) |
|
|
| `SPOON_AGENT_JOB_IMAGE` | Per-user box (Fedora) image the worker `exec`s into |
|
|
| `SPOON_AGENT_RUNTIME` | Runtime mode, currently Docker/Podman-oriented |
|
|
| `SPOON_AGENT_CONTAINER_RUNTIME` | Container CLI used by worker, `docker`/`podman` |
|
|
| `SPOON_AGENT_BOX_IDLE_MS` | Idle time before a per-user box is reaped (default `1800000`) |
|
|
| `SPOON_AGENT_MAX_CONCURRENT_JOBS` | Worker concurrency limit |
|
|
| `SPOON_AGENT_JOB_TIMEOUT_MS` | Agent turn timeout |
|
|
| `SPOON_AGENT_WORKDIR` | Worker work directory; per-user homes live under `homes/{username}` |
|
|
| `SPOON_AGENT_HOST_WORKDIR` | Host path matching `SPOON_AGENT_WORKDIR` when the worker runs in Docker and controls the host Docker socket |
|
|
| `SPOON_AGENT_NETWORK` | Optional box container network |
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><strong>Deployment and observability</strong></summary>
|
|
|
|
| Variable | Used for |
|
|
| ----------------------- | --------------------------------- |
|
|
| `NODE_ENV` | Runtime environment |
|
|
| `SENTRY_AUTH_TOKEN` | Sentry source map/upload auth |
|
|
| `REDACT_LOGS_TO_CLIENT` | Convex log redaction setting |
|
|
| `DISABLE_BEACON` | Self-hosted Convex beacon setting |
|
|
| `DO_NOT_REQUIRE_SSL` | Self-hosted Convex SSL behavior |
|
|
| `CI_ENV_FILE` | CI-provided env file path |
|
|
|
|
</details>
|
|
|
|
## Current Status
|
|
|
|
<details open>
|
|
<summary><strong>Implemented</strong></summary>
|
|
|
|
- Thread-first Next.js product shell
|
|
- GitHub App connection and fork creation foundation
|
|
- GitHub drift refresh, commit cache, PR cache, and sync-run history
|
|
- Effective drift and ignored upstream change records
|
|
- Global Threads page and Spoon-scoped Threads tab
|
|
- Persistent per-user box the worker `exec`s into for agent turns, terminal, and
|
|
commands
|
|
- Three agent runtimes (Codex, OpenCode, Claude Code) behind one adapter, with
|
|
queue-time runtime validation
|
|
- My Machine surface (box status/start/stop/restart, terminal, home browser)
|
|
- Interactive workspace terminal (xterm.js → box PTY)
|
|
- Per-user dotfiles overlay + optional dotfiles repo
|
|
- Signature-verified GitHub webhook with hourly cron fallback
|
|
- In-app + email notifications with per-user preferences
|
|
- Monaco editor with optional Vim mode
|
|
- Diff viewer, command panel, worker logs, and artifacts
|
|
- Encrypted Spoon secrets and bulk `.env` import
|
|
- Encrypted AI provider profiles, including Codex/Anthropic auth JSON and
|
|
API-key provider support
|
|
- Authentik, GitHub, and password auth through Convex Auth
|
|
- Self-hosted Convex/Postgres deployment model
|
|
|
|
</details>
|
|
|
|
<details>
|
|
<summary><strong>Intentionally not done yet</strong></summary>
|
|
|
|
- Autonomous merging for custom/diverged forks
|
|
- Non-GitHub provider automation
|
|
- Pushing agent branches to additional remotes
|
|
- Long-running preview stacks for arbitrary forked projects
|
|
- Direct browser access to worker containers
|
|
- Public self-hosting setup documentation
|
|
- Production mobile release flow
|
|
|
|
</details>
|
|
|
|
## Notes
|
|
|
|
Spoon is built for a very specific maintenance problem: "I want to fork this
|
|
project, but I do not want to permanently become its maintenance team."
|
|
|
|
The current product direction is to make that maintenance visible, threaded,
|
|
reviewable, and increasingly automated where it is safe. Clean forks can stay
|
|
close automatically. Custom forks get context, workspace help, and draft PRs.
|