Subject: kortix-ai/suna (product name Kortix; the repo name is a historical leftover still called Suna) Based on commit:
5862e95671cfd198c6a94ceb9c1fefe579f00a8b(full-checkout deepened edition;git rev-parse HEAD) Version: rootVERSIONis0.10.14; the README badge still says0.9.98(README.md:12) — treat theVERSIONfile as canonical Analysis date: 2026-07-24 Document version: full-checkout deepened edition (relative to the old sparse edition, this substantially thickensapps/web, the full change-request path, snapshot/providers, andpackages/{manifest-schema,starter,registry,executor-sdk}; every claim can be traced tofile:line) Audience: beginners with a little programming. Each chapter tries to follow “metaphor → real source → critical-path explanation → design trade-offs.” Note: this lecture uses the same 12-chapter frame as the sibling analyses of Claude Code / opencode / hermes-agent / Raven / CodeWhale / nanobot / goose / OpenManus, so you can compare them sideways. Allfile:linecitations are relative to参考项目/suna/. This workspace is a full tree (includingapps/web,desktop-electron,mobile, and everypackages/*). Features that cannot be found, or that we are not sure about, are explicitly marked not found — never invented. Product axis: Kortix aims at a “company operating system / general-task Agent platform” (research, deliverables, automation, multi-channel kickoff). It is not a Claude Code–style “write code in this repo first” single-machine coding agent. It treats OpenCode as the brain inside the sandbox, and itself owns isolation, permissions, connectors, billing, and change requests.
Chapter 1 · Project overview: what it is, who builds it
One-sentence version
Kortix (GitHub repo kortix-ai/suna) is an AI Command Center that takes “the company” literally and makes it a Git repository: configuration, agents, skills, memory, and sandbox specs all live in the repo; each session boots an isolated sandbox that runs OpenCode; only changes that have passed a change request review can merge back to main. The license is Elastic License 2.0 (LICENSE:1), not MIT — it explicitly restricts offering this software as a hosted service to third parties (LICENSE:18-20).
The README title is the product name (README.md opens with # Kortix); site metadata pins the positioning as “open-source AI command center for your company” (apps/web/src/lib/site-metadata.ts:16). The MANIFESTO’s first sentence is even blunter (MANIFESTO.md:3):
A company is going to be a git repository.
Understand the positioning with a metaphor
If Claude Code is a “fine-dining sushi bar” (the menu is built around writing code), and OpenManus is a “street-stall research travel agency” (single-machine ReAct + a browser), then Kortix is more like a gated tech park plus a self-serve workstation system:
- The blueprint of the company building is a Git repo (
kortix.yaml+.kortix/opencode/); - Each time you start work, you get an independent workstation (a cloud sandbox) with the toolchain and OpenCode already installed;
- If you want to keep the work, you can only open a CR (change request) for human review — you cannot graffiti the shared lobby wall;
- The front desk can be Web / CLI / Desktop / Mobile / Slack / Teams / Email — every utterance still enters the same session lifecycle.
The MANIFESTO states the other half of the bet in the open (MANIFESTO.md:7-9): the AGI era needs a “WordPress / command center” — context, agents, skills, triggers, connectors, and memory in one place, not forty tools taped together.
Who builds it / what it is called
| Item | Evidence |
|---|---|
| Org / repo | kortix-ai/suna (README / repo URL) |
| Product name | Kortix (README title, CLI kortix, package names @kortix/*) |
| Historical name Suna | repo name, suna-migration/, some comments still keep it |
| License | Elastic 2.0 (LICENSE:1-20) |
| Version | VERSION → 0.10.14; README badge still 0.9.98 (README.md:12) |
| Language | main repo TypeScript; runtime Bun (API / daemon); pnpm workspace (package.json:60; pnpm-workspace.yaml:1-3) |
| Web package name | Kortix-Computer-Frontend, Next 15.5.21 (apps/web/package.json:2,159) |
| Desktop | Electron shell package @kortix/desktop-electron, wrapping Web (apps/desktop-electron/package.json:2-7) |
| Mobile | Expo app kortix (apps/mobile/package.json:2-4) |
Stack at a glance
| Layer | Tech | Location / evidence |
|---|---|---|
| Frontend host | Next.js 15 App Router (Turbopack in development) | apps/web/package.json:6,159; AGENTS.md:179 |
| Client SDK | @kortix/sdk (SSoT; hosts must not talk to the API/OpenCode directly) |
AGENTS.md:91-137; packages/sdk/src/index.ts:1-39 |
| API | Bun + Hono OpenAPI, default :8008/v1 |
AGENTS.md:180; apps/api/src/index.ts |
| Data | Supabase + Drizzle (@kortix/db) |
AGENTS.md:181; packages/db/ |
| Manifest contract | @kortix/manifest-schema (kortix.yaml validation SSoT) |
packages/manifest-schema/package.json:2-4 |
| Scaffolding | @kortix/starter (init / create-repo templates) |
packages/starter/package.json:2-4 |
| Marketplace engine | @kortix/registry (shadcn-compatible registry) |
packages/registry/package.json:2-4 |
| Connector client | @kortix/executor-sdk (server-side proxy send) |
packages/executor-sdk/package.json:2-5 |
| Sandbox providers | Daytona / Platinum / E2B / local-docker | provider-parity.test.ts:11-34 |
| Brain inside the sandbox | OpenCode (opencode serve) supervised by kortix-agent |
apps/sandbox/README.md:15-19; kortix-sandbox-agent-server/README.md:6-12 |
| Model gateway | apps/llm-gateway + API-side routing |
apps/api/src/llm-gateway/; index.ts:740-741 |
| Connector execution | /v1/executor, credentials stay on the server |
apps/api/src/executor/execute.ts:1-5 |
flowchart LR
subgraph Hosts["Host surface"]
WEB["apps/web Next.js"]
DESK["desktop-electron"]
MOB["mobile Expo"]
CLI["kortix CLI"]
CH["Slack / Teams / Email / …"]
end
SDK["@kortix/sdk"]
API["apps/api :8008"]
DB[(Supabase / @kortix/db)]
SBX["Cloud sandbox
session_id == sandbox_id"]
DAEMON["kortix-agent"]
OC["opencode serve"]
CR["change_requests"]
WEB --> SDK
DESK --> WEB
MOB --> SDK
CLI --> API
CH --> API
SDK --> API
API --> DB
API -->|"/v1/p/:id/8000"| SBX
SBX --> DAEMON --> OC
OC -->|"commit + kortix cr"| CR
CR --> API
Axis differences versus other projects in this series (plant the target first)
- vs Claude Code / opencode on a single machine: Kortix embeds OpenCode inside a multi-tenant sandbox OS, and itself owns identity, billing, connectors, and CRs.
- vs OpenManus: OpenManus is an in-process ReAct inheritance chain; Kortix’s “think one step, do one step” lives in the OpenCode binary. This repo writes the platform spine. See the short note at the end of this chapter and Chapter 12.
- vs nanobot / Raven: nanobot is a personal IM butler + a small core; Kortix is an enterprise command center + Elastic license + sandbox isolation.
Contrast with OpenManus (short note)
Same axis (general tasks / research / deliverables), different bets: OpenManus bets on “a ReAct inheritance chain you can understand in three hours”; Kortix bets on “the company repo + one session, one sandbox + change request.” On license, OpenManus is MIT; Kortix is Elastic License 2.0. Details are in the sibling lecture OpenManus-vs-Suna side-by-side comparison.
Chapter 2 · Panorama architecture: a layered map
Metaphor: six floors of the tech park
- Gate and reception (Hosts + SDK): Web/CLI/IM/Mobile enter the park only through
@kortix/sdkor the official CLI (AGENTS.md:110-118). - Dispatch center (
apps/api): session lifecycle, project Git, billing, IAM, channel webhooks, sandbox proxy. - Tool broker (executor + llm-gateway): third-party APIs and model traffic are sent from the server; the sandbox holds only a scoped token.
- Workstation (sandbox + kortix-agent): clone the repo, switch to the session branch, bring up OpenCode, expose 8000 / health checks / static preview.
- Archives (git + change_requests + DB mirror): truth lives in the sandbox and git; the DB is for list/sync acceleration (
MANIFESTO.md:50). - Contract layer (
packages): manifest-schema / starter / registry / sdk / db / executor-sdk — so CLI, API, and Web share one set of shapes.
Directory map (full checkout)
| Path | Responsibility |
|---|---|
apps/web/ |
Next.js Command Center UI; thin consumer of SDK |
apps/api/ |
multi-tenant control plane; src/index.ts mounts almost every /v1/* |
apps/cli/ |
kortix init/ship/sessions/cr/… |
apps/sandbox/ |
sandbox Docker reference image and entrypoint |
apps/kortix-sandbox-agent-server/ |
in-sandbox daemon source (compiled into the image) |
apps/llm-gateway/ |
gateway service implementation (API also has routing hooks) |
apps/desktop-electron/ |
Electron shell, loads the Web URL |
apps/mobile/ |
Expo mobile client |
packages/sdk/ |
the host’s only data layer |
packages/db/ |
Drizzle schema (including change_requests, session_sandboxes) |
packages/shared/ |
cross-package constants/utils |
packages/manifest-schema/ |
kortix.yaml spec and validator |
packages/starter/ |
project templates (init / create-repo) |
packages/registry/ |
Marketplace discovery/build primitives |
packages/executor-sdk/ |
connector-call client |
packages/agent-tunnel/ |
device tunnel |
packages/api-contract/ |
Zod wire contracts |
packages/llm-catalog/ |
model catalog |
What the Web-side “Command Center” actually looks like
Product copy repeatedly calls it a command center (apps/web/src/lib/site-metadata.ts:16; marketing hero apps/web/src/features/marketing/hero.tsx:46). The real in-app operating surface is the project workspace under the App Router:
| Route | File | Role |
|---|---|---|
/projects/[id] |
apps/web/src/app/(app)/projects/[id]/page.tsx |
project home: ProjectHome composer opens a new session |
/projects/[id]/sessions/[sessionId] |
…/sessions/[sessionId]/page.tsx |
the session page that mounts useSession |
/projects/[id]/customize/… |
customize pages | Agents / Secrets / Sandbox / Review, etc. |
/review |
apps/web/src/app/(app)/review/page.tsx |
Review Center prototype (mock, no API) |
| In-project Review | features/workspace/…/review-view.tsx |
real data wired through ReviewCenterConnected |
features/ is cut by domain: session/ (chat and tool rendering), workspace/ (sidebar/home), review-center/, project-files/, marketplace/, providers/ (Auth / LLM provider UI).
API route spine (one table for the control plane)
apps/api/src/index.ts hangs subsystems off a unified Hono app (excerpt, index.ts:693-851):
/v1/accounts,/v1/auth,/scim/v2— identity and enterprise directory/v1/router,/v1/generation,/v1/usage— model routing and usage/v1/billing— Stripe and related billing/v1/platform— sandbox platform capabilities/v1/projects— Git-backed projects and sessions (including change-requests)/v1/marketplace— Agent/skill marketplace/v1/git— git-proxy (conditionally mounted)/v1/executor— connector execution/v1/webhooks/*— Slack/Teams/Telegram/Email/Meet/sandbox lifecycle/v1/tunnel— device tunnel/v1/p— sandbox preview/proxy (the external entry for OpenCode SSE/HTTP)
The process-level crash guard is deliberately “log only, do not take down the whole machine” (index.ts:98-119) — a typical trade-off for a long-lived multi-tenant service.
Two “main loops” you must separate first (the key)
| Loop | Lives where | Does what |
|---|---|---|
| Platform session loop | apps/api/.../session-lifecycle/ |
create → provision sandbox → start/ready → continue/deliver prompt → stop/restart; queues, idempotency, backpressure |
| Model–tool loop | OpenCode (binary inside the sandbox) | classic Agent: read context → call tools → observe → think again; this repo does not rewrite that chain |
The daemon itself states the boundary (kortix-sandbox-agent-server/README.md:6-24): it only supervises opencode serve, reverse-proxies HTTP/SSE, health-checks, and static preview; triggers/channels/connectors/secrets are not the daemon’s job.
The SDK is the host’s only data layer (architectural iron law)
AGENTS.md:91-137 writes this in stone:
- Logic goes into the SDK; hosts must not raw-
fetchor use@opencode-ai/sdk; - One host, one client:
createKortix/configureKortix; - One hook for an entire session:
useSession(projectId, sessionId); apps/web’sstores/server-store,hooks/opencode/use-*are shims (e.g.apps/web/src/stores/server-store.ts:2:export * from '@kortix/sdk/server-store';hooks/opencode/use-session-sync.ts:2:export * from '@kortix/sdk/react').
Web’s only wiring port: apps/web/src/lib/kortix-config.ts:3-30 uses configureKortix({ backendUrl, getToken, … }) to inject the Supabase JWT and toast/notification sinks.
Four often-ignored packages (summary)
@kortix/manifest-schema (packages/manifest-schema/src/index.ts:1-15): the canonical schema for kortix.yaml plus a pure-function validator. Consumers: CLI ship pre-check, backend CR-merge gate, kortix validate. No I/O, no DB.
@kortix/starter (packages/starter/src/index.ts:1-48): folder templates + getStarterFiles(); the default user template is general-knowledge-worker; internally it also keeps minimal for clone/seed. API create-repo and kortix init share it.
@kortix/registry (packages/registry/src/index.ts:1-12): shadcn-compatible registry discovery/build; install has already been changed to agent import (read the source, merge files, land a CR) — there is no longer a deterministic install engine.
@kortix/executor-sdk (packages/executor-sdk/src/index.ts:41-50): a typed client that carries a token; can go through /executor/projects/:projectId/..., so “CLI on a laptop” and “session token inside the sandbox” call the same shape, while secrets stay on the server.
Design trade-offs (architecture layer)
- Thick platform, thin host: logic goes into SDK/API; Web is a thin consumer (
AGENTS.md:91-133). The cost is a huge monorepo and a long path for newcomers. - Truth is in the sandbox + git; the DB is a mirror: lists are fast, but debugging requires looking at three places at once (
MANIFESTO.md:50). - Elastic license: you can self-host, but you cannot easily turn it into “someone else’s competing SaaS” — the commercial moat is written into the license.
🧠 One sentence: thick platform, thin host — all the logic is pressed into the SDK and API; Web is only a consumer; truth lives in the sandbox and git; the database is just an acceleration mirror.
Chapter 3 · Startup flow: from command to UI
Metaphor: get a park pass → be assigned a workstation → power on → open the door for guests
3.1 Two “from zero to chat” paths: local / cloud
Scaffolding (build the company repo on a laptop)
kortix init creates a project that already has an OpenCode runtime and kortix.yaml (apps/cli/src/commands/init.ts; templates come from @kortix/starter, packages/starter/src/index.ts:1-14). You can symlink .kortix/opencode into Claude/Codex/Cursor native directories so the same agents/skills are shared by multiple coding agents.
Ship
kortix ship: validate the manifest → commit → fill secrets → push → connect connectors (apps/cli/src/commands/ship.ts help text; manifest validation goes through @kortix/manifest-schema, packages/manifest-schema/src/index.ts:5-11). The product promise is “the same repo, laptop and cloud behave the same” (MANIFESTO.md:38).
Local full-stack development
pnpm dev brings up Web(:3000) + API(:8008) + local Supabase + a cloudflared tunnel (AGENTS.md:178-193; root package.json:5). Secrets enter the repo as dotenvx ciphertext (AGENTS.md:195-202).
3.2 How a Session is “born” on the control plane
The core state machine lives in apps/api/src/projects/session-lifecycle/engine.ts:
createSession(engine.ts:44)- Can queue for backpressure via
queuePolicy(always/on_backpressure/never). - Supports idempotency keys; on conflict, checks connector bindings / secrets allowlist; a cross-tenant key collision is rejected outright.
-
Actually building the session goes through
executeCreateSession→createProjectSession/ sandbox provision. -
Sandbox provision (
apps/api/src/platform/services/session-sandbox.ts:1-12) - Inserts a row into
kortix.session_sandboxes; the primary key is the UUID the caller supplied, and it equals the project session id. - Fire-and-forget: return
provisioningfirst; providercreate()runs in the background. - When minting the executor token, a comment writes it in stone:
session_id == sandbox_id, so the LLM gateway can attribute usage to that session (session-sandbox.ts:147-149). -
Route-layer invariant:
session_id == sandbox_id == git branch name(apps/api/src/projects/routes/r7.ts:348). -
startSession(engine.ts:206) -
Calls
openSession; optionalwaitMsis a server-side long-poll untilreadyor the deadline (so the client does not spin empty). -
Host side (Web is checked out)
- SDK:
configureKortix/createKortix→useSession(packages/sdk/src/react/use-session.ts:3-19). - Web session page:
ProjectSessionPagemountsuseSession(projectId, sessionId, { enabled, replayStartStash:false, chatEngine:false })once (apps/web/src/app/(app)/projects/[id]/sessions/[sessionId]/page.tsx:46-100).
3.3 Snapshot builder: where the workstation image comes from (deepened)
apps/api/src/snapshots/builder.ts:1-14 positions the builder as a “thin orchestrator”:
(project, slug)→ResolvedTemplate;- Compute a content-addressed snapshot name;
- Ask the provider: if the image is already active, use it; otherwise inline-build.
The key philosophy (same-file comment): the boot path never trusts a DB row to decide “does the image exist” — it asks the provider every time; DB project_snapshot_builds is only cache + audit + UI “Fix with agent” (builder.ts:9-13).
Trigger-source enum (builder.ts:56-62): session-start / project-create / cr-merge / manual / background / startup — so a CR merge also drives a rebuild; company-config changes show up in the next sandbox.
Cross-replica dedup: waitForProviderBuild polls to settlement when the provider already has a same-name build; on timeout it stays building and fails closed, so you do not get two same-name builds (builder.ts:67-72).
The provider list is constrained by a single source of truth, KNOWN_PROVIDERS; the parity test requires daytona/platinum/e2b/local-docker to match the template provider set (apps/api/src/platform/providers/provider-parity.test.ts:11-34). local-docker can be admitted without a cloud-vendor API key (same file :70-74).
The sandbox image itself (apps/sandbox/README.md:15-28) only bakes git, opencode, and kortix-agent; triggers/channels/connectors/secrets do not enter the image — they rely on create-time env / HTTP.
3.4 In-sandbox boot order (daemon)
kortix-sandbox-agent-server README boot flow (README.md:6-24 + Boot flow section):
- The supervisor process brings up
opencode serve; Hono reverse-proxies toKORTIX_SERVICE_PORT(default 8000); /kortix/health,/kortix/refresh;- Static preview port
KORTIX_STATIC_PORT(default 3211) —apps/webcomposes preview URLs with/proxy/3211/*and thep3211-<sandboxId>subdomain (daemon README:14-18); - Materialize the repo first, then resolve OpenCode config (source comments emphasize the order).
3.5 Web: opening the door from project home to session page
/projects/[id]rendersProjectHome(welcome wall +ComposerChatInput) (project-home.tsx:46-58;page.tsx:6-9).- User sends →
writeStartStashstuffs the first sentence into the SDK start-stash (page.tsx:17;use-session.tsheader comment) →useNewProjectSessioncreates the session and navigates. - Arrive at
/sessions/[sessionId]:useSessionowns POST/start, sandbox switch, SSE, ready-seed, and the canonical OpenCode id (page.tsx:46-59,87-100). - Fresh sessions use
InstantSessionShellwith a cross-fade; resume usesSessionStartingLoader(page.tsx:362-410). ActiveSessionChatthen mountsSessionLayout+SessionChat(page.tsx:649-659); chat-message truth comes fromuseSessionSync(shim → SDK) (session-chat.tsx:3482-3491).
Design trade-offs
- Async provision + long-poll start: first packet can take minutes; queues and
waitMsabsorb it. - Snapshot treats the provider as truth: avoids DB drift; the cost is one extra remote query on every boot.
- The page splits
useSessionlifecycle from theSessionChatmessage engine:chatEngine:falseavoids double-mountinguseSessionSync(page.tsx:91-95).
Chapter 4 · Input capture and fan-out: the journey of one sentence
Metaphor: many storefronts take orders; the kitchen only recognizes one kind of “work ticket”
4.1 Entry matrix
| Entry | How it enters the park | Critical path |
|---|---|---|
| Web | ProjectHome / SessionChat → SDK |
project-home.tsx; session-chat.tsx; use-session.ts |
| Desktop | Electron loads the Web URL | apps/desktop-electron/package.json:12-14 |
| Mobile | Expo + SDK | apps/mobile/package.json |
| CLI | kortix sessions / sessions-chat |
apps/cli/src/commands/sessions*.ts |
| Slack | webhook → binding → lifecycle | index.ts:788-792; channels/slack/ |
| Teams / Telegram / Email / Meet | each has its own webhook | index.ts:790-796 |
| Triggers | projectWebhooksApp + scheduler |
index.ts:775 |
Channel messages eventually have to become: a session on some project + a piece of text (or a first-time create+prompt).
4.2 Web composer path (deepened)
Project-home onSend: billing gate → writeStartStash(sessionId, { prompt }) → create session → router into the session page. The session-page comment is explicit: replayStartStash:false, because Web has its own pending-prompt handoff (page.tsx:91-92); ActiveSessionChat migrateStash(routeId → chatSessionId) after the canonical OpenCode id is resolved (page.tsx:521-537).
InstantSessionShell also reads/writes start-stash (instant-session-shell.tsx imports readStartStash, writeStartStash), which keeps the “sandbox is still waking, user can already type” instant feel.
4.3 Continue-chat delivery: continueSession
When the session already exists, a channel or scheduler calls continueSession (engine.ts:252):
- Read
projectSessions; abandon immediately iffailed; ifmetadata.deletedAtexists, treat it as deleted (even if user status isstopped) — so a Slack reply cannot “resurrect” a deleted session. stopped/completedflip back torunning.- Poll
openSessionuntilready, total budgetREADY_DEADLINE_MS = 300_000(5 minutes) (engine.ts:39,320). deliverWithRetry: keep delivering inside a 45s window, healing “just-woke sandbox 404/5xx” (deliver.ts:12-13,28).
What actually stuffs the utterance into the brain is postPrompt (engine.ts:668-685):
forwardToSandbox(externalId, port=8000, …)
POST /session/{opencodeSessionId}/prompt_async?directory=/workspace
body: { parts: [{ type: 'text', text }] }
The control plane does not parse tool_calls; it only asynchronously dumps user text into OpenCode.
4.4 SDK / cache-key fan-out
The useSession comment calls itself “the one hook a host needs to open a session and stream chat” (use-session.ts:3-19). The cache key deliberately includes activeServerKey() (the current sandbox id), because each project session is an independent sandbox (session_id == sandbox_id); otherwise switching sessions would mix data (packages/sdk/src/react/use-opencode-sessions/shared.ts:17-30).
4.5 Slack thread stickiness (short)
bindChatThread writes (platform, workspaceId, threadId) → sessionId into chat_threads — create and webhook continue-chat share the same registry, otherwise follow-ups cannot find a home.
Design trade-offs
- Delivery and ack are decoupled: Slack acks the webhook first, then delivers in the background — you must have heal/retry (
deliver.ts:7-11). - Delete is a soft-delete semantic: it relies on
metadata.deletedAt; you cannot look only at the status enum. - Web instant shell vs SDK default replay: the host can turn off
replayStartStashand use its own UX to hand off the first sentence.
Chapter 5 · Context assembly: packing for the model
Metaphor: employee handbook (repo) + this work ticket (session) + badge permissions (grant)
Kortix’s context is not a giant messages[] array assembled in Python the way nanobot does it; it is:
- Static intelligence in the repo: agents / skills / opencode.jsonc
- Governance layer compiled on the server: connectors / secrets allowlist / enable flags
- Env and
OPENCODE_CONFIG_CONTENTinjected into the sandbox - The session transcript OpenCode itself maintains (inside the sandbox)
- Manifest contract:
@kortix/manifest-schemaguaranteeskortix.yamlhas a legal shape
5.1 One concern, one place: behavior vs governance
The long comment in compile-agent-config.ts writes the architectural turn clearly:
- Behavior (mode/model/temperature/steps/permission/prompt…) lives in
.kortix/opencode/agents/<name>.md— as-is, that file is already a legal OpenCode agent file. - Governance (connectors/secrets/skills/kortix_cli/workspace/enabled) lives in the
agents:map ofkortix.yaml. - The compiler is a pure function: copy the md parse result into the OpenCode
agentmap;enabled: falseforcesdisable; skills fold intopermission.skill.
5.2 How manifest validation hard-stops bad config
The header comment of @kortix/manifest-schema lists three gates (packages/manifest-schema/src/index.ts:5-11):
kortix shippre-check (a bad manifest does not get pushed);- Backend CR-merge gate (raw git / web edits that bypass the CLI are still blocked);
kortix validateexplicit report.
Errors are structured (path + severity + message + optional row/column); callers can color diagnostics. This contrasts with OpenManus’s “bad config only explodes at runtime” — Kortix lifts config correctness into a product latch.
5.3 How the daemon stuffs the “packed result” into OpenCode
buildOpencodeConfigContent merges several forces (Executor MCP, Kortix LLM gateway provider, Slack permission overlays, KORTIX_COMPILED_AGENT_CONFIG). If all are empty, it uses the in-repo OpenCode config entirely. The daemon only layers; it does not invent behavior itself.
5.4 Runtime env dictionary (birth certificate)
buildSessionRuntimeEnv writes the contract keys between the control plane and the daemon (see the old deepened appendix; now woven in):
| Env | Meaning |
|---|---|
KORTIX_REPO_URL / KORTIX_BASE_REF |
which company to clone, which baseline to grow from |
KORTIX_BRANCH_NAME |
equals sessionId — session branch name and session identity are one |
KORTIX_PROJECT_ID / KORTIX_SESSION_ID |
coordinates when calling back the API |
KORTIX_SERVICE_PORT=8000 |
daemon’s external port, also the convention for /v1/p/:id/8000 |
KORTIX_COMPILED_AGENT_CONFIG |
v2 compiled-product JSON string |
KORTIX_INITIAL_PROMPT |
optional first sentence |
If you understand this table, you understand where “context assembly” lands on the platform side: it is not assembling messages[]; it is writing env + writing a git branch + writing compiled config.
Design trade-offs
- “OpenCode-native files are the behavior source” lowers dual-config drift; the cost is that the control plane must learn to read md in git.
- Governance is not delegated into md: so an Agent cannot rewrite its own permission boundary and skip YAML — it only takes effect after a CR merges.
- manifest-schema reused in three places: one schema serves CLI/API/future tools, avoiding “everyone writes their own YAML dialect.”
🧠 One sentence: Kortix’s “context assembly” is not assembling a message array in memory; it is writing a git branch + writing environment variables + writing compiled config — truth lands in files, not in a process.
Chapter 6 · The agent main loop: the project’s heart
Cold water first: this repo has no while True: think(); act()
OpenManus’s heart is ToolCallAgent.think/act; nanobot’s heart is AgentRunner.
Kortix’s “model heart” is inside the OpenCode process; this repo’s heart is Session Lifecycle + Daemon Supervisor + Proxy + (on the host) useSession. Putting that in the title is to stop readers from spinning in apps/api looking for ReAct.
6.0 First look at a real task: how one Slack message becomes a CR waiting for review
The abstract “three loops” sound winding. First watch a concrete task land. A colleague @’s the bot in Slack: “Make the landing-page title a bit more forceful.” Follow that sentence and you will see which baton each loop takes:
| Stage | Who is doing it | What they did | Source anchor |
|---|---|---|---|
| ① Take the order | Platform loop | Slack webhook comes in; find (or create) this conversation by (platform, workspace, thread) → session |
index.ts:788-792; bindChatThread |
| ② Wake the workstation | Platform loop | continueSession: if the session was hibernating, wake the sandbox; wait up to 5 minutes until “ready” |
engine.ts:252; READY_DEADLINE_MS |
| ③ Deliver the utterance | Platform loop | deliverWithRetry keeps delivering inside a 45-second window, healing “the just-woke sandbox has not recovered yet” |
deliver.ts:28; postPrompt (engine.ts:668-685) |
| ④ Actually think | OpenCode loop | OpenCode in the sandbox reads the repo, thinks one step, edits the landing-page file, thinks again — this is where ReAct happens | inside the OpenCode process (not implemented in this repo) |
| ⑤ Keep a record | OpenCode loop | After editing, it does not go straight to trunk; it git commit + kortix cr open to open a change request |
cr.ts:38-40 |
| ⑥ Report back | Host loop | If you have Web open, useSession paints every step onto the screen in real time via SSE |
use-session.ts:3-19 |
| ⑦ Govern | Platform loop | The CR appears in Review Center; only after a human clicks merge does the company actually change; merge also triggers a sandbox-image rebuild | r9.ts:19-21; builder source cr-merge |
Once you understand this table, you have Kortix’s biggest difference from OpenManus: the endpoint of a task is not “the model finished speaking”; it is “produce a CR a human can review.” The platform loop is responsible for reliably delivering the utterance and waking the workstation; real thinking and doing happen in OpenCode; and every change must pass the change-request human gate before it lands in “the company,” this Git repository.
flowchart LR
SLACK["💬 Slack: tweak the landing-page title"] --> PLAT
subgraph PLAT["① Platform loop (this repo)"]
FIND["Find the session"] --> WAKE["Wake the sandbox"] --> SEND["Reliably deliver the utterance"]
end
SEND --> OC
subgraph OC["② OpenCode loop (inside the sandbox)"]
THINK["Read repo → think → edit files → think again"] --> COMMIT["git commit + kortix cr open"]
end
COMMIT --> CR["📋 change request (open)"]
CR --> REVIEW["🧑⚖️ Human reviews in Review Center"]
REVIEW -->|"merge"| MAIN["Company trunk changed
+ trigger image rebuild"]
OC -.->|"SSE live report"| HOST["🖥️ Host loop useSession"]
style PLAT fill:#eaf1ff
style OC fill:#e7f8f1
style CR fill:#fff3df
style MAIN fill:#fff3df
🧠 One sentence: the platform loop is responsible for “reliably delivering the utterance and waking the workstation”; the OpenCode loop is responsible for “think and do”; the host loop is responsible for “live display” — three batons in a relay; the finish line is a CR waiting for human review, not a sentence that says “I’m done.”
6.1 Separate the two loops (this chapter’s core)
flowchart TB
subgraph PLATFORM["Platform loop (implemented in this repo)"]
C["createSession"] --> P["provisionSessionSandbox"]
P --> S["startSession / openSession"]
S --> D["continueSession / deliverWithRetry"]
D --> PP["postPrompt → prompt_async"]
S --> STOP["stop / restart / queue drain"]
end
subgraph OPENCODE["OpenCode loop (sandbox binary)"]
T["Read transcript + tools"] --> M["Call LLM (via gateway)"]
M --> TOOL["Local tools / Executor / CLI"]
TOOL --> T
end
subgraph HOST["Host loop (SDK + Web)"]
US["useSession: /start + SSE + sync"] --> UI["SessionChat renders turns"]
end
PP --> OPENCODE
US --> S
OPENCODE -->|SSE via /v1/p| US
| Question | Platform loop answers | OpenCode loop answers |
|---|---|---|
| Is this workstation ready? | stage==='ready' (server-truth) |
— |
| Did this sentence get in? | deliverWithRetry / postPrompt |
receives prompt_async |
| Which tool next? | not responsible | model + tool schema |
| Where does the message list come from? | DB mirror can sync | in-sandbox transcript is truth |
| How does the host subscribe? | — | SSE; useSession / useSessionSync |
6.2 Platform heart: the session-lifecycle state machine
stateDiagram-v2
[*] --> Creating: createSession
Creating --> Queued: backpressure
Queued --> Creating: drain queue
Creating --> Provisioning: insert session_sandboxes
Provisioning --> Ready: openSession stage=ready
Provisioning --> Failed: provider/boot error
Ready --> Running: prompt_async / user chat
Running --> Stopped: stop/hibernate
Stopped --> Running: continueSession wake
Running --> Failed: terminal error
Ready --> Deleted: deleteSession metadata.deletedAt
Key code anchors:
- Create and idempotency:
engine.ts:44 - Start and long-poll:
engine.ts:206 - Continue-chat and 5-minute ready wait:
engine.ts:252,39,320 - Delivery heal:
deliver.ts:28 postPrompt:engine.ts:668-685
openSession (projects/routes/shared.ts) is responsible for resolving DB row + provider state into stage, and can resume when stopped / provision when there is no box — what continue waits for is not just “read status”; it may be waking a hibernated disk.
6.3 Sandbox heart: supervise OpenCode, do not replace it
Daemon duty list (README:6-12):
spawn/ crash-restart / signal-drainopencode serve- Reverse-proxy its HTTP + SSE to port 8000
/kortix/health,/kortix/refresh- Static preview port 3211
After ready, it lowers liveness frequency (so idle sandboxes do not eat a full CPU) — typical platform-taste optimization.
6.4 Host heart: the useSession mount contract on Web
SDK header comment (use-session.ts:3-19):
Everything sandbox-shaped is internal… The host imports
createKortix+useSessionand NOTHING else runtime-related.
The Web session page executes this faithfully (page.tsx:46-100):
- Mount
useSession(projectId, sessionId)once; - Billing gate blocks accounts with no plan, so you do not poll forever a sandbox that will never provision;
chatEngine:false: the page only eats boot/lifecycle;SessionChatitself mountsuseSessionSync;- Mid-session reconnects use
useSandboxConnection; initial ready trusts serverstage(page.tsx:55-58,419-424).
This is why Chapters 4/6 talk about both the “platform loop” and the “host loop”: without useSession, Web would still hand-roll a 7-step mount — the page-header comment says they already deleted that set (page.tsx:49-51).
6.5 End-to-end timing of one user turn
sequenceDiagram
participant U as User/Web/Slack/CLI
participant SDK as @kortix/sdk
participant API as apps/api
participant P as /v1/p proxy
participant D as kortix-agent
participant O as opencode
participant X as /v1/executor
participant G as LLM gateway
U->>SDK: useSession / send
SDK->>API: /start + stream
API->>API: continueSession / startSession
API->>P: forwardToSandbox prompt_async
P->>D: POST /session/:id/prompt_async
D->>O: reverse proxy
O->>G: chat.completions
O->>X: connector call (scoped token)
X-->>O: tool result
O-->>D: SSE events
D-->>P: SSE
P-->>SDK: stream
SDK-->>U: SessionChat turns
Note over O: afterwards the agent can git commit + kortix cr open
6.6 Faults and self-heal
- Delivery layer: 45s
deliverWithRetry(deliver.ts). - Web layer: a hibernated but resumable sandbox will automatically invalidate
/startat most 3 times (page.tsx:106-139). - Daemon layer: on recoverable errors it can auto-resume a turn (source
turn-auto-resume.ts; sub-agent failure is left to the parent model). - Control plane: dead-letter marks the session failed.
Design trade-offs
- Reuse OpenCode = stand on a giant’s shoulders: you inherit the tool ecosystem immediately; the cost is debugging across three processes: API + daemon + OpenCode.
- The platform loop is extremely heavy: idempotency, backpressure, cross-tenant isolation, delete guards — the SaaS multi-tenant tax OpenManus’s single-machine script does not have to pay.
- The host loop collapses into one hook: when you swap in mobile/whitelabel you reuse the same ready semantics.
🔍 Source pointers (go check the source):
session-lifecycle/engine.ts:44(createSession)|:206(startSession long-poll)|:252(continueSession)|:668-685(postPrompt → prompt_async)|deliver.ts:28(delivery heal)|kortix-sandbox-agent-server/README.md:6-24(daemon supervises OpenCode)|packages/sdk/.../use-session.ts:3-19(host’s only hook)
Contrast with OpenManus (short note)
OpenManus: BaseAgent.run → think/act in the same process. Kortix: the platform only guarantees “workstation ready + the utterance got in”; thinking and tools are in another process. When choosing, do not pair engine.ts against ToolCallAgent.py.
Chapter 7 · Tool system and permissions: the model’s “hands” and “tight hoop”
7.1 Three pairs of hands
- OpenCode local tools: files, shell, its plugins — they run inside the sandbox OS, naturally isolated by session branch. Web uses
features/session/tool/tools/*to render these tool results (e.g.bash-tool.tsx,read-tool.tsx,task-tool.tsx); it does not execute them in the browser. - Kortix Executor (connectors): OpenAPI/HTTP/MCP etc., sent from the API process; credentials are assembled on the server (
execute.ts:1-5). The sandbox only sees a scoped token. Clients can use@kortix/executor-sdk(packages/executor-sdk/src/index.ts:41-50). - In-sandbox
kortixCLI:cr/secrets/sessions… callbacks withKORTIX_CLI_TOKEN(cr.ts:38-40).
7.2 Executor: secrets never cross the border
This is the most important move in Kortix’s entire security model; pin it with one diagram first: the Agent in the sandbox never gets real credentials. When it wants to call a third party (send Slack, write Notion), it can only hand the request to the server-side Executor; the real API key is attached only on this side of the API process; from start to finish the sandbox holds only a restricted temporary token.
flowchart LR
subgraph SBX["Sandbox (session workstation) · scoped token only"]
AGENT["OpenCode Agent
“help me call connector X”"]
end
subgraph API["apps/api process (trusted) · keeps real credentials"]
POLICY{"policy engine
is this action allowed?"}
SECRET["Take out resolved secret
attach auth headers"]
FETCH["fetch third party"]
end
THIRD["🌐 Third-party API"]
AGENT -->|"binding + args
no real key"| POLICY
POLICY -->|"always_run"| SECRET
POLICY -->|"require_approval"| WAIT["🧑 Wait for a human to nod in Review Center"]
POLICY -->|"block"| DENY["Deny"]
WAIT -->|"approved"| SECRET
SECRET --> FETCH --> THIRD
THIRD -->|"result"| AGENT
style SBX fill:#fdeded
style API fill:#eaf1ff
execute.ts opens with the contract: given binding + resolved secret + args, here attach auth headers and fetch; sandbox never sees credentials (execute.ts:1-5). API mount: /v1/executor (index.ts:772).
The policy engine (executor/policy.ts) evaluates always_run | require_approval | block by glob; it can link with Review Center executor-approval rows (see Chapter 10). Its default gear is clever — under default_mode = risk, reads go through directly; writes/dangerous ops need a human nod (policy.ts:126-129,161-176), much safer than “allow everything,” and not so annoying that every read needs confirmation.
🧠 One sentence: the Agent only hands over the request, never touches the key; real credentials are locked in the API process. Dangerous actions default to needing a human nod — that is “secrets never cross the sandbox boundary.”
7.3 Agent permissions and Grant
- The
permissiontree in Markdown frontmatter is issued with OpenCode behavior config. - Account-level agent grant:
resolveAgentGrantwhenmintExecutorToken(nearsession-sandbox.ts:147-154); fail-safe on failure. - IAM:
session-gate.tsis human login-session policy — same name, different object from “project session sandbox.”
7.4 Sandbox and network boundary
- Providers:
daytona/platinum/e2b/local-docker(provider-parity.test.ts:11-34). - Preview proxy:
/v1/p/:sandboxId/:port/*must authenticate + rate-limit. - Snapshot upload URLs have SSRF-protection tests — platform security awareness is stronger than OpenManus’s default-open browser tools.
7.5 Approval and the human latch
- Review Center: native review items + adapted CRs + executor approvals (
review-center-connected.tsx:3-14). - Change request: only merging into main changes company truth (full path in Chapter 10).
- Web sessions also have permission / question self-heal (SDK
usePermissionSelfHeal/useQuestionSelfHeal,session-chat.tsx:151-154).
Design trade-offs
- Connectors execute centrally: friendly to security and audit; the cost is one extra hop.
- UI only renders tools: Web’s tool components are a presentation layer, so execution logic does not leak into the browser.
- Dual-token model (user JWT vs sandbox CLI/executor token): higher mental load, but it separates “human” from “workstation robot.”
Chapter 8 · Context compaction and memory
Honest inventory
| Capability | Status in this repo |
|---|---|
| Conversation transcript storage | sandbox OpenCode + DB sync mirror (MANIFESTO.md:50) |
| Company long-term memory | files into the repo (MANIFESTO.md:67: “Memory — files for now…”) |
| LLM auto-compaction / Dream | Dream / company-brain compound implementation not found (vision is in the MANIFESTO) |
OpenCode compress tool |
SDK tool registry has a display label; execution is inside OpenCode |
| Web tool rendering | dcp-compress-tool.tsx / dcp-distill-tool.tsx etc. are UI presentation |
| prompt cache | platform-level unified implementation not found |
| Context-window field | model-catalog metadata includes context window — it is not a compressor |
How memory becomes a “compound”
MANIFESTO strategy: skills and memory live in the company repo as Markdown/files, evolving with CRs (MANIFESTO.md:67). This is different from Raven/EverOS vector memory — Kortix first bets on git-diffable text memory.
The SDK provides formatTranscript to export a session as Markdown (packages/sdk/src/index.ts transcript export), convenient for humans to read and archive, but it is not an automatic compressor.
Design trade-offs
- Memory = versioned files: reviewable, rollbackable, greppable; long sessions still depend on OpenCode’s own context management.
- For nanobot users who “want a long-resident private-chat butler”: Kortix leans toward “project/company working memory,” not an IM personal diary.
Chapter 9 · Sub-agents and multi-agent: the cloning trick
9.1 Multiple Agents in the same project (role clones)
- Multiple
.mdagents +agents:governance items inkortix.yaml. mode: primary | subagent | allappears in compiled config — semantics are interpreted by OpenCode.- Web customize → Agents view can edit (
features/workspace/customize/sections/view/agents-view.tsx). - SDK
useVisibleAgentslets the session side choose visible Agents.
9.2 Massive parallelism of the same config (workstation clones)
This is Kortix’s key difference from OpenManus PlanningFlow: parallelism comes from isolated sandboxes, not from scheduling multiple roles in one process.
MANIFESTO.md:48: fifty sessions do not pollute each other; conflicts are handed to git merge.
Each session is independent session_id == sandbox_id (SDK shared.ts:17; r7.ts:348).
9.3 Marketplace and inheritance
apps/api/src/marketplace/: catalog-style distribution.@kortix/registry: discovery/build; install goes through agent import + CR (packages/registry/src/index.ts:8-11).- CLI
kortix agents/marketplace.
9.4 Where sub-agent failure belongs
Daemon auto-resume comments lean: Task/subagent failure should be handled by the parent model. Web has renderers such as task-tool.tsx, agent-spawn-tool.tsx, proving nesting happens at the OpenCode tool layer.
Design trade-offs
- Parallel = many sandboxes: high bill and cold-start cost; the correctness model is simple (git).
- OpenManus-style PlanningFlow: isomorphic implementation not found in this repo; long-task planning depends more on Agent prompt / skills / human CR cadence.
Contrast with OpenManus (short note)
OpenManus uses PlanningFlow to dispatch [AGENT] step by step in one process; Kortix trades isolation and auditability with “many sandboxes + many agent md + CR.”
Chapter 10 · Ecosystem: commands, extensions, and the full Change Request path
10.1 CLI surface (the company remote)
Visible under apps/cli/src/commands/: init, ship, sessions, sessions-chat, cr, agents, skills, connectors, secrets, triggers, marketplace, channels, executor, sandbox(es), login/logout, self-host, doctor… — covering the closed loop from scaffolding to approval.
10.2 Change Request full path (this chapter’s hard goods)
Data model
packages/db/src/schema/kortix.ts:2719-2755:
- Status enum:
open | merged | closed(:2726-2730) - Key columns:
number,base_ref/head_ref,head_commit_sha,origin_session_id,merge_commit_sha… - Comment: a CR is metadata; the real fetch/diff/merge goes through
apps/api/src/projects/git.ts; the backend can be GitHub/GitLab/pure git (:2720-2724).
Serialization and “request changes” notes land in metadata.requested_changes (apps/api/src/projects/change-requests.ts:1-14,72-95).
API routes
| Method | Path | File |
|---|---|---|
| GET | /:projectId/change-requests |
routes/r8.ts:225-227 |
| POST | /:projectId/change-requests |
r8.ts:272-274 |
| GET/PATCH | /:projectId/change-requests/:crId |
r8.ts:547-586 |
| POST | …/request-changes |
r8.ts:641-643 |
| GET | …/diff, …/merge-preview |
r8.ts:731-796 |
| POST | …/merge |
r9.ts:19-21 (and the tail of r8) |
| POST | …/close, …/reopen |
r9.ts:180-229 |
After a successful merge it can trigger connector reconcile and a snapshot rebuild sourced as cr-merge (builder trigger-source enum; r9.ts merge handling).
CLI
kortix cr (apps/cli/src/commands/cr.ts:13-40): ls/show/diff/open/merge/close/reopen. Inside the sandbox it automatically reads KORTIX_CLI_TOKEN + KORTIX_PROJECT_ID (:38-40) — the Agent does not need to log in.
Web UI
- Project-files side:
change-requests-panel.tsx,change-request-detail-dialog.tsx,open-change-request-dialog.tsx; hooksfeatures/project-files/hooks/use-change-requests.ts. - Sidebar entry:
project-change-requests-nav.tsx. - Review Center (real data):
ReviewCenterConnected(review-center-connected.tsx:3-14,88-148) - CR approve →
useMergeChangeRequest - reject →
useCloseChangeRequest - request changes → persist notes + deliver back to the originating agent (backend can wake the sandbox)
cr:adapted ids do not go through the generic/act(it would 409); they must go through the source flow (:107-109)/reviewroute: mock prototype only (review/page.tsx:8-18); do not treat it as the production API. The production entry is the Review view in project customize (review-view.tsx:4-32).- SDK:
packages/sdk/src/core/rest/projects-client/change-requests.ts;packages/sdk/src/react/use-change-requests.ts.
Agent-side docs/skills
The starter template includes change-requests reference docs (packages/starter/templates/base/.kortix/opencode/skills/kortix-system/references/kortix/change-requests.md); Web docs: apps/web/content/docs/work/change-requests.mdx.
Lifecycle in one sentence
commit inside the sandbox → kortix cr open
→ DB change_requests(status=open)
→ Web Review / CLI show/diff
→ merge → git merge + status=merged + (optional) snapshot rebuild
→ or close / request-changes → deliver again for the agent to revise
10.3 Connectors and MCP / registry / starter
- Pipedream and other catalogs live in
apps/api/src/executor/. @kortix/executor-sdkgives external TS code isomorphic calls.@kortix/registry+ API marketplace: discovery and agent import.@kortix/starter: unified scaffolding, so CLI/API do not each write a template set.
10.4 Channel ecosystem
Slack/Teams/Telegram/Email/Meet webhooks enter the API (index.ts:788-796). Slack sessions rewrite OpenCode permission (to avoid getting stuck on question).
10.5 How to extend (for second-party developers)
- Add an agent
.md/ skill in the repo, open a CR. - Hang connector / trigger / sandbox specs on
kortix.yaml(pass manifest-schema first). - Go through marketplace agent import.
- Build your own connector and expose it via the executor protocol.
- Deep change: touch
@kortix/sdk(TDD and export contract,AGENTS.md:99-108) or the daemon.
10.6 Migration and historical baggage
projects/suna-migration/: migrate from the old Suna/AgentPress world to OpenCode DB mapping — proof the product went through one “runtime heart transplant.”
🧠 One sentence: Change Request is Kortix’s product primitive, not an accessory. Whatever the Agent changed does not count until a CR is merged into trunk by a human — that is the landing form of “AI does the labor, humans govern.”
Chapter 11 · Feature set: auth, models, and “thinking”
11.1 Auth
- Humans: Supabase JWT; Web
AuthProvider(apps/web/src/features/providers/auth-provider.tsx:29+) +configureKortix({ getToken })(kortix-config.ts:30-32). - Programs: API key / PAT / account token.
- Sandbox:
KORTIX_TOKEN/ CLI token / executor token, layered. - Preview:
POST /v1/p/authplants a cookie. - Enterprise: SCIM (
/scim/v2), SSO-related oauth routes. - Session gate: lifetime / idle / revoked (IAM
session-gate) — human sessions.
11.2 Model routing and gateway
- API
mountLlmGateway(index.ts:740-741) +/v1/router. - Project-level routing policy; Web customize has a Gateway view (
gateway-routing.tsxetc.) +useGatewayRoutingPolicy. - Daemon: when a gateway exists, OpenCode goes through a synthetic
kortixprovider. - Billing:
billing/+ compute metering; the Web session page hascan_run/ upgrade-dialog gates (page.tsx:66-85).
11.3 “Thinking” / Reasoning
- Gateway wire can carry
reasoning_effort; project policy can clamp it. - Web has
reasoning-effort-selector.tsx(wired touseGatewayRoutingPolicy). - When sharing a session externally, the API can omit reasoning and mark
reasoning_omitted. - Not found: a first-class cross-model thinking-blocks peel-off service; rendering is mainly in the SessionChat turn UI.
11.4 Other features, quick lookup
| Feature | Status | Evidence |
|---|---|---|
| Multi-channel | yes | index.ts webhooks |
| Change Request | yes | DB + r8/r9 + CLI + Web Review |
| Review Center | yes (real data is in-project; /review is mock) |
review-center-connected.tsx; review/page.tsx |
| Preview/static site | yes | daemon 3211; /v1/p |
| Tunnel | yes | /v1/tunnel |
| Self-host | has an entry | CLI self-host; self-host/ |
| Desktop / Mobile | yes | apps/desktop-electron; apps/mobile |
| Web Command Center | checked out | apps/web full paths as above |
Chapter 12 · Summary: design philosophy and trade-offs
flowchart TB
GOAL["Goal: a self-hostable company-grade AI Command Center
company = Git repo"]
GOAL --> P1["Pillar 1 session = isolated sandbox
session_id == sandbox_id == branch
→ Chapters 3/6"]
GOAL --> P2["Pillar 2 outsource the brain to OpenCode
platform only does lifecycle/proxy/config
host only mounts useSession
→ Chapters 5/6"]
GOAL --> P3["Pillar 3 change must go through a CR
API+CLI+Review Center
→ Chapters 7/10"]
P1 & P2 & P3 --> BASE["Foundation: SDK SSoT + manifest-schema + Executor keeps secrets
+ LLM Gateway + multi-provider snapshot
→ Chapters 2/3/7/11"]
Five distinctive ideas
① The company is literally a repository. agents/skills/memory/sandbox specs all go into the repo; greppable, diffable (MANIFESTO.md:27-36).
② session_id == sandbox_id == git branch. Route comment (r7.ts:348); executor-token minting comment (session-sandbox.ts:147-149); SDK cache-key comment (shared.ts:17).
③ Platform loop ≠ model loop ≠ host loop. The API intensively farms create/start/continue/deliver; ReAct is left to OpenCode; Web uses useSession to eat server-truth ready (Chapter 6).
④ Secrets never cross the sandbox boundary. Executor attaches credentials on the server; executor-sdk likewise.
⑤ Change Request is a product primitive. Schema (kortix.ts:2719+) + API r8/r9 + CLI cr + Review Center adapter — “AI labor + human governance.”
Main costs
- Complexity tax: monorepo + multi-provider + queues + idempotency + SDK contract; the learning curve is far above OpenManus.
- Cold-start tax: snapshot/sandbox at minute scale; compensated with warm, long-poll, deliver heal, Web instant shell.
- License tax: Elastic 2.0 restricts hosting a competing product (
LICENSE:18-20). - The brain is invisible: deep Agent behavior requires reading OpenCode upstream; this-repo analysis cannot replace that.
Sideways comparison at a glance (for the series docs)
| Dimension | Suna / Kortix |
|---|---|
| Team / license | kortix-ai; Elastic License 2.0 |
| Stack | TS/Bun monorepo + Hono + Supabase + Next + OpenCode-in-sandbox |
| Architecture | control-plane session-lifecycle + sandbox daemon + OpenCode; SDK SSoT |
| Models | LLM Gateway routing; OpenCode kortix/ provider |
| Tool scale | OpenCode tools + server-side Executor/MCP + built-in CLI |
| Extension | yaml + md agents/skills + registry/marketplace + connectors |
| Main loop | platform: session-lifecycle/engine.ts; model: OpenCode; host: useSession |
| Permissions | IAM + agent grant + executor keeps secrets + CR governance |
| Compaction / memory | repo-file memory; platform-level LLM compaction not seen |
| Frontend | full checkout: Command Center + Session + Review |
One-sentence close: Claude Code answers “how to write the code well in this repo”; OpenManus answers “how to use a readable ReAct chain for research deliverables”; nanobot answers “how to let a personal Agent live long-term in a chat app”; and Kortix/Suna answers — how to turn a company into a cloneable Git repository, so that hundreds or thousands of isolated-sandbox OpenCode laborers, under change-request discipline, improve the company itself day and night.
Suggested reading order (full checkout)
MANIFESTO.md→ worldviewAGENTS.mdSDK iron laws +apps/web/src/lib/kortix-config.tspackages/sdk/src/react/use-session.ts+ Websessions/[sessionId]/page.tsxsession-lifecycle/engine.tscreate/start/continue/postPromptsnapshots/builder.ts+session-sandbox.tsheader commentskortix-sandbox-agent-server/README.mdchange-requests.ts+routes/r8.ts/r9.ts+review-center-connected.tsxpackages/manifest-schema/starter/registry/executor-sdkeachpackage.json+src/index.ts
Naming mines
| Name you see | What it actually means |
|---|---|
| Suna | historical product/repo name; migration code in suna-migration/ |
| Kortix | current product name, CLI, npm scope @kortix/* |
| session (IAM) | human login session |
| session (project) | Agent workstation; often the same id as sandbox |
| sandbox_id | on most paths == project session id |
| opencode_session_id | OpenCode internal conversation id, not equal to project session id |
/review vs Review Center |
the former is mock; the latter is in-project Connected real data |
VERSION 0.10.14 vs badge 0.9.98 |
treat the VERSION file as canonical |
Deepen-weave A: Web Command Center UX close read (reinforces Chapters 2/3/4)
A.1 Routes and shell
Under the App Router, the (app) group is the main logged-in shell. Project page /projects/[id] does not chat directly; it is ProjectHome: welcome area + composer + shortcut entries (Agents / Secrets / Sandbox / Triggers / Marketplace icon buttons, see the second half of project-home.tsx). On send, the parent ProjectIndexPage (page.tsx:24+) is responsible for:
useProjectCanRun/useAccountStatebilling gate;writeStartStashwrites the first sentence;useNewProjectSessioncreates the session and navigates to/sessions/[sessionId].
The session-page comments cut responsibility cleanly (sessions/[sessionId]/page.tsx:46-59): the SDK owns runtime lifecycle; the page keeps the billing gate, instant-shell/loader cross-fade, fresh-session hand-off, restart/error cards.
A.2 useSession parameter semantics (the host must understand)
useSession(projectId, sessionId, {
enabled: !!user && !billingGatePending && !noPlan,
replayStartStash: false,
chatEngine: false,
})
Evidence: page.tsx:96-100.
| Parameter | Meaning |
|---|---|
enabled |
do not fire /start when there is no user / billing pending / no plan, to avoid spinning empty |
replayStartStash |
Web turns off SDK default replay; uses migrateStash + InstantShell instead |
chatEngine |
when false, do not mount useSessionSync at the page layer; hand it to SessionChat |
Inside the SDK it still: long-polls /start, writes readiness into the connection store, opens SSE, resolves the canonical OpenCode id (use-session.ts:5-16). Fresh-session 404 has a grace retry: FRESH_START_404_RETRIES = 12 × 800ms (use-session.ts:66-70,79-88).
A.3 Shim inventory (proof of thin consumer)
| Web path | Actual export |
|---|---|
apps/web/src/stores/server-store.ts:2 |
@kortix/sdk/server-store |
apps/web/src/hooks/opencode/use-session-sync.ts:2 |
@kortix/sdk/react |
apps/web/src/hooks/opencode/use-canonical-opencode-session.ts:2 |
@kortix/sdk/react |
apps/web/src/hooks/opencode/use-opencode-events.ts |
SDK react re-export |
apps/web/src/lib/api-client.ts:1 |
@kortix/sdk/api-client |
apps/web/src/lib/opencode-sdk.ts:1 |
@kortix/sdk/opencode-client |
AGENTS.md:128-133 is explicit: on conflict keep the shim, port new logic into the SDK.
A.4 Session feature directory map (features/session/)
After full checkout this directory is huge (200+ files scale). Responsibilities layered:
- Shell:
session-layout.tsx,instant-session-shell.tsx,session-starting-loader.tsx,sandbox-loading-boundary.tsx - Chat core:
session-chat.tsx(thousands of lines: turns, optimistic send, permission/question) - Composer:
composer-chat-input.tsx,session-chat-input - Tool rendering:
tool/tools/register.tsregisters bash/read/write/grep/task/connector/executor/dcp-compress… - Resume semantics:
session-resume.ts,session-terminal-state.ts
Tool renderers only draw UI; they do not execute tools — execution is in sandbox OpenCode (Chapter 7).
A.5 Do not mix the two Review Center entries
| Entry | Nature | Evidence |
|---|---|---|
/review |
clickable mock prototype, no API/auth | review/page.tsx:8-18 |
| Project Customize → Review | Connected real data | review-view.tsx:4-32; review-center-connected.tsx |
ReviewCenterConnected also:
- Pulls
useReviewItems; - Maps API → inbox view model (
mapApiReviewItem); - Executor approvals go through
resolveApproval(same client as in-session approval prompts); - CRs go through merge/close/request-changes (
:88-148); - Read-only when
canAct=false(:46-49).
Sidebar project-change-requests-nav.tsx and the session list read useReviewCenterEnabled / useReviewSessionSummary, exposing pending-review counts on the nav.
Deepen-weave B: Snapshot / Provider / Boot close read (reinforces Chapters 3/6)
B.1 Provision and builder call chain
session-sandbox.ts:1-12: after inserting session_sandboxes (primary key = caller UUID = session id), fire-and-forget. Background then goes ensureSandboxImage (imported from builder.ts, session-sandbox.ts:39-44) then getProvider().create().
Builder SnapshotBuildSource (builder.ts:56-62) shows images are not only built on “first session”:
project-create: pre-warm when creating the project;cr-merge: rebuild after manifest/Dockerfile change;session-start: fill in when the image is missing;manual/background/startup: ops and pre-warm paths.
B.2 Provider single source of truth
provider-parity.test.ts:29-31: config.KNOWN_PROVIDERS must equal the SANDBOX_TEMPLATE_PROVIDERS set. The four listed: daytona, platinum, e2b, local-docker (:11,34). Local development can use local-docker and does not require a cloud API key (:70-74).
Selection/balancing: session-sandbox.ts imports selectProvider, readActiveRouting, providerFallbackSetting — supports routing switch and fallback, not a hard-coded vendor.
B.3 Boot-by-template-id
sessionBootByTemplateIdEnabled (session-sandbox.ts:161-169) defaults ON: boot by the already-activated pinned template id; only fall back to name-boot on GC’d-pin 404. You can emergency-off with env KORTIX_SESSION_BOOT_BY_TEMPLATE_ID=0 — a production-rollout escape hatch.
B.4 Sandbox image boundary (emphasized again)
apps/sandbox/README.md:15-28: the image only has what a running session needs; triggers/channels/connectors/secrets are not in the image. This matches the MANIFESTO “inject secrets, connectors go through a proxy” (MANIFESTO.md:62-63), and also explains why changing the daemon requires rebuilding the snapshot (the feedback loop is on the slow side).
B.5 openSession wakes hibernation
openSession is not just a DB read: when stopped + has externalId it may resumeStoppedSandbox (keep disk/workspace); when there is no usable box it can provision on the open path. So continueSession’s 5-minute wait window (READY_DEADLINE_MS, engine.ts:39) covers real-world “resume or create” time, not mere spinning.
Deepen-weave C: Change Request front/back correspondence table (reinforces Chapter 10)
C.1 State machine
stateDiagram-v2
[*] --> open: POST change-requests / kortix cr open
open --> merged: POST .../merge
open --> closed: POST .../close
closed --> open: POST .../reopen
open --> open: POST .../request-changes\n(metadata + optional deliver to agent)
Enum definition: kortix.ts:2726-2730. After merge, write merged_at / merged_by / merge_commit_sha (table fields, kortix.ts:2757+).
C.2 “Request changes” is not an empty comment
recordRequestedChange (change-requests.ts:85-95) appends the human note into metadata.requested_changes. API …/request-changes (r8.ts:641-643) after persist may try to send the prompt back to the originating session (delivery failure only warns; the note is not dropped — see the request-changes prompt not delivered log path in r8).
Web Connected layer: if there is no note, toast asks you to write an explanation first (review-center-connected.tsx:128-134); on success, distinguish “already sent to the agent” vs “saved only” by res.delivering (:139-144).
C.3 CLI ↔ API mapping
CLI (cr.ts) |
Roughly the API |
|---|---|
cr ls |
GET .../change-requests |
cr show |
GET .../change-requests/:id |
cr diff |
GET .../diff |
cr open |
POST .../change-requests |
cr merge |
POST .../merge |
cr close / reopen |
POST .../close / .../reopen |
Login-free inside the sandbox: env vars KORTIX_CLI_TOKEN + KORTIX_PROJECT_ID (cr.ts:38-40). Note KORTIX_TOKEN is the sandbox service key, not the CLI token (comment in the same place).
C.4 Product difference from GitHub PRs
The CR layer is not bound to a single hoster (change-requests.ts:4-8; kortix.ts:2720-2724). Permissions, notifications, Review Center can live on Kortix IAM. The cost is that “click Merge in the GitHub UI” will not automatically sync Kortix CR status — unless you go through the Kortix merge API (or a later integration; this analysis does not invent bidirectional sync that was not found).
C.5 E2E / documentation anchors
- API script:
apps/api/scripts/e2e-change-requests.sh - CLI script:
apps/cli/scripts/e2e-cr.sh - Flow tests:
tests/src/flows/change-requests.flow.ts - User docs:
apps/web/content/docs/work/change-requests.mdx - Design-doc citation:
docs/REVIEW_CENTER_DESIGN.md(Connected header comment,review-center-connected.tsx:14)
Deepen-weave D: four packages expanded again (reinforces Chapters 2/5/10)
D.1 @kortix/manifest-schema
- Package description: shared by CLI ship pre-check + backend CR-merge gate + validate (
package.json:4;src/index.ts:5-11). - Pure function:
(rawToml: string | object) → ManifestValidationResult(index.ts:14-15). - Constants surface is wide: channel platforms, connector auth/policy, sandbox CPU/memory/disk bounds, trigger types, grantable CLI actions (
index.ts:22-36imported from./constants). - v2 agent validation is split into
index.v2.ts(index.ts:42-48). connector-headersis an independent module: forbid certain header names, limit length/count — API parser and executor share the same rules (index.ts:64-66).
D.2 @kortix/starter
getStarterFiles()walks the directory +{{var}}substitution, returns{ path, content }[](src/index.ts:4-9).- User default template:
general-knowledge-worker;minimalis internal clone seed only (index.ts:36-48). - When compiled into the CLI binary,
embedded.generated.jsonis the fallback (index.ts:20-27) — editing templates in the source tree hot-updates; compiled binaries do not lose templates.
D.3 @kortix/registry
- Provides
buildRegistry/loadItem/loadRegistry/ validate (src/index.ts:1-12export list). - Deliberately has no deterministic install engine: marketplace install is changed to the agent reading the source, merging files, and opening a CR (
index.ts:8-11). This is consistent with “the company is git + CR discipline” — install itself is also a reviewable change.
D.4 @kortix/executor-sdk
- Elastic-2.0 publishable package (
package.json:5-6). ExecutorClientOptions: whenprojectIdis present, go through the project-explicit route; user token and sandbox session token are isomorphic (src/index.ts:41-50).- Results can carry
pending_approval+execution_id(index.ts:28-38) — the same business object as Review Center executor rows and in-session approval UI.
Deepen-weave E: platform delivery vs host send (reinforces Chapters 4/6)
E.1 Control-plane delivery (channels / automation)
continueSession
→ wait ready (≤300s)
→ deliverWithRetry (≤45s, interval 1.5s)
→ postPrompt → POST .../prompt_async
deliver.ts:2-11 explains the motive: the old path, on first 404/5xx, told the Slack user “say it again” and dropped the message; now, given the webhook is already acked, it keeps healing.
E.2 Host send (inside a Web session)
SessionChat uses SDK primitives such as sendAndRecover / beginOptimisticSend / classifySendError (session-chat.tsx:142-154); message truth comes from useSessionSync(sessionId) (:3482-3491). Billing 402 becomes a KortixSendError of kind billing (use-session.ts:96-107); the page can pop upgrade.
Both eventually land on OpenCode’s prompt/stream, but:
- The channel path emphasizes server-side heal;
- The Web path emphasizes optimistic UI + SSE sync.
E.3 Proxy chain (SSE return path)
forwardToSandbox/ host via/v1/p/<external>/8000/...- sandbox-proxy auth
- daemon reverse-proxies
opencode serve - SDK
openEventStream(framework-agnostic, reusable in a worker) manages timeout, heartbeat, reconnect backoff
AGENTS.md writes that the proxy URL looks like http://localhost:8008/v1/p/<external_id>/8000/..., SSE at …/event (AGENTS.md:184-186).
Deepen-weave F: main-loop comparison with OpenManus / nanobot (reinforces Chapters 6/12)
| Step | OpenManus | nanobot | Kortix/Suna |
|---|---|---|---|
| Receive message | stdin | Channel → MessageBus | Webhook/CLI/SDK/useSession → session-lifecycle |
| Assemble context | Memory list hard-truncated | system+memory+history | repo md + env + OpenCode transcript + manifest |
| Call model | LLM.ask_tool |
AgentRunner + in-house provider | OpenCode → LLM Gateway |
| Call tools | in-process ToolCollection | in-process tools/MCP | sandbox local tools + server-side Executor |
| End | terminate tool |
model stops or policy | user stop / sandbox reclaim; persistence via CR |
| Permissions | almost open | tool-level + UNTRUSTED | IAM + grant + keep secrets + CR + Review |
| Multi-tenant | none | one person, many channels | account/project/session/sandbox four layers |
| Frontend | almost none | WebUI SPA | Next Command Center + Desktop/Mobile |
Paste this table next to Chapter 6 and you will stop asking “why is there no ReAct in the API.”
Deepen-weave G: beginner debugging checklist (practical)
When you feel “the Agent is not responding,” debug by layer:
- API health:
curl localhost:8008/v1/health(AGENTS.md:193) - Session row: is
project_sessions.statusprovisioning / running / failed? - Sandbox row: is
session_sandboxes.statusactive? is there anexternal_id? (primary key should equal session id) - Proxy: does
/v1/p/<external>/8000/kortix/healthshow daemon=ok and opencode=ok? - OpenCode id: is
opencode_session_idempty? On Web look atuseSession(...).opencodeSessionId - Delivery: do API logs show
prompt_async non-ok/ deliver deadline (engine.ts:687-695;deliver.ts) - Web billing gate: is
noPlanblockingenabled? (page.tsx:83-97) - Permissions: is the connector
blocked by policy? Does Review Center have a pending approval? - CR: the Agent said it fixed it but main did not change — did it only commit without
kortix cr open/ without merge? - Manifest: did
kortix ship/ validate get blocked by@kortix/manifest-schema?
Deepen-weave H: inventory still explicitly not found after full checkout
Mark the boundary honestly, so OpenCode upstream capabilities are not counted into this repo:
| Item | Status |
|---|---|
In-repo ReAct think/act loop |
not found (in OpenCode) |
| Dream / automatic company-brain compaction service | not found (MANIFESTO vision) |
| Platform-level unified prompt-cache service | not found |
/review production API |
none (mock only) |
| Registry deterministic install engine | removed (changed to agent import) |
| OpenCode upstream auto-compact implementation details | not in this repo; no assertion |
Writing notes
- Analysis baseline (full-checkout deepened edition): commit
5862e95671cfd198c6a94ceb9c1fefe579f00a8b; version treatsVERSION=0.10.14as canonical (README badge0.9.98inconsistency already marked). - Verification method: critical paths were opened in source and checked;
apps/web, allpackages/*, desktop/mobile are included. Full OpenCode upstream source is still not in this repo; related conclusions are marked not found or “execution is inside OpenCode.” - Relative to the old sparse edition: delete every “
apps/webnot checked out” phrasing; weave command center /useSessionmount, CR front/back full path, snapshot builder / providers, and the four new packages into the main chapters, rather than stacking them in an appendix. - Product contrast: comparisons with OpenManus / nanobot / Claude Code are based on public positioning and architectural differences; they do not assert closed-source implementation details.
End of lecture.