diff --git a/README.md b/README.md index c7551c28..6d55e12d 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,12 @@ Not ready to sign in? `medulla --mock` runs a full offline demo, with no account Prebuilt binaries ship for Linux (x86_64, aarch64), macOS (Apple Silicon), and Windows (x86_64). Building from source, pinning a version, and embedding the SDK are covered in [Getting Started](https://tinyhumans.gitbook.io/medulla/developers/getting-started). +## No tmux, no multi-agent wrapper + +Running a lot of agents has meant one of two workarounds: split the terminal into panes and be the scheduler yourself, or wrap the harnesses in another agent and hope it can read everything they produce. Medulla is one process, in one terminal, built for the job instead. + +Opening another agent is `Ctrl-T` — pick a harness or a shell, pick a directory, it is running. There is no ceiling on how many you keep open. Each gets its own terminal, kept live in the background whether or not it is the one on screen, and Medulla reads all of them for the things that need a human: a permission prompt, a blocking error, a dead session, a turn finished and waiting on review. Those arrive as `⚠ 3 waiting on you`, not as three panes you were supposed to have been watching. + ## What you get Your whole fleet, legible. One lane per agent, live. You can see what each agent is doing, answer the one that has a question, and cancel the one that has gone wrong, without losing your place in everything else. diff --git a/docs/TERMINOLOGY.md b/docs/TERMINOLOGY.md index 83d03ebd..c881442f 100644 --- a/docs/TERMINOLOGY.md +++ b/docs/TERMINOLOGY.md @@ -46,6 +46,8 @@ that CLI: | Codex | ACP over stdio | | `codex-server` | JSON-RPC over stdio to a shared, long-lived `codex app-server` | | OpenCode | ACP over stdio | +| OpenHuman | In-process: no binary spawned, no transport. The wire value is `HarnessProvider::Openhuman`; the agent turn runs inside the `medulla` process on the vendored `tinyagents` crate with Medulla's own tools (`src/sdk/src/daemon/providers/local/mod.rs`). Never auto-selected by provider detection — a node reaches it only by naming it. | +| Shell | None: a plain interactive shell (`bash`, `zsh`, whatever `$SHELL` names), not a coding agent. Never detected as an available provider and never dispatchable; it exists so an operator can open a terminal beside their agents in the same pane, host, and working directory. | `codex-server` is a **flavor** of Codex rather than a separate harness type: it authenticates, bills, and configures as Codex and differs only in that one @@ -97,6 +99,13 @@ wants work done, the **hub** delivers a `TaskRequest` (carrying a `task_id`, **worker** and collects the `TaskOutcome`. It is the outbound half of the daemon's task loop. +The hub also carries the **workflow plane** (`src/sdk/src/hub/plane/`): the wire +contract with the Medulla orchestration backend for saved workflow graphs — +Socket.IO shapes and `medulla:*` event names (`payloads.rs`) — and the +store-side `WorkflowBridge` trait (`bridge.rs`) an embedding host installs to +answer them. It used to be re-exported from the embedded OpenHuman core; with +that core removed, Medulla is the only host left that speaks it. + ## Cycle One orchestrator turn: **user input → reasoning → tool calls → reply**. A cycle @@ -220,10 +229,15 @@ with `medulla init` and registered with `medulla workspace add`. ## Provider A coding-assistant CLI: the same axis as an agent's **harness** type, seen from -the process end. The three supported providers are `claude` (Claude Code), +the process end. The three coding-CLI providers are `claude` (Claude Code), `codex` (OpenAI Codex), and `opencode`. The daemon spawns the CLI as a subprocess and communicates over ACP or legacy JSONL. +Two more providers sit outside that coding-CLI set: `openhuman`, the in-process +harness that runs an agent turn inside the `medulla` process instead of +spawning one (see **Harness** above), and `shell`, a plain interactive shell +that is never dispatchable or auto-detected. + A provider is chosen together with a **transport**, and the pair is named by one word, a **flavor**. `codex` is Codex on its CLI; `codex-server` is the same provider on a shared `codex app-server` process. Anything that follows from diff --git a/docs/attribution-proxy.md b/docs/attribution-proxy.md index f381ce19..5680c395 100644 --- a/docs/attribution-proxy.md +++ b/docs/attribution-proxy.md @@ -88,14 +88,17 @@ All three spawned harnesses are covered. OpenCode is accepted as a `customHarnesses` base even though it can reach OpenRouter natively, because that native path is exactly the one this proxy needs to take over. -The embedded OpenHuman core is covered too, by a different mechanism. It is not a -child process, so there is no environment to inject into and nothing to scrub: -Medulla resolves the preset's key, exchanges it for a loopback token, and hands -the core the mount and the token as a *per-call* route on -`inference_agent_chat`. The core applies that route to the turn's own in-memory -configuration and never persists it, so borrowing an endpoint for one node does -not repoint the account's own inference. As with a spawned harness, the core is -given the token and never the OpenRouter key. +The local in-process harness is covered too, by the same mechanism through a +different call shape. Naming `openhuman` as a node's harness no longer dispatches +into a separate core; it runs the turn in-process on the vendored `tinyagents` +loop, with Medulla's own tools. That turn is not a child process, so there is no +environment to inject into and nothing to scrub: Medulla resolves the preset's +key, exchanges it for a loopback token, and hands the turn the mount and the +token directly as the route it calls inference on. Unlike a spawned harness, this +one has no ambient inference configuration to fall back to — with no router +preset naming an endpoint and a model, the turn refuses to run rather than +resolving one some other way. As with a spawned harness, the turn is given the +token and never the OpenRouter key. One limitation applies. Medulla injects environment variables at the spawn seam and never writes a harness's own configuration file. A harness you have diff --git a/docs/e2e-live-harness.md b/docs/e2e-live-harness.md index d3515b80..86e2378b 100644 --- a/docs/e2e-live-harness.md +++ b/docs/e2e-live-harness.md @@ -11,8 +11,8 @@ One end-to-end round trip over the **host link** (`docs/host-link-protocol.md`), with no real keys and no network egress: ``` -owner driver (examples/coordination_owner.rs; a real medulla-link endpoint) - → mock link forwarder (examples/mock_link_forwarder.rs; blind UDP, §5) +owner driver (src/sdk/examples/coordination_owner/main.rs; a real medulla-link endpoint) + → mock link forwarder (src/sdk/examples/mock_link_forwarder.rs; blind UDP, §5) → medulla daemon (real binary, `--providers `, the host end) → the real coding CLI (spawned by the daemon as its provider) → mock LLM (e2e/coordination/mock_llm.py) @@ -57,8 +57,8 @@ routed at the mock and which wire dialect it lands on. | `e2e/coordination/Dockerfile` | the harness image: a rust build stage layered onto that base | | `e2e/coordination/build-image.sh` | build (and optionally push) either image | | `e2e/coordination/run-docker.sh` | build + run the whole harness in a container | -| `examples/mock_link_forwarder.rs` | blind UDP forwarder implementing protocol §5 rules 1-8 | -| `examples/coordination_owner.rs` | owner-side driver: enrolls pairs, serves legs, prints terminal frame JSON | +| `src/sdk/examples/mock_link_forwarder.rs` | blind UDP forwarder implementing protocol §5 rules 1-8 | +| `src/sdk/examples/coordination_owner/main.rs` | owner-side driver: enrolls pairs, serves legs, prints terminal frame JSON | ## Running diff --git a/docs/plans/harness-workflow-skills.md b/docs/plans/harness-workflow-skills.md index 2fdc2dec..e7df7b51 100644 --- a/docs/plans/harness-workflow-skills.md +++ b/docs/plans/harness-workflow-skills.md @@ -26,7 +26,7 @@ Facts this plan rests on, each checked against the tree at `11351b34`: - **Declared inputs are already on the listing view.** `WorkflowSummary.inputs` (`src/sdk/src/workflows/types/workflow.rs:176`) carries `Vec` — `name`, `type`, `description`, `required`, `default` - (`vendor/openhuman/vendor/tinyflows/src/model/inputs.rs:112`). Rendering a skill needs + (`vendor/tinyflows/src/model/inputs.rs:112`). Rendering a skill needs only `workflow_list`, not a graph fetch. - **Tool-surface gating already exists.** `ToolMode::{Full,Propose}` + `MEDULLA_WORKFLOW_TOOLS` (`src/sdk/src/workflows/mcp/evolve.rs`) withhold verbs from diff --git a/docs/vendoring.md b/docs/vendoring.md index 9e73919b..76f3488f 100644 --- a/docs/vendoring.md +++ b/docs/vendoring.md @@ -1,110 +1,77 @@ # Vendored dependencies -Some upstream crates are consumed as git submodules under `vendor/` rather than -from crates.io. The workspace `exclude = ["vendor"]` keeps them out of `members`, -so they carry their own lints and tests instead of joining this repository's CI -gates. +Three upstream crates are consumed as git submodules under `vendor/` rather than +from crates.io. The workspace `exclude = ["vendor", "worktrees"]` keeps them out +of `members`, so they carry their own lints and tests instead of joining this +repository's CI gates. | Submodule | Upstream | Consumed as | | --- | --- | --- | -| `vendor/tinyplace` | `tinyhumansai/tiny.place` | path dependency | -| `vendor/tinycortex` | `tinyhumansai/tinycortex` | path dependency | -| `vendor/tinyflows` | `tinyhumansai/tinyflows` | registry coordinate + `[patch.crates-io]` | +| `vendor/tinyagents` | `tinyhumansai/tinyagents` | registry coordinate `2.1` + `[patch.crates-io]` | +| `vendor/tinyflows` | `tinyhumansai/tinyflows` | registry coordinate `0.8` + `[patch.crates-io]` | +| `vendor/tinyhumans-sdk` | `tinyhumansai/sdk` | path dependency (no registry coordinate) | -Initialize everything with: +Initialize with: ```sh -git submodule update --init --recursive +bash scripts/init-submodules.sh ``` -## tinyflows +not `git submodule update --init --recursive`. `vendor/tinyagents` carries a +`wiki` documentation submodule that nothing here compiles, and `--recursive` +descends unconditionally. The script is also the one place the vendored set is +written down, and must stay in lockstep with the root manifest's +`[patch.crates-io]` table. -`tinyflows` is the DAG workflow engine behind the `workflows` feature. It is -declared in the root `Cargo.toml` as a *registry* dependency and redirected to -the submodule: +All three submodules are self-contained: none declares a path or git dependency +of its own, and none carries code submodules of its own. `.gitmodules` uses +HTTPS URLs so CI clones them without a deploy key. -```toml -[workspace.dependencies] -tinyflows = { version = "0.5", features = ["mock"] } +## What each one is for + +`tinyagents` is the agent harness — the bounded model/tool loop that +`src/sdk/src/daemon/providers/local/` runs in-process for the `openhuman` +harness provider, replacing what used to be an `inference_agent_chat` RPC into +the embedded core. The `sqlite` feature brings `tinyagents::session`, the durable +store behind `src/sdk/src/agent/history/`; `tools` brings the builtin tool family +the loop dispatches. Neither is on by default in the crate. + +`tinyflows` is the DAG workflow engine behind the SDK's `workflows` feature, reached +through the adapter seam in `src/sdk/src/flow_engine/`. Its `mock` feature is a +normal dependency feature rather than a dev-only one: the authoring surface +dry-runs graphs against the engine's deterministic capability stand-ins in +ordinary builds, not just in tests. `host-caps` and `store` supply the host +capability set and the graph store. + +`tinyhumans-sdk` is the shared TinyHumans HTTP transport (`TinyHumansClient`) +that `src/sdk/src/client/` builds the typed Medulla surface on: auth, durable +sessions, SSE event streaming, one-shot orchestration, and the public feedback +board. It owns credential headers, the `{success, data}` envelope, and path +percent-encoding. + +## One declaration style per crate + +Declare each vendored crate exactly one way: either as a direct path dependency +or as a registry coordinate redirected by the patch table, never both. Mixing the +two yields two `PackageId`s for one crate and an `E0308` where the types look +identical, the first time a value crosses the seam. `tinyhumans-sdk` is a path +dependency because it has no registry coordinate to patch. + +`[patch.crates-io]` applies only from the workspace root: +```toml [patch.crates-io] -tinyflows = { path = "vendor/tinyflows" } +tinyagents = { path = "vendor/tinyagents" } +tinyflows = { path = "vendor/tinyflows" } ``` -This is the same shape the sibling `openhuman` host uses. The indirection exists -because crates.io lags the branch we track (the published maximum is `0.3.0` -while the pinned tree reports `0.5.1`), so a plain registry dependency would not -resolve to the code we build against. Keeping the registry coordinate (rather -than a bare path dependency) means the two hosts share one pin and one upgrade -cadence. - -**Pinned commit:** `fb24363aea921f957958bc8f4aeb5b0a244e41c7` (`v0.3.0-37-gfb24363`), -matching `openhuman`. - -The `mock` feature is a normal dependency feature, not a dev-only one: the -authoring surface dry-runs graphs against the engine's deterministic capability -stand-ins in ordinary builds, not just in tests. - -### One `tinyagents`, and the patch entry is mandatory - -The graph must resolve exactly one `tinyagents`, and it must come from the -vendored tree. OpenHuman's `vendor/tinycortex` requires `tinyagents = "2.1"`, so -sourcing `tinycortex` from there collapses the graph to a single `tinyagents`, -and the root `[patch.crates-io]` entry for `tinyagents` is **mandatory**. -Omitting it does not fail loudly: `tinyagents 2.1.0` is published on crates.io, -so the build silently resolves the registry copy instead of the vendored tree -(~14 commits ahead). - -Verify with `cargo tree -i tinyagents`: the source must read `path+file://…`, -never `registry+…`. `cargo tree -d` must report no duplicate `tiny*`. - -## Vendored OpenHuman core - -`vendor/openhuman` carries the OpenHuman core that medulla embeds. Several rules -about it are load-bearing and easy to get wrong. - -Initialize it with `scripts/init-submodules.sh`, never with `--recursive`. -`vendor/openhuman` has submodules of its own, two of which belong to the -OpenHuman *desktop* app, including a Tauri fork that bundles CEF. `--recursive` -clones both, and nothing in medulla's graph references either (the Cargo -workspace excludes `vendor/`, so `app/src-tauri` is not a member). A git -dependency would not help: Cargo updates git-dependency submodules recursively -with no opt-out, which makes the CEF clone mandatory. The submodule plus an -explicit init list is the only way to avoid it. - -The root `[patch.crates-io]` table is load-bearing. `[patch.crates-io]` -applies only from the workspace root, so once OpenHuman is a path dependency -*its* patch table is ignored, as is -`vendor/openhuman/vendor/tinycortex/.cargo/config.toml`, which is CWD-scoped. -This workspace's root manifest must therefore reproduce OpenHuman's entire table -with paths rewritten to `vendor/openhuman/vendor/*`. Drop an entry and Cargo -quietly resolves the published crate instead of the vendored tree. +Dropping an entry does not fail loudly. Both crates are published, so Cargo +silently resolves the registry copy instead of the vendored tree. -Declare each vendored crate exactly one way: either as a direct path dependency -or as a registry coordinate redirected by the patch table, and never both for the -same crate. Mixing the two styles yields two `PackageId`s for one crate and an -`E0308` where the types look identical, the first time a value crosses the -medulla↔OpenHuman seam. Guard with `cargo tree -d`, which must report no -duplicate `tiny*`. - -When the two repos disagree on a shared pin, the newer pin wins and the bump -lands in OpenHuman. Adopting an older OpenHuman pin wholesale is a hard compile -break (for example, a `tinyplace` ancestor missing `signal::maintain`, which -`src/sdk/src/daemon/transport/mod.rs` calls), so advance the pin *in OpenHuman* -and let this gitlink follow rather than patching around it here. - -OpenHuman must never depend on the `medulla` crate. Its default-ON -`medulla-local` feature currently has an empty dependency list. The day someone -gives it a real edge, `medulla-public → openhuman → medulla` becomes a Cargo -dependency cycle and a hard failure. - -Coverage excludes the vendored tree. `vendor/` path deps are *local* -packages under the workspace root, so `cargo-llvm-cov`'s default registry filter -does not drop them; the gate's `--ignore-filename-regex` starts with -`(^|/)vendor/` for that reason. Removing it sinks the 95% gate to roughly the -first-party share of a very large tree. +Verify with `cargo tree -i tinyagents` — the source must read `path+file://…`, +never `registry+…` — and `cargo tree -d`, which must report no duplicate `tiny*`. -### Updating the pin +## Updating a pin ```sh cd vendor/tinyflows @@ -115,7 +82,27 @@ cargo build # confirm the adapter seam still compiles cargo test ``` -Then update the pinned commit recorded above and commit the gitlink. Because -`tinyflows` is pre-1.0 and still changing its `engine` entry points, expect the -adapter seam in `src/sdk/src/tinyflows/` to need attention on an update; the rest -of the SDK should not. +Then commit the gitlink. Because `tinyflows` is pre-1.0 and still changing its +`engine` entry points, expect `src/sdk/src/flow_engine/` to need attention on an +update; the rest of the SDK should not. + +## Coverage + +Coverage excludes the vendored tree. `vendor/` path dependencies are local +packages under the workspace root, so `cargo-llvm-cov`'s default registry filter +does not drop them; the gate's `--ignore-filename-regex` starts with +`(^|/)vendor/` for that reason. + +## History + +Until v0.11.0 the runtime was an embedded OpenHuman core vendored at +`vendor/openhuman`. It carried sixteen submodules of its own — two belonging to +the OpenHuman desktop app, including a Tauri fork bundling CEF — and because +`[patch.crates-io]` applies only from the workspace root, this manifest had to +reproduce that core's entire patch table rebased onto `vendor/openhuman/vendor/*`: +ten entries, eight of them pinning a crate nothing here linked directly, each one +silently resolving to a published crate if dropped. `scripts/init-submodules.sh` +had to enumerate the same set by hand because Cargo reported one missing entry +per resolution failure, costing a CI round trip each. Removing the core removed +that whole class of failure; what is left is the three self-contained crates +above. diff --git a/docs/workspace-profiles.md b/docs/workspace-profiles.md index 1c39ee2a..12d0a517 100644 --- a/docs/workspace-profiles.md +++ b/docs/workspace-profiles.md @@ -86,13 +86,20 @@ Drafts a profile for `dir` (default: the current directory) and writes `MEDULLA.md` there: 1. Reads the directory's `AGENTS.md`, `CLAUDE.md`, and `README.md` (whichever - exist). + exist) — recorded as `sources`, but not otherwise used. 2. Scans the file layout. -3. Asks the configured model to distil them into a summary plus routing hints. -4. Writes the result for you to review and edit. +3. Writes a deterministic stub body alongside the scanned layout, for you to + review and edit. -The draft is a starting point. The summary is what the orchestrator actually -reads, so it is worth a pass by hand. +The model-drafted body went out with the memory layer that owned the provider +seam, so this stub is the only behaviour `init` has: `--offline` is accepted +but is now a no-op, since there is no model call left to skip. `--config +` is likewise accepted but unused — `init` neither reads backend +settings nor writes the registry. + +The layout is the part of the profile that carries real information, since it +is read straight off the tree rather than drafted. The summary is a starting +point for you to fill in by hand. `init` authors the file and stops there; it does **not** register the workspace. Use `medulla workspace add` for both. @@ -107,15 +114,8 @@ template outside the crate root fails that build. | Flag | Effect | | --- | --- | | `--force`, `-f` | Overwrite an existing `MEDULLA.md`. Without it, `init` refuses rather than discarding an authored profile. | -| `--offline` | Skip the model call and write the editable stub. | -| `--config ` | Explicit config file for the backend/model settings. | - -### Model resolution - -Resolution takes one order: an explicit `OPENROUTER_API_KEY` wins, otherwise the -backend's inference surface is used with the JWT from `medulla login`. With -neither, or if the model call fails, `init` writes the stub and says so, so it -always leaves you a usable file. +| `--offline` | Accepted for backward compatibility; a no-op, since `init` never calls a model. | +| `--config ` | Accepted but currently unused by `init`. | ## How a profile reaches the orchestrator diff --git a/gitbooks/README.md b/gitbooks/README.md index 42d46a0d..cc32b6b6 100644 --- a/gitbooks/README.md +++ b/gitbooks/README.md @@ -36,6 +36,16 @@ Medulla commands fleets of agent harnesses. Instead of driving [Claude Code](htt That differs from pointing a harness at other harnesses in two ways that matter. Every running harness streams its input back as it happens, so what the orchestrator knows about the fleet is current rather than assembled after the fact. And the orchestrator's own reasoning surface stays small, because the bulk of the fleet's output is distilled before it arrives instead of being read into one context window. +## No tmux, no wrapper + +Running many agents at once has, until now, meant one of two workarounds. Split the terminal and manage the panes yourself, or wrap the harnesses in another agent and hope it can read everything they produce. Medulla replaces both. + +It is one process in one terminal. Opening another agent is `Ctrl-T`: pick a harness or a shell, pick a directory, and it is running. There is no ceiling on how many you keep open — each gets its own PTY and its own live terminal state, maintained in the background whether or not it is the one on screen. A rail lists every one of them; the pane beside it shows whichever you have selected, switching instantly because that session's screen was never stale. + +And you are not the one polling. Medulla reads every backgrounded session for the signals that mean it needs a human — a permission prompt, a startup dialog, a blocking error, a bell, a dead session, a finished turn awaiting review — and surfaces them as one mark per row and a count in the title: `⚠ 3 waiting on you`. `Ctrl-]` attaches your keyboard to a session and detaches it again; the rest keep running. + +A multiplexer gives you N panes and no opinion about them. Medulla gives you the one that needs you. + ## Correctness first, by design Medulla is built around one principle: get the right answer. When a worker fails, it re-delegates. When results look thin, it verifies. When a task splits, it fans out rather than guessing. Every task settles into a definite state and every budget is enforced, so an operation too large to eyeball still finishes with an answer you can act on. diff --git a/gitbooks/developers/README.md b/gitbooks/developers/README.md index 7d13c9fc..e76087dd 100644 --- a/gitbooks/developers/README.md +++ b/gitbooks/developers/README.md @@ -8,7 +8,7 @@ description: >- This is the developer home for Medulla: how to install and run the terminal app, how to embed the SDK in your own Rust code, how it is put together, and how to build the repository from source. -The [product overview](../) is the high-level story; these pages are the hands-on detail. Everything here tracks the [`tinyhumansai/medulla-src`](https://github.com/tinyhumansai/medulla-src) repository, a two-crate Cargo workspace: the [`medulla`](https://github.com/tinyhumansai/medulla-src/tree/main/src/sdk/) SDK library and the [`medulla-tui`](https://github.com/tinyhumansai/medulla-src/tree/main/src/tui/) app crate, which ships the `medulla` binary. +The [product overview](../) is the high-level story; these pages are the hands-on detail. Everything here tracks the [`tinyhumansai/medulla-src`](https://github.com/tinyhumansai/medulla-src) repository, a three-crate Cargo workspace: the [`medulla`](https://github.com/tinyhumansai/medulla-src/tree/main/src/sdk/) SDK library and the [`medulla-tui`](https://github.com/tinyhumansai/medulla-src/tree/main/src/tui/) app crate, which ships the `medulla` binary. ## Read next @@ -28,7 +28,7 @@ The [product overview](../) is the high-level story; these pages are the hands-o | [Attribution and Routing](attribution-and-routing.md) | The loopback proxy that rewrites OpenRouter attribution and keeps the provider key out of the harness. | | [Host Link Protocol](host-link-protocol.md) | The normative `medulla-link/1` wire specification, its forwarder rules, and its conformance tests. | | [Testing](testing.md) | Where tests live, the shared stand-ins, the offline and live suites, and the coverage gate. | -| [Vendoring](vendoring.md) | The OpenHuman submodule, the patch table, and the rules a source build depends on. | +| [Vendoring](vendoring.md) | The three vendored crates, the patch table, and the rules a source build depends on. | | [Contributing](contributing.md) | Build, test, lint, coverage, and the release process. | ## Install and run diff --git a/gitbooks/developers/architecture.md b/gitbooks/developers/architecture.md index 815d8744..5aeede56 100644 --- a/gitbooks/developers/architecture.md +++ b/gitbooks/developers/architecture.md @@ -2,11 +2,13 @@ See [Why an Orchestrator](../why-an-orchestrator-model.md) for the product argument. This page is about the code: how the open-source SDK and TUI are put together, how they talk to the backend, and how the pieces named in the product story map onto modules you can read. -## Two crates +## Three crates -The public repository is a two-crate [Cargo](https://doc.rust-lang.org/cargo/) workspace with a strict separation between logic and rendering: +The public repository is a three-crate [Cargo](https://doc.rust-lang.org/cargo/) workspace with a strict separation between logic and rendering: -* [`src/sdk/`](https://github.com/tinyhumansai/medulla-src/tree/main/src/sdk/) is the `medulla` SDK crate, a UI-free logic library. It holds the backend HTTP/SSE client, the runtime adapters over the embedded core, sessions, workflows, and the host-link integration. It is reusable from any Rust program. +* [`src/link/`](https://github.com/tinyhumansai/medulla-src/tree/main/src/link/) is the `medulla-link` crate, the host-link transport on its own — no dependency on the SDK, so the wire can be read and tested without the rest. + +* [`src/sdk/`](https://github.com/tinyhumansai/medulla-src/tree/main/src/sdk/) is the `medulla` SDK crate, a UI-free logic library. It holds the backend HTTP/SSE client, the runtime adapters over it, the in-process agent loop, sessions, workflows, and the host-link integration. It is reusable from any Rust program. * [`src/tui/`](https://github.com/tinyhumansai/medulla-src/tree/main/src/tui/) is the `medulla-tui` crate, shipping the `medulla` binary: a [ratatui](https://ratatui.rs/) terminal UI over the SDK. It owns state, rendering, input, and theming, and re-exports the SDK's UI-facing data modules. Reusable APIs live in the SDK; rendering and process wiring live in the app crate. The SDK depends only on its own traits and types, never on the TUI. @@ -15,8 +17,12 @@ Reusable APIs live in the SDK; rendering and process wiring live in the app crat The UI drives everything through one trait, `Runtime`, plus its snapshot contract. The UI depends only on that trait, not on any concrete implementation, which is what makes the runtimes interchangeable and the whole thing testable offline. Two implementations ship: -* `openhuman`, the embedded OpenHuman core, which is what the product runs on. It boots inside the `medulla` process, so there is no socket and no attach handshake. -* `mock`, a scripted runtime for tests and demos, with no network, reached with `--mock`. +* [`cloud`](https://github.com/tinyhumansai/medulla-src/tree/main/src/sdk/src/runtime/cloud/), which is what the product runs on. It drives the orchestration backend directly through the `client` module below: HTTP for mutations, a polled event cursor for the live feed. +* [`mock`](https://github.com/tinyhumansai/medulla-src/tree/main/src/sdk/src/runtime/mock/), a scripted runtime for tests and demos, with no network, reached with `--mock`. + +`cloud` reclaims a name it already had. Before v0.11.0 an OpenHuman core was embedded in front of the transport, so every backend call became an RPC hop onto that core's own client — against the same deployment, with a second wire-type set and an error-string decode in the middle. Sessions were never local state; they live on the backend, so nothing was gained by asking an in-process core to fetch them. Dropping the core removed the hop, not the transport. + +Inside `cloud` the split is deliberate: `fold.rs` is pure translation from backend wire types to a render snapshot, `cell.rs` holds that snapshot plus a broadcast channel and the echo/pending-turn bookkeeping, and `mod.rs` is the thin part that needs a client — minting a session lazily on first submit, refreshing the roster, and polling events from a sequence cursor (backing off between an active 120 ms and an idle 1 s). [`connect/`](https://github.com/tinyhumansai/medulla-src/tree/main/src/sdk/src/runtime/cloud/connect/) builds the client and reports readiness as `Ready`, `SignedOut`, or `Unusable`; two of the three answers are decisions this host can make without a round trip, and it now makes them locally instead of asking a booted core and classifying the error it returned. Alongside them sit the pieces both share: @@ -35,9 +41,25 @@ The [`client`](https://github.com/tinyhumansai/medulla-src/tree/main/src/sdk/src * Durable sessions (`/medulla/v1`): persistent orchestration sessions. * SSE event streaming: the live event feed the UI folds into agent lanes and traces. * One-shot orchestration (`/orchestration/v1`): fire-and-collect delegation. +* The public feedback board (`/feedback`). Every response is wrapped in a `{ "success": true, "data": ... }` envelope; errors arrive as `{ "success": false, "error": ..., "errorCode": ... }` and are surfaced as a typed `ClientError::Api` that preserves the `errorCode`. +`client` is a typed Medulla surface over the shared `tinyhumans-sdk` transport rather than a second HTTP client of its own. The transport owns credential headers, the envelope, and path percent-encoding; the typed routes, the not-exposed-route gate, and the error taxonomy live here. See [Vendoring](vendoring.md). + +## The `openhuman` provider runs in-process + +`openhuman` is a harness provider name you can still type — in a task frame, in `baseHarness`, in the TUI's harness picker — and it is the one provider with no binary to spawn. Every other provider shells out to a coding-agent CLI and reads its JSONL; this one runs the agent loop inside the `medulla` process. + +The name is now the only thing OpenHuman about it. What runs is [`agent/`](https://github.com/tinyhumansai/medulla-src/tree/main/src/sdk/src/agent/): a bounded model-and-tool loop on the vendored `tinyagents` harness, with Medulla's own tools (`fs`, `shell`, and the guard around them) and per-thread transcript history in `agent/history/`. The daemon side is [`daemon/providers/local/`](https://github.com/tinyhumansai/medulla-src/tree/main/src/sdk/src/daemon/providers/local/), which builds the route and workspace, runs one turn, folds the harness event stream into Medulla's event vocabulary, and keeps a watchdog on a turn that is genuinely working. + +Two consequences are worth stating plainly, because both used to be the other way round: + +* **A turn carries no operator state.** It gets the checkout and the route, nothing else. There is no memory engine, no channel providers, no cron scheduler, and no access to an operator's credentials, because there is no core left to hold them. Dispatching a task no longer pulls a whole desktop product into the binary. +* **There is no approval gate.** The embedded core refused external-effect tools from an unlabelled caller and parked them for a human to approve. The local harness runs what the model asks for, inside the workspace and the environment scrubbing described in [Environment Variables](environment-variables.md#what-an-agent-turn-cannot-see). + +Hooks, managed skills and MCP tools are deliberately absent here: those are installed onto a *child's* command line, and there is no child. The provider is also never auto-selected — detection only ever offers real CLIs, so a node reaches it by naming `openhuman` explicitly. + ## Distillation is server-side The orchestrator does not read raw fleet traffic into one context window. Harness output is folded and distilled before it reaches the orchestrator's reasoning surface, and that machinery runs server-side. The SDK's job is to stream the distilled slice to and from the UI; the folding the client itself does is in [`ui`](https://github.com/tinyhumansai/medulla-src/tree/main/src/sdk/src/ui/) (see below). @@ -68,6 +90,12 @@ The rest splits by responsibility: What makes Medulla's use of that engine different from any other host embedding it is that an `agent` node is a *dispatched task*, not a model call. [`run/`](https://github.com/tinyhumansai/medulla-src/tree/main/src/sdk/src/workflows/run/) executes and resumes, [`store/`](https://github.com/tinyhumansai/medulla-src/tree/main/src/sdk/src/workflows/store/) is where workflows and run records live, [`authoring.rs`](https://github.com/tinyhumansai/medulla-src/tree/main/src/sdk/src/workflows/authoring.rs) edits a graph as a series of checked patches, [`ops/`](https://github.com/tinyhumansai/medulla-src/tree/main/src/sdk/src/workflows/ops/) exposes the whole thing as one JSON-in/JSON-out surface, and [`mcp/`](https://github.com/tinyhumansai/medulla-src/tree/main/src/sdk/src/workflows/mcp/) serves those same operations to a harness over MCP. The whole module sits behind the default `workflows` feature, so a slim build can drop the engine and its jq expression stack. +## The workflow plane + +[`hub/plane/`](https://github.com/tinyhumansai/medulla-src/tree/main/src/sdk/src/hub/plane/) is the workflow plane's contract with the orchestration backend, kept apart from the plumbing so the contract can be read on its own. `payloads.rs` is the Socket.IO wire shape — `RegisterWorkflows`, `WorkflowRequest`/`WorkflowResult` correlated by `request_id`, `WorkflowOp`, `CopilotOutcome`, and the `medulla:*` event names. `bridge.rs` is `WorkflowBridge`, the store-side trait an embedding host implements: reads are synchronous and dispatched on a blocking thread, `copilot` is async because it is a full agent turn rather than a read, and every method returns `Result<_, String>` because the error lands directly on a model's prompt surface as a tool result. The transport itself is in `hub::workflows` and `hub/socket/workflow.rs`, whose invariant is that every request is answered. + +Both halves used to be re-exported from the embedded core. That was sound while two hosts shared one socket implementation; with the core gone there is one host left, and sourcing a Medulla-to-Medulla-backend contract from a desktop product would have been the tail wagging the dog. The move was a relocation rather than a redefinition — field names, `rename_all`, and event strings are unchanged. + ## Host-link integration Medulla's orchestration layer drives its own native workers, and it can also dispatch tasks to full coding-agent CLI instances (Claude Code, Codex, OpenCode) running anywhere, over the **host link**. Four modules make that work: diff --git a/gitbooks/developers/authentication.md b/gitbooks/developers/authentication.md index 3206926f..fce6859d 100644 --- a/gitbooks/developers/authentication.md +++ b/gitbooks/developers/authentication.md @@ -1,7 +1,8 @@ # Authentication -Medulla authenticates to the backend with a JWT. You can supply it directly with -an environment variable, or sign in and let the CLI store a verified credential. +Medulla authenticates to the backend with a JWT. You can supply one directly +through config or an environment variable, or sign in and let Medulla store a +verified session itself. Two sign-in flows are available, chosen by where your browser is: @@ -15,6 +16,46 @@ The browser flow cannot work over SSH. The backend redirects your browser to the *browser*, not the remote host running Medulla, so the listener there never sees the callback. Use the code flow. +## Where the credential comes from + +Three sources are consulted, in this order, and the first that yields a token +wins: + +1. `backend.token` — a JWT written inline in the [config](configuration.md). +2. `backend.tokenEnv` — the name of an environment variable to read, default + `MEDULLA_TOKEN`. An empty value is ignored rather than treated as a token. +3. The stored session, `session.json` in the account's + [Medulla home](configuration.md#medulla-home). + +Every backend-facing surface resolves the token through that one chain — the +TUI's readiness check, `medulla hub`, and `medulla login`/`logout` alike — so +there is no path where one part of Medulla considers you signed in and another +does not. + +The first two are *external credential sources*: the operator stated them +explicitly, so they outrank anything Medulla stores for itself. That has two +visible consequences, both deliberate: + +* `medulla login` refuses to save a session underneath one. It signs you in, + reports who you are, and then fails with `signed in, but outranks the + stored session — this shell would keep using that credential, so the login was + not saved`. Storing a session that nothing would ever read is worse than + refusing. +* `medulla logout` clears the store, then checks the chain again. If an external + source still yields a token it exits non-zero: `the stored session was cleared, + but this shell is still authenticated from — remove it to finish + signing out`. Logout means no source authenticates you, not "one of three was + emptied". + +### The stored session is scoped to its issuer + +`session.json` records the `baseUrl` the token was issued by alongside the token +itself, and the runtime hands the stored bearer only to a `backend.baseUrl` whose +origin — scheme, host and port, normalized — matches it. Point a config at a +different deployment and the stored session is simply not offered to it; sign in +again against that deployment instead. `backend.token` and `backend.tokenEnv` are +not filtered this way, because those are the operator's own explicit choice. + ## `medulla login` ```sh @@ -31,32 +72,96 @@ backend's OAuth page, and captures the JWT the backend redirects back with. With `--code` it binds nothing. It prints `/auth//login?redirect=cli` and waits on stdin. Open that URL -anywhere, on another terminal's browser, your laptop, or your phone, sign in, and -the page shows a one-time code. Paste it back and the CLI exchanges it for a JWT -via `POST /auth/login-token/consume`. The code is single-use and expires after 15 +anywhere — another terminal's browser, your laptop, your phone — sign in, and the +page shows a one-time code. Paste it back and the CLI exchanges it for a JWT via +`POST /auth/login-token/consume`. The code is single-use and expires after 15 minutes; the long-lived session is only ever issued to whoever redeems it, so what crosses your clipboard is not a bearer token. -Either way `login` then verifies the token via `/auth/me`, prints who you are, -and hands the JWT to the embedded OpenHuman core as its app session. The base URL -comes from `backend.baseUrl` in the [config](configuration.md) (`--config ` -to point at a different config). +Either way, `login` then: + +1. Verifies the token with `GET /auth/me` and prints who you are. +2. Refuses to continue if an external credential source outranks the store. +3. Adopts the account: sanitizes the account id the backend returned, and seeds + that account's `config.toml` with the `backend.baseUrl` it signed in against + (an existing, different value is reported rather than overwritten). +4. Verifies once more, writes `session.json`, and only then publishes the account + marker. +5. Re-checks that the home it just wrote to is the home this process resolves, + and fails loudly with the reason if not. +6. Adopts any legacy `credentials.json` left by an older install. + +The base URL comes from `backend.baseUrl` in the [config](configuration.md); pass +`--config ` to point at a different one. + +### What is stored, and where + +| Path | Contents | +|---|---| +| `/session.json` | `{ "token", "userId", "baseUrl" }` — the session itself | +| `/active_user.toml` | `user_id = "..."` — which account directory this install reads | + +`` is `~/.medulla` (or `MEDULLA_HOME`, or `./.medulla` under +`MEDULLA_DEV`), and `` is `/` — `/local` before +you have ever signed in. There is no OS keychain involved: `session.json` is +written to a temporary file created `0600`, fsynced, and renamed over the +destination, so a reader never sees a half-written credential and the file is +never world-readable even briefly. + +The account marker is written by the credential store, after the session file +lands. Nothing can leave you with a marker pointing at an account whose session +failed to write. -The core is the only place a session lives. On the next `medulla` run the TUI -finds the session and starts straight into the app. `medulla logout` ends the -session and nothing else: the account stays selected, so signing back in returns -to the same home and the same deployment. See -[Medulla home](configuration.md#medulla-home). Precedence for the backend token -is inline `backend.token`, then `backend.tokenEnv`, then the core's session. +### Account ids and `MEDULLA_USER` + +Every account's directory is named by the id the backend reports, so that id must +be safe as a path segment: non-empty, not starting with `.`, and made only of +`A-Z a-z 0-9 _ -`. It is checked before it is ever joined into a path. A backend +response that names no account, or names an unusable one, is refused with an +explicit error rather than guessed at — there is nowhere correct to store the +session. + +`MEDULLA_USER` pins the active account for one process without touching the +shared marker. Because it is a pin rather than a preference, Medulla refuses +anything that would make it a lie: + +* Signing in as a different account than `MEDULLA_USER` names → + `MEDULLA_USER pins account , but this token belongs to `. Nothing is + stored. +* The same conflict inside a running TUI → nothing is stored and the shared + account selection is left unchanged. + +`MEDULLA_USER=local` is the escape hatch back to the pre-login home. + +## `medulla logout` + +`logout` clears `session.json` (running it twice is not an error), removes the +retired credential files described below, and then re-checks the precedence chain +as described above. + +It deliberately leaves the account marker and the account's directory alone. +Logging out is not forgetting which account and which deployment this install +belongs to; signing back in returns you to the same home, config and logs. See +[Medulla home](configuration.md#medulla-home). ### Upgrading from a standalone credentials file -Older installs kept a `credentials.json` beside the core's store, which gave two -independent answers to "am I signed in?": `medulla login` could report success -while the core, whose session drives the runtime, stayed signed out and the TUI -kept opening its login screen. `login` and `logout` delete any such file left -behind, since nothing reads it now and it holds a bearer token no logout could -invalidate. +Two credential files predate the current store: a `credentials.json` under the +Medulla home, and an older one under the OS config directory. Both held a real +JWT back when Medulla owned its own credential file. While sessions lived +elsewhere, `login` and `logout` simply deleted them — a bearer token in a file no +code path can invalidate is strictly worse than one in use. + +Now that the store is Medulla's again, deleting is the wrong move on login: the +token in that file is the same app session the new store wants, so an install +that predates the change is signed in and should stay signed in. `login` +therefore **adopts** it — the JWT is verified through the same path as any other +and, only once the new `session.json` is written, the old file is removed. + +`logout` still deletes them, and that is exactly why. Adoption is a startup +behaviour; leaving a retired file behind at logout would mean the next launch +verified the retired bearer and signed the operator straight back in, having just +been told they were logged out. ## Token via environment @@ -66,14 +171,18 @@ Skip login entirely and pass a JWT directly: MEDULLA_TOKEN= medulla ``` +This is `backend.tokenEnv` at its default name. Remember it outranks the stored +session for as long as it is exported: `medulla logout` will tell you so rather +than silently leaving you authenticated. + ## Logging in from the TUI -When you start `medulla` and the embedded core has no app session, the TUI opens -a login screen before the main app. There is no offline fallback: the mock -runtime is reached only by asking for it with `--mock`. +When you start `medulla` with no usable credential, the TUI opens a login screen +before the main app. There is no offline fallback: the mock runtime is reached +only by asking for it with `--mock`. -Everything is one list, navigated with `↑↓` and `Enter`; nothing is bound to -a bare letter, so no stray keystroke can start a flow. `Ctrl-C` quits from +Everything is one list, navigated with `↑↓` and `Enter`; nothing is bound to a +bare letter, so no stray keystroke can start a flow. `Ctrl-C` quits from anywhere. ``` @@ -86,26 +195,35 @@ anywhere. Quit ``` -Pick the method first, then the provider (google / github / twitter): +Pick the method first, then the provider (google / github / twitter / discord): -* Sign in with a browser opens your browser and waits for the callback on +* **Sign in with a browser** opens your browser and waits for the callback on `127.0.0.1:`. `Esc` goes back. -* Sign in with a code shows the URL to open on any device and a field for - the code that page produces. `Ctrl-O` tries to open the URL here anyway (best +* **Sign in with a code** shows the URL to open on any device and a field for the + code that page produces. `Ctrl-O` tries to open the URL here anyway (best effort, since the point of this flow is that there may be nothing to open), `Enter` submits, `Esc` goes back. -* Paste an API key takes a JWT or key you already hold. +* **Paste an API key** takes a JWT or key you already hold. A submitted value is classified by shape: 64 lowercase hex is redeemed via `/auth/login-token/consume`, anything else is treated as a ready-made JWT. A rejected value leaves you on the same screen, with the verification URL still up so you can fetch a fresh code. -On a token from any path the TUI verifies it via `/auth/me`, flashes who you -are, and hands it to the core, which validates it once more before storing it. -The app then starts on the embedded core, with no restart. A core that cannot -reach Medulla at all (no backend URL, or the surface compiled out) stops with -that error instead of opening a login screen that could not fix it. +On a token from any path the TUI verifies it via `/auth/me`, flashes who you are, +stores it, and continues into the app with no restart. + +Signing in again from inside a running app is decided by which account the new +token belongs to: + +* **The same account** — the session is stored and you carry on. +* **A different account** — the marker moves and that account's config is seeded, + but no session is stored for it: `Signed in as a different account. Restart + medulla to finish signing in as them.` A running process has the previous + account's home, logs and workers open; adopting a new identity underneath that + is not something a restart-free path can honestly promise. +* **Refused** — the backend named no account, named an unusable one, or + `MEDULLA_USER` pins this process elsewhere. ## Security model @@ -114,8 +232,8 @@ that error instead of opening a login screen that could not fix it. The code is a one-time, 15-minute `type: login` session token bound to the account that just authenticated. It is not a JWT. It is redeemed exactly once (`/auth/login-token/consume` deletes it atomically), and the long-lived app -session is issued to whoever redeems it. A code read off a shared screen or -left in a clipboard therefore buys an attacker one race against you, within 15 +session is issued to whoever redeems it. A code read off a shared screen or left +in a clipboard therefore buys an attacker one race against you, within 15 minutes, rather than a standing credential, and a code that has already been pasted is inert. @@ -127,14 +245,21 @@ apply and are not needed; no local socket is involved in this flow at all. The loopback listener hardens the callback against a hostile page sharing the same `127.0.0.1` origin: -* A random 32-hex state nonce is appended to the `redirectUri` before it - reaches the backend, and the listener rejects any `/auth` callback whose `state` - is missing or mismatched (HTTP 400) while continuing to wait. +* A random 32-hex state nonce is appended to the `redirectUri` before it reaches + the backend, and the listener rejects any `/auth` callback whose `state` is + missing or mismatched (HTTP 400) while continuing to wait. * It drops non-loopback peers, replies 405 to non-GET and 404 to non-`/auth` requests, and bounds each connection with a 5s read timeout and an 8 KiB buffer. -The session is written by the core, into its own credential store (the OS -keychain where one is available). Never commit tokens or `.env`; prefer -`MEDULLA_TOKEN` and documented environment variables over inline credentials in -committed config. +### Credentials and agent turns + +An agent turn that runs a shell command never inherits Medulla's environment. The +child is spawned from a cleared environment with a scrubbed set explicitly +re-added: `MEDULLA_HOME` and `MEDULLA_USER` are always dropped, along with every +variable whose name contains `TOKEN`, `KEY`, `SECRET`, `PASSWORD`, `PASSWD`, +`CREDENTIAL`, `AUTH` or `SESSION`. See +[Environment Variables](environment-variables.md#what-an-agent-turn-cannot-see). + +Never commit tokens or `.env` files; prefer `MEDULLA_TOKEN` and the documented +environment variables over inline credentials in committed config. diff --git a/gitbooks/developers/cli-reference.md b/gitbooks/developers/cli-reference.md index 5a688237..99d4faff 100644 --- a/gitbooks/developers/cli-reference.md +++ b/gitbooks/developers/cli-reference.md @@ -13,6 +13,7 @@ the host link, and self-updating. | `medulla codex` / `claude` / `opencode` | [Harness wrappers](#harness-wrappers): run a CLI, bridged to your orchestrator. | | `medulla sessions` | List recent claude/codex sessions as JSON. | | `medulla workflow ` | [Workflows](#medulla-workflow): author, inspect, and run multi-step plans. | +| `medulla skills ` | [Harness skills](#medulla-skills): put harness-native skills that trigger saved workflows on disk (`list` / `install` / `sync` / `uninstall`). | | `medulla init [dir]` | [Draft a MEDULLA.md](#medulla-init) workspace profile. | | `medulla workspace ` | [Workspace registry](#medulla-workspace): `add [dir]` / `list` / `remove `. | | `medulla hub` | [Relay hosted-backend tasks](#medulla-hub) to configured host-link workers. | @@ -20,16 +21,19 @@ the host link, and self-updating. | `medulla version` / `help` | Version string; usage. | Unknown first arguments are treated as arguments to the main TUI rather than -rejected as unknown subcommands. +rejected as unknown subcommands. Two further subcommands exist but are not for +a human to type: `medulla mcp` serves Medulla's own tools over MCP on +stdin/stdout, and `medulla hook ` is the shim Medulla installs as every +launched harness's own lifecycle hook. Medulla spawns both itself. ## The TUI A [ratatui](https://ratatui.rs/) terminal UI over the SDK: chat with the -orchestrator and watch agent lanes, traces, and context live. It runs on an -OpenHuman core embedded in the same process, so there is no server to start and -no socket to attach to. See [Runtimes](configuration.md#runtimes) for what happens -when nobody is signed in, and -[Upgrading from the external core socket](configuration.md#upgrading-from-the-external-core-socket) +orchestrator and watch agent lanes, traces, and context live. It drives the +Medulla backend directly over the network — there is no embedded core process +to boot and no socket to attach to. See [Runtimes](configuration.md#runtimes) +for what happens when nobody is signed in, and +[The retired `--core-socket` flag](configuration.md#the-retired---core-socket-flag) if you are coming from a version that used `--core-socket`. TUI flags: @@ -45,11 +49,35 @@ Feedback, and Settings. Workflows is present only in a build with the default `workflows` feature. See [The TUI](the-tui.md#the-tabs) for what each one holds and for the surfaces that are not in the tab bar of this build. +### Slash commands + +Typed into the composer, `/` opens a peek of matching commands as you type; an +exact match keeps showing its description until you press Enter. + +| Command | Description | +| --- | --- | +| `/new` | Open a new thread beside this one | +| `/resume` | Pick up an earlier saved session | +| `/session [harness] [path]` (alias `/harness`) | Start a local session | +| `/abort` | Stop the running cycle | +| `/clear` | Reset the view (history is kept) | +| `/copy [all\|last]` | Copy the transcript to the clipboard | +| `/usage` | Show account token usage | +| `/settings` (alias `/theme`) | Open the appearance settings | +| `/config` | Show the loaded configuration | +| `/feedback` (alias `/fb`) | Open the feedback board | +| `/mouse` | Toggle mouse capture (off to select text) | +| `/help` | List the commands and keys | +| `/quit` (aliases `/exit`, `/q`) | Exit medulla | + ## `medulla run` A headless, scriptable path to a single instruction, and the one to drive from CI -or a container, since it needs no TTY. It boots the same embedded core the TUI -uses, submits one instruction, and streams the folded cycle events to stdout as +or a container, since it needs no TTY. It resolves the same layered config the +TUI does, builds a client against the configured backend with the same token +precedence every backend-facing surface shares (an inline `backend.token`, then +`backend.tokenEnv`, then the stored `medulla login` session), submits one +instruction, and streams the folded cycle events to stdout as newline-delimited JSON: ```sh @@ -59,9 +87,15 @@ medulla run --config ./medulla.toml "..." Everything that is not a flag is joined into one instruction; with no instruction it errors. It emits each folded event as a JSON line and returns when the cycle -ends. It binds the core's state directory, action directory, and endpoints from -the resolved config before booting, so a scripted run pointed at `MEDULLA_HOME` -reads and writes that home rather than the developer's real one. +ends. Because the resolved config and the stored login session both come out of +the operator's Medulla home, a scripted run pointed at `MEDULLA_HOME` reads and +writes that home rather than the developer's real one. It fails immediately when +there is nothing to sign in with (`not signed in — run medulla login, or set +MEDULLA_TOKEN`). + +`--core-socket` used to point this at an external `medulla-serve` process; it is +gone, and passing it is a hard error rather than being folded into the +instruction text. | Flag | Effect | | --- | --- | @@ -101,14 +135,11 @@ Daemon flags: | `--providers ` | Restrict the accepted harnesses (default: all found on `PATH`). | | `--default-provider ` | Choose the default harness among those available. | | `--workspace ` | Set the primary task working directory (default: cwd). | -| `--handle ` | Register an `@handle` on startup. | | `--name