Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
version: 2
updates:
- package-ecosystem: cargo
directory: /runtime
directory: /
schedule:
interval: weekly
open-pull-requests-limit: 5
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2
with:
workspaces: runtime
workspaces: .

# Ordered so a formatting nit cannot mask a test failure. The previous
# workflow put fmt first and failed fast, and the result was that clippy
Expand Down
File renamed without changes.
7 changes: 7 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# The repo is one crate today — the runtime in `runtime/` — but the workspace
# lives at the root so `cargo test --workspace`, `cargo fmt --all` and
# `cargo clippy --workspace` work from the top of a checkout instead of only
# inside the member directory.
[workspace]
members = ["runtime"]
resolver = "2"
12 changes: 7 additions & 5 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -30,20 +30,22 @@ RUN apt-get update \
&& rustup target add x86_64-unknown-linux-musl

WORKDIR /src
COPY runtime/Cargo.toml runtime/Cargo.lock ./
COPY Cargo.toml Cargo.lock ./
COPY runtime/Cargo.toml ./runtime/
# Prime the dependency layer against the manifests alone, so editing src/ does
# not re-download and rebuild the tree.
RUN mkdir -p src && echo 'fn main() {}' > src/main.rs && echo '' > src/lib.rs \
RUN mkdir -p runtime/src && echo 'fn main() {}' > runtime/src/main.rs \
&& echo '' > runtime/src/lib.rs \
&& cargo build --release --locked --target x86_64-unknown-linux-musl \
&& rm -rf src
&& rm -rf runtime/src

COPY runtime/src ./src
COPY runtime/src ./runtime/src
# The touch is load-bearing, not tidiness. Cargo decides freshness by mtime, COPY
# preserves the context's timestamps, and if those land older than the stub
# artifacts above then cargo declares the stub build fresh and the image ships a
# binary whose main() does nothing -- a failure that builds green and only shows
# up as a container that exits instantly.
RUN touch src/main.rs src/lib.rs \
RUN touch runtime/src/main.rs runtime/src/lib.rs \
&& cargo build --release --locked --target x86_64-unknown-linux-musl \
&& strip target/x86_64-unknown-linux-musl/release/openab-pty

Expand Down
12 changes: 7 additions & 5 deletions Dockerfile.nightly
Original file line number Diff line number Diff line change
Expand Up @@ -49,19 +49,21 @@ RUN apt-get update \
&& rustup target add "$(cat /rust-target)"

WORKDIR /src
COPY runtime/Cargo.toml runtime/Cargo.lock ./
RUN mkdir -p src && echo 'fn main() {}' > src/main.rs && echo '' > src/lib.rs \
COPY Cargo.toml Cargo.lock ./
COPY runtime/Cargo.toml ./runtime/
RUN mkdir -p runtime/src && echo 'fn main() {}' > runtime/src/main.rs \
&& echo '' > runtime/src/lib.rs \
&& cargo build --release --locked --target "$(cat /rust-target)" \
&& rm -rf src
&& rm -rf runtime/src

COPY runtime/src ./src
COPY runtime/src ./runtime/src
# The touch is load-bearing: cargo decides freshness by mtime and COPY preserves
# the context's timestamps, so without it the stub build above is declared fresh
# and the image ships a binary whose main() does nothing.
#
# The built artifact is copied to an arch-independent path so the final-stage
# COPY does not need to know the triple.
RUN touch src/main.rs src/lib.rs \
RUN touch runtime/src/main.rs runtime/src/lib.rs \
&& cargo build --release --locked --target "$(cat /rust-target)" \
&& strip "target/$(cat /rust-target)/release/openab-pty" \
&& cp "target/$(cat /rust-target)/release/openab-pty" /openab-pty
Expand Down
6 changes: 4 additions & 2 deletions docs/k8s-howto.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,10 @@ tailscale status | grep openab-pty # find the address
## 6. Lending a Mac to a session (optional)

With `PTY_TOOLS_LISTEN` set (the manifest sets `127.0.0.1:8091`), a Mac running
`oab-instance-mcp` can **dial in** and lend its tools to one session; the coding
CLI inside that session then finds them at the URL in `$OPENAB_TOOLS_MCP_URL`.
`oab-instance-mcp` can **dial in** and lend its tools to one session. The coding
CLI inside that session finds them at the URL in `$OPENAB_TOOLS_MCP_URL` — and
on a known variant (today: `kiro-cli`) the spawn already wrote the `computer`
server and its `allowedTools` trust, so no `mcp add` step exists at all.
The pod initiates nothing and stores only a hash. Design:
[reverse attach](https://github.com/openabdev/instance-mcp/blob/main/docs/adr/reverse-attach.md);
wire contract: §9 of [`../runtime/CLIENT-CONTRACT.md`](../runtime/CLIENT-CONTRACT.md).
Expand Down
71 changes: 59 additions & 12 deletions runtime/CLIENT-CONTRACT.md
Original file line number Diff line number Diff line change
Expand Up @@ -421,10 +421,55 @@ does not edit any CLI's config; whatever installs the CLI does that. Properties:

#### Wiring the URL into the coding CLI

The runtime sets the env vars; it does **not** edit the CLI's config. Whatever owns
the session's workspace does that.
The runtime sets the env vars **and** — for the CLI variants it knows — writes
their config at spawn too, so the session's agent starts with the `computer`
server present and its tools pre-trusted. A variant it does not know is left
entirely alone and still works through the env vars and the manual steps below.

**What the runtime writes (kiro-cli).** At every spawn, when `kiro-cli` is on the
child's PATH (or under the installer's `~/.local` layout) and a JS runtime exists
to run the bridge:

- `~/.local/share/openab-pty/computer-mcp-bridge.js` — a small stdio MCP
bridge the runtime owns and rewrites each spawn. Every stdin line is one
JSON-RPC message, POSTed to `$OPENAB_TOOLS_MCP_URL`; it handles a JSON or SSE
reply and echoes `Mcp-Session-Id`.
- `~/.kiro/settings/mcp.json` gains (or refreshes) exactly the `computer`
server — every other server and setting in the file is preserved:

**Preferred — one config for every session.** The file never changes, across
```json
{
"mcpServers": {
"computer": {
"command": "<kiro's bundled bun, else bun/node on PATH>",
"args": ["<home>/.local/share/openab-pty/computer-mcp-bridge.js"],
"env": { "OPENAB_TOOLS_MCP_URL": "${OPENAB_TOOLS_MCP_URL}" }
}
}
}
```

- every `~/.kiro/agents/*.json` gains `@computer/*` in `allowedTools` — the
served set under the platform-neutral alias — plus `@computer` in a
restrictive `tools` list, and an agent that opted out of mcp.json
(`includeMcpJson: false`) gets the `computer` entry merged into its own
`mcpServers`. Agent files are never *created*: a session with no agent file
keeps the manual trust path below, which is safer than writing one that
could shadow a built-in agent.

**Why a bridge and not a URL.** The workspace `mcp.json` is shared by every
session in the pod, so it cannot name one session's `/mcp/<session>/<key>`
URL — the key rotates at each spawn, and a hard-coded URL silently dials the
wrong session after a re-lend. The bridge form is session-independent: the
config forwards `$OPENAB_TOOLS_MCP_URL` through `env`, and each CLI process
reads its *own* value. Rotation therefore needs no rewrite — there is nothing
in the file to refresh.

If a spawn finds no `kiro-cli`, or no `bun`/`node` to run the bridge, nothing
is written and the manual path below applies. A malformed or foreign-shaped
config file is left byte-identical rather than replaced.

**Manual path — one config for every session.** The file never changes, across
sessions, restarts or re-lends, as long as the CLI expands environment variables in
HTTP headers. `kiro-cli` does (`${VAR}` in `headers`; it does **not** expand `url`):

Expand All @@ -442,8 +487,8 @@ HTTP headers. `kiro-cli` does (`${VAR}` in `headers`; it does **not** expand `ur
The port is fixed by `PTY_TOOLS_LISTEN`, so the URL is a constant. Each CLI process
expands `${OPENAB_TOOLS_MCP_TOKEN}` from its own session's environment.

**Per session — only when the config is private to one session.** For `kiro-cli`
(2.13+), inside the session shell:
**Manual path — per session, only when the config is private to one session.**
For `kiro-cli` (2.13+), inside the session shell:

```sh
kiro-cli mcp add --name computer --url "$OPENAB_TOOLS_MCP_URL" --scope global
Expand Down Expand Up @@ -484,12 +529,12 @@ kiro-cli mcp add --name computer --url "$OPENAB_TOOLS_MCP_URL" --scope global
Update `@mac/*` entries in an agent's `allowedTools` to `@computer/*` at the same
time; keeping both aliases duplicates every served tool.

**Caveat — the key rotates** (this is what the header form above avoids). The `<key>` in `$OPENAB_TOOLS_MCP_URL` is per session
*generation*: a restart-in-place, or tearing down and re-lending a Mac, mints a new
URL, and any config that hard-codes the old one (both `mcp.json` and the agent's
`allowedTools` server entry) must be updated. Re-run `mcp add`, or read
`$OPENAB_TOOLS_MCP_URL` again, after each re-lend. Injecting and refreshing this
automatically at session spawn is [tracked as #39](https://github.com/openabdev/openab-pty/issues/39); until then it is a documented manual step.
**Caveat — the key rotates** (this is what both session-independent forms
above avoid). The `<key>` in `$OPENAB_TOOLS_MCP_URL` is per session
*generation*: a restart-in-place, or tearing down and re-lending a Mac, mints
a new URL. A spawn writes the runtime-owned form again, so an injected
`computer` entry is never stale; a *hand-configured* one that hard-codes a
URL must be re-run or re-read after each re-lend.

### 9.4 Minimum viable lender

Expand All @@ -500,4 +545,6 @@ automatically at session spawn is [tracked as #39](https://github.com/openabdev/
other 4xxx stop.
5. Operator: `DELETE …/tools-attach` to withdraw; `POST` again before expiry to renew.

The CLI side is zero steps: the URL is already in its environment.
The CLI side is zero steps: on a known variant the `computer` server is
already configured and trusted; on any other, the URL is already in its
environment.
2 changes: 0 additions & 2 deletions runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,3 @@ libc = "0.2"

[dev-dependencies]
tokio-tungstenite = "0.30"

[workspace]
Loading