Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 5 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ ops inventory).
| Optional SCIM (`SCIM_BEARER_TOKEN`) | **Not Done** | SCIM 2.0 Users at `/api/scim/v2` — set only when directory provisioning is intended; fail-closed 404 when unset |
| Blob transcript store (`BLOB_READ_WRITE_TOKEN`, phase 0 #515) | **Not Done** (documented seam only) | Vercel Blob (or BYO S3/R2 behind the same seam) for the session **transcript**; Redis keeps the small envelope. Server-minted short-lived scoped upload URLs; client→Blob uploads; legacy full-record GET stays for roll-forward while old blobs stay small. Configure in Vercel/env-manager, never a laptop ritual; no migrate/backfill/seed. Docs: [docs/bring-your-own.md](docs/bring-your-own.md), [docs/session-model.md](docs/session-model.md). When unset the app runs an in-memory transcript store (dev/tests only) |
| Vercel Workflows (backend-agents) | **Done (code)** | Workflow SDK wired (`workflow` dep + `withWorkflow` in `next.config.js`), authed smoke route (`app/api/workflows/smoke`), dispatch GHA **`workflows-smoke`** (always targets Production; no preview env is built). Workflows **activates automatically when you deploy with the SDK** — there is no dashboard enable button; the operator step is **deploy to Production** then dispatch **`workflows-smoke`** (`confirm=smoke`). Never the tab-owned `/api/agent` as the smoke path. Ops: [docs/feature-divide.md](docs/feature-divide.md) |
| Vercel Workflows turn probe (spike, plan #787) | `app/api/turns/*`, `lib/workflows/turnsFixtureWorkflow.ts` + `turnsFixtureEvents.ts` | **Spike-only, NOT the turn owner.** `POST /api/turns` → `x-workflow-run-id` + `{ runId }`; `GET /api/turns/:runId/stream?startIndex=` resumes the fixture's `AgentStreamEvents` emitted to `getWritable()` (reconnect primitive). Present on `main`, **preview/spike** — `/api/agent` stays the production turn owner until slice E swaps in; durable turn-owner/attach/detach is slice I (#772). Fail-closed 503 on a Workflows-disabled start; never falls back to `/api/agent` |


**Agent behavior (origin):**
Expand Down Expand Up @@ -293,7 +294,9 @@ ops inventory).
smoke poll budget `WORKFLOWS_SMOKE_POLL_TIMEOUT_MS` 120000 / interval
`WORKFLOWS_SMOKE_POLL_INTERVAL_MS` 2000 (±0.5 s jitter); per-process
`POST` start guard `WORKFLOWS_SMOKE_POST_MIN_INTERVAL_MS` = 15000 (429) —
bounded, generous, NEW caps in the plan's Caps table. `maxDuration` stays as
bounded, generous, NEW caps in the plan's Caps table. The `/api/turns` spike
POST carries its own sibling guard `TURNS_POST_MIN_INTERVAL_MS` = 15000 (429,
NEW spike cap in plan #787 Caps table style). `maxDuration` stays as
the plan pinned; a LOWER ceiling would be a human-gated cap change.

IDs and URLs (maintainer sample): [`docs/project-ids.md`](docs/project-ids.md).
Expand Down Expand Up @@ -332,6 +335,7 @@ invincible/
| DOM site chrome nav (hamburger Account menu) | `app/components/AppNav.tsx` (brand wordmark; optional `busy?: boolean` from `HarnessHost` only — TEAL outline + neon bloom + sine pulse + motes while the active harness turn is Busy; never poll the bridge; settings/admin omit the prop), `app/components/AuthNavLinks.tsx` (server: `soleMembership`+`canAccessAdmin` → `showAdmin`), `app/components/NavMenu.tsx` (client dropdown: ARIA `menu`, Arrow/Tab/Home/End, Escape + click-outside close + focus return, ≥44px touch targets, palette-only TEAL), `lib/navMenu.ts` + `lib/navMenu.test.ts` (`buildSignedInNavItems` — pure ordering/gating rule, unit-tested), footer slot `app/logout/LogoutButton.tsx`; unauth keeps inline `Sign in` header control. Client holds **zero** role-gate logic — it renders pre-gated inert `items` only |
| API / AI Gateway / agent | `app/api/*`, `lib/agent/*`, `lib/sandbox/*` |
| Agent SSE stream (tools + text + reasoning) | `lib/agent/agentStream.ts`, `lib/agent/runAgent.ts`, `lib/agent/reasoningConfig.ts`, `app/api/agent/route.ts`, `lib/agentApi.ts`, `docs/agent-stream.md` |
| Workflow turn probe (spike-only `/api/turns` prefix, plan #787) | `app/api/turns/route.ts`, `app/api/turns/[runId]/stream/route.ts`, `lib/workflows/turnsFixtureWorkflow.ts` (+ `turnsFixtureEvents.ts`) | **Vercel backend owns this; it is a throwaway probe, NOT the turn owner.** `/api/agent` stays the production turn owner until slice E (#768) swaps in; durable turn-owner/attach/detach is slice I (#772). Never add a client/Wasm path to these spike routes |
| Agent read-before-edit / file freshness | `lib/agent/fileFreshness.ts`, `lib/agent/pathLock.ts` (per-path apply serialization), `lib/agent/tools.ts`, `lib/agent/runAgent.ts`, [docs/sandbox.md](docs/sandbox.md) |
| Logical agent cwd + workspace-root↔abs canonicalization (`change_dir` / session / default env; **`sandbox_info`** is the bind/cwd/caps/env introspector — do not `exec env`; `canonicalizePath(R, p)` / `workspaceAbsToRel(R, abs)` / `resolvePathForTool(R, cwd, p)` / `rewriteExecRootToRel(R, text)` in `lib/agent/workPath.ts`) + **`search`** (read-grant-only code-grep via `rg`; `lib/agent/tools.ts`) | `lib/agent/workPath.ts`, `lib/agent/tools.ts`, `lib/agent/runAgent.ts`, `lib/agent/agentBody.ts`, `lib/sandbox/config.ts`, `lib/sessionStore.ts`, `lib/harnessChat.ts`, `lib/agentApi.ts`, `lib/sessionCloudCaps.ts` (shared client-safe `sanitizeSessionCwd` + Redis-safe opaque id predicate), [docs/sandbox.md](docs/sandbox.md), [docs/session-model.md](docs/session-model.md), [docs/agent-stream.md](docs/agent-stream.md). Tool paths accept **in-jail absolute paths** on all FS tools + `change_dir` + `exec` cwd: an absolute under the per-binding jail root R (`resolved.value.workspaceRoot` → `RunAgentParams.workspaceRoot` → `createAgentTools`) is canonicalized to the same workspace-relative freshness key as its relative form (BYO + Vercel parity); out-of-jail absolutes and `..`/symlink escapes fail closed. Absolute paths under `R` that **appear in `exec` stdout/stderr** are likewise rewritten to workspace-relative (`rewriteExecRootToRel` in `lib/agent/workPath.ts`, applied to `result.stdout`/`result.stderr` separately) so `exec pwd` ≡ `pwd`/annotations; when `R` is unresolvable the exec output passes through byte-for-byte (fail-open), and rewrites are capped and never throw. When R is unresolvable (BYO daemon down/pre-v2 — `workspaceRoot === null`) absolute is rejected (“root unavailable — use workspace-relative”) while relative + cwd still work. Initial request/session `cwd` stays relative-only; `.` is the workspace-root default session start (there is no `SANDBOX_DEFAULT_CWD` env knob), `..` walks up toward the workspace root and errors only past it, and an **exact ancestor** of cwd re-roots cleanly (`change_dir invincible` from `cwd=invincible/docs` → `invincible`, not the phantom `invincible/docs/invincible`) while a name-prefix sibling is never re-rooted. P1/GAP-1 (#452/#330): `cwd` + `activeSandboxId` are **session-owned** and ride the Redis record (`meta.{logicalCwd,activeSandboxId}`). `activeSandboxId` is now **server-resolved** (routing override via `lib/tenancy/resolveSandbox.ts` `requestedSandboxId`), not carry-only. A **confirmed successful `change_dir`** is persisted as the session cwd even when the turn later cancels / times out / hard-errors (`lib/harnessChat.ts` host-side `liveCwd`); the success path still prefers the authoritative `agentResult.cwd`, and only a confirmed `change_dir` (never an errored one) is stored on a failed/aborted turn. The **`exec` tool** returns a **compact summary**, not a raw dump: first `EXEC_LOG_HEAD_LINES` (10) + last `EXEC_LOG_TAIL_LINES` (10) lines per stdout/stderr with line/byte counts and `... (N lines truncated)`, each shown line byte-clipped (`EXEC_SUMMARY_LINE_MAX_BYTES`=4096) so a single fat stdio line can't inline the stream or truncate the `log:` pointer off; and when either stream is non-empty writes the full redacted output to `<workspace>/.invincible/logs/exec-<ts>-<seq>.log` via `client.write_file(..., mkdir: true)` (a brand-new hidden workspace dir; backends never auto-create parents; the `-<seq>` monotonic counter keeps same-ms parallel execs from overwriting), reporting two `read_file` pointers — `log: <rel path>` (cwd-relative, from nested cwd `../.invincible/logs/…`) and `log (root): <root path>` (workspace-root-relative, read from the workspace root `cwd .`, so a depth-changing `change_dir` can never strand the full output); the write stays workspace-root, and `.invincible/` is gitignored; both pointers ride immediately after `exit=`/`TIMED_OUT` — empty output (`exec true`) writes no file, and a log-write failure fails soft with a `⚠ log write failed` note whose reason is **sanitized** (a backend/jail path never surfaces) (caps `EXEC_LOG_HEAD_LINES`/`EXEC_LOG_TAIL_LINES`=10 and `EXEC_LOG_MAX_BYTES`=8 MiB in `lib/sandbox/config.ts`) |
| Cloud multi-device harness session (Redis multi-session, `/api/sessions*`, hybrid local+cloud; **phase 0 #515 envelope + Blob transcript carrier**) | `app/api/sessions/*` (+ `app/api/sessions/[id]/envelope/*`, `[id]/transcript/*`), `lib/sessionRepository.ts`, `lib/sessionCloudCaps.ts`, `lib/sessions/*` (+ `lib/sessions/blobStore.ts`, `blobStores.ts`), `lib/tenancy/harnessSessionsRedis.ts`, `lib/tenancy/harnessSessions.ts` (archive read + shared validator), `lib/di/index.ts` (root), `app/harness/HarnessHost.tsx`, `middleware.ts`, [docs/session-model.md](docs/session-model.md), [docs/bring-your-own.md](docs/bring-your-own.md), [SECURITY.md](SECURITY.md) — one-shot Postgres→Redis backfill: GHA **`sessions-redis-backfill`** (idempotent per-user marker); Postgres `harness_sessions` is a read-only archive. P1/GAP-1 (#452): session-carrier `meta.{logicalCwd,activeSandboxId}` folds into the PUT body and restores on pull/adopt; **plan #616 (source #610)** adds the reserved `meta.selectedModel` session carrier for the selected model pick (restore by id after the model catalog push; server **drops a poisoned value to unset**, never a 400). **Phase 0 (#515):** the transcript lives in **Vercel Blob** (`BLOB_READ_WRITE_TOKEN` / BYO S3-R2 seam) pointed to by `meta.transcriptPointer` on the small Redis envelope (`harness:envelope:*`); server mints short-lived scoped upload URLs for **client→Blob** uploads; legacy full-record GET stays for roll-forward while old blobs stay small. Envelope upsert/read: `PUT`/`GET /api/sessions/:id/envelope`; mint/read: `POST`/`GET /api/sessions/:id/transcript` |
Expand Down
Loading