Skip to content

Commit aac17ad

Browse files
authored
Merge pull request #178 from CryptoLabInc/release/v0.4.1
release v0.4.1
2 parents 8cb8268 + dc8bc28 commit aac17ad

29 files changed

Lines changed: 1266 additions & 504 deletions

.github/workflows/pr-tests.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ name: PR Tests
44

55
on:
66
pull_request:
7-
branches: [main]
7+
branches: [main, "release/**"]
88
push:
9-
branches: [main]
9+
branches: [main, "release/**"]
1010

1111
permissions:
1212
contents: read

AGENT_INTEGRATION.md

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
# Agent Integration Guide
22

33
Rune works with all major AI agents via native MCP (Model Context Protocol)
4-
support. In v0.4 the MCP server is a single Go binary
5-
(`bin/rune-mcp`) that the host CLI auto-spawns over stdio — no Python
6-
runtime, no `pip install`, no manual `mcp add` for the supported CLIs.
4+
support. In v0.4 the MCP server is a single Go binary (`rune-mcp`) that the
5+
host CLI auto-spawns over stdio through the committed bash wrapper
6+
`bin/rune mcp-server` — no Python runtime, no `pip install`, no manual
7+
`mcp add` for the supported CLIs.
78

89
## Integration Principles
910

1011
### Cross-agent common (single source of truth)
11-
- The Go binary at `cmd/rune-mcp/` is the only MCP server entry point.
12-
Plugin / extension manifests point each CLI at the same binary.
13-
- Runtime preparation happens at install time (the binary is already
14-
built and shipped with the plugin tarball — see Task #30 for the
15-
release pipeline). Nothing needs to be (re)bootstrapped at session
16-
start.
12+
- The CLI entry point is `cmd/rune/` (the `rune` binary). Plugin /
13+
extension manifests point each CLI at the committed bash wrapper
14+
`bin/rune` invoked as `rune mcp-server`, which execs the downloaded
15+
`rune-mcp` MCP server.
16+
- Runtime preparation happens on the first MCP spawn, not at plugin
17+
install: the wrapper self-installs the `rune` CLI and downloads the
18+
pinned `rune-mcp` binary (per `.release-pins.yaml`) into `~/.rune/bin/`,
19+
then execs it — so the server comes online in the same session with no
20+
manual `/mcp` reconnect or restart.
1721

1822
### Agent-specific adapters (thin layer only)
1923
- Codex-only tasks: `codex mcp add/remove/list` registration flows
@@ -48,10 +52,12 @@ $ claude plugin install rune
4852
> /plugin install rune
4953
```
5054

51-
The plugin manifest (`.claude-plugin/plugin.json`) declares the binary
52-
path; Claude Code spawns `${CLAUDE_PLUGIN_ROOT}/bin/rune-mcp` via stdio
53-
on session start. enVector Cloud credentials are delivered automatically
54-
via the Vault bundle — you never set `ENVECTOR_*` env vars directly.
55+
The plugin manifest (`.claude-plugin/plugin.json`) declares the wrapper
56+
path; Claude Code spawns `${CLAUDE_PLUGIN_ROOT}/bin/rune mcp-server` via
57+
stdio on session start (on a fresh install the wrapper self-installs
58+
rune-mcp first, then execs it). enVector Cloud credentials are delivered
59+
automatically via the Vault bundle — you never set `ENVECTOR_*` env vars
60+
directly.
5561

5662
### Configure credentials
5763

SKILL.md

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -109,12 +109,15 @@ If in Active state but operations fail:
109109

110110
Note: enVector credentials are delivered automatically via the Vault bundle — no user input needed.
111111

112-
4. Create `~/.rune/config.json` with `state: "active"` and the values
113-
above (`mkdir -p ~/.rune && chmod 700 ~/.rune`, then `chmod 600` the
114-
file).
115-
5. Call the `reload_pipelines` MCP tool. The MCP server's boot loop
116-
dials Vault, fetches the agent manifest (EncKey + envector
117-
creds), connects to enVector, and transitions to Active.
112+
4. Call the `configure` MCP tool with the collected values
113+
(`endpoint`, `token`, `ca_cert_path`, `tls_disable`). The server does
114+
the atomic 0600 write to `~/.rune/config.json`, sets `state: "active"`,
115+
refreshes `metadata.lastUpdated`, and runs a best-effort Vault probe.
116+
The agent never writes the config file itself.
117+
5. Call the `activate` MCP tool to bring pipelines online. It runs the
118+
prereq checks server-side and drives the boot loop: dials Vault,
119+
fetches the agent manifest (EncKey + enVector creds), connects to
120+
enVector, and transitions to Active.
118121
6. Confirm health by calling `diagnostics` and applying the
119122
**Boot Failure — Fast-Fail Rule** (see section below). If
120123
`vault.last_boot_error` is present, surface its `hint` verbatim
@@ -195,28 +198,37 @@ Recommendations:
195198

196199
**Note**: In most cases, simply asking naturally ("Why did we choose PostgreSQL?") triggers Retriever automatically — no command needed.
197200

198-
### `/rune:activate` (or `/rune:wakeup`)
201+
### `/rune:activate`
199202
(or `$rune activate` for Codex CLI)
200203

201204
**Purpose**: Attempt to activate plugin after infrastructure is ready
202205

203206
**Use Case**: Infrastructure was not ready during configure, but now it's deployed and running.
204207

205208
**Steps**:
206-
1. Check if config exists
207-
- NO → Redirect to `/rune:configure` (or `$rune configure` for Codex CLI)
208-
- YES → Continue
209-
2. If `state` is already `"active"`, skip to step 4 (just verify health).
210-
3. If `state` is `"dormant"`, set it to `"active"` and clear any
211-
`dormant_reason` / `dormant_since` fields.
212-
4. Call the `reload_pipelines` MCP tool. From a terminal Dormant the
213-
boot loop is re-spawned; from Active it is a no-op.
214-
5. Call `diagnostics` and apply the **Boot Failure — Fast-Fail Rule**
215-
(section below).
216-
6. If `vault.last_boot_error` is present: surface its `hint` verbatim,
217-
suggest the matching recovery action, and stop. Do NOT loop on
218-
`reload_pipelines` or probe with shell tools — the classifier has
219-
already done that work. Otherwise render the per-subsystem snapshot.
209+
1. Call the `activate` MCP tool — no Read, no Edit, no manual state
210+
inspection. It runs the prereq checks server-side (config present,
211+
runed socket reachable + Health probe) and only triggers the boot
212+
loop when everything is ready. It returns a `status`:
213+
`configure_required` | `install_pending` | `waiting_for_bootstrap` |
214+
`active` | `waiting_for_vault` | `dormant`.
215+
2. Branch on `status`:
216+
- `configure_required` → redirect to `/rune:configure`; use the `hint`
217+
verbatim and stop.
218+
- `install_pending` → invoke the recovery in `hint` (the agent runs
219+
`rune install`, never the user), then retry `/rune:activate` once.
220+
- `waiting_for_bootstrap` → runed is still downloading llama-server /
221+
the embedding model; summarize `.bootstrap` progress, tell the user
222+
no further action is needed, and stop (do NOT poll).
223+
- `active` → optionally call `diagnostics` once and render the
224+
per-subsystem snapshot.
225+
- `waiting_for_vault` / `dormant` → apply the **Boot Failure —
226+
Fast-Fail Rule** (below): surface `reload.last_boot_error.hint`
227+
verbatim, suggest one recovery, and stop.
228+
229+
(Older rune-mcp binaries without the `activate` tool fall back to the
230+
legacy flow: set `state: "active"`, call `reload_pipelines` directly, and
231+
branch on `diagnostics.vault.last_boot_error`.)
220232

221233
### `/rune:reset`
222234
(or `$rune reset` for Codex CLI)

bin/rune

Lines changed: 132 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -44,30 +44,135 @@ mkdir -p "$RUNE_HOME"
4444
LOCK_DIR="$RUNE_HOME/bootstrap.lock.d"
4545
TMP=""
4646
SUMS=""
47+
OWNER_TOKEN=""
4748
cleanup() {
4849
[ -n "$TMP" ] && rm -f "$TMP"
4950
[ -n "$SUMS" ] && rm -f "$SUMS"
50-
rmdir "$LOCK_DIR" 2>/dev/null || true
51+
# Release lock after checking token is valid or not
52+
if [ -n "$OWNER_TOKEN" ] && [ "$(cat "$LOCK_DIR/owner" 2>/dev/null || true)" = "$OWNER_TOKEN" ]; then
53+
rm -f "$LOCK_DIR/owner" 2>/dev/null || true
54+
rmdir "$LOCK_DIR" 2>/dev/null || true
55+
fi
56+
}
57+
58+
# Network time budget
59+
# - `mcp-server`: MCP entrypoint run by Claude Code session with ~30s timeout.
60+
# SIGKILL after timeout skip cleanup which leave unreleased bootstrap lock;
61+
# so overall time should be less than 30s.
62+
# Worst case: API resolve up to 7s + binary download up to 13s + checksum up to 7s
63+
# - other: matched with each downloaded binaries' deadline
64+
if [ "${1:-}" = mcp-server ]; then
65+
NET_RETRY=3; NET_RETRY_DELAY=1; NET_RETRY_MAXTIME=3
66+
NET_API_MAXTIME=4; NET_BIN_MAXTIME=10; NET_CHECKSUM_MAXTIME=4
67+
else
68+
NET_RETRY=3; NET_RETRY_DELAY=2; NET_RETRY_MAXTIME=60
69+
NET_API_MAXTIME=20; NET_BIN_MAXTIME=120; NET_CHECKSUM_MAXTIME=30
70+
fi
71+
72+
# retries fast transient errors such as Github CDN failures (504, timeouts) only;
73+
# slow/hung requests are intentionally not retried to stay within the spawn budget.
74+
# Caller add NET_{API|BIN|CHECKSUM}_MAXTIME properly on each step
75+
fetch() {
76+
curl --fail --silent --show-error --location --connect-timeout 5 \
77+
--retry "$NET_RETRY" --retry-delay "$NET_RETRY_DELAY" \
78+
--retry-max-time "$NET_RETRY_MAXTIME" "$@"
79+
}
80+
81+
# Lock waiting budget to exit before Claude code MCP spawn timeout (~30s)
82+
LOCK_WAIT_BUDGET="${RUNE_LOCK_WAIT_BUDGET:-20}"
83+
# Lock's wall-clock age to prevent alive but stuck holder
84+
# Worst case: NET_RETRY_MAXTIME + NET_{API|BIN|CHECKSUM}_MAXTIME (about 350s) when !mcp-server
85+
LOCK_STALE_AFTER="${RUNE_LOCK_STALE_AFTER:-360}"
86+
87+
# Atomically take stale lock and remove it
88+
clear_stale_lock() {
89+
if mv "$LOCK_DIR" "$LOCK_DIR.reclaim.$$" 2>/dev/null; then
90+
rm -rf "$LOCK_DIR.reclaim.$$" 2>/dev/null || true
91+
fi
92+
return 0
5193
}
5294

5395
waited=0
54-
while ! mkdir "$LOCK_DIR" 2>/dev/null; do # another session hold lock
96+
wait_count=0
97+
while true; do
98+
# Claim lock atomically
99+
if mkdir "$LOCK_DIR" 2>/dev/null; then
100+
OWNER_TOKEN="$$ $(date +%s)" # "<pid> <timestamp>"
101+
if ( set -C; printf '%s\n' "$OWNER_TOKEN" > "$LOCK_DIR/owner" ) 2>/dev/null; then
102+
trap cleanup EXIT INT TERM
103+
# Double-check if mkdir -> write gap affect lock
104+
if [ "$(cat "$LOCK_DIR/owner" 2>/dev/null || true)" = "$OWNER_TOKEN" ]; then
105+
break
106+
fi
107+
trap - EXIT INT TERM
108+
OWNER_TOKEN=""
109+
continue
110+
fi
111+
112+
OWNER_TOKEN=""
113+
if [ ! -d "$LOCK_DIR" ]; then
114+
continue # lock is cleared, retry claim
115+
fi
116+
117+
# Real write error (disk full, permission, or others)
118+
if [ ! -e "$LOCK_DIR/owner" ]; then
119+
echo "rune: cannot record install bootstrap lock owner (file write failed)" >&2
120+
exit 1
121+
fi
122+
fi
123+
124+
# Wait for another process as we failed to claim lock
55125
if [ -x "$TARGET" ]; then
56126
exec "$TARGET" "$@" # bootstrap finished
57127
fi
58128

129+
# Validate owner
130+
owner="$(cat "$LOCK_DIR/owner" 2>/dev/null || true)"
131+
pid="${owner%% *}"
132+
case "$owner" in
133+
*" "*) ts="${owner##* }" ;;
134+
*) ts="" ;;
135+
esac
136+
137+
if [ -z "$owner" ]; then
138+
# Dir is created but no owner yet; holder in the middle of claim or died
139+
wait_count=$((wait_count + 1))
140+
if [ "$wait_count" -ge 5 ]; then
141+
echo "rune: bootstrap lock not claimed for ${wait_count}s; reclaiming" >&2
142+
clear_stale_lock; wait_count=0; continue
143+
fi
144+
else
145+
wait_count=0
146+
if [ -n "$pid" ] && ! kill -0 "$pid" 2>/dev/null; then
147+
# Holder process not found; lock is leaked
148+
echo "rune: bootstrap lock holder (pid $pid) is not found; reclaiming" >&2
149+
clear_stale_lock; continue
150+
fi
151+
152+
# Check wall-clock age
153+
case "$ts" in
154+
''|*[!0-9]*) age=0 ;;
155+
*) age=$(( $(date +%s) - ts )) ;;
156+
esac
157+
158+
if [ "$age" -ge "$LOCK_STALE_AFTER" ]; then
159+
echo "rune: bootstrap lock stale (${age}s); reclaiming" >&2
160+
clear_stale_lock; continue
161+
fi
162+
163+
if [ "$waited" -ge "$LOCK_WAIT_BUDGET" ]; then
164+
echo "rune: another rune bootstrap is in progress over MCP spawn budget." >&2
165+
echo " Retry in a moment, or run it out-of-band:" >&2
166+
echo " bash -c \"\${CLAUDE_PLUGIN_ROOT:-.}/bin/rune install\"" >&2
167+
exit 1
168+
fi
169+
fi
170+
59171
sleep 1
60172
waited=$((waited + 1))
61-
if [ "$waited" -ge 120 ]; then
62-
echo "rune: bootstrap lock held >120s, reclaiming" >&2
63-
rmdir "$LOCK_DIR" 2>/dev/null || true
64-
waited=0
65-
fi
66173
done
67174

68-
# Check error
69-
trap cleanup EXIT INT TERM
70-
175+
# Double-check: install is completed right before we won the lock
71176
if [ -x "$TARGET" ]; then
72177
cleanup
73178
trap - EXIT INT TERM
@@ -85,12 +190,9 @@ if [ -z "$RUNE_VERSION" ]; then
85190
# Use token if exist
86191
token="${GITHUB_TOKEN:-${GH_TOKEN:-}}"
87192
if [ -n "$token" ]; then
88-
body="$(curl --fail --silent --show-error --location --connect-timeout 10 --max-time 20 \
89-
--retry 3 --retry-delay 2 \
90-
--header "Authorization: Bearer $token" "$api" || true)"
193+
body="$(fetch --max-time "$NET_API_MAXTIME" --header "Authorization: Bearer $token" "$api" || true)"
91194
else
92-
body="$(curl --fail --silent --show-error --location --connect-timeout 10 --max-time 20 \
93-
--retry 3 --retry-delay 2 "$api" || true)"
195+
body="$(fetch --max-time "$NET_API_MAXTIME" "$api" || true)"
94196
fi
95197

96198
RUNE_VERSION="$(printf '%s' "$body" \
@@ -128,12 +230,22 @@ mkdir -p "$(dirname "$TARGET")"
128230
TMP="$(mktemp "$(dirname "$TARGET")/.rune-bootstrap-XXXXXX")"
129231
SUMS="$(mktemp -t rune-bootstrap-sums-XXXXXX)"
130232

131-
# --retry rides out transient GitHub CDN failures (504, timeouts) instead
132-
# of aborting the whole bootstrap on the first blip.
133-
curl --fail --silent --show-error --location --connect-timeout 10 --max-time 120 --retry 3 --retry-delay 2 "$RELEASE_BASE/$ASSET" -o "$TMP"
134-
curl --fail --silent --show-error --location --connect-timeout 10 --max-time 30 --retry 3 --retry-delay 2 "$RELEASE_BASE/checksums.txt" -o "$SUMS"
233+
if ! fetch --max-time "$NET_BIN_MAXTIME" "$RELEASE_BASE/$ASSET" -o "$TMP"; then
234+
echo "rune: could not download $ASSET ($RUNE_VERSION) after retries." >&2
235+
echo " The release endpoint may be slow or temporarily unavailable (e.g. HTTP 504)." >&2
236+
echo " Recover out-of-band, then reconnect /mcp:" >&2
237+
echo " bash -c \"\${CLAUDE_PLUGIN_ROOT:-.}/bin/rune install\"" >&2
238+
exit 1
239+
fi
240+
if ! fetch --max-time "$NET_CHECKSUM_MAXTIME" "$RELEASE_BASE/checksums.txt" -o "$SUMS"; then
241+
echo "rune: could not download checksums.txt ($RUNE_VERSION) after retries." >&2
242+
echo " The release endpoint may be slow or temporarily unavailable (e.g. HTTP 504)." >&2
243+
echo " Recover out-of-band, then reconnect /mcp:" >&2
244+
echo " bash -c \"\${CLAUDE_PLUGIN_ROOT:-.}/bin/rune install\"" >&2
245+
exit 1
246+
fi
135247

136-
EXPECTED="$(grep " $ASSET\$" "$SUMS" | cut -d' ' -f1)"
248+
EXPECTED="$(grep " $ASSET\$" "$SUMS" | cut -d' ' -f1 || true)"
137249
if [ -z "$EXPECTED" ]; then
138250
echo "rune: $ASSET not listed in checksums.txt for $RUNE_VERSION" >&2
139251
exit 1

cmd/rune/install.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"flag"
77
"fmt"
88
"io"
9+
"os"
910

1011
"github.com/CryptoLabInc/rune-cli/internal/bootstrap"
1112
)
@@ -20,8 +21,21 @@ func runInstall(ctx context.Context, args []string, stdout, stderr io.Writer) in
2021
return 2
2122
}
2223

24+
// Check RUNE_MANIFEST before fail
2325
if *manifest == "" {
24-
fmt.Fprintln(stderr, "rune install: no manifest URL configured (set --manifest-url or RUNE_MANIFEST)")
26+
if env := os.Getenv("RUNE_MANIFEST"); env != "" {
27+
*manifest = env
28+
}
29+
}
30+
31+
if *manifest == "" {
32+
const msg = "no manifest URL configured (set --manifest-url or RUNE_MANIFEST)"
33+
if *jsonOut {
34+
_ = json.NewEncoder(stdout).Encode(jsonEvent{Event: "summary", Error: msg})
35+
} else {
36+
fmt.Fprintln(stderr, "rune install: "+msg)
37+
}
38+
2539
return 2
2640
}
2741

0 commit comments

Comments
 (0)