From d7d21cad7e385ac946687cccdf54ec8ea507a20c Mon Sep 17 00:00:00 2001 From: The Hand Date: Mon, 20 Jul 2026 21:44:04 -0500 Subject: [PATCH] =?UTF-8?q?feat(agent):=20bring-your-own-MODEL=20=E2=80=94?= =?UTF-8?q?=20any=20AI=20drives=20the=20frozen=20MCP=20surface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The BYO-key agent (#231) was Claude-only. The frozen MCP surface (ADR-0020) was built for ANY external consumer, so the agent now is too: - scripts/qos_agent.py — provider-agnostic entrypoint. --provider anthropic (default, Claude via tool_runner + MCP helpers, unchanged behavior) or --provider openai: ANY OpenAI-compatible /chat/completions endpoint (OpenAI, OpenRouter, Groq, DeepSeek, LM Studio, llama.cpp, keyless local Ollama via --base-url). The openai path converts the same 22 frozen MCP tools to function-calling schemas directly — still zero tool duplication — and feeds tool errors back to the model instead of dying. Credentials are resolved BEFORE any VM spawns; the guaranteed-shutdown finally and the --max-turns key-burn guard cover both providers. - scripts/qos_claude_agent.py — compat shim; old CLI works verbatim. - requirements-agent.txt (anthropic[mcp] + openai + mcp); the old requirements-claude-agent.txt now -r-includes it. - Makefile: qos-agent / qos-agent-list (PROVIDER/MODEL/BASE_URL knobs); qos-claude / qos-claude-list kept as aliases. - README post-launch refresh: the roadmap still listed ADR-0019/0020/0021 as planned — all three shipped in v0.5; new Done block + honest Next (N=5, audit actor attribution). MCP tool count corrected 21 -> 22 and the BYO agent surfaced in "What runs today". Verified (WSL): py_compile both entrypoints; --list-tools enumerates all 22 tools through qos_agent.py AND the shim; missing-key/missing-model guards exit cleanly before any VM boot; MCP->OpenAI schema conversion of the real 22-tool list is well-formed and JSON-serializable. Live loops remain BYO-key. Co-Authored-By: Claude Fable 5 --- Makefile | 37 ++- README.md | 25 +- requirements-agent.txt | 16 ++ requirements-claude-agent.txt | 14 +- scripts/qos_agent.py | 526 ++++++++++++++++++++++++++++++++++ scripts/qos_claude_agent.py | 312 +------------------- 6 files changed, 595 insertions(+), 335 deletions(-) create mode 100644 requirements-agent.txt create mode 100644 scripts/qos_agent.py diff --git a/Makefile b/Makefile index e6f4844..98daab7 100644 --- a/Makefile +++ b/Makefile @@ -118,7 +118,7 @@ OBJECTS = $(KERNEL_SOURCES:$(KERNEL_DIR)/src/%.c=$(BUILD_DIR)/%.o) \ -include $(OBJECTS:.o=.d) # Targets -.PHONY: all clean kernel run debug dump test test-list test-coverage ci-smoke ci-smoke-mem256 ci-smoke-mem512 ci-smoke-sched ci-smoke-disk ci-smoke-disk-upgrade ci-smoke-net ci-smoke-http ci-smoke-httpd ci-smoke-quiet ci-smoke-resonant ci-smoke-qseed ci-smoke-swarm ci-smoke-mcp ci-smoke-mcp-gate ci-smoke-qsubmit ci-smoke-society ci-smoke-society-gate ci-smoke-society-agents ci-smoke-society-agents-gate ci-smoke-society3 ci-smoke-society3-gate ci-smoke-society4 ci-smoke-society4-gate ci-smoke-society-agents-n ci-smoke-society-agents-n-gate ci-smoke-iso ci-smoke-kbd ci-smoke-noserial ci-smoke-screen swarm-pingpong qos-claude qos-claude-list +.PHONY: all clean kernel run debug dump test test-list test-coverage ci-smoke ci-smoke-mem256 ci-smoke-mem512 ci-smoke-sched ci-smoke-disk ci-smoke-disk-upgrade ci-smoke-net ci-smoke-http ci-smoke-httpd ci-smoke-quiet ci-smoke-resonant ci-smoke-qseed ci-smoke-swarm ci-smoke-mcp ci-smoke-mcp-gate ci-smoke-qsubmit ci-smoke-society ci-smoke-society-gate ci-smoke-society-agents ci-smoke-society-agents-gate ci-smoke-society3 ci-smoke-society3-gate ci-smoke-society4 ci-smoke-society4-gate ci-smoke-society-agents-n ci-smoke-society-agents-n-gate ci-smoke-iso ci-smoke-kbd ci-smoke-noserial ci-smoke-screen swarm-pingpong qos-agent qos-agent-list qos-claude qos-claude-list all: kernel @@ -2180,24 +2180,35 @@ ci-smoke-swarm: kernel # Local two-way exercise: COM2 as a TCP server; drive PING/PONG + a DATA request # routed to ghostd over capability-checked IPC. Not part of CI (needs a client). -# Claude ↔ QuantumOS (scripts/qos_claude_agent.py): a bring-your-own-key Claude -# agent that drives a live VM through the FROZEN MCP tool surface (ADR-0020) — -# Claude is the external consumer that freeze was built for, reusing +# Any model ↔ QuantumOS (scripts/qos_agent.py): a bring-your-own-key agent +# that drives a live VM through the FROZEN MCP tool surface (ADR-0020) — an +# external model is the consumer that freeze was built for, reusing # scripts/qos_mcp.py verbatim (no tool duplication). Needs the guest built -# (kernel prereq) and Anthropic credentials (ANTHROPIC_API_KEY or `ant auth -# login`). Pass a free-form prompt with TASK=..., or a curated preset with +# (kernel prereq) and your credentials. Providers: PROVIDER=anthropic (the +# default — Claude, ANTHROPIC_API_KEY or `ant auth login`) or PROVIDER=openai +# (any OpenAI-compatible endpoint — OPENAI_API_KEY, plus MODEL=... and +# optionally BASE_URL=http://localhost:11434/v1 for a keyless local Ollama). +# Pass a free-form prompt with TASK=..., or a curated preset with # EXPERIMENT={recall,society,quantum,explore}; deps: pip install -r -# requirements-claude-agent.txt -qos-claude: kernel - @echo "=== Claude drives QuantumOS (BYO key) ===" - QOS_KERNEL=$(BUILD_DIR)/kernel.elf32 python3 scripts/qos_claude_agent.py \ +# requirements-agent.txt +qos-agent: kernel + @echo "=== $(if $(PROVIDER),$(PROVIDER),anthropic) drives QuantumOS (BYO key) ===" + QOS_KERNEL=$(BUILD_DIR)/kernel.elf32 python3 scripts/qos_agent.py \ + $(if $(PROVIDER),--provider $(PROVIDER)) \ + $(if $(MODEL),--model "$(MODEL)") \ + $(if $(BASE_URL),--base-url "$(BASE_URL)") \ $(if $(TASK),"$(TASK)",--experiment $(if $(EXPERIMENT),$(EXPERIMENT),recall)) -# Prove the Claude→MCP→qos_bridge handshake end to end with NO API call and no +# Back-compat alias: the original Claude-only spelling, same defaults. +qos-claude: qos-agent + +# Prove the model→MCP→qos_bridge handshake end to end with NO API call and no # key: spawn the MCP server, list its tools, exit. A fast sanity check that the # agent's plumbing is intact. -qos-claude-list: - @python3 scripts/qos_claude_agent.py --list-tools +qos-agent-list: + @python3 scripts/qos_agent.py --list-tools + +qos-claude-list: qos-agent-list swarm-pingpong: kernel @echo "=== QuantumOS swarm bridge two-way (PING/PONG + DATA->ghostd) ===" diff --git a/README.md b/README.md index a0179aa..0934c50 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ QuantumOS boots on x86-64 under QEMU (`make run`), and every claim below is gate - **Associative memory as a syscall** — `SYS_IMPRINT`/`SYS_RECALL` (epic #95): capability-scoped kernel field regions a process can store byte patterns into and recall associatively against — ranked Q15 wave-resonance scoring, one bounded integer pass, no floats in the kernel. From the shell: `imprint the cat sat on the mat`, then `recall the cxt sxt on thx mxt` recovers the exact stored text. CI proves the noisy-probe recall, the capless-caller EPERM (by attack), cross-region isolation, and that a degenerate probe can't fault the kernel ([docs/RUNTIME.md](docs/RUNTIME.md)) - **…and those memories survive reboot** (epic #96): `sync` serializes the field to a checksummed section of the QDSK disk volume; the next boot restores it before services start, and the shell inherits its region exactly once, audibly. CI proves it in three boots: imprint+sync, then recall-from-corrupted-probe in a boot that never typed the pattern, then a deliberately corrupted blob that must cold-start honestly while the filesystem still restores ([docs/PERSISTENT_STORAGE.md](docs/PERSISTENT_STORAGE.md)) - **Two kernels, one field** (epic #97): two QuantumOS instances couple their `ghostd` oscillator fields over UDP — distributed Kuramoto phase synchronization between running kernels. To get there, QuantumOS gained a static-IP path and an **ARP responder** (SLIRP always answered for the guest; two guests on a raw L2 must do it themselves). A `fieldsyncd` service exchanges 256-phase snapshots each second; the cross-node order parameter R_x climbs from divergent (each node seeded from its own quantum entropy) to locked. CI boots **two guests on one wire** and asserts both fields start uncorrelated and synchronize — while each node's own associative recall keeps passing ([docs/NETWORKING.md](docs/NETWORKING.md)) -- **Serves a page, then serves agents** (epics #98/#99/#100): TCP `listen`/`accept` + a ring-3 `httpd` make QuantumOS *serve* a live status page; a host-side **MCP server** (`scripts/qos_mcp.py`) exposes a running kernel to any MCP-speaking agent as 21 tools — boot, script the shell, imprint/recall the field, run citizens, fetch over the guest's own TCP, read the audit ledger — every result carrying the verified Lamport boot identity. And the whole thing **boots in a browser**: the real 64-bit kernel under `qemu-system-x86_64` compiled to WebAssembly at [flaukowski.github.io/QuantumOS](https://flaukowski.github.io/QuantumOS/) +- **Serves a page, then serves agents** (epics #98/#99/#100): TCP `listen`/`accept` + a ring-3 `httpd` make QuantumOS *serve* a live status page; a host-side **MCP server** (`scripts/qos_mcp.py`) exposes a running kernel to any MCP-speaking agent as 22 tools (the surface frozen as `contracts/mcp/v1-tools.json` — ADR-0020) — boot, script the shell, imprint/recall the field, run citizens, fetch over the guest's own TCP, read the audit ledger — every result carrying the verified Lamport boot identity. A **bring-your-own-model agent** (`scripts/qos_agent.py`) puts an AI of *your* choice in the driver's seat over those same frozen tools: Claude by default, or any OpenAI-compatible endpoint (OpenAI, OpenRouter, Groq, a keyless local Ollama) — `make qos-agent`, key and VM never leave your machine. And the whole thing **boots in a browser**: the real 64-bit kernel under `qemu-system-x86_64` compiled to WebAssembly at [flaukowski.github.io/QuantumOS](https://flaukowski.github.io/QuantumOS/) - **Structural authority — conscience before wallet** (Phase D, epics #133/#135/#137/#144): a capability **authority ledger** records every grant, denial, spawn, and revocation, kernel-written and durable across reboots, so a citizen cannot forge or suppress its own entry; **explicit intent manifests** bind a per-pid allow-set at spawn and enforce real quotas (spawn count, CPU ticks, quantum submissions); **one-hop capability delegation** (`SYS_CAP_DERIVE`) lets an agent hand a *narrowed* authority to a sub-agent, cascade-revoked when the delegator dies. Authority *is* the capability set, and every attempt to exceed it is provable - **A quantum stack that never lies about precision** (epics #148/#149/#150): `qsv`, an *exact* Gaussian-integer state-vector engine in ring 3 (zero rounding — its unitarity is an integer identity), gated in CI against an independent host mirror by SHA-256 digest; a capability-gated kernel **QPU job broker** (`SYS_QPU`) that brokers *opaque* circuits under a per-manifest quota; and a host gateway dispatching **PennyLane (incl. cuQuantum GPU), CUDA-Q, and real qBraid QPUs**, every backend cross-oracled against the exact engine to ≤1e-9 - **An agent society** (epics #139/#171–#178): N kernels synchronize one field over a multicast L2 (min-pairwise Kuramoto verdict); and an `agentd` orchestrator delegates narrowed field capabilities to sub-agents it spawns itself over **spawn-time parent↔child IPC channels** — the society *self-assembles*, reaches content consensus by digest, divides labor across private field workspaces, and exchanges verified results with a second VM's society @@ -391,10 +391,17 @@ Target architecture — today the HAL is x86-64 only: See [CHANGELOG.md](CHANGELOG.md) for the full per-arc history and [docs/adr/](docs/adr/) for the architecture behind each milestone. +### ✅ **v0.5 - Frozen contracts & N-way societies** (Done — released as v0.5.1) +- [x] Honest memory to the 1 GB window (dynamic PMM sized from the multiboot map) — ADR-0021 +- [x] v1 contracts **FROZEN**: syscall ABI + MCP tool schemas + COM2 wire/attestation format, each a CI-diffed golden — ADR-0020 +- [x] 9–19x faster agent wire (I/O-priority boost — ADR-0022) and rebirth-proof IPC (ADR-0023) +- [x] Authenticated swarm plane: a host-admitted group key gates the field wire at N>2 — ADR-0019 +- [x] N=4 society ceiling proven + N-way society-of-societies exchanging real cross-node work +- [x] Bring-your-own-model agent: your AI drives the frozen MCP surface (`scripts/qos_agent.py`) + ### 🔭 **Next** (planned — see [docs/adr/](docs/adr/)) -- [ ] Authenticate the swarm plane (host-admitted keys + HMAC over the field wire) — ADR-0019 -- [ ] Freeze the v1 agent-surface contracts (syscall ABI + MCP schemas + wire format) — ADR-0020 -- [ ] Honest memory to the 1 GB window (dynamic PMM sized from the multiboot map) — ADR-0021 +- [ ] N=5 field societies (lift the two lockstep peer ceilings + the host bridge arrays) +- [ ] Actor attribution in the authority ledger (the reserved `audit_entry_t` field — ADR-0009) ### 🌟 **v1.0 - Production Ready** (Future) - [ ] Complete microkernel architecture @@ -407,11 +414,11 @@ See [CHANGELOG.md](CHANGELOG.md) for the full per-arc history and [docs/adr/](do We believe the future of computing is collaborative, and we welcome contributions from everyone! Whether you're a kernel developer, quantum computing researcher, or just curious about OS design, there's a place for you in QuantumOS. ### 🎯 **Where to start** -The v0.1–v0.4 foundations (process management, capabilities, quantum resources, IPC, the -services framework, and the whole agent-native stack) are **done and CI-gated** — see -[CHANGELOG.md](CHANGELOG.md). Good next directions are tracked as Proposed -[ADRs](docs/adr/) (authenticate the swarm plane, the v1 contract freeze, honest memory) and -in the [open issues](https://github.com/flaukowski/QuantumOS/issues). Pick one, or open a new +The v0.1–v0.5 foundations (process management, capabilities, quantum resources, IPC, the +services framework, the whole agent-native stack, and the frozen v1 contracts) are **done +and CI-gated** — see [CHANGELOG.md](CHANGELOG.md). Good next directions are tracked in the +[ADRs](docs/adr/) (N=5 societies, actor attribution in the authority ledger) and in the +[open issues](https://github.com/flaukowski/QuantumOS/issues). Pick one, or open a new issue to propose your own. ### 🛠️ **How to Contribute** diff --git a/requirements-agent.txt b/requirements-agent.txt new file mode 100644 index 0000000..c7e184f --- /dev/null +++ b/requirements-agent.txt @@ -0,0 +1,16 @@ +# Dependencies for scripts/qos_agent.py — the bring-your-own-key agent that +# drives a live QuantumOS VM through its frozen MCP tool surface with WHATEVER +# model you bring. Both provider SDKs are listed; each is imported only when +# its provider is selected (--provider anthropic|openai), so installing this +# file works for either. Python >= 3.10. +# +# The anthropic `mcp` extra pulls in the MCP conversion helpers +# (anthropic.lib.tools.mcp) used to reuse scripts/qos_mcp.py verbatim; the +# openai path converts the same MCP tools to function-calling schemas itself. +# +# Not a CI dependency — the mcp-schema freeze gate (requirements-mcp-gate.txt) +# pins mcp/pydantic exactly for reproducible schema extraction; this agent is a +# developer tool and floats within compatible ranges. +anthropic[mcp]>=0.116.0 +mcp>=1.0 +openai>=1.60 diff --git a/requirements-claude-agent.txt b/requirements-claude-agent.txt index 7010b19..bcd7c1d 100644 --- a/requirements-claude-agent.txt +++ b/requirements-claude-agent.txt @@ -1,10 +1,4 @@ -# Dependencies for scripts/qos_claude_agent.py — the bring-your-own-key Claude -# agent that drives a live QuantumOS VM through its frozen MCP tool surface. -# The `mcp` extra pulls in the MCP conversion helpers (anthropic.lib.tools.mcp) -# used to reuse scripts/qos_mcp.py verbatim. Python >= 3.10. -# -# Not a CI dependency — the mcp-schema freeze gate (requirements-mcp-gate.txt) -# pins mcp/pydantic exactly for reproducible schema extraction; this agent is a -# developer tool and floats within compatible ranges. -anthropic[mcp]>=0.116.0 -mcp>=1.0 +# Compat: the agent moved to scripts/qos_agent.py (any provider, Claude +# default) and its deps to requirements-agent.txt — one source of truth. +# This file remains so existing instructions keep working verbatim. +-r requirements-agent.txt diff --git a/scripts/qos_agent.py b/scripts/qos_agent.py new file mode 100644 index 0000000..5334f2d --- /dev/null +++ b/scripts/qos_agent.py @@ -0,0 +1,526 @@ +#!/usr/bin/env python3 +"""Bring-your-own-model ↔ QuantumOS: a BYO-key agent that drives a live +QuantumOS VM through its FROZEN MCP tool surface (ADR-0020 lane B, contracts/ +mcp/v1-tools.json). An external model is exactly the consumer that freeze was +built for — so this script REUSES scripts/qos_mcp.py verbatim rather than +duplicating a single tool definition. One source of truth: the same 22 tools +CI diffs against the golden are the tools your model sees. + +Any model, your choice of provider: + + --provider anthropic (default) Claude, via the Anthropic SDK's + tool_runner + MCP conversion helpers + --provider openai ANY OpenAI-compatible /chat/completions endpoint: + OpenAI itself, OpenRouter, Groq, DeepSeek, Mistral, + LM Studio, llama.cpp server — or a keyless local + Ollama (--base-url http://localhost:11434/v1) + +Architecture (all local, no hosted anything): + + your model (your key — or no key at all for a local server) + │ provider-specific agentic loop + ▼ + MCP tool conversion (anthropic helpers / plain function-calling) + │ stdio + ▼ + scripts/qos_mcp.py (FastMCP, unmodified) + │ thin delegation + ▼ + qos_bridge.QosVM / QosSociety → QEMU → QuantumOS + +The key never leaves your machine; the VM never leaves your machine. The model +plans and runs field-society / associative-memory experiments over the wire, +reads the results, and iterates. + +HONESTY (mirrors qos_mcp.py's module docstring): a tool result's `verified` +means the boot attestation is self-consistent (frames + Lamport signature + +qseed), NOT that the VM is live-now — a captured attestation replays. Only the +key-admitted STATUS reply is per-request nonce+HMAC fresh. The system prompt +tells the model this so it never overstates what a result proves. + +Run: + # 1. build the guest once (produces build/x86_64/kernel.elf32) + make kernel + # 2. install deps + pip install -r requirements-agent.txt + + # Claude (default) — bring Anthropic credentials, either works: + export ANTHROPIC_API_KEY=sk-ant-... # or: ant auth login + python3 scripts/qos_agent.py "Boot a VM and imprint three phrases, then recall a corrupted probe of the first." + + # any OpenAI-compatible provider: + export OPENAI_API_KEY=... + python3 scripts/qos_agent.py --provider openai --model gpt-5.1 --experiment society + + # a local model, no key at all: + python3 scripts/qos_agent.py --provider openai --model llama3.3 \ + --base-url http://localhost:11434/v1 + + # prove the MCP handshake without spending a token / needing a key: + python3 scripts/qos_agent.py --list-tools + +Deps: requirements-agent.txt (anthropic[mcp], openai, mcp) — each provider SDK +is imported only when that provider is selected. Python >= 3.10. +""" + +import argparse +import asyncio +import json +import os +import sys + +HERE = os.path.dirname(os.path.abspath(__file__)) +REPO = os.path.dirname(HERE) + +PROVIDERS = ("anthropic", "openai") + +# Curated on-ramp experiments (--experiment NAME). Each is a goal, not a +# recipe: the model designs the actual tool sequence. "recall" is the default. +EXPERIMENTS = { + "recall": ( + "Design and run a small associative-memory experiment: boot a VM, " + "imprint three distinct short phrases into the kernel Hopfield-Kuramoto " + "field, then recall a corrupted probe of one of them and report whether " + "the field relaxed to the right basin and with what order parameter R. " + "Draw one quantum-seeded number to salt the run. Shut the VM down when " + "finished and give a plain-language readout of what the field did." + ), + "society": ( + "Boot a 3-node field society, wait for the members to mean-field " + "synchronize, then read each node's status and the aggregates it " + "received from its peers. Report whether all three synchronized (and to " + "what R_x), and whether each node saw the OTHER two nodes' distinct " + "qseed-salted aggregates. Shut the society down when finished." + ), + "quantum": ( + "Draw several quantum-seeded random numbers from the guest and submit a " + "small quantum circuit (e.g. a Bell pair) to the in-OS QPU broker. " + "Report the results and explain, honestly, what is and isn't attested " + "about them. Shut the VM down when finished." + ), + "explore": ( + "You have a live QuantumOS instance. Explore it: read a machine " + "snapshot, inspect the holographic field read-only, look at the " + "capability authority ledger, and form your own small hypothesis about " + "how the field behaves — then run one experiment to test it. Shut down " + "cleanly and tell me what you found." + ), +} +DEFAULT_TASK = EXPERIMENTS["recall"] + +# Idempotent power-off tools called on exit so a booted VM/society is never +# orphaned, however the run ends (the system prompt asks the model to shut +# down; this guarantees it even on error, Ctrl-C, or the turn cap). +CLEANUP_TOOLS = ("qos_shutdown", "qos_society_shutdown") + +SYSTEM = """You are an experimentalist driving a live QuantumOS instance — a \ +capability-secured microkernel whose kernel hosts a Hopfield–Kuramoto \ +associative-memory field ("ghostd"), quantum-seeded entropy, and multi-node \ +"field societies" that couple over the wire. You act only through the provided \ +QuantumOS tools; there is no shell. + +What you can do: boot and shut down VMs; imprint and recall associative \ +memories at the kernel field and read that field's live state; form N-node \ +societies (3–4 members) that mean-field-synchronize and exchange qseed-salted \ +aggregates; draw quantum-seeded entropy; run citizens off the initrd; read \ +machine snapshots; and bridge memories to/from the host Kannaka HRM. + +Method: state your hypothesis, run the smallest experiment that tests it, read \ +the actual tool results (do not assume — a boot can fail, a recall can miss), \ +and iterate. Prefer one clear result over an exhaustive sweep. + +Honesty about trust — this matters: a result's `verified` flag means the boot \ +attestation is internally consistent (frames + Lamport signature + qseed), NOT \ +that the VM is live at read time; a captured attestation replays. Only a \ +key-admitted STATUS reply is per-request authenticated. Never describe an \ +ordinary tool result as cryptographically attested data. Report what the field \ +did, plainly, and distinguish measured from inferred. + +Operational: booting a VM or a society takes tens of seconds to a few minutes \ +— that is normal, not a hang. Always shut down what you booted before ending, \ +even on failure. When done, give a plain-language readout: what you ran, what \ +the field did, and what it demonstrates.""" + + +async def _open_tools(mcp_client_ctx): + """List the QuantumOS MCP tools from an initialized ClientSession.""" + result = await mcp_client_ctx.list_tools() + return result.tools + + +def _kernel_hint(): + """Warn early if no built guest is discoverable — every tool that boots a + VM would otherwise fail deep inside a tool call with a confusing error.""" + env_kernel = os.environ.get("QOS_KERNEL") + default = os.path.join(REPO, "build", "x86_64", "kernel.elf32") + if env_kernel and os.path.exists(env_kernel): + return None + if os.path.exists(default): + return None + return ( + "No QuantumOS kernel found (looked at $QOS_KERNEL and " + f"{default}). Build one first with `make kernel`, or point QOS_KERNEL " + "at a kernel.elf32." + ) + + +def _server_params(): + """stdio parameters that launch scripts/qos_mcp.py as an MCP server. + + Running `python qos_mcp.py` puts scripts/ on sys.path[0], so its bare + `from qos_bridge import ...` resolves; cwd is the repo root so any + relative kernel path resolves the same way the CI gates see it. QOS_KERNEL + (and the rest of the environment) is inherited by the child, so the VM the + agent boots is the one you built.""" + from mcp import StdioServerParameters + + return StdioServerParameters( + command=sys.executable, + args=[os.path.join(HERE, "qos_mcp.py")], + cwd=REPO, + env=os.environ.copy(), + ) + + +async def list_tools_only(): + """Prove the model→MCP→qos_bridge plumbing end to end WITHOUT calling any + API or needing a key: spawn the server, initialize, list the tools.""" + from mcp import ClientSession + from mcp.client.stdio import stdio_client + + async with stdio_client(_server_params()) as (read, write): + async with ClientSession(read, write) as session: + await session.initialize() + tools = await _open_tools(session) + print(f"QuantumOS MCP server exposes {len(tools)} tools:") + for t in sorted(tools, key=lambda x: x.name): + summary = (t.description or "").strip().splitlines()[0] if t.description else "" + print(f" {t.name:<26} {summary[:90]}") + return 0 + + +async def _cleanup(session): + """Power off anything the agent may have booted. Best-effort and idempotent: + both tools no-op when nothing is running, so calling both is always safe.""" + for name in CLEANUP_TOOLS: + try: + await session.call_tool(name, {}) + except Exception: # noqa: BLE001 — cleanup must never mask the real outcome + pass + + +def _fmt_input(obj): + """Compact one-line rendering of a tool_use input for the trace.""" + if not obj: + return "" + return ", ".join(f"{k}={v!r}" for k, v in obj.items()) + + +# -------------------------------------------------------------------------- +# provider: anthropic (Claude — the default) +# -------------------------------------------------------------------------- + +def _prepare_anthropic(): + """Import the SDK and resolve credentials BEFORE any VM is spawned. + Returns a client, or an int exit code on failure.""" + try: + from anthropic import AsyncAnthropic + from anthropic.lib.tools.mcp import async_mcp_tool # noqa: F401 — fail early if the MCP extra is missing + except ImportError: + print( + "The `anthropic` SDK with MCP extras is required for " + "--provider anthropic:\n pip install -r requirements-agent.txt", + file=sys.stderr, + ) + return 2 + try: + return AsyncAnthropic() # resolves ANTHROPIC_API_KEY or an `ant auth login` profile + except Exception as exc: # noqa: BLE001 — surface any credential-resolution failure clearly + print( + f"Could not construct the Anthropic client ({exc}).\n" + "Bring your own key: `export ANTHROPIC_API_KEY=sk-ant-...` or " + "`ant auth login`.", + file=sys.stderr, + ) + return 2 + + +async def _drive_anthropic(client, session, mcp_tools, task, model, max_tokens, effort, max_turns): + """The Claude loop: the SDK's MCP conversion helpers + tool_runner drive + the agentic loop, streaming the trace to stdout.""" + from anthropic.lib.tools.mcp import async_mcp_tool + + tools = [async_mcp_tool(t, session) for t in mcp_tools] + runner = client.beta.messages.tool_runner( + model=model, + max_tokens=max_tokens, + # Adaptive thinking with a visible summary so the reasoning between + # tool calls isn't a silent pause; effort is the depth knob (high + # suits multi-step experiment design). + thinking={"type": "adaptive", "display": "summarized"}, + output_config={"effort": effort}, + system=SYSTEM, + messages=[{"role": "user", "content": task}], + tools=tools, + ) + + # Each yielded message is one assistant turn (before its tools run). + # The MCP tools execute long VM operations; the runner awaits them. + turns = 0 + async for message in runner: + turns += 1 + for block in message.content: + if block.type == "text" and block.text.strip(): + print(block.text, flush=True) + elif block.type == "thinking" and getattr(block, "thinking", ""): + print(f"[thinking] {block.thinking}", flush=True) + elif block.type == "tool_use": + print(f"[tool] {block.name}({_fmt_input(block.input)})", flush=True) + if message.stop_reason == "refusal": + print("\n[The model declined this request.]", file=sys.stderr) + break + if turns >= max_turns: + print( + f"\n[Reached the {max_turns}-turn cap — stopping so a " + "runaway loop can't keep spending. Raise --max-turns " + "to allow more.]", + file=sys.stderr, + ) + break + return 0 + + +# -------------------------------------------------------------------------- +# provider: openai (any OpenAI-compatible /chat/completions endpoint) +# -------------------------------------------------------------------------- + +def _prepare_openai(base_url): + """Import the SDK and resolve credentials BEFORE any VM is spawned. + Returns a client, or an int exit code on failure.""" + try: + from openai import AsyncOpenAI + except ImportError: + print( + "The `openai` SDK is required for --provider openai:\n" + " pip install -r requirements-agent.txt", + file=sys.stderr, + ) + return 2 + api_key = os.environ.get("OPENAI_API_KEY") + if not api_key: + if base_url: + # Local/self-hosted servers (Ollama, LM Studio, llama.cpp) ignore + # the key entirely; the SDK just insists one exists. + api_key = "byo-no-key" + else: + print( + "Bring your own key: `export OPENAI_API_KEY=...` — or point " + "--base-url at a keyless local server (e.g. Ollama at " + "http://localhost:11434/v1).", + file=sys.stderr, + ) + return 2 + return AsyncOpenAI(api_key=api_key, base_url=base_url or None) + + +def _openai_tools(mcp_tools): + """MCP tool → OpenAI function-calling schema. The inputSchema IS the + frozen contract (name + JSON Schema), so this is a direct re-labelling, + not a re-description — still zero tool duplication.""" + return [ + { + "type": "function", + "function": { + "name": t.name, + "description": (t.description or "").strip()[:1024], + "parameters": t.inputSchema or {"type": "object", "properties": {}}, + }, + } + for t in mcp_tools + ] + + +async def _call_mcp_tool(session, name, arguments_json): + """Execute one MCP tool for the openai loop and flatten the result to + text. Errors are RETURNED as tool output (not raised) so the model can + read them and adjust — a bad tool call must not kill the run.""" + try: + args = json.loads(arguments_json) if arguments_json else {} + except json.JSONDecodeError as exc: + return f"TOOL-CALL ERROR: arguments were not valid JSON ({exc}); retry with corrected JSON." + try: + result = await session.call_tool(name, args) + except Exception as exc: # noqa: BLE001 — feed the failure back to the model + return f"TOOL-CALL ERROR: {exc}" + parts = [] + for item in getattr(result, "content", None) or []: + text = getattr(item, "text", None) + parts.append(text if text is not None else str(item)) + text = "\n".join(parts).strip() or "(empty result)" + if getattr(result, "isError", False): + text = f"TOOL ERROR:\n{text}" + return text + + +async def _chat(client, mt, **kw): + """One completion call. Newer OpenAI models only accept + max_completion_tokens; many compatible servers only know max_tokens — + try the modern name first and fall back on a 400 that names it.""" + from openai import BadRequestError + + try: + return await client.chat.completions.create(max_completion_tokens=mt, **kw) + except BadRequestError as exc: + if "max_completion_tokens" not in str(exc): + raise + return await client.chat.completions.create(max_tokens=mt, **kw) + + +async def _drive_openai(client, session, mcp_tools, task, model, max_tokens, max_turns): + """A plain function-calling loop against any OpenAI-compatible endpoint: + send messages+tools, execute every tool_call against the MCP session, + append the results, repeat until the model stops calling tools.""" + tools = _openai_tools(mcp_tools) + messages = [ + {"role": "system", "content": SYSTEM}, + {"role": "user", "content": task}, + ] + for _turn in range(max_turns): + rsp = await _chat(client, max_tokens, model=model, messages=messages, tools=tools) + msg = rsp.choices[0].message + if getattr(msg, "refusal", None): + print(f"\n[The model declined this request: {msg.refusal}]", file=sys.stderr) + return 0 + if msg.content and msg.content.strip(): + print(msg.content, flush=True) + if not msg.tool_calls: + return 0 + messages.append( + { + "role": "assistant", + "content": msg.content or None, + "tool_calls": [ + { + "id": tc.id, + "type": "function", + "function": { + "name": tc.function.name, + "arguments": tc.function.arguments, + }, + } + for tc in msg.tool_calls + ], + } + ) + for tc in msg.tool_calls: + print(f"[tool] {tc.function.name}({tc.function.arguments or ''})", flush=True) + out = await _call_mcp_tool(session, tc.function.name, tc.function.arguments) + messages.append({"role": "tool", "tool_call_id": tc.id, "content": out}) + print( + f"\n[Reached the {max_turns}-turn cap — stopping so a runaway loop " + "can't keep spending. Raise --max-turns to allow more.]", + file=sys.stderr, + ) + return 0 + + +# -------------------------------------------------------------------------- +# shared driver +# -------------------------------------------------------------------------- + +async def run_agent(task, provider, model, base_url, max_tokens, effort, max_turns): + """Drive QuantumOS with the chosen model: resolve provider credentials + FIRST (no VM is ever spawned for a run that can't talk to a model), then + spawn the MCP server and hand the session to the provider loop. Guarantees + the guest is powered off on exit and caps the number of turns so a runaway + loop cannot silently burn the user's key.""" + prep = _prepare_anthropic() if provider == "anthropic" else _prepare_openai(base_url) + if isinstance(prep, int): + return prep + client = prep + + hint = _kernel_hint() + if hint: + print(f"WARNING: {hint}\n", file=sys.stderr) + + from mcp import ClientSession + from mcp.client.stdio import stdio_client + + async with stdio_client(_server_params()) as (read, write): + async with ClientSession(read, write) as session: + await session.initialize() + mcp_tools = await _open_tools(session) + print( + f"Connected to QuantumOS ({len(mcp_tools)} tools). " + f"Provider: {provider}, model: {model}.\n" + ) + # cleanup runs in finally so a booted guest is never orphaned — on + # normal completion, the turn cap, a refusal, or Ctrl-C. + try: + if provider == "anthropic": + return await _drive_anthropic( + client, session, mcp_tools, task, model, max_tokens, effort, max_turns + ) + return await _drive_openai( + client, session, mcp_tools, task, model, max_tokens, max_turns + ) + finally: + print("\n[shutting down any VM the agent booted…]", file=sys.stderr) + await _cleanup(session) + + +def main(): + p = argparse.ArgumentParser( + description=( + "Drive a live QuantumOS VM with the model of your choice over its " + "frozen MCP tools (BYO key; Claude by default)." + ) + ) + p.add_argument("task", nargs="?", default=None, + help="a free-form experiment for the model to run (overrides --experiment)") + p.add_argument("--experiment", choices=sorted(EXPERIMENTS), default="recall", + help="a curated experiment when no free-form task is given (default: recall)") + p.add_argument("--provider", choices=PROVIDERS, default=None, + help="anthropic = Claude via the Anthropic SDK (default); openai = any " + "OpenAI-compatible endpoint (env QOS_AGENT_PROVIDER)") + p.add_argument("--model", default=None, + help="model id. anthropic default: claude-opus-4-8 (env QOS_CLAUDE_MODEL; " + "try claude-fable-5 for the hardest reasoning). REQUIRED for openai — " + "whatever your endpoint serves, e.g. gpt-5.1 or llama3.3") + p.add_argument("--base-url", default=os.environ.get("QOS_AGENT_BASE_URL"), + help="OpenAI-compatible endpoint, e.g. http://localhost:11434/v1 for a local " + "Ollama (env QOS_AGENT_BASE_URL); implies --provider openai") + p.add_argument("--max-tokens", type=int, default=16000) + p.add_argument("--effort", default="high", choices=["low", "medium", "high", "xhigh", "max"], + help="reasoning effort (anthropic only; ignored for openai)") + p.add_argument("--max-turns", type=int, default=40, + help="hard cap on agent turns so a runaway loop can't burn your key (default 40)") + p.add_argument("--list-tools", action="store_true", + help="list the QuantumOS MCP tools and exit (no API call, no key needed)") + args = p.parse_args() + + if args.list_tools: + return asyncio.run(list_tools_only()) + + provider = args.provider or os.environ.get("QOS_AGENT_PROVIDER") or ( + "openai" if args.base_url else "anthropic" + ) + if provider not in PROVIDERS: + p.error(f"unknown provider {provider!r} (QOS_AGENT_PROVIDER?) — choose from {PROVIDERS}") + if provider == "anthropic": + model = (args.model or os.environ.get("QOS_CLAUDE_MODEL") + or os.environ.get("QOS_AGENT_MODEL") or "claude-opus-4-8") + else: + model = args.model or os.environ.get("QOS_AGENT_MODEL") + if not model: + p.error("--model is required with --provider openai — pass whatever your " + "endpoint serves (e.g. gpt-5.1, or llama3.3 for a local Ollama)") + + task = args.task if args.task is not None else EXPERIMENTS[args.experiment] + return asyncio.run( + run_agent(task, provider, model, args.base_url, args.max_tokens, args.effort, args.max_turns) + ) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/qos_claude_agent.py b/scripts/qos_claude_agent.py index 0aa3ede..69d5e88 100644 --- a/scripts/qos_claude_agent.py +++ b/scripts/qos_claude_agent.py @@ -1,314 +1,20 @@ #!/usr/bin/env python3 -"""Claude ↔ QuantumOS: a bring-your-own-key agent that drives a live QuantumOS -VM through its FROZEN MCP tool surface (ADR-0020 lane B, contracts/mcp/ -v1-tools.json). Claude is exactly the external consumer that freeze was built -for — so this script REUSES scripts/qos_mcp.py verbatim rather than duplicating -a single tool definition. One source of truth: the same 22 tools CI diffs -against the golden are the tools Claude sees. +"""Compat shim — the BYO-key agent moved to scripts/qos_agent.py, which now +speaks to ANY provider (Claude stays the default). This entrypoint preserves +the original Claude CLI exactly: `python3 scripts/qos_claude_agent.py ...` +behaves as it always did (provider=anthropic unless you say otherwise). -Architecture (all local, no hosted anything): +New capability lives in qos_agent.py, e.g. a local model with no key at all: - Claude (Anthropic API, your key) - │ tool_runner drives the agentic loop - ▼ - anthropic MCP conversion helpers (async_mcp_tool) - │ stdio - ▼ - scripts/qos_mcp.py (FastMCP, unmodified) - │ thin delegation - ▼ - qos_bridge.QosVM / QosSociety → QEMU → QuantumOS - -The key never leaves your machine; the VM never leaves your machine. Claude -plans and runs field-society / associative-memory experiments over the wire, -reads the results, and iterates. - -HONESTY (mirrors qos_mcp.py's module docstring): a tool result's `verified` -means the boot attestation is self-consistent (frames + Lamport signature + -qseed), NOT that the VM is live-now — a captured attestation replays. Only the -key-admitted STATUS reply is per-request nonce+HMAC fresh. The system prompt -tells Claude this so it never overstates what a result proves. - -Run: - # 1. build the guest once (produces build/x86_64/kernel.elf32) - make kernel - # 2. bring your own Anthropic credentials — either works: - export ANTHROPIC_API_KEY=sk-ant-... # or: ant auth login - # 3. install deps and run - pip install -r requirements-claude-agent.txt - python3 scripts/qos_claude_agent.py "Boot a VM and imprint three phrases, then recall a corrupted probe of the first." - - # prove the MCP handshake without spending a token / needing a key: - python3 scripts/qos_claude_agent.py --list-tools - -Deps: anthropic[mcp]>=0.116.0, mcp>=1.0 (Python >=3.10) + python3 scripts/qos_agent.py --provider openai --model llama3.3 \ + --base-url http://localhost:11434/v1 """ -import argparse -import asyncio import os import sys -HERE = os.path.dirname(os.path.abspath(__file__)) -REPO = os.path.dirname(HERE) - -# Default experiment when none is given on the command line. Deliberately -# open-ended: Claude designs the run rather than following a script (Fable-5 -# does better with a goal than a recipe). -# Curated on-ramp experiments (--experiment NAME). Each is a goal, not a -# recipe: Claude designs the actual tool sequence. "recall" is the default. -EXPERIMENTS = { - "recall": ( - "Design and run a small associative-memory experiment: boot a VM, " - "imprint three distinct short phrases into the kernel Hopfield-Kuramoto " - "field, then recall a corrupted probe of one of them and report whether " - "the field relaxed to the right basin and with what order parameter R. " - "Draw one quantum-seeded number to salt the run. Shut the VM down when " - "finished and give a plain-language readout of what the field did." - ), - "society": ( - "Boot a 3-node field society, wait for the members to mean-field " - "synchronize, then read each node's status and the aggregates it " - "received from its peers. Report whether all three synchronized (and to " - "what R_x), and whether each node saw the OTHER two nodes' distinct " - "qseed-salted aggregates. Shut the society down when finished." - ), - "quantum": ( - "Draw several quantum-seeded random numbers from the guest and submit a " - "small quantum circuit (e.g. a Bell pair) to the in-OS QPU broker. " - "Report the results and explain, honestly, what is and isn't attested " - "about them. Shut the VM down when finished." - ), - "explore": ( - "You have a live QuantumOS instance. Explore it: read a machine " - "snapshot, inspect the holographic field read-only, look at the " - "capability authority ledger, and form your own small hypothesis about " - "how the field behaves — then run one experiment to test it. Shut down " - "cleanly and tell me what you found." - ), -} -DEFAULT_TASK = EXPERIMENTS["recall"] - -# Idempotent power-off tools called on exit so a booted VM/society is never -# orphaned, however the run ends (the system prompt asks Claude to shut down, -# this guarantees it even on error, Ctrl-C, or the turn cap). -CLEANUP_TOOLS = ("qos_shutdown", "qos_society_shutdown") - -SYSTEM = """You are an experimentalist driving a live QuantumOS instance — a \ -capability-secured microkernel whose kernel hosts a Hopfield–Kuramoto \ -associative-memory field ("ghostd"), quantum-seeded entropy, and multi-node \ -"field societies" that couple over the wire. You act only through the provided \ -QuantumOS tools; there is no shell. - -What you can do: boot and shut down VMs; imprint and recall associative \ -memories at the kernel field and read that field's live state; form N-node \ -societies (3–4 members) that mean-field-synchronize and exchange qseed-salted \ -aggregates; draw quantum-seeded entropy; run citizens off the initrd; read \ -machine snapshots; and bridge memories to/from the host Kannaka HRM. - -Method: state your hypothesis, run the smallest experiment that tests it, read \ -the actual tool results (do not assume — a boot can fail, a recall can miss), \ -and iterate. Prefer one clear result over an exhaustive sweep. - -Honesty about trust — this matters: a result's `verified` flag means the boot \ -attestation is internally consistent (frames + Lamport signature + qseed), NOT \ -that the VM is live at read time; a captured attestation replays. Only a \ -key-admitted STATUS reply is per-request authenticated. Never describe an \ -ordinary tool result as cryptographically attested data. Report what the field \ -did, plainly, and distinguish measured from inferred. - -Operational: booting a VM or a society takes tens of seconds to a few minutes \ -— that is normal, not a hang. Always shut down what you booted before ending, \ -even on failure. When done, give a plain-language readout: what you ran, what \ -the field did, and what it demonstrates.""" - - -async def _open_tools(mcp_client_ctx): - """List the QuantumOS MCP tools from an initialized ClientSession.""" - result = await mcp_client_ctx.list_tools() - return result.tools - - -def _kernel_hint(): - """Warn early if no built guest is discoverable — every tool that boots a - VM would otherwise fail deep inside a tool call with a confusing error.""" - env_kernel = os.environ.get("QOS_KERNEL") - default = os.path.join(REPO, "build", "x86_64", "kernel.elf32") - if env_kernel and os.path.exists(env_kernel): - return None - if os.path.exists(default): - return None - return ( - "No QuantumOS kernel found (looked at $QOS_KERNEL and " - f"{default}). Build one first with `make kernel`, or point QOS_KERNEL " - "at a kernel.elf32." - ) - - -def _server_params(): - """stdio parameters that launch scripts/qos_mcp.py as an MCP server. - - Running `python qos_mcp.py` puts scripts/ on sys.path[0], so its bare - `from qos_bridge import ...` resolves; cwd is the repo root so any - relative kernel path resolves the same way the CI gates see it. QOS_KERNEL - (and the rest of the environment) is inherited by the child, so the VM the - agent boots is the one you built.""" - from mcp import StdioServerParameters - - return StdioServerParameters( - command=sys.executable, - args=[os.path.join(HERE, "qos_mcp.py")], - cwd=REPO, - env=os.environ.copy(), - ) - - -async def list_tools_only(): - """Prove the Claude→MCP→qos_bridge plumbing end to end WITHOUT calling the - API or needing a key: spawn the server, initialize, list the tools.""" - from mcp import ClientSession - from mcp.client.stdio import stdio_client - - async with stdio_client(_server_params()) as (read, write): - async with ClientSession(read, write) as session: - await session.initialize() - tools = await _open_tools(session) - print(f"QuantumOS MCP server exposes {len(tools)} tools:") - for t in sorted(tools, key=lambda x: x.name): - summary = (t.description or "").strip().splitlines()[0] if t.description else "" - print(f" {t.name:<26} {summary[:90]}") - return 0 - - -async def _cleanup(session): - """Power off anything the agent may have booted. Best-effort and idempotent: - both tools no-op when nothing is running, so calling both is always safe.""" - for name in CLEANUP_TOOLS: - try: - await session.call_tool(name, {}) - except Exception: # noqa: BLE001 — cleanup must never mask the real outcome - pass - - -async def run_agent(task, model, max_tokens, effort, max_turns): - """Drive QuantumOS with Claude: spawn the MCP server, convert its tools, - and run the tool-runner loop, streaming the trace to stdout. Guarantees the - guest is powered off on exit and caps the number of turns so a runaway loop - cannot silently burn the user's key.""" - try: - from anthropic import AsyncAnthropic - from anthropic.lib.tools.mcp import async_mcp_tool - except ImportError: - print( - "The `anthropic` SDK with MCP extras is required:\n" - " pip install -r requirements-claude-agent.txt", - file=sys.stderr, - ) - return 2 - - from mcp import ClientSession - from mcp.client.stdio import stdio_client - - try: - client = AsyncAnthropic() # resolves ANTHROPIC_API_KEY or an `ant auth login` profile - except Exception as exc: # noqa: BLE001 — surface any credential-resolution failure clearly - print( - f"Could not construct the Anthropic client ({exc}).\n" - "Bring your own key: `export ANTHROPIC_API_KEY=sk-ant-...` or " - "`ant auth login`.", - file=sys.stderr, - ) - return 2 - - hint = _kernel_hint() - if hint: - print(f"WARNING: {hint}\n", file=sys.stderr) - - async with stdio_client(_server_params()) as (read, write): - async with ClientSession(read, write) as session: - await session.initialize() - mcp_tools = await _open_tools(session) - tools = [async_mcp_tool(t, session) for t in mcp_tools] - print( - f"Connected to QuantumOS ({len(tools)} tools). Model: {model}, " - f"effort: {effort}.\n" - ) - - runner = client.beta.messages.tool_runner( - model=model, - max_tokens=max_tokens, - # Adaptive thinking with a visible summary so the reasoning - # between tool calls isn't a silent pause; effort is the depth - # knob (high suits multi-step experiment design). - thinking={"type": "adaptive", "display": "summarized"}, - output_config={"effort": effort}, - system=SYSTEM, - messages=[{"role": "user", "content": task}], - tools=tools, - ) - - # Each yielded message is one assistant turn (before its tools run). - # The MCP tools execute long VM operations; the runner awaits them. - # cleanup runs in finally so a booted guest is never orphaned — on - # normal completion, on the turn cap, on a refusal, or on Ctrl-C. - try: - turns = 0 - async for message in runner: - turns += 1 - for block in message.content: - if block.type == "text" and block.text.strip(): - print(block.text, flush=True) - elif block.type == "thinking" and getattr(block, "thinking", ""): - print(f"[thinking] {block.thinking}", flush=True) - elif block.type == "tool_use": - print(f"[tool] {block.name}({_fmt_input(block.input)})", flush=True) - if message.stop_reason == "refusal": - print("\n[Claude declined this request.]", file=sys.stderr) - break - if turns >= max_turns: - print( - f"\n[Reached the {max_turns}-turn cap — stopping so a " - "runaway loop can't keep spending. Raise --max-turns " - "to allow more.]", - file=sys.stderr, - ) - break - finally: - print("\n[shutting down any VM the agent booted…]", file=sys.stderr) - await _cleanup(session) - return 0 - - -def _fmt_input(obj): - """Compact one-line rendering of a tool_use input for the trace.""" - if not obj: - return "" - return ", ".join(f"{k}={v!r}" for k, v in obj.items()) - - -def main(): - p = argparse.ArgumentParser( - description="Drive a live QuantumOS VM with Claude over its frozen MCP tools (BYO key)." - ) - p.add_argument("task", nargs="?", default=None, - help="a free-form experiment for Claude to run (overrides --experiment)") - p.add_argument("--experiment", choices=sorted(EXPERIMENTS), default="recall", - help="a curated experiment when no free-form task is given (default: recall)") - p.add_argument("--model", default=os.environ.get("QOS_CLAUDE_MODEL", "claude-opus-4-8"), - help="Anthropic model id (default claude-opus-4-8; try claude-fable-5 for the hardest reasoning)") - p.add_argument("--max-tokens", type=int, default=16000) - p.add_argument("--effort", default="high", choices=["low", "medium", "high", "xhigh", "max"]) - p.add_argument("--max-turns", type=int, default=40, - help="hard cap on agent turns so a runaway loop can't burn your key (default 40)") - p.add_argument("--list-tools", action="store_true", - help="list the QuantumOS MCP tools and exit (no API call, no key needed)") - args = p.parse_args() - - if args.list_tools: - return asyncio.run(list_tools_only()) - task = args.task if args.task is not None else EXPERIMENTS[args.experiment] - return asyncio.run(run_agent(task, args.model, args.max_tokens, args.effort, args.max_turns)) - +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from qos_agent import main # noqa: E402 if __name__ == "__main__": sys.exit(main())