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 AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ pnpm install --frozen-lockfile
pnpm typecheck
pnpm build
pnpm test
bash -n ops/provision.sh
bash -n ops/provision.sh ops/claudex
```

## Architecture
Expand Down
37 changes: 35 additions & 2 deletions daemon/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,17 @@ pnpm install --frozen-lockfile
pnpm typecheck
pnpm build
pnpm test
bash -n ops/provision.sh
bash -n ops/provision.sh ops/claudex ops/claudex-fable ops/proxy-accounts.sh ops/codex-provider-gate.sh
```

The Vitest suite is hermetic: it uses loopback HTTP servers and temporary real SQLite
databases, requires no environment variables, and makes no internet or Linear requests.
Tests cover phase-1 ingress plus durable planner queues, real temporary git worktrees, and a
fake stream-json Claude executable. They require no network, Linear credentials, or Claude
account. Real Linear/Claude/systemd acceptance remains a deploy-time gate in `ops/runbook.md`.
account. When `CLIPROXY_BIN` points to the pinned CLIProxyAPI binary, the proxy integration
suite additionally checks aliases, credential management, hot-loading, disabling, and log
redaction. Real account, Linear, Claude, and systemd acceptance remains a deploy-time gate in
`ops/runbook.md`.

## Run locally

Expand All @@ -53,9 +56,39 @@ app prefix are test-only static overrides and are ignored unless `DAEMON_TEST_MO
production uses client credentials. Optional settings are `PORT`, `BIND_ADDR`,
`REPLAY_WINDOW_MS`, `LINEAR_GRAPHQL_URL`, and `LINEAR_TOKEN_URL`.

Set `ARTIFACT_TOKEN` to enable artifact hosting. An authenticated `POST /a` creates a
bundle with a server-generated id; authenticated `PUT /a/<id>` atomically replaces an
existing bundle. Both accept a JSON manifest whose file contents are base64 encoded:

```json
{
"files": [
{ "path": "item.md", "contentBase64": "IyBJdGVtCg==" },
{ "path": "refs/explainer.html", "contentBase64": "PGgxPkV4cGxhaW5lcjwvaDE+" }
]
}
```

Writes require `Authorization: Bearer <ARTIFACT_TOKEN>`. `GET /a/<id>/` is an
unauthenticated, self-contained viewer; `GET /a/<id>/index.json` returns the live version's
file paths as a no-cache JSON array, and `GET /a/<id>/<path>` serves raw files. The name
`index.json` is reserved at the bundle root. Nothing above the unguessable `/a/<id>/` URL
enumerates bundles. `ARTIFACTS_DIR` defaults to an `artifacts` directory beside the database,
and `ARTIFACT_MAX_BODY_BYTES` defaults to 32 MiB. Provisioning creates the default directory
under `/var/lib/linear-agent-daemon`, outside the deployed application tree, so content
survives provision and deploy reruns. It is not backed up yet; loss of the VM disk loses
stored bundles.

Planner sessions default on. `TARGET_REPO_PATH` and `LINEAR_API_KEY` are required when
enabled. Optional session settings are `WORKTREES_ROOT` (defaults beside the database),
`CLAUDE_BIN` (default `claude`, whitespace-split for a command prefix),
`CLAUDEX_BIN` (optional, whitespace-split; enables a one-shot retry of validated
Claude usage/rate-limit failures through the Claudex proxy runtime — point it at
the provisioned `claudex` wrapper), `CLAUDEX_ENV` (optional JSON string map of
extra child env for `CLAUDEX_BIN`; requires `CLAUDEX_BIN`),
`FABLE_BIN` (optional; normally the installed `ops/claudex-fable` launcher),
`CLIPROXY_ENV_FILE`, `CLIPROXY_URL`, `PROVIDER_PROBE_INTERVAL_MS`,
`PROVIDER_STATE_STALE_MS`, and `PROVIDER_INITIAL_PROBE_TIMEOUT_MS`,
`CLAUDE_PERMISSION_MODE` (`bypassPermissions`), `CLAUDE_MAX_TURNS` (100),
`DO_PERMISSION_MODE` (`bypassPermissions`; production rejects every other value),
`DO_MAX_TURNS` (300), `DO_MAX_BUDGET_USD` (optional positive number),
Expand Down
18 changes: 18 additions & 0 deletions daemon/ops/claudex
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#!/bin/sh
set -eu

. /etc/linear-agent-daemon/cliproxyapi.env

export CLIPROXY_API_KEY
export ANTHROPIC_BASE_URL=http://127.0.0.1:8317
export ANTHROPIC_AUTH_TOKEN="${CLIPROXY_API_KEY}"
export ANTHROPIC_DEFAULT_HAIKU_MODEL=gpt-5.6-sol-low
export ANTHROPIC_DEFAULT_SONNET_MODEL=gpt-5.6-sol-low
export ANTHROPIC_DEFAULT_OPUS_MODEL=gpt-5.6-sol-medium
export ANTHROPIC_DEFAULT_FABLE_MODEL=gpt-5.6-sol-xhigh
export CLAUDE_CODE_MAX_CONTEXT_TOKENS=250000
export CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1
export CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY=3
export ENABLE_TOOL_SEARCH=true

exec /var/lib/linear-agent-daemon/.local/bin/claude --model gpt-5.6-sol "$@"
55 changes: 55 additions & 0 deletions daemon/ops/claudex-fable
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/bin/sh
set -eu

CLIPROXY_ENV_FILE="${CLIPROXY_ENV_FILE:-/etc/linear-agent-daemon/cliproxyapi.env}"
FABLE_MODELS_ENV_FILE="${FABLE_MODELS_ENV_FILE:-/etc/linear-agent-daemon/fable-models.env}"
PROXY_URL="${PROXY_URL:-http://127.0.0.1:8317}"
FABLE_CLAUDE_BIN="${FABLE_CLAUDE_BIN:-/var/lib/linear-agent-daemon/.local/bin/claude}"

fail() { echo "claudex-fable: $1" >&2; exit 1; }
[ -r "$CLIPROXY_ENV_FILE" ] || fail "missing proxy environment file: $CLIPROXY_ENV_FILE"
[ -r "$FABLE_MODELS_ENV_FILE" ] || fail "missing Fable model file: $FABLE_MODELS_ENV_FILE"
# shellcheck disable=SC1090
. "$CLIPROXY_ENV_FILE"
# shellcheck disable=SC1090
. "$FABLE_MODELS_ENV_FILE"

validate_model() {
variable="$1" model="$2"
[ -n "$model" ] || fail "missing $variable"
case "$model" in claude-*) ;; *) fail "$variable must name a claude-* model, got: $model" ;; esac
}
catalog_has_model() {
variable="$1" model="$2"
printf '%s' "$MODEL_CATALOG" | python3 -c \
'import json,sys; p=json.load(sys.stdin); needle=sys.argv[1]; raise SystemExit(0 if any(x.get("id")==needle for x in p.get("data",[])) else 1)' \
"$model" || fail "$variable model absent from proxy catalog: $model"
}

[ -n "${CLIPROXY_API_KEY:-}" ] || fail "CLIPROXY_API_KEY is missing"
validate_model FABLE_MAIN_MODEL "${FABLE_MAIN_MODEL:-}"
validate_model FABLE_HAIKU_MODEL "${FABLE_HAIKU_MODEL:-}"
validate_model FABLE_SONNET_MODEL "${FABLE_SONNET_MODEL:-}"
validate_model FABLE_OPUS_MODEL "${FABLE_OPUS_MODEL:-}"
validate_model FABLE_FABLE_MODEL "${FABLE_FABLE_MODEL:-}"
MODEL_CATALOG="$(printf 'header = "Authorization: Bearer %s"\n' "$CLIPROXY_API_KEY" |
curl -fsS --connect-timeout 2 --max-time 15 -K - "${PROXY_URL%/}/v1/models")" || fail "proxy model catalog unavailable"
catalog_has_model FABLE_MAIN_MODEL "$FABLE_MAIN_MODEL"
catalog_has_model FABLE_HAIKU_MODEL "$FABLE_HAIKU_MODEL"
catalog_has_model FABLE_SONNET_MODEL "$FABLE_SONNET_MODEL"
catalog_has_model FABLE_OPUS_MODEL "$FABLE_OPUS_MODEL"
catalog_has_model FABLE_FABLE_MODEL "$FABLE_FABLE_MODEL"

export CLIPROXY_API_KEY
export ANTHROPIC_BASE_URL="${PROXY_URL%/}"
export ANTHROPIC_AUTH_TOKEN="$CLIPROXY_API_KEY"
export ANTHROPIC_DEFAULT_HAIKU_MODEL="$FABLE_HAIKU_MODEL"
export ANTHROPIC_DEFAULT_SONNET_MODEL="$FABLE_SONNET_MODEL"
export ANTHROPIC_DEFAULT_OPUS_MODEL="$FABLE_OPUS_MODEL"
export ANTHROPIC_DEFAULT_FABLE_MODEL="$FABLE_FABLE_MODEL"
export CLAUDE_CODE_MAX_CONTEXT_TOKENS=250000
export CLAUDE_CODE_ALWAYS_ENABLE_EFFORT=1
export CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY=3
export ENABLE_TOOL_SEARCH=true

exec "$FABLE_CLAUDE_BIN" --model "$FABLE_MAIN_MODEL" "$@"
37 changes: 37 additions & 0 deletions daemon/ops/cliproxyapi.service
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
[Unit]
Description=CLIProxyAPI for Claude Code
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=linear-daemon
Group=linear-daemon
Environment=HOME=/var/lib/linear-agent-daemon
WorkingDirectory=/var/lib/linear-agent-daemon
ExecStart=/usr/local/bin/cliproxyapi -config /etc/linear-agent-daemon/cliproxyapi.yaml
Restart=always
RestartSec=2
NoNewPrivileges=true
PrivateTmp=true
PrivateDevices=true
ProtectHome=true
ProtectSystem=strict
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true
ProtectKernelLogs=true
RestrictSUIDSGID=true
RestrictNamespaces=true
RestrictRealtime=true
LockPersonality=true
RemoveIPC=true
CapabilityBoundingSet=
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
SystemCallFilter=@system-service
SystemCallArchitectures=native
UMask=0077
ReadWritePaths=/var/lib/linear-agent-daemon/.cli-proxy-api

[Install]
WantedBy=multi-user.target
Loading