Piyush-era Cortex design (2026-01-20 → 2026-01-23)
A study, not an inheritance source. Nine commits under Piyush Jha that preceded Tejas's containerized rewrite. Worth reading in full because the shape is closer to what picortex-as-a-personal-tool wants than the current Cortex is.
Final SHA: d2d6a534 (2026-01-23 03:45 PST). Replaced wholesale starting af3a76f5 (Tejas, 2026-01-26).
Architecture
┌───────────────────────────────────────────────┐
│ Frontend (Vercel) │
│ React 19 + Vite + Socket.IO + xterm.js │
│ Auth │ File Browser │ Chat │ Terminal │
└──────────────┬────────────────────────────────┘
│ REST + WebSocket
┌──────────────┴────────────────────────────────┐
│ Backend (separate server) │
│ Node.js + Express + Prisma + Socket.IO │
│ • auth • files • claudeService • ssh │
└──────────────┬────────────────────────────────┘
│ SSH (ed25519, keyed)
┌──────────────┴────────────────────────────────┐
│ EC2 workspace host (single shared box) │
│ cortex_admin service user │
│ └─ sudoers drop-in: useradd/userdel/chmod │
│ cortex_user_<id> per signup │
│ ├─ ~/workspace/ │
│ ├─ ~/.claude/ (API key via apiKeyHelper) │
│ └─ claude CLI pre-installed │
└───────────────────────────────────────────────┘
Three machines, three roles. This separation is what picortex has been missing — Jacob's question "could the workspace just live on a remote box?" is the Piyush-era answer.
The turn loop (the important bit)
backend/src/websocket/handlers/chatHandler.ts + services/claudeService.ts:
// Per inbound message:
socket.on('chat:message', async ({ content }) => {
const workspace = await prisma.workspace.findUnique({ where: { userId } });
const username = workspace.linuxUsername;
await prisma.message.create({ data: { userId, role: 'user', content } });
// The actual Claude call:
const cmd = `claude -c --dangerously-skip-permissions -p "${escaped}"`;
const result = await sshService.execAsUser(username, cmd);
await prisma.message.create({ data: { userId, role: 'assistant', content: result.stdout } });
socket.emit('chat:response', { content: result.stdout });
});
No tmux. No sentinel protocol. No long-lived REPL to babysit. Each turn:
- Backend picks the chat's Linux username from Prisma.
- Backend opens an SSH session as that user on the EC2 host.
- Runs
claude -c --dangerously-skip-permissions -p "<prompt>". -cis Claude CLI's "continue the previous conversation" flag — session memory is managed by Claude itself in~/.claude/, not by the backend.- stdout is the reply. Close SSH. Done.
This is the design codex told picortex to try (the claude --print spike — picortex-b92, picortex-3vj). Piyush already ran the experiment and it worked.
What else Piyush built
From backend/src/ at d2d6a534:
services/sshService.ts— pooled SSH connections to the workspace host, one keyed connection per chat-user. Mock mode for dev.services/workspaceService.ts— 4-step provisioning with WebSocket progress events:create_user→install_cli→configure_api→complete. Each user gets their Anthropic key stored encrypted server-side; written to~/.claude/config.jsonviaapiKeyHelper.services/fileService.ts/controllers/fileController.ts— read/list/write over SSH.services/voiceService.ts— OpenAI-backed voice chat.websocket/handlers/terminalHandler.ts— xterm.js ↔ SSH PTY bridge.websocket/handlers/provisioningHandler.ts— emits provisioning progress.routes/authRoutes.ts+middleware/auth.ts— JWT auth with user-supplied Anthropic API key at signup.prisma/schema.prisma—User,Workspace,Message. SQLite-first.
Frontend frontend/src/components/:
chat/{ChatContainer, MessageInput, MessageList, VoiceButton}.tsxfiles/{FileBrowser, FilePreview, FileTree}.tsxterminal/WebTerminal.tsxprovisioning/ProvisioningScreen.tsxauth/{LoginForm, SignupForm, ProtectedRoute}.tsxsidebar/Sidebar.tsx(chat list)
What Piyush got right that picortex should steal
- Three-tier physical layout. Frontend (Vercel) ↔ backend (whatever) ↔ workspace host (separate). The bot box and the workspace box are different machines. This is the natural home for Jacob's "remote box for privacy" instinct.
claude -c -pper turn, no tmux. Session continuity is Claude's own feature. Replace the sentinel-protocol design inspec/002-tmux-session-spawning.mdwith this. Simpler, survives backend restarts, fewer moving parts.- Per-user provisioning with real-time progress events.
provisioning:statusWebSocket events with{step, progress, message}is a nice UX pattern; picortex's mobile UI should mirror it for cold-start chats. apiKeyHelpervia Claude CLI config. Lets the backend inject/rotate keys without writing them into workspace files. Decoupled secret management.- Mock mode for SSH. The
sshService.isMockMode()check lets the whole frontend+chat flow run without real EC2. picortex needs the same — linq-sim for the Linq side, ansshMockor local-exec fallback for the workspace side. - Backend's sudoers are scoped to user-management binaries only:
useradd,userdel,chown,chmod,mkdir,sudo -u *. Nobash, notmux. That's tighter than picortex's current spec and closer to what codex asked for (picortex-5sc).
What Piyush got wrong (and why it was replaced)
- Per-user, not per-chat workspaces. Fine for "I log in and chat with one Claude." Useless for group texts where each chat needs its own context and filesystem. Cortex pivoted on this.
- No attention gating / group model. Piyush's chat is a single web interface, not a group-text agent.
- Single shared EC2 box with Linux users for isolation — acceptable for a v0 but blocked the enterprise multi-tenant direction. Fly.io Docker containers replaced it.
- No Linq / iMessage / SMS. Web-chat-only. Adding texting was a wholesale rewrite, not an add-on.
- No per-file ACL, no sharing bridge, no cross-chat context. All deferred.
- SQLite-first schema would have needed a migration to scale; Tejas moved to Postgres.
- Claude's
-ccontinuation is global per user, not per-topic — subtle UX problem: one user's chats all share the same Claude memory. Needs--session-idinstead of-cto get per-chat isolation.
Mapping Piyush's design onto the "awesome texting" framing
Piyush's design was "web chat + dev surface for one user." Jacob wants "iMessage for Jacob + group texts with shared brain." The overlap:
| Piyush element | picortex need | Fit |
|---|---|---|
| Separate workspace host | "remote box for privacy" | ✅ perfect |
claude -c -p per turn |
replaces tmux sentinel fragility | ✅ perfect |
| Per-user Linux isolation | generalize to per-chat | ⚠️ adapt |
| apiKeyHelper | keeps keys out of workspace | ✅ steal |
| Sudoers scoped to useradd etc. | replaces picortex's broad sudoers | ✅ steal |
| Provisioning progress WS events | mobile UI UX | ✅ steal for web UI |
| File browser / xterm terminal | may or may not be needed for texting-first | 🤷 defer, probably skip in v0.1 |
| Web chat UI | redundant if Linq is the primary surface | ❌ skip |
| User signup / own API keys | Jacob is the only user | ❌ skip |
| Voice | covered by existing voice-assistant project | ❌ skip |
Verbatim quotes worth remembering
From the original README at d2d6a534:
Each user gets their own isolated Linux environment on EC2
Messages sent via WebSocket, executed as
claude -p "..."via SSH
From claudeService.ts:
-ccontinues the previous conversation session--dangerously-skip-permissionsallows file operations without interactive prompts
From setup-ec2.sh sudoers block:
cortex_admin ALL=(ALL) NOPASSWD: /usr/sbin/useradd
cortex_admin ALL=(ALL) NOPASSWD: /usr/sbin/userdel
cortex_admin ALL=(ALL) NOPASSWD: /bin/chown
cortex_admin ALL=(ALL) NOPASSWD: /bin/chmod
cortex_admin ALL=(ALL) NOPASSWD: /bin/mkdir
cortex_admin ALL=(ALL) NOPASSWD: /usr/bin/sudo -u *
References
- Piyush's commits:
238052c4→d2d6a534inIdeaFlowCo/cortex - Tejas's replacement: starting
af3a76f5(2026-01-26, "Implement Fly.io workspace infrastructure") - Related brainstorm: docs/plans/2026-04-23-prototype-options.md
- The old "skip Cortex pre-container history" rule in cortex-inheritance.md is softened — pre-container is still not a source of patterns to inherit, but is a source of ideas to study.