- Move lsp-integration.md and secure-console-v3.md to docs/archive-obsolete/ (these features are now in the Nextra SaaS product) - Update CLAUDE.md with current project state - Simplify run_organism.py - Fix test fixtures for shared backend compatibility - Minor handler and llm_api cleanups Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
14 KiB
Secure Console Design — v3.0
Status: Design Draft (Partially Implemented) Date: January 2026
Implementation Note: This document describes the target design for v3.0. The current implementation has the console working with password authentication and most commands, but the OOB network port has not yet been removed. See
configuration.mdfor current OOB configuration. Full keyboard-only mode is planned for a future release.
Overview
The console becomes the sole privileged interface to the organism. In the target design, the OOB channel is eliminated as a network port — privileged operations are only accessible via local keyboard input.
Current State (v2.1):
- Console with password protection: ✅ Implemented
/config,/status,/listenerscommands: ✅ Implemented/config -eeditor with LSP support: ✅ Implemented- OOB network port removed: ❌ Not yet (still in configuration.md)
- Keyboard-only privileged ops: ❌ Partial (console commands work, but OOB port still exists)
Security Model
Threat Model
| Vector | Risk | Mitigation |
|---|---|---|
| Remote attacker | Send privileged commands | No network port — keyboard only |
| Malicious agent | Forge privileged XML | Agents only speak through bus; console hooks directly to handlers |
| Local malware | Keylog password | Out of scope (compromised host = game over) |
| Shoulder surfing | See password | Password not echoed; hash stored, not plaintext |
Trust Hierarchy
┌─────────────────────────────────────────┐
│ Keyboard Input (prompt_toolkit) │ ← TRUSTED (local human)
│ Password-protected privileged commands │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Console Handler │
│ /commands → direct privileged hooks │
│ @messages → message bus (untrusted) │
└─────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────┐
│ Message Bus │ ← UNTRUSTED (agents, network)
│ All traffic validated, sandboxed │
└─────────────────────────────────────────┘
Key Principle
Keyboard = Local = Trusted
No privileged port. No OOB socket. The only way to issue privileged commands is to be physically present at the keyboard.
Password Protection
Password Hash Storage
Password hash stored in ~/.xml-pipeline/console.key (chmod 600):
# console.key
algorithm: argon2id
hash: $argon2id$v=19$m=65536,t=3,p=4$...
created: 2026-01-10T12:00:00Z
Password Workflow
- First run: Console prompts to set password
- Startup: Console prompts for password before accepting any input
- Protected commands: Require password re-entry (see below)
Password-Protected Commands
| Command | Requires Password | Rationale |
|---|---|---|
/restart |
Yes | Disrupts all in-flight operations |
/kill <thread> |
Yes | Terminates agent work |
/quit |
No | Just exits cleanly |
/config |
No | Read-only view |
/status |
No | Informational |
Console Commands
Informational (No Password)
| Command | Action |
|---|---|
/help |
Show available commands |
/status |
Organism stats (uptime, message count, etc.) |
/listeners |
List registered listeners |
/threads |
List active threads with age and depth |
/buffer <thread-id> |
Inspect context buffer for thread |
/config |
View current organism.yaml (read-only) |
Operational (Password Required)
| Command | Action |
|---|---|
/restart |
Restart the pipeline (requires password) |
/kill <thread-id> |
Terminate a thread (requires password) |
/pause |
Pause message processing |
/resume |
Resume message processing |
Session
| Command | Action |
|---|---|
/quit |
Graceful shutdown |
/passwd |
Change console password |
Configuration Philosophy
Read-Only at Runtime
For v3.0, the organism.yaml is read-only while running:
/configshows the current config (view only)- To modify:
/quit→ edit yaml → restart - No hot-reload complexity
Future Consideration (v4.0+)
Hot-reload is complex:
- What happens to in-flight messages when a listener is removed?
- How to drain a listener before removing?
- Schema changes mid-conversation?
Deferred to future version with careful design.
Implementation
Dependencies
prompt_toolkit # Rich terminal input
argon2-cffi # Password hashing
Console Architecture
class SecureConsole:
"""Privileged console with password protection."""
def __init__(self, pump: StreamPump, key_path: Path):
self.pump = pump
self.key_path = key_path
self.password_hash: str | None = None
self.authenticated = False
self.paused = False
async def run(self):
"""Main console loop."""
# Load or create password
await self._ensure_password()
# Authenticate
if not await self._authenticate():
print("Authentication failed. Exiting.")
return
# Main loop with prompt_toolkit
session = PromptSession(history=FileHistory('~/.xml-pipeline/history'))
while True:
try:
line = await session.prompt_async('> ')
await self._handle_input(line)
except EOFError:
break
except KeyboardInterrupt:
continue
async def _handle_input(self, line: str):
"""Route input to handler."""
if line.startswith('/'):
await self._handle_command(line)
elif line.startswith('@'):
await self._handle_message(line)
else:
print("Use @listener message or /command")
async def _handle_command(self, line: str):
"""Handle privileged command."""
cmd, *args = line[1:].split(None, 1)
if cmd in PROTECTED_COMMANDS:
if not await self._verify_password():
print("Password required.")
return
handler = getattr(self, f'_cmd_{cmd}', None)
if handler:
await handler(args[0] if args else None)
else:
print(f"Unknown command: /{cmd}")
async def _verify_password(self) -> bool:
"""Prompt for password verification."""
password = await prompt_async('Password: ', is_password=True)
return argon2.verify(self.password_hash, password)
OOB Channel Removal (Planned)
Not Yet Implemented: The OOB port is still present in v2.1. This section describes the target design where the OOB port is removed.
In the target design, the OOB port in privileged-msg.xsd is removed. Privileged operations are:
- Defined as Python methods on
SecureConsole - Invoked directly via keyboard commands
- Never exposed on any network interface
# OLD (removed):
# oob_server = await start_oob_server(port=8766)
# NEW:
# Privileged ops are just methods on SecureConsole
async def _cmd_restart(self, args: str | None):
"""Restart the pipeline."""
print("Restarting pipeline...")
await self.pump.shutdown()
# Re-bootstrap and run
self.pump = await bootstrap('config/organism.yaml')
asyncio.create_task(self.pump.run())
print("Pipeline restarted.")
UI/UX
Startup
$ python run_organism.py
╔══════════════════════════════════════════╗
║ xml-pipeline console v3.0 ║
╚══════════════════════════════════════════╝
Password: ********
Organism 'hello-world' ready.
5 listeners registered.
Type /help for commands.
>
Example Session
> /status
Organism: hello-world
Uptime: 00:05:23
Threads: 3 active
Messages: 47 processed
Buffer: 128 slots across 3 threads
> /listeners
console Interactive console
console-router Routes console input
greeter [agent] Greeting agent
shouter Shouts responses
response-handler Forwards to console
> @greeter Hello world
[sending to greeter]
[shouter] HELLO WORLD!
> /threads
a1b2c3d4... age=00:02:15 depth=3 greeter→shouter→response-handler
e5f6g7h8... age=00:00:45 depth=1 greeter
> /kill a1b2c3d4
Password: ********
Thread a1b2c3d4 terminated.
> /restart
Password: ********
Restarting pipeline...
Pipeline restarted.
> /quit
Goodbye!
Security Checklist
- Password hash file has mode 600
- Password never stored in plaintext
- Password never logged
- Password not echoed during input
- No network port for privileged operations
- Protected commands require password re-entry
- Argon2id for password hashing (memory-hard)
Migration from v2.x (Future)
When the OOB removal is implemented, migration will involve:
- Remove OOB port configuration from organism.yaml
- Remove
privileged-msg.xsdnetwork handling - First run prompts for password setup
- Existing privileged operations become console commands
Current v2.1: OOB is still present. Console provides an alternative privileged interface but doesn't replace OOB yet.
Attach/Detach Model
The console is a proper handler in the message flow. It can attach and detach without stopping the organism.
Flow
┌─────────────────────────────────────────────────────────────┐
│ Startup │
│ Password: ******** │
│ /attach issued automatically │
└─────────────────────────────────────────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────┐
│ Attached State │
│ Console handler awaits keyboard input │
│ > @greeter hello │
│ > /status │
└─────────────────────────────────────────────────────────────┘
↓ (idle timeout, e.g. 30 min)
┌─────────────────────────────────────────────────────────────┐
│ Detached State │
│ Console handler returns None → await closed │
│ Organism keeps running headless │
│ Output queued to ring buffer (last N messages) │
└─────────────────────────────────────────────────────────────┘
↓ (/attach + password)
┌─────────────────────────────────────────────────────────────┐
│ Re-attached │
│ Queued output displayed │
│ Console resumes awaiting input │
└─────────────────────────────────────────────────────────────┘
Implementation
async def handle_console_prompt(payload: ConsolePrompt, metadata: HandlerMetadata):
"""Console handler with timeout support."""
# Display output
if payload.output:
print_colored(payload.output, source=payload.source)
# Wait for input with timeout
try:
line = await asyncio.wait_for(read_input(), timeout=IDLE_TIMEOUT)
except asyncio.TimeoutError:
print_colored("Idle timeout. Detaching console.", Colors.YELLOW)
return None # ← Detach: closes the await, organism continues
# Process input...
return HandlerResponse(...)
Detached Behavior
When console is detached, prompt changes and message sending is disabled:
Attached: Detached:
> @greeter hello ← works # @greeter hello ← rejected
> /status ← works # /status ← works
# /attach
Password: ********
> _ ← re-attached
| Concern | Behavior |
|---|---|
| Prompt | Changes from > to # |
/commands |
Still work (can check status, attach, quit) |
@messages |
Rejected: "Console detached. Use /attach" |
| Organism | Continues running normally |
| Logging | Output logged to file while detached |
Commands
| Command | Action |
|---|---|
/attach |
Attach console (requires password if detached) |
/detach |
Manually detach (organism keeps running) |
/timeout <minutes> |
Set idle timeout (0 = disabled) |
Open Questions
- Audit log? Log all privileged commands to file?
- Multi-user? Multiple passwords with different privilege levels?
- Remote console? SSH tunnel? (deferred — complexity)
- Detached notifications? Beep/alert when important messages arrive?
This design prioritizes simplicity and security. The keyboard-only model eliminates an entire class of remote attacks while providing a rich local interface for operators.