|
| 1 | +# Shep Cloud — `cloud` branch |
| 2 | + |
| 3 | +Multi-tenant SaaS build of shep that ships at **app.shep.bot**. Same web UI you'd see at `localhost:4050`, but every user is logged in via Keycloak, every row is scoped to their org, and every "run agent" spawns an isolated k8s Job in the cluster instead of a local subprocess. |
| 4 | + |
| 5 | +This branch is **structural scaffolding, not yet a working build**. The skeleton lays down the swap points; the punchlist below is what's left. |
| 6 | + |
| 7 | +## Mental model |
| 8 | + |
| 9 | +The CLI build (`main`) is unchanged: SQLite, local subprocesses, single user. The cloud build is selected by `SHEP_MODE=cloud` at boot — that swaps: |
| 10 | + |
| 11 | +| Concern | `main` (CLI) | `cloud` | |
| 12 | +|----------------|----------------------------|--------------------------------------------------------| |
| 13 | +| Persistence | SQLite (`~/.shep/shep.db`) | Postgres on the cluster, RLS + `org_id` on every table | |
| 14 | +| Auth | none (local user) | Keycloak OIDC, NextAuth session, request-scoped tenant | |
| 15 | +| Agent execution| `child_process.spawn` | k8s `batch/v1.Job` per run, ephemeral PVC, NetworkPol | |
| 16 | +| Worktree FS | `~/.shep/worktrees/...` | Per-Job ephemeral `emptyDir` or PVC; git remote is SoT | |
| 17 | +| Settings store | `~/.shep/settings.json` | Postgres `org_settings` table | |
| 18 | + |
| 19 | +**No fork from main:** all cloud-mode code is additive (new files under `*/postgres/`, `register-cloud.ts`, etc.). The only edits to existing files are conditional (`if (SHEP_MODE === 'cloud')`). Rebases from `main` should be clean. |
| 20 | + |
| 21 | +## What's scaffolded already |
| 22 | + |
| 23 | +- `packages/core/src/infrastructure/persistence/postgres/` — connection, tenant context, RLS-based migration baseline |
| 24 | +- `packages/core/src/infrastructure/repositories/postgres-feature.repository.ts` — proof-of-pattern; matches `IFeatureRepository` |
| 25 | +- `packages/core/src/infrastructure/di/modules/register-cloud.ts` — DI module that swaps adapters when `SHEP_MODE=cloud` |
| 26 | +- `packages/core/src/infrastructure/di/container.ts` — branches on `SHEP_MODE` to call `registerCloud()` instead of (some of) the SQLite registrations |
| 27 | +- `packages/core/src/infrastructure/services/agent-executor/k8s-job.executor.ts` — stub implementing `IInteractiveAgentExecutor` |
| 28 | +- `src/presentation/web/middleware.ts` — Keycloak gate, request-scoped tenant context |
| 29 | +- `.github/workflows/cloud-build.yml` — CI builds `ghcr.io/shep-ai/shep:cloud-<sha>` on push to this branch |
| 30 | + |
| 31 | +## Punchlist (in order of dependency) |
| 32 | + |
| 33 | +### Persistence layer |
| 34 | +- [ ] **Port the remaining ~30 repositories** to Postgres. The pattern is `postgres-feature.repository.ts` — copy, swap query syntax, rely on RLS for tenant filter. |
| 35 | +- [ ] **Settings repository** is the next gating one (the UI hits it on every page load). Then `repository-repository`, `application-repository`, the PM family. |
| 36 | +- [ ] **Migrations** — flesh out `0001_init.sql` to cover every table currently in SQLite (`migrations/sqlite/*.sql` is the source). Use a per-table `org_id uuid not null` column + RLS policy `org_id = current_setting('app.org_id')::uuid`. |
| 37 | +- [ ] **`org_id` propagation in domain code** — the domain entities (`Feature`, `Application`, etc.) don't carry `org_id`. We have two options: (a) thread it through the domain (invasive), (b) keep RLS-only and let Postgres enforce. Going with (b). Repos take tenant context from a request-scoped tsyringe child container. |
| 38 | + |
| 39 | +### Auth & tenancy |
| 40 | +- [ ] **Org/Member model** — currently no `orgs` or `org_members` tables. Add them in migrations. UI for create org / invite member. |
| 41 | +- [ ] **Keycloak token → org_id** — middleware reads JWT, looks up the user's default org, sets `app.org_id` on the connection. Multi-org users get an org switcher. |
| 42 | +- [ ] **Per-route guards** — every server action and API route currently assumes a single user. Add a higher-order wrapper that asserts `getServerSession()` and resolves tenant before the handler runs. |
| 43 | + |
| 44 | +### Agent execution |
| 45 | +- [ ] **k8s-job.executor.ts** is a stub. Real impl needs to: |
| 46 | + - render a `batch/v1.Job` with the agent CLI (Claude Code / Cursor / Gemini) in the image |
| 47 | + - mount a fresh `emptyDir` at `/workspace`, clone the repo (using a per-org GitHub deploy token from `org_secrets`) |
| 48 | + - inject the org's AI provider API key via `secretRef` |
| 49 | + - stream pod logs back via SSE — reuse the existing `agent-events` API route, swap its source |
| 50 | + - report exit/result, post-process to update `agent_runs` row + open PR |
| 51 | +- [ ] **NetworkPolicy** scoped to the agent's namespace allowing egress only to `api.github.com` + the chosen AI provider host. |
| 52 | +- [ ] **Sandboxing tier** — for v1 use default runtime + restrictive securityContext. Migrate to gVisor (`runtimeClassName: gvisor`) once we have one untrusted real user. |
| 53 | + |
| 54 | +### Filesystem |
| 55 | +- [ ] **No local worktrees.** Every Job clones afresh. State lives in git. Future: persistent worktrees via per-org PVCs if performance requires. |
| 56 | +- [ ] **Settings/uploads/attachments** — currently disk-based. Move to MinIO or skip until needed. |
| 57 | + |
| 58 | +### Deployment |
| 59 | +- [ ] **shep-infra:** swap the placeholder Deployment at `app.shep.bot` to pull `ghcr.io/shep-ai/shep:cloud-<sha>`. Wire `SHEP_MODE=cloud`, `DATABASE_URL`, Keycloak env, k8s SA token. |
| 60 | +- [ ] **Run drizzle/postgres migrations** as initContainer (or built-in app boot, idempotent). |
| 61 | +- [ ] **Stop CLI bits** — at boot in cloud mode, don't initialize local SQLite, daemon, notification watcher, IDE integration, etc. `register-cloud.ts` should branch. |
| 62 | + |
| 63 | +### Hardening / nice-to-have |
| 64 | +- [ ] Org-secrets vault (encrypt-at-rest, ideally via Infisical when we wire ESO back on). |
| 65 | +- [ ] Rate-limit per org on agent runs (so one tenant can't burn the cluster). |
| 66 | +- [ ] Audit log on tenant-impacting actions. |
| 67 | +- [ ] Idle-org reaper that pauses long-unused orgs' resources. |
| 68 | + |
| 69 | +## How to work this branch |
| 70 | + |
| 71 | +1. Pick a repository from the punchlist. |
| 72 | +2. Copy `postgres-feature.repository.ts` to `postgres-<name>.repository.ts`. Implement the interface methods. |
| 73 | +3. Add the registration to `register-cloud.ts`. |
| 74 | +4. Add migrations for any new tables in `migrations/postgres/`. |
| 75 | +5. Run locally with `SHEP_MODE=cloud DATABASE_URL=postgres://... pnpm dev` against a local Postgres. |
| 76 | +6. Push. CI builds `ghcr.io/shep-ai/shep:cloud-<sha>`. |
| 77 | + |
| 78 | +Stay strict about additive-only changes to files that exist on `main`. Anything more is a recipe for merge hell. |
0 commit comments