Metadata-Version: 2.5
Name: opensaddle
Version: 1.2.0rc5
Summary: Local-first control plane for routing work across coding agents.
Project-URL: Homepage, https://github.com/AkeBoss-tech/opensaddle
Project-URL: Repository, https://github.com/AkeBoss-tech/opensaddle
Project-URL: Issues, https://github.com/AkeBoss-tech/opensaddle/issues
Project-URL: Changelog, https://github.com/AkeBoss-tech/opensaddle/blob/main/CHANGELOG.md
Author: OpenSaddle contributors
License-Expression: MIT
License-File: LICENSE
Keywords: agents,automation,cli,llm,routing
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Build Tools
Classifier: Topic :: Software Development :: Testing
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: cryptography<51,>=50
Requires-Dist: fastapi<1.0,>=0.110
Requires-Dist: httpx<1.0,>=0.27
Requires-Dist: jsonschema<5.0,>=4.23
Requires-Dist: pydantic<3.0,>=2.8
Requires-Dist: pyjwt[crypto]<3.0,>=2.10
Requires-Dist: pyyaml<7.0,>=6.0
Requires-Dist: rfc8785<0.2,>=0.1.4
Requires-Dist: typer<1.0,>=0.16
Requires-Dist: uvicorn<1.0,>=0.27
Provides-Extra: dev
Requires-Dist: jsonschema<5.0,>=4.23; extra == 'dev'
Requires-Dist: pytest<10.0,>=8.3; extra == 'dev'
Provides-Extra: krail
Requires-Dist: krail<2.0,>=1.2.0rc2; extra == 'krail'
Description-Content-Type: text/markdown

# opensaddle

`opensaddle` is a local-first control plane for routing work across coding agents, models, councils, auditors, and verification loops.

It ships with a Typer CLI, a SQLite episode store, benchmark tooling, history import helpers, and a local dashboard.

The desktop service now exposes granular authoritative domains for threads and
messages, durable run lifecycle/output, discovered harness capabilities,
project files and artifacts, wiki jobs, workflows, Git collaboration, and
permission-aware search. New UI work should use these APIs instead of adding
state to a broad `GET/PUT /api/workspace` document. See
[`specs/12-authoritative-desktop-domain-api.md`](specs/12-authoritative-desktop-domain-api.md).

V1.1 supports optional declarative governance profiles for route planning and decision receipts. The experimental local daemon additionally provides an opt-in, OpenAI Responses-compatible gateway that can enforce model-call policy before forwarding to one configured upstream. It does not provide a general network proxy, sandboxing, IAM, OAuth, or broad credential isolation.

Permission policies default to `mode: observe`, which logs violations without stopping the run. You can also opt into `mode: enforce` for local policy enforcement that fails a run after diff capture when observed command or path rules are violated. This is not OS sandboxing or container isolation: the agent subprocess still runs locally, and enforcement happens from captured local evidence after execution.

## V1 Boundary And Safety Posture

V1 is a single-user, local developer tool. It runs user-configured subprocesses against local repositories and stores run data in local SQLite/JSON files. Only run commands and benchmark suites you trust.

It does **not** provide sandboxing, credential or secret isolation, network isolation, enterprise IAM, a hosted or multi-user service, or a corporate tool broker. Local `mode: enforce` is post-run evidence-based policy handling, not a preventative security control.

The separate experimental outbound container worker supports one deliberately
bounded runtime class; it does not change the general V1 boundary above. See
[`docs/container-worker.md`](docs/container-worker.md) for its fixed-entrypoint,
no-network contract and remaining limitations.

## Experimental OpenAI Responses Gateway

The local daemon can serve `POST /openai/v1/responses` for Codex-compatible model traffic and `POST /anthropic/v1/messages` for Claude Code-compatible traffic. It authenticates callers with the daemon token, evaluates `model.infer` against the explicit runtime policy, then streams the request to configured protocol-compatible upstreams. The upstream keys remain in the daemon process; only request/response digests and model-call metadata are recorded locally.

Gateway callers must send `X-OpenSaddle-Project`. When a policy returns `approval_required`, the response includes an opaque run ID and action digest. A local daemon administrator can create a short-lived, single-use approval at `POST /api/v1/runs/{run_id}/approvals`, and the client retries the byte-identical request with `X-OpenSaddle-Run-ID` and `X-OpenSaddle-Approval-ID`. Production approval identity and leases belong in the v2 control plane; the local endpoint is an interim single-user capability.

For a production gateway, the v2 control plane issues an opaque approval lease after authenticated human approval. The gateway submits the lease to `POST /api/v2/approvals/{approval_id}/consume` with the run ID, canonical request digest, policy hash, and authenticated principal. Consumption is atomic and one-use; the gateway stores no lease state or prompt content. Configure `ControlPlaneApprovalLeaseClient` with a per-principal trusted-proxy/OIDC assertion source—never a shared static user header—and send the returned lease ID plus `X-OpenSaddle-Policy-Hash` on the replay.

Start it with an explicit policy that allows the selected `provider/model` resource:

```bash
export OPENAI_API_KEY='your-upstream-key'
uv run opensaddle serve-daemon \
  --runtime-policy .opensaddle/runtime-policy.json \
  --gateway-upstream https://api.openai.com/v1 \
  --gateway-provider openai \
  --anthropic-gateway-upstream https://api.anthropic.com/v1 \
  --anthropic-gateway-provider anthropic
```

This is not an egress firewall. For a coding harness to be unable to bypass OpenSaddle, launch it inside an execution environment that blocks direct provider access. Cross-protocol model translation is not part of this initial gateway slice.

Use the launcher to configure a local coding harness without copying the daemon token into its command line:

```bash
uv run opensaddle gateway-run --harness codex --project project-1 -- codex exec --model gpt-5 "Inspect this repository"
uv run opensaddle gateway-run --harness claude --project project-1 -- claude
```

## Install

### User install

When the package is published, install the CLI for normal use with one of:

```bash
uv tool install opensaddle
# or
pip install opensaddle
```

### Local development install

For local development in this repository:

```bash
uv sync --dev
```

If you want an editable environment instead of a synced project environment:

```bash
uv pip install -e ".[dev]"
```

## Dev And Test

```bash
uv run opensaddle --help
uv run pytest
uv build
```

## Manual Smoke Checklist

These commands mirror the automated CLI smoke suite. The direct run uses inline Python, and the configured-route run uses a temporary local-only config so the checklist never depends on paid agent CLIs being installed.

```bash
uv run opensaddle init --root .
uv run opensaddle config validate --config ./opensaddle.yaml
uv run opensaddle estimate --repo . --config ./opensaddle.yaml --task "Fix a small UI bug in the settings panel"
cat > ./opensaddle.smoke.yaml <<'YAML'
project:
  name: smoke
  default_mode: balanced
  storage:
    backend: sqlite
    path: .opensaddle/smoke.db
providers:
  openrouter:
    enabled: false
agents:
  smoke_agent:
    adapter: cli
    command: python
    modes:
      exec:
        argv:
          - python
          - -c
          - "from pathlib import Path; Path('SMOKE.txt').write_text('configured\\n')"
    models:
      - id: smoke-model
        success_prior: strong
success_priors:
  strong:
    p_success: 0.95
    expected_minutes: 3
    uncertainty: 0.1
permissions:
  observe_all:
    mode: observe
    allowed_paths: ["**"]
verification:
  pass:
    checks:
      - name: smoke
        command: "python -c \"raise SystemExit(0)\""
routes:
  smoke_route:
    kind: single_agent
    agent: smoke_agent
    model: smoke-model
    permissions: observe_all
    verification: pass
    artifact_target: patch
YAML
uv run opensaddle run --repo . --instruction "Update README.md with a smoke note" --agent-arg python --agent-arg -c --agent-arg "from pathlib import Path; Path('SMOKE.txt').write_text('ok\\n')" --verify "smoke=python -c \"raise SystemExit(0)\""
uv run opensaddle config validate --config ./opensaddle.smoke.yaml
uv run opensaddle run --repo . --config ./opensaddle.smoke.yaml --instruction "Run the configured smoke route"
uv run opensaddle episodes list --db ./.opensaddle/smoke.db --json
uv run opensaddle routes stats --db ./.opensaddle/smoke.db --json
uv run opensaddle bench run ./examples/benchmark-suite.yaml --db ./.opensaddle/smoke.db --json
uv run opensaddle dashboard --root . --config ./opensaddle.smoke.yaml
```

## Quickstart

The repository includes ready-to-use examples in [`examples/`](examples/):

- [`examples/opensaddle.yaml`](examples/opensaddle.yaml)
- [`examples/benchmark-suite.yaml`](examples/benchmark-suite.yaml)

### Discover existing Codex and Claude Code projects

OpenSaddle can build a project-picker candidate list from local coding-agent
metadata without modifying Codex or Claude Code state:

```bash
uv run opensaddle discover-projects
uv run opensaddle discover-projects --source codex --json
```

Discovery reads only working-directory, session-count, and timestamp metadata.
It does not return or store prompts, replies, tool output, or credentials.
Missing directories and linked Git worktrees are hidden by default; use
`--include-missing` or `--include-worktrees` to show them.

The desktop API exposes the same read-only scan at
`GET /api/project-candidates`. Registering a selected root is a separate,
explicit `POST /api/projects`; that operation writes only OpenSaddle state.

### Continue project sessions and attach KRAIL knowledge

After a folder is registered, OpenSaddle Desktop exposes a **Project
sessions** resource. Its backend contract is:

```text
GET /api/projects/{project_id}/sessions
GET /api/projects/{project_id}/knowledge
```

The sessions route returns only native harness, session ID, working directory,
branch, version, and modification-time metadata for Codex or Claude Code
sessions inside that registered root. The user then explicitly chooses
**Resume** or **Fork**, plus whether future work remains source-managed,
OpenSaddle-managed, or hybrid. Transcript bodies are not copied into the
project picker or OpenSaddle workspace.

The knowledge route follows the repo-backed model from
[AkeBoss-tech/knowledge](https://github.com/AkeBoss-tech/knowledge), now named
KRAIL. When `rail.yaml` or `krail.yaml` already exists, OpenSaddle safely
projects its project identity, knowledge folders, stable capability families,
and the `rail-mcp --local --path …` launch specification. Discovery does not
import KRAIL, run `doctor` or integrity repair, initialize folders, or execute
workflows. Effectful KRAIL tools and `krail-flow/v1` workflows remain explicit
future actions behind the selected project policy.

External provider interoperability is separately defined by the versioned,
read-only [`krail.provider.v1`](contracts/krail.provider.v1/README.md)
contract. It uses issuer-qualified `ResourceRef` values and source-version-bound
`EvidencePacket` values; it does not turn desktop discovery into a KRAIL
runtime or a second knowledge store.

1. Initialize a new project layout:

```bash
uv run opensaddle init --root .
```

The generated `opensaddle.yaml` now includes built-in CLI presets for `codex`, `claude_code`, `github_copilot`, `aider`, `cursor`, `amp`, `antigravity`, `claw`, `hermes`, and `pi`, while still using the same generic `adapter: cli` execution path.

Config compatibility in v1 stays intentionally lightweight: additive fields may land without migrations, but breaking config-shape changes will require an explicit new config version plus documented migration notes. `opensaddle` does not silently rewrite user configs.

2. Estimate a task against the sample config:

```bash
uv run opensaddle estimate \
  --repo . \
  --config examples/opensaddle.yaml \
  --task "Improve the README command examples"
```

3. Run an agent command inside a worktree:

```bash
uv run opensaddle run \
  --repo . \
  --instruction "Add a short status note to README.md" \
  --agent-arg codex \
  --agent-arg exec \
  --agent-arg=--model \
  --agent-arg gpt-5 \
  --agent-arg "{instruction}" \
  --verify "test=pytest"
```

For interactive or TUI-first CLIs, you can switch the direct runner onto a pseudo-terminal:

```bash
uv run opensaddle run \
  --repo . \
  --instruction "Inspect the repo and propose a plan" \
  --execution-mode pty \
  --agent-arg codex \
  --agent-arg "{instruction}"
```

Configured agents can also opt into PTY mode with `modes.pty.enabled: true`, an optional `modes.pty.argv`, and terminal settings such as `rows`, `columns`, and `term`. When `modes.pty.argv` is omitted, PTY mode falls back to the configured `modes.exec.argv`.

If you want to copy a built-in preset into another config, the example file shows practical command templates for Codex CLI, Claude Code, GitHub Copilot CLI, Aider, Cursor, Amp, Antigravity, OpenClaw, Hermes, and Pi under `agents:`.

Configured routes may also define ordered `fallbacks:`. These are route-level retries, not provider-request retries: if a route times out, exits non-zero, fails verification, trips enforced policy, or returns a non-pass audit status, `opensaddle run` can retry the task on the next configured route in the chain.

You can also export the current config schema for documentation or editor integration:

```bash
uv run opensaddle config schema > opensaddle.schema.json
```

4. Run a benchmark suite:

```bash
uv run opensaddle bench run examples/benchmark-suite.yaml --db .opensaddle/opensaddle.db
```

The checked-in suite exercises both deterministic mock output and a configured local route. Configured routes use the normal local worktree execution path and evaluate their declared `success_checks`. The legacy `existing` route form instead runs its explicit argv directly in its configured working directory and reports command exit status; keep those commands non-destructive or use a disposable repository.

5. Inspect local route outcome stats:

```bash
uv run opensaddle routes stats --db .opensaddle/opensaddle.db
```

6. Sync and inspect model metadata if you have an OpenRouter API key:

```bash
uv run opensaddle models sync openrouter --config examples/opensaddle.yaml
uv run opensaddle models list --config examples/opensaddle.yaml
uv run opensaddle models health set --config examples/opensaddle.yaml --model openai/gpt-4o --status degraded --reason "local retry spikes"
uv run opensaddle models health list --config examples/opensaddle.yaml
```

`opensaddle estimate` now carries those local provider/model health signals into route scoring as conservative penalties with explicit rationale lines, but it does not block runtime execution.

7. Inspect prior agent history as dry-run proposals:

```bash
uv run opensaddle import-history codex --source ~/.codex
uv run opensaddle import-history claude --source ~/.claude
uv run opensaddle import-history copilot --source ~/.github/copilot
```

`opensaddle import-history` is proposal-only in v1. It scans local transcript files, redacts secret-like values before emitting summaries, and prints structured hints such as observed agents, task patterns, estimated usage, and candidate route suggestions. It does not rewrite `opensaddle.yaml`, and it does not persist raw transcript contents into SQLite by default.

8. Launch the dashboard:

```bash
uv run opensaddle dashboard --root .
```

When `.opensaddle/opensaddle.db` exists, the dashboard reads episode data from the SQLite `EpisodeStore`. If the database is absent, it falls back to JSON snapshots under `.opensaddle/episodes/`.
The dashboard also surfaces lightweight environment and audit review signals through the HTML view and `/api/reviews`.
The V1 operational view answers five questions directly: what ran, what failed, which routes perform best, which failures are recurring, and what route to try next. Route ops visibility now includes per-route aggregates, task-family routing stats, route comparison, recurring failures, benchmark summaries, and rolled-up verification/audit pass rates. Health is derived from local outcomes, and fallback-specific fields stay `n/a` until those slices are recorded in stored episode schemas.
The dashboard API is local-first and intentionally has no authentication when bound to localhost. Do not expose it on a hosted or multi-user interface in V1.

## Auditor Visibility

Auditors only receive the packet inputs explicitly granted by `can_see`, minus anything removed by `cannot_see`.

- `trajectory` includes only `auditor_visible` events.
- `internal_only` events are never forwarded to auditors.
- visible command output, diff patches, and trajectory text are redacted for secret-like values and bounded before they are persisted or sent to auditors.
- auditors do not receive full repo contents or hidden execution state unless you explicitly model and expose those inputs in a future slice.

## What It Does

`opensaddle` sits above agent runtimes and chooses, runs, observes, verifies, audits, and learns from execution strategies.

The main conceptual object is `AgentRouter`.

An `AgentRouter` chooses a route such as:

```text
agent runtime + model + context strategy + permission policy + audit policy + verification plan + fallback policy
```

`ModelRouter` is a lower-level primitive used inside routes, councils, and adapters when a model choice is needed.

## First Target

V1 is local-first:

- Python package named `opensaddle`
- CLI named `opensaddle`
- SQLite episode store
- YAML configuration
- local git worktree execution
- observe-only and local policy enforcement modes
- configurable CLI adapters for Codex, Claude Code, GitHub Copilot, Aider, Cursor, Amp, Antigravity, OpenClaw, Hermes, Pi, and other agent CLIs
- built-in YAML presets for those CLIs on top of the generic CLI adapter
- optional OpenRouter model registry integration
- lightweight local web dashboard

## V1 Support Matrix

| Surface | V1 status | Notes |
| --- | --- | --- |
| `opensaddle` CLI | supported | Stable command groups: `init`, `config validate`, `estimate`, `run`, `episodes`, `routes stats`, `bench run`, `models sync/list/health`, `dashboard`, and `import-history`. |
| `opensaddle.yaml` | supported | Strict user-owned YAML schema with stable top-level sections and route forms documented in [`specs/08-v1-public-contract.md`](specs/08-v1-public-contract.md). |
| Route kinds for `opensaddle run` | supported | `single_agent`, `agent_with_auditors`, and `review_council`. |
| Additional route kinds | planned/experimental | `diverse_generation`, `diverse_generation_council`, `leader_veto_council`, `loop_council`, and `planner_worker_verifier_loop` are recognized for config/planning but are not executable in v1. |
| Episode persistence | supported | Raw run JSON artifacts live under `.opensaddle/episodes/`; summarized episodes live in SQLite at `.opensaddle/opensaddle.db`. |
| Python imports | provisional | No stable Python API is promised for v1 beyond `opensaddle.__version__`; prefer the CLI, YAML, and storage formats as the public contract. |
| Explicitly post-v1 | not included yet | Container sandboxing, network/secret isolation, learned routing, and full execution for non-`review_council` council families. |

## Specs

- [`specs/00-product-brief.md`](specs/00-product-brief.md)
- [`specs/01-architecture.md`](specs/01-architecture.md)
- [`specs/02-configuration.md`](specs/02-configuration.md)
- [`specs/03-execution-and-audit.md`](specs/03-execution-and-audit.md)
- [`specs/04-benchmarking.md`](specs/04-benchmarking.md)
- [`specs/05-dashboard.md`](specs/05-dashboard.md)
- [`specs/06-implementation-slices.md`](specs/06-implementation-slices.md)
- [`specs/08-v1-public-contract.md`](specs/08-v1-public-contract.md)
- [`specs/10-v2-enterprise-pilot-adr.md`](specs/10-v2-enterprise-pilot-adr.md)
- [`specs/99-open-questions.md`](specs/99-open-questions.md)
