Metadata-Version: 2.5
Name: conning
Version: 0.5.0
Summary: A control room for your agent's test machines: an MCP server and CLI that run commands on remote targets without the agent ever seeing a secret.
Project-URL: Homepage, https://github.com/EranYonai/Conning
Project-URL: Issues, https://github.com/EranYonai/Conning/issues
Author: Eran Yonai
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: agents,mcp,remote-execution,ssh,testing,vault
Classifier: Development Status :: 2 - Pre-Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Testing
Classifier: Topic :: System :: Systems Administration
Requires-Python: >=3.11
Requires-Dist: keyring>=25.7.0
Requires-Dist: mcp<3,>=2.3
Description-Content-Type: text/markdown

# Conning

<p align="center"><img src="docs/brand/handler-hero.svg" alt="Handler, the Conning mascot: a friendly cartoon glove working puppet strings on three lab VMs" width="360"></p>

**A control room for your agent's test machines.** Your agent (Claude Code, Codex, Cursor...) stays on
your own laptop. Conning holds the keys to your lab VMs and gives the agent four lean tools to run
commands and move files there. The agent never sees a password, and you approve anything risky.

> **Status: pre-alpha (M2 done).** Running commands, long jobs, remote file reads and transfers
> work over OpenSSH, with pinned host keys, password or key auth, approvals and leases. You can approve
> in the agent client, in `conn watch` or with `conn approve`; `conn doctor` checks your setup. Long
> jobs run detached on Linux, macOS and Windows targets, and `conn mcp install` sets up claude, codex,
> cursor, opencode or pi. See
> [docs/design.md](docs/design.md) for every decision so far.

## Why

Putting an agent harness inside every CLI on every remote machine is redundant. Developers already
have a capable agent on their own machine. What's missing is a safe, token-lean way for that agent to
reach the machines it tests on:

- **No secrets in context.** Targets hold *references* to the OS keychain, `op://` or ssh-agent.
  Secrets are resolved inside Conning at the last moment and never returned by any tool.
- **Lean results.** Exit code first, output capped at 8 KB by default (the agent may ask for more, up
  to a ceiling), with a handle to page the rest. Fan-out to many targets collapses identical results.
- **Guardrails in code, not in the prompt.** Each target has a mode (`observe`, `ask`, `lease`).
  Commands outside the allowlist wait for your approval; "always allow" remembers two words, so
  `systemctl status` never lets through `systemctl stop`.
- **Plain OpenSSH underneath.** Linux and Windows (OpenSSH + PowerShell 7), pinned host keys,
  forwarding forced off.

## Install

```sh
uv tool install conning        # or: pipx install conning
conn mcp install claude        # or codex, cursor, opencode, pi: adds the server and denies raw ssh/scp
```

The MCP server needs no install at all:

```sh
claude mcp add conning -- uvx conning mcp
```

### Or run the server in Docker

```sh
docker run -i --rm --network host \
  -v ~/.config/conning:/conning \
  -v "$SSH_AUTH_SOCK":/ssh-agent -e SSH_AUTH_SOCK=/ssh-agent \
  ghcr.io/eranyonai/conning:latest
```

The image carries Python, the OpenSSH client and Conning, and nothing else. Targets, pinned host
keys and job output live on the `/conning` volume, and `conn` runs as that folder's owner, so files it
writes there stay yours on the host. On Docker Desktop for Mac, forward the agent with
`-v /run/host-services/ssh-auth.sock:/ssh-agent` instead. Two things stay on your machine (D46):

- **Approving.** `conn add`, `conn approve`, `conn lease` and `conn allow` need your terminal. Install
  the CLI on the host and point it at the same folder: `CONNING_HOME=~/.config/conning conn approve a:1`.
  Approving in the agent client works from the container as usual.
- **Secrets.** The OS keychain is not reachable from a container, so use `env:` or `op://`
  references, or forward your ssh-agent as above and use a key. `docker run --rm ... conning doctor`
  checks the rest.

Targets on a lab LAN or VPN need `--network host` (Linux) or a route the container can use.

## The agent's four tools

| Tool | What it does |
|---|---|
| `targets(filter?, check?)` | One line per target: name, OS, shell, user, tags, mode. Never a host or secret. |
| `run(target, cmd \| argv, wait=30, max_bytes=8192, ...)` | Runs on one target, a list or `tag:<tag>`. Exit code first. Still running after `wait` seconds: returns a job id (`wait=0` is async). |
| `read(id, wait?, range?, grep?, signal?)` | Long-polls a job for new lines, pages a saved output, or reads a remote file. |
| `transfer(source, dest)` | Copies files between `local:<path>` and `<target>:<path>`, checked by sha256. |

Agents without MCP can use the same commands through the `conn` CLI; the output is identical.

## For agents: CLI reference

This block is generated by `conn help --markdown`, so you don't need to call `--help`. A test keeps it
in sync with the code.

<!-- conn-help:start -->
```
conn targets [--check] [filter]
    List targets: name, OS, shell, user, tags, mode. Never hosts or secrets.

conn run [--wait WAIT] [--timeout TIMEOUT] [--cwd CWD] [--shell]
                [--max-bytes MAX_BYTES] [--grep GREP]
                target [argv ...]
    Run a command on one target, a list, or tag:<tag>. Exit code first.

conn read [--wait WAIT] [--range RANGE] [--grep GREP] [--signal {INT,TERM,KILL}]
                 [--max-bytes MAX_BYTES]
                 id
    Page a job (new lines only), a saved output, an approval, or a remote file.

conn transfer source dest
    Copy files: local:<path> to <target>:<path>, or back. sha256 checked.

conn add [--host HOST] [--user USER] [--like TARGET] [--port PORT] [--jump TARGET]
                [--auth {ssh-agent,key,password}] [--secret SECRET] [--key KEY]
                [--os {linux,windows,darwin}] [--mode {observe,ask,lease}] [--tag TAG] [--retry]
                name
    Add a target. Without a terminal it is saved as pending until a human approves it.

conn approve [--always] what
    Human only: make a pending target live, or allow a pending command a:<id>.

conn import [--match MATCH]
    Import hosts from ~/.ssh/config as pending targets (unsafe options dropped).

conn rm name
    Remove a target and its pinned host key.

conn rename old new
    Human only: rename a target; its host keys, password, lease and pending work follow.

conn test name
    Probe a target: reachability, OS, shell, pinned host key.

conn lease [--minutes MINUTES] name
    Human only: give a target a full shell for a time box (1-60 minutes).

conn allow target [words ...]
    Human only: let a command run without asking (no words: list the rules).

conn deny target words [words ...]
    Remove an allow rule, so the command asks again.

conn watch [--history HISTORY] [--no-prompt]
    Live control room: every command on every target, job output, approval prompts.

conn doctor
    Check this machine's setup: config and state dirs, ssh, keychain, targets.

conn mcp [--approve-in-client | --no-approve-in-client] <serve|install> ...
    Run the MCP server on stdio, or install it into an agent client.

conn help [--markdown]
    Show this help; --markdown prints the block cached in README.md.
```
<!-- conn-help:end -->

A target is one login on one machine. To use the same machine as another user, add it again:
`conn add lab-tester --like lab --user tester` copies the host, port, OS and tags and asks for the new
user's password. `conn targets` shows which user each target uses.

A target behind a bastion names it as its jump host: add the bastion as a target first, then
`conn add lab --host 10.0.0.5 --user tester --jump gw`. Every hop gets its own login and its own pinned
host key, and `conn import` keeps a `ProxyJump` to another host it imports. The bastion is a target the
agent can see too, so give it `--mode observe` if the agent should only pass through it.

A target you add with `conn add` from an agent session is saved as **pending**. It does nothing until
the human runs `conn approve <name>`, which shows the host, the pinned host-key fingerprint and where
the secret comes from.

A command outside a target's allowlist returns `status=pending_approval id=a:<id>`, and a desktop
notification tells the human. They answer in one of three places (allow once, always allow its first
two words, or deny), and the agent picks up the result with `read a:<id>`:

- **In the agent client**: any MCP client that supports elicitation shows the question before the
  command is queued, and an approved command runs in the same call. Clients without it fall back to
  the two below. If a client would answer such prompts by itself instead of asking you, run the server
  as `conn mcp --no-approve-in-client`.
- **In `conn watch`**, which also shows every command and its output as it happens.
- **With `conn approve a:<id>`** in any terminal. When you run `conn run` yourself at a terminal, it
  just asks you there and prints the result.

Read-only commands such as `uname`, `hostname`, `df`, `ps`, `ls`, `ip addr`, `systemctl status` and
`journalctl` run without asking on every target; each is checked so its changing forms (`hostname x`,
`ip addr add`) still ask, and `safe = false` in a target's entry turns the list off.
`conn allow <target> make test` lets another command run without asking from then on; `conn deny`
takes it back. Every command, answer and rule change lands in a hash-chained audit log, and
`conn doctor` checks your setup when something doesn't work.

## Safety model

| Mode | What the agent can do | Default for |
|---|---|---|
| `observe` | The built-in read-only list and allowlisted commands only; everything else is refused. | targets tagged `shared` or `prod` |
| `ask` | The built-in read-only list and allowlisted commands run; anything else waits for your approval. | new lab targets |
| `lease` | Any command, for a time box you grant (15 min default, 60 max). | never by default |

- No tool returns a secret, adds a live target or changes policy.
- Secrets travel over stdin or the SSH channel's env, never argv. Output is redacted before it is saved.
- Remote output is marked untrusted. Shell wrappers (`sh -c`, `python -c`, `sudo`...) and shell syntax
  (`;`, `|`, `$(...)`) always ask.
- `conn mcp install` also denies the agent raw `ssh`/`scp`, so it can't go around Conning (pi has no
  permission rules, so there it only adds the server).
- The audit log is hash-chained. It runs as your user, so it is tamper-evident, not tamper-proof.

## Development

```sh
uv sync
uv run pytest --cov=conning      # CI requires 95% coverage on Linux
uv run ruff check && uv run ruff format --check
```

The end-to-end tests drive the real `conn` and MCP server against a throwaway local sshd (Linux, root
to start it; CI's `e2e` job does this on every push):

```sh
eval "$(sudo sh tests/e2e/start-sshd.sh /tmp/conning-sshd | sed 's/^/export /')"
uv run --with keyrings.alt pytest -m e2e   # keyrings.alt: a file keychain for typed passwords
```

After changing the CLI, refresh the README block with `uv run conn help --markdown`.

## Docs

- [docs/design.md](docs/design.md): the design and every decision, with its status.
- [docs/blog.html](docs/blog.html): the development blog, from the first idea to today.
- [docs/manual-test.md](docs/manual-test.md): a ten-minute check of every feature against a throwaway target.
- [docs/field-test.md](docs/field-test.md): using it for real work, and how to report what gets in the way.

## License

[Apache-2.0](LICENSE)
