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:

  1. Backend picks the chat's Linux username from Prisma.
  2. Backend opens an SSH session as that user on the EC2 host.
  3. Runs claude -c --dangerously-skip-permissions -p "<prompt>".
  4. -c is Claude CLI's "continue the previous conversation" flag — session memory is managed by Claude itself in ~/.claude/, not by the backend.
  5. 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_userinstall_cliconfigure_apicomplete. Each user gets their Anthropic key stored encrypted server-side; written to ~/.claude/config.json via apiKeyHelper.
  • 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.prismaUser, Workspace, Message. SQLite-first.

Frontend frontend/src/components/:

  • chat/{ChatContainer, MessageInput, MessageList, VoiceButton}.tsx
  • files/{FileBrowser, FilePreview, FileTree}.tsx
  • terminal/WebTerminal.tsx
  • provisioning/ProvisioningScreen.tsx
  • auth/{LoginForm, SignupForm, ProtectedRoute}.tsx
  • sidebar/Sidebar.tsx (chat list)

What Piyush got right that picortex should steal

  1. 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.
  2. claude -c -p per turn, no tmux. Session continuity is Claude's own feature. Replace the sentinel-protocol design in spec/002-tmux-session-spawning.md with this. Simpler, survives backend restarts, fewer moving parts.
  3. Per-user provisioning with real-time progress events. provisioning:status WebSocket events with {step, progress, message} is a nice UX pattern; picortex's mobile UI should mirror it for cold-start chats.
  4. apiKeyHelper via Claude CLI config. Lets the backend inject/rotate keys without writing them into workspace files. Decoupled secret management.
  5. 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, an sshMock or local-exec fallback for the workspace side.
  6. Backend's sudoers are scoped to user-management binaries only: useradd, userdel, chown, chmod, mkdir, sudo -u *. No bash, no tmux. 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)

  1. 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.
  2. No attention gating / group model. Piyush's chat is a single web interface, not a group-text agent.
  3. 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.
  4. No Linq / iMessage / SMS. Web-chat-only. Adding texting was a wholesale rewrite, not an add-on.
  5. No per-file ACL, no sharing bridge, no cross-chat context. All deferred.
  6. SQLite-first schema would have needed a migration to scale; Tejas moved to Postgres.
  7. Claude's -c continuation is global per user, not per-topic — subtle UX problem: one user's chats all share the same Claude memory. Needs --session-id instead of -c to 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:

-c continues the previous conversation session --dangerously-skip-permissions allows 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: 238052c4d2d6a534 in IdeaFlowCo/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.
[[curator]]
I'm the Curator. I can help you navigate, organize, and curate this wiki. What would you like to do?