Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
49ff7a2
feat: add Telegram and WhatsApp options to OpenClaw setup picker
AhmedTMM Mar 12, 2026
ba88dd4
fix(e2e): dynamically calculate DigitalOcean parallel capacity from a…
la14-1 Mar 12, 2026
3390f63
fix(e2e): add retry-with-backoff for DigitalOcean 422 droplet limit e…
la14-1 Mar 12, 2026
4c590a4
feat: run WhatsApp QR scan interactively before TUI launch
AhmedTMM Mar 12, 2026
a028a6f
fix: update WhatsApp hint to reflect pre-TUI QR scanning
AhmedTMM Mar 12, 2026
49ec76b
test: add cron-triggered Telegram reminder to soak test (#2519)
AhmedTMM Mar 12, 2026
b9db0f3
security: validate localPath in uploadFile() and harden runServer() i…
la14-1 Mar 12, 2026
82a09a7
test: remove conditional-expect anti-patterns from 3 test files (#2525)
la14-1 Mar 12, 2026
96b9a8d
security: validate base64 in digitalocean.sh SSH exec (defense-in-dep…
la14-1 Mar 12, 2026
f0d0ee0
security: validate base64 output in cloud_exec and soak.sh (defense-i…
la14-1 Mar 12, 2026
ae5667a
refactor: deduplicate generateCsrfState into shared/oauth.ts (#2530)
la14-1 Mar 12, 2026
875dea1
test: Remove duplicate and theatrical tests (#2531)
la14-1 Mar 12, 2026
b443d4c
security: harden shellQuote and consolidate shell escaping in gcp.ts …
la14-1 Mar 12, 2026
5677514
test: Remove duplicate and theatrical tests (#2534)
la14-1 Mar 12, 2026
6b0da91
security: consolidate shellQuote across all clouds (defense-in-depth)…
la14-1 Mar 12, 2026
0d47c21
fix(e2e): fix input test prompt delivery and agent flags (#2536)
la14-1 Mar 12, 2026
985181b
security: add DO_CLIENT_SECRET env var override (#2538)
la14-1 Mar 12, 2026
068d9ec
test: remove duplicate and theatrical tests (#2539)
la14-1 Mar 12, 2026
4c29475
fix: write openclaw config atomically to preserve gateway auth token
AhmedTMM Mar 12, 2026
b33687d
test: add OpenClaw config, messaging, and tunnel test coverage
AhmedTMM Mar 12, 2026
cc2a14f
fix: add junie to tarball build pipeline (#2541)
la14-1 Mar 12, 2026
c746111
feat: add --model flag and preferences file for LLM model override (#…
AhmedTMM Mar 12, 2026
7fc7063
fix: update Codex default model to gpt-5.3-codex and add agent model …
AhmedTMM Mar 12, 2026
b720c78
security: use shellQuote() in agent-setup.ts for consistent null-byte…
la14-1 Mar 12, 2026
8138799
feat(qa): telegram soak test on digitalocean + fix bun -e (#2547)
la14-1 Mar 12, 2026
f0ee18d
chore: standardize featured_cloud to digitalocean + sprite for all ag…
la14-1 Mar 12, 2026
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
23 changes: 23 additions & 0 deletions .claude/rules/agent-default-models.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Agent Default Models

**Source of truth for the default LLM each agent uses via OpenRouter.**
When updating an agent's default model, update BOTH the code and this file. This prevents regressions from stale model IDs.

Last verified: 2026-03-12

| Agent | Default Model | How It's Set |
|---|---|---|
| Claude Code | _(routed by Anthropic)_ | `ANTHROPIC_BASE_URL=https://openrouter.ai/api` — model selection handled by Claude's own routing |
| Codex CLI | `openai/gpt-5.3-codex` | Hardcoded in `setupCodexConfig()` → `~/.codex/config.toml` |
| OpenClaw | `openrouter/openrouter/auto` | `modelDefault` field in agent config; written to OpenClaw config via `setupOpenclawConfig()` |
| ZeroClaw | _(provider default)_ | `ZEROCLAW_PROVIDER=openrouter` — model selection handled by ZeroClaw's OpenRouter integration |
| OpenCode | _(provider default)_ | `OPENROUTER_API_KEY` env var — model selection handled by OpenCode natively |
| Kilo Code | _(provider default)_ | `KILO_PROVIDER_TYPE=openrouter` — model selection handled by Kilo Code natively |
| Hermes | _(provider default)_ | `OPENAI_BASE_URL=https://openrouter.ai/api/v1` + `OPENAI_API_KEY` — model selection handled by Hermes |
| Junie | _(provider default)_ | `JUNIE_OPENROUTER_API_KEY` — model selection handled by Junie natively |

## When to update

- When OpenRouter adds a newer version of a model (e.g., `gpt-5.1-codex` → `gpt-5.3-codex`)
- When an agent changes its default provider integration
- Verify the model ID exists on OpenRouter before committing: `curl -s https://openrouter.ai/api/v1/models | jq '.data[].id' | grep <model>`
6 changes: 3 additions & 3 deletions .claude/rules/shell-scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ macOS ships bash 3.2. All scripts MUST work on it:

## Use Bun + TypeScript for Inline Scripting — NEVER python/python3
When shell scripts need JSON processing, HTTP calls, crypto, or any non-trivial logic:
- **ALWAYS** use `bun eval '...'` or write a temp `.ts` file and `bun run` it
- **ALWAYS** use `bun -e '...'` or write a temp `.ts` file and `bun run` it
- **NEVER** use `python3 -c` or `python -c` for inline scripting — python is not a project dependency
- Prefer `jq` for simple JSON extraction; fall back to `bun eval` when jq is unavailable
- Pass data to bun via environment variables (e.g., `_DATA="${var}" bun eval "..."`) or temp files — never interpolate untrusted values into JS strings
- Prefer `jq` for simple JSON extraction; fall back to `bun -e` when jq is unavailable
- Pass data to bun via environment variables (e.g., `_DATA="${var}" bun -e "..."`) or temp files — never interpolate untrusted values into JS strings
- For complex operations (SigV4 signing, API calls with retries), write a heredoc `.ts` file and `bun run` it

## ESM Only — NEVER use require() or CommonJS
Expand Down
23 changes: 23 additions & 0 deletions .claude/skills/setup-agent-team/qa.sh
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,29 @@ if [[ "${RUN_MODE}" == "e2e" ]]; then
fi
fi

# --- Load Telegram credentials for soak mode ---
if [[ "${RUN_MODE}" == "soak" ]]; then
if [[ -f /etc/spawn-qa-auth.env ]]; then
while IFS='=' read -r _tkey _tval || [[ -n "${_tkey}" ]]; do
_tkey="${_tkey#"${_tkey%%[! ]*}"}"
_tkey="${_tkey%"${_tkey##*[! ]}"}"
[[ -z "${_tkey}" || "${_tkey}" == \#* ]] && continue
case "${_tkey}" in
TELEGRAM_BOT_TOKEN|TELEGRAM_TEST_CHAT_ID|SOAK_CLOUD)
export "${_tkey}=${_tval}"
;;
esac
done < /etc/spawn-qa-auth.env
if [[ -n "${TELEGRAM_BOT_TOKEN:-}" ]] && [[ -n "${TELEGRAM_TEST_CHAT_ID:-}" ]]; then
log "Telegram credentials loaded for soak test (cloud: ${SOAK_CLOUD:-sprite})"
else
log "WARNING: TELEGRAM_BOT_TOKEN or TELEGRAM_TEST_CHAT_ID missing from /etc/spawn-qa-auth.env — soak test will fail"
fi
else
log "WARNING: /etc/spawn-qa-auth.env not found — soak test requires TELEGRAM_BOT_TOKEN and TELEGRAM_TEST_CHAT_ID"
fi
fi

# Launch Claude Code with mode-specific prompt
# Enable agent teams (required for team-based workflows)
export CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1
Expand Down
9 changes: 7 additions & 2 deletions .github/workflows/qa.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
name: QA
on:
schedule:
- cron: '0 */4 * * *'
- cron: '0 */4 * * *' # Every 4 hours — quality sweep
- cron: '0 3 * * 1' # Every Monday 3am UTC — Telegram soak test (OpenClaw on DigitalOcean)
workflow_dispatch:
inputs:
reason:
Expand All @@ -24,7 +25,11 @@ jobs:
SPRITE_URL: ${{ secrets.QA_SPRITE_URL }}
TRIGGER_SECRET: ${{ secrets.QA_TRIGGER_SECRET }}
run: |
REASON="${{ github.event.inputs.reason || 'schedule' }}"
if [ "${{ github.event_name }}" = "schedule" ] && [ "${{ github.event.schedule }}" = "0 3 * * 1" ]; then
REASON="soak"
else
REASON="${{ github.event.inputs.reason || 'schedule' }}"
fi
curl -sS --fail-with-body -X POST \
"${SPRITE_URL}/trigger?reason=${REASON}" \
-H "Authorization: Bearer ${TRIGGER_SECRET}"
16 changes: 8 additions & 8 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
}
},
"icon": "https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/assets/agents/claude.png",
"featured_cloud": ["gcp", "aws", "digitalocean"],
"featured_cloud": ["digitalocean", "sprite"],
"creator": "Anthropic",
"repo": "anthropics/claude-code",
"license": "Proprietary",
Expand Down Expand Up @@ -61,7 +61,7 @@
}
},
"icon": "https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/assets/agents/openclaw.png",
"featured_cloud": ["gcp", "aws", "digitalocean"],
"featured_cloud": ["digitalocean", "sprite"],
"creator": "OpenClaw",
"repo": "openclaw/openclaw",
"license": "MIT",
Expand Down Expand Up @@ -99,7 +99,7 @@
},
"notes": "Rust-based agent framework built by Harvard/MIT/Sundai.Club communities. Natively supports OpenRouter via OPENROUTER_API_KEY + ZEROCLAW_PROVIDER=openrouter. Requires compilation from source (~5-10 min).",
"icon": "https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/assets/agents/zeroclaw.png",
"featured_cloud": ["hetzner", "gcp", "aws"],
"featured_cloud": ["digitalocean", "sprite"],
"creator": "Sundai.Club",
"repo": "zeroclaw-labs/zeroclaw",
"license": "Apache-2.0",
Expand All @@ -126,7 +126,7 @@
},
"notes": "Works with OpenRouter via OPENAI_BASE_URL override pointing to openrouter.ai/api/v1",
"icon": "https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/assets/agents/codex.png",
"featured_cloud": ["gcp", "aws", "digitalocean"],
"featured_cloud": ["digitalocean", "sprite"],
"creator": "OpenAI",
"repo": "openai/codex",
"license": "Apache-2.0",
Expand All @@ -151,7 +151,7 @@
},
"notes": "Natively supports OpenRouter via OPENROUTER_API_KEY env var. Go-based TUI using Bubble Tea.",
"icon": "https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/assets/agents/opencode.png",
"featured_cloud": ["gcp", "aws", "digitalocean"],
"featured_cloud": ["digitalocean", "sprite"],
"creator": "SST",
"repo": "sst/opencode",
"license": "MIT",
Expand All @@ -178,7 +178,7 @@
},
"notes": "Natively supports OpenRouter as a provider via KILO_PROVIDER_TYPE=openrouter. CLI installable via npm as @kilocode/cli, invocable as 'kilocode' or 'kilo'.",
"icon": "https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/assets/agents/kilocode.png",
"featured_cloud": ["gcp", "aws", "digitalocean"],
"featured_cloud": ["digitalocean", "sprite"],
"creator": "Kilo-Org",
"repo": "Kilo-Org/kilocode",
"license": "MIT",
Expand All @@ -205,7 +205,7 @@
},
"notes": "Natively supports OpenRouter via OPENROUTER_API_KEY. Also works via OPENAI_BASE_URL + OPENAI_API_KEY for OpenAI-compatible mode. Installs Python 3.11 via uv.",
"icon": "https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/assets/agents/hermes.png",
"featured_cloud": ["sprite", "hetzner", "gcp"],
"featured_cloud": ["digitalocean", "sprite"],
"creator": "Nous Research",
"repo": "NousResearch/hermes-agent",
"license": "MIT",
Expand All @@ -231,7 +231,7 @@
},
"notes": "Natively supports OpenRouter via JUNIE_OPENROUTER_API_KEY. Subagent tasks may require GPT-4.1 Mini, GPT-4.1, or GPT-5 models to be enabled on your OpenRouter account.",
"icon": "https://raw.githubusercontent.com/OpenRouterTeam/spawn/main/assets/agents/junie.png",
"featured_cloud": ["hetzner", "aws", "digitalocean"],
"featured_cloud": ["digitalocean", "sprite"],
"creator": "JetBrains",
"repo": "JetBrains/junie",
"license": "Proprietary",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.16.15",
"version": "0.17.1",
"type": "module",
"bin": {
"spawn": "cli.js"
Expand Down
71 changes: 71 additions & 0 deletions packages/cli/src/__tests__/gcp-shellquote.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, expect, it } from "bun:test";
import { shellQuote } from "../shared/ui.js";

describe("shellQuote", () => {
it("should wrap simple strings in single quotes", () => {
expect(shellQuote("hello")).toBe("'hello'");
expect(shellQuote("ls -la")).toBe("'ls -la'");
});

it("should escape embedded single quotes", () => {
expect(shellQuote("it's")).toBe("'it'\\''s'");
expect(shellQuote("a'b'c")).toBe("'a'\\''b'\\''c'");
});

it("should handle strings with no special characters", () => {
expect(shellQuote("simple")).toBe("'simple'");
expect(shellQuote("/usr/bin/env")).toBe("'/usr/bin/env'");
});

it("should safely quote shell metacharacters", () => {
expect(shellQuote("$(whoami)")).toBe("'$(whoami)'");
expect(shellQuote("`id`")).toBe("'`id`'");
expect(shellQuote("a; rm -rf /")).toBe("'a; rm -rf /'");
expect(shellQuote("a | cat /etc/passwd")).toBe("'a | cat /etc/passwd'");
expect(shellQuote("a && curl evil.com")).toBe("'a && curl evil.com'");
expect(shellQuote("${HOME}")).toBe("'${HOME}'");
});

it("should handle double quotes inside single-quoted string", () => {
expect(shellQuote('echo "hello"')).toBe("'echo \"hello\"'");
});

it("should handle empty string", () => {
expect(shellQuote("")).toBe("''");
});

it("should reject null bytes (defense-in-depth)", () => {
expect(() => shellQuote("hello\x00world")).toThrow("null bytes");
expect(() => shellQuote("\x00")).toThrow("null bytes");
expect(() => shellQuote("cmd\x00; rm -rf /")).toThrow("null bytes");
});

it("should handle strings with newlines", () => {
const result = shellQuote("line1\nline2");
expect(result).toBe("'line1\nline2'");
});

it("should handle strings with tabs", () => {
const result = shellQuote("col1\tcol2");
expect(result).toBe("'col1\tcol2'");
});

it("should handle backslashes", () => {
expect(shellQuote("a\\b")).toBe("'a\\b'");
});

it("should handle multiple consecutive single quotes", () => {
expect(shellQuote("''")).toBe("''\\'''\\'''");
});

it("should produce output that is safe for bash -c", () => {
// Verify the quoting pattern: the result, when interpreted by bash,
// should yield the original string without executing anything
const dangerous = "$(rm -rf /)";
const quoted = shellQuote(dangerous);
// The quoted string wraps in single quotes, preventing expansion
expect(quoted).toBe("'$(rm -rf /)'");
expect(quoted.startsWith("'")).toBe(true);
expect(quoted.endsWith("'")).toBe(true);
});
});
2 changes: 0 additions & 2 deletions packages/cli/src/__tests__/icon-integrity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,6 @@ describe("Icon Integrity", () => {
});

it(`${id}.png is actual PNG data`, () => {
expect(existsSync(pngPath)).toBe(true);
expect(isPng(pngPath)).toBe(true);
});

Expand Down Expand Up @@ -94,7 +93,6 @@ describe("Icon Integrity", () => {
});

it(`${id}.png is actual PNG data`, () => {
expect(existsSync(pngPath)).toBe(true);
expect(isPng(pngPath)).toBe(true);
});

Expand Down
Loading