Skip to content

Commit 0ae7aa4

Browse files
authored
Merge pull request #985 from coleam00/dev
Release 0.3.0
2 parents a884262 + c5be9bb commit 0ae7aa4

69 files changed

Lines changed: 2297 additions & 176 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [0.3.0] - 2026-04-08
11+
12+
Env-leak gate hardening, SSE reliability fixes, isolation cleanup smarter merge detection, build/version improvements, and deploy hardening.
13+
14+
### Added
15+
16+
- **Env-leak gate (target repo `.env` keys)**: scan auto-loaded `.env` filenames for 7 sensitive keys (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc.) and refuse to register or spawn into a codebase whose `.env` would silently re-inject keys into Claude/Codex subprocesses. Default is fail-closed (`allow_env_keys = false`). Includes a per-codebase consent column, registration gate, pre-spawn check in both Claude and Codex clients, and a 422 API error with web UI checkbox (#1036).
17+
- **CLI `--allow-env-keys` flag** for `archon workflow run` — grant env-leak-gate consent during auto-registration without needing the Web UI. Audit-logged as `env_leak_consent_granted` with `actor: 'user-cli'` (#973, #983).
18+
- **Global `allow_target_repo_keys` flag** in `~/.archon/config.yaml` — bypass the env-leak gate for all codebases on this machine. Per-repo `.archon/config.yaml` `allow_target_repo_keys: false` re-enables the gate for that repo. The server emits `env_leak_gate_disabled` once per process per source the first time `loadConfig` resolves the bypass as active (#973, #983).
19+
- **`PATCH /api/codebases/:id`** endpoint to flip `allow_env_keys` on existing codebases without delete/re-add. Audit-logged at `warn` level on every grant and revoke, including a `scanStatus` field that distinguishes "scanned" from "scan failed" so audit reviewers can tell empty key lists apart (#973, #983).
20+
- **Settings → Projects per-row toggle** to grant or revoke env-key consent retroactively, with an "env keys allowed" badge and inline error feedback if the PATCH fails (#973, #983).
21+
- **Startup env-leak scan**: when `allow_target_repo_keys` is not set, the server emits one `startup_env_leak_gate_will_block` warn per registered codebase whose `.env` would block the next spawn. Skipped entirely when the global bypass is active (#973, #983).
22+
- **Squash-merge and PR-merge detection** for `isolation cleanup --merged`. Unions three signals (ancestry via `git branch --merged`, patch equivalence via `git cherry`, and PR state via `gh`) to safely clean up worktrees whose branches were squash-merged. Adds `--include-closed` flag to also remove worktrees whose PRs were closed without merging (#1027).
23+
- **Git commit hash in `archon version`** output. Read at runtime via `git rev-parse` in dev or from a build-time constant in compiled binaries; falls back to `unknown` (#1035).
24+
25+
### Changed
26+
27+
- **Env-leak gate error messages** are now context-aware: separate remediation copy for Web Add-Project, CLI auto-register, and pre-spawn-of-existing-codebase paths. Previously every error pointed at the Web UI checkbox even from the CLI (#973, #983).
28+
- **SSE event buffer TTL** raised from 3s to 60s and capacity from 50 to 500 events, fixing dropped `tool_result` events during the 5s reconnect grace window that left tool cards perpetually spinning. Cleanup timer now resets on each new event so the buffer is held for TTL past the most recent event, not the first one. Buffer overflow and TTL expiration now log at `warn` level for observability (#1037).
29+
- **Binary build detection** moved from runtime env sniffing (`import.meta.dir` / `process.execPath`) to a build-time `BUNDLED_IS_BINARY` constant in `@archon/paths`. Logger uses `pino-pretty` as a destination stream on the main thread instead of a worker-thread transport, eliminating the `require.resolve('pino-pretty')` lookup that crashed inside Bun's `$bunfs` virtual filesystem in compiled binaries. Same code path runs in dev and binaries — no environment detection (#982).
30+
- **Cloud-init deployment script** hardened: dedicated `archon` user (docker group, no sudo) with SSH keys copied from the default cloud user, 2GB swapfile to prevent OOM during docker build on small VPSes, `ufw allow 443/tcp` and `443/udp` for HTTP/3 QUIC, fail-fast on network errors, and clearer setup-complete messaging (#981).
31+
32+
### Fixed
33+
34+
- **Env-leak gate worktree path lookup**: pre-spawn consent check now falls back to `findCodebaseByPathPrefix()` when the exact path lookup misses, so workflow runs in `.../worktrees/feature-branch` correctly inherit consent from the source codebase (#1036).
35+
- **`EnvLeakError` FATAL classification** in the workflow executor now checks `error.name === 'EnvLeakError'` directly instead of pattern-matching the message, immune to message rewording (#1036).
36+
- **Scanner unreadable-file handling**: distinguishes `ENOENT` (skip) from `EACCES` and other errors so unreadable `.env` files surface as findings instead of silently bypassing the gate (#1036).
37+
38+
### Security
39+
40+
- The default `allow_env_keys` per codebase is `false` (fail-closed). Codebases with sensitive keys in their auto-loaded `.env` files (`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, etc.) are blocked at the next workflow run. **Remediation paths** (any one): (1) remove the key from `.env`, (2) rename to `.env.secrets`, (3) toggle "Allow env keys" in Settings → Projects, (4) `archon workflow run --allow-env-keys ...`, (5) set `allow_target_repo_keys: true` in `~/.archon/config.yaml`. See `docs/reference/security.md` for full details (#1036, #973, #983).
41+
42+
1043
## [0.2.12] - 2026-03-20
1144

1245
Chat-first navigation redesign, DAG graph viewer, per-node MCP and skills, and extensive bug fixes across the web UI and workflow engine.

CLAUDE.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,10 @@ bun run cli workflow run implement --branch feature-auth "Add auth"
198198
# Opt out of isolation (run in live checkout)
199199
bun run cli workflow run quick-fix --no-worktree "Fix typo"
200200

201+
# Grant env-leak-gate consent during auto-registration (for repos whose .env
202+
# contains sensitive keys). Audit-logged with actor: 'user-cli'.
203+
bun run cli workflow run plan --cwd /path/to/leaky/repo --allow-env-keys "..."
204+
201205
# Show running workflows
202206
bun run cli workflow status
203207

@@ -224,6 +228,9 @@ bun run cli isolation cleanup 14 # Custom days
224228
# Clean up environments with branches merged into main (also deletes remote branches)
225229
bun run cli isolation cleanup --merged
226230

231+
# Also remove environments with closed (abandoned) PRs
232+
bun run cli isolation cleanup --merged --include-closed
233+
227234
# Validate workflow definitions and their referenced resources
228235
bun run cli validate workflows # All workflows
229236
bun run cli validate workflows my-workflow # Single workflow
@@ -740,6 +747,12 @@ Pattern: Use `classifyIsolationError()` (from `@archon/isolation`) to map git er
740747
- `POST /api/workflows/runs/{runId}/abandon` - Abandon a non-terminal run (marks as cancelled)
741748
- `DELETE /api/workflows/runs/{runId}` - Delete a terminal workflow run and its events
742749

750+
**Codebases:**
751+
- `GET /api/codebases` / `GET /api/codebases/:id` - List / fetch codebases
752+
- `POST /api/codebases` - Register a codebase (clone or local path); body accepts `allowEnvKeys` for the env-leak gate
753+
- `PATCH /api/codebases/:id` - Flip the `allow_env_keys` consent bit; body: `{ allowEnvKeys: boolean }`. Audit-logged at `warn` level on every grant/revoke (`env_leak_consent_granted` / `env_leak_consent_revoked`) with `codebaseId`, `path`, `files`, `keys`, `scanStatus`, `actor`
754+
- `DELETE /api/codebases/:id` - Delete a codebase and clean up resources
755+
743756
**Artifact Files:**
744757
- `GET /api/artifacts/:runId/*` - Serve a workflow artifact file by run ID and relative path; returns `text/markdown` for `.md` files, `text/plain` otherwise; 400 on path traversal (`..`), 404 if run or file not found
745758

bun.lock

Lines changed: 1 addition & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

deploy/cloud-init.yml

Lines changed: 72 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,30 @@
66
#
77
# Paste this into your VPS provider's "User Data" field when creating a server.
88
# Tested on: Ubuntu 22.04+, Debian 12+
9+
# Works with any cloud-init compatible provider (DigitalOcean, Hetzner, Linode,
10+
# Vultr, AWS EC2, Hostinger, etc.)
911
#
1012
# What this does:
1113
# 1. Installs Docker + Docker Compose plugin
1214
# 2. Opens firewall ports (SSH, HTTP, HTTPS)
13-
# 3. Clones the repo to /opt/archon
14-
# 4. Prepares .env and Caddyfile from examples
15-
# 5. Builds the Docker image (~5 min)
15+
# 3. Creates a 2GB swapfile (helps small VPS builds avoid OOM)
16+
# 4. Clones the repo to /opt/archon
17+
# 5. Prepares .env and Caddyfile from examples
18+
# 6. Creates a dedicated 'archon' user (docker group only, no sudo)
19+
# 7. Builds the Docker image (~5 min) as the archon user
1620
#
17-
# After the server boots (~5-8 min), SSH in and:
21+
# Note: On VPS with <2GB RAM, the docker build step can OOM without swap.
22+
# Note: The 'archon' user has docker access but NOT sudo. For administrative
23+
# tasks (updates, reboots), use the default cloud user or root.
24+
#
25+
# After the server boots (~5-8 min), SSH in as the archon user:
26+
# ssh archon@your-server-ip
1827
# 1. Edit /opt/archon/.env — set your AI credentials, DOMAIN, DATABASE_URL
1928
# 2. cd /opt/archon && docker compose --profile with-db --profile cloud up -d
2029
# 3. Open https://your-domain.com
2130
#
2231
# IMPORTANT: Before starting, point your domain's DNS A record to this server's IP.
32+
# SSH keys from the default cloud user are copied to 'archon'.
2333
#
2434

2535
package_update: true
@@ -30,29 +40,66 @@ packages:
3040
- git
3141
- ufw
3242

43+
users:
44+
- default
45+
- name: archon
46+
gecos: Archon Service User
47+
shell: /bin/bash
48+
lock_passwd: true
49+
3350
runcmd:
51+
# --- Swap (helps small VPS avoid OOM during docker build) ---
52+
- |
53+
if [ ! -f /swapfile ]; then
54+
fallocate -l 2G /swapfile || dd if=/dev/zero of=/swapfile bs=1M count=2048
55+
chmod 600 /swapfile
56+
mkswap /swapfile
57+
swapon /swapfile
58+
echo '/swapfile none swap sw 0 0' >> /etc/fstab
59+
fi
60+
3461
# --- Docker ---
3562
- curl -fsSL https://get.docker.com | sh
36-
- systemctl enable docker
37-
- systemctl start docker
63+
- usermod -aG docker archon
3864

39-
# --- Firewall ---
65+
# --- Copy SSH keys from default user to archon (so login works immediately) ---
66+
- |
67+
DEFAULT_USER=$(getent passwd 1000 | cut -d: -f1)
68+
if [ -n "$DEFAULT_USER" ] && [ -f /home/$DEFAULT_USER/.ssh/authorized_keys ]; then
69+
mkdir -p /home/archon/.ssh
70+
cp /home/$DEFAULT_USER/.ssh/authorized_keys /home/archon/.ssh/authorized_keys
71+
chmod 700 /home/archon/.ssh
72+
chmod 600 /home/archon/.ssh/authorized_keys
73+
chown -R archon:archon /home/archon/.ssh
74+
elif [ -f /root/.ssh/authorized_keys ]; then
75+
mkdir -p /home/archon/.ssh
76+
cp /root/.ssh/authorized_keys /home/archon/.ssh/authorized_keys
77+
chmod 700 /home/archon/.ssh
78+
chmod 600 /home/archon/.ssh/authorized_keys
79+
chown -R archon:archon /home/archon/.ssh
80+
fi
81+
82+
# --- Firewall (443/udp needed for HTTP/3 QUIC via Caddy) ---
4083
- ufw allow 22/tcp
4184
- ufw allow 80/tcp
42-
- ufw allow 443
85+
- ufw allow 443/tcp
86+
- ufw allow 443/udp
4387
- ufw --force enable
4488

45-
# --- Clone and configure ---
46-
- git clone https://github.com/coleam00/Archon.git /opt/archon
47-
- cp /opt/archon/.env.example /opt/archon/.env
48-
- cp /opt/archon/Caddyfile.example /opt/archon/Caddyfile
89+
# --- Clone and configure (fail fast — single shell so set -e applies) ---
90+
- |
91+
set -e
92+
git clone https://github.com/coleam00/Archon.git /opt/archon
93+
cp /opt/archon/.env.example /opt/archon/.env
94+
cp /opt/archon/Caddyfile.example /opt/archon/Caddyfile
95+
chown -R archon:archon /opt/archon
4996
50-
# --- Pre-pull external images ---
51-
- docker pull postgres:17-alpine
52-
- docker pull caddy:2-alpine
97+
# --- Pre-pull external images (as archon, via docker group) ---
98+
- sudo -u archon docker pull postgres:17-alpine
99+
- sudo -u archon docker pull caddy:2-alpine
53100

54-
# --- Build the app image ---
55-
- cd /opt/archon && docker compose build
101+
# --- Build the app image as archon ---
102+
- sudo -u archon -H bash -c 'cd /opt/archon && docker compose build'
56103

57104
# --- Signal completion ---
58105
- |
@@ -61,6 +108,13 @@ runcmd:
61108
Archon server setup complete!
62109
============================================
63110
111+
Log in as the 'archon' user (not root):
112+
ssh archon@<server-ip>
113+
114+
Note: the 'archon' user has docker access but no sudo. For system
115+
maintenance (apt upgrade, reboots), log in as the default cloud user
116+
or root.
117+
64118
Next steps:
65119
66120
1. Edit credentials and domain:
@@ -85,7 +139,7 @@ runcmd:
85139
86140
Logs: docker compose logs -f
87141
Health: curl https://your-domain.com/api/health
88-
Docs: https://github.com/coleam00/Archon/blob/main/docs/docker.md
142+
Docs: https://archon.diy/deployment/docker/
89143
============================================
90144
DONE
91145
- echo "[archon] Setup complete. Edit /opt/archon/.env and run docker compose up."

migrations/000_combined.sql

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ CREATE TABLE IF NOT EXISTS remote_agent_codebases (
3030
repository_url VARCHAR(500),
3131
default_cwd VARCHAR(500) NOT NULL,
3232
ai_assistant_type VARCHAR(20) DEFAULT 'claude',
33+
allow_env_keys BOOLEAN NOT NULL DEFAULT FALSE,
3334
commands JSONB DEFAULT '{}'::jsonb,
3435
created_at TIMESTAMP DEFAULT NOW(),
3536
updated_at TIMESTAMP DEFAULT NOW()
@@ -307,3 +308,7 @@ ALTER TABLE remote_agent_conversations
307308
-- From migration 016: ended_reason on sessions
308309
ALTER TABLE remote_agent_sessions
309310
ADD COLUMN IF NOT EXISTS ended_reason TEXT;
311+
312+
-- From migration 021: allow_env_keys on codebases
313+
ALTER TABLE remote_agent_codebases
314+
ADD COLUMN IF NOT EXISTS allow_env_keys BOOLEAN NOT NULL DEFAULT FALSE;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
-- Add per-codebase consent bit for subprocess .env key leakage
2+
-- DEFAULT FALSE = safe by default; user must explicitly opt in
3+
ALTER TABLE remote_agent_codebases
4+
ADD COLUMN allow_env_keys BOOLEAN NOT NULL DEFAULT FALSE;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "archon",
3-
"version": "0.2.13",
3+
"version": "0.3.0",
44
"private": true,
55
"workspaces": [
66
"packages/*"

packages/cli/src/cli.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,9 @@ Options:
127127
--json Output machine-readable JSON (for workflow list)
128128
--workflow <name> Workflow to run for 'continue' (default: archon-assist)
129129
--no-context Skip context injection for 'continue'
130+
--allow-env-keys Grant env-key consent during auto-registration
131+
(bypasses the env-leak gate for this codebase;
132+
logs an audit entry)
130133
131134
Examples:
132135
archon chat "What does the orchestrator do?"
@@ -190,6 +193,7 @@ async function main(): Promise<number> {
190193
reason: { type: 'string' },
191194
workflow: { type: 'string' },
192195
'no-context': { type: 'boolean' },
196+
'allow-env-keys': { type: 'boolean' },
193197
},
194198
allowPositionals: true,
195199
strict: false, // Allow unknown flags to pass through
@@ -211,6 +215,7 @@ async function main(): Promise<number> {
211215
const resumeFlag = values.resume as boolean | undefined;
212216
const spawnFlag = values.spawn as boolean | undefined;
213217
const jsonFlag = values.json as boolean | undefined;
218+
const allowEnvKeysFlag = values['allow-env-keys'] as boolean | undefined;
214219

215220
// Handle help flag
216221
if (values.help) {
@@ -323,6 +328,7 @@ async function main(): Promise<number> {
323328
fromBranch,
324329
noWorktree,
325330
resume: resumeFlag,
331+
allowEnvKeys: allowEnvKeysFlag,
326332
quiet: values.quiet as boolean | undefined,
327333
verbose: values.verbose as boolean | undefined,
328334
};
@@ -459,7 +465,8 @@ async function main(): Promise<number> {
459465
// Check for --merged flag in remaining args
460466
const mergedFlag = args.includes('--merged') || positionals.includes('--merged');
461467
if (mergedFlag) {
462-
await isolationCleanupMergedCommand();
468+
const includeClosed = args.includes('--include-closed');
469+
await isolationCleanupMergedCommand({ includeClosed });
463470
} else {
464471
const days = parseInt(positionals[2] ?? '7', 10);
465472
await isolationCleanupCommand(days);

packages/cli/src/commands/bundled-version.ts

Lines changed: 0 additions & 10 deletions
This file was deleted.

packages/cli/src/commands/isolation.test.ts

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* Tests for isolation complete command
33
*/
44
import { describe, it, expect, beforeEach, afterEach, mock, spyOn } from 'bun:test';
5-
import { isolationCompleteCommand } from './isolation';
5+
import { isolationCompleteCommand, isolationCleanupMergedCommand } from './isolation';
66

77
const mockLogger = {
88
fatal: mock(() => undefined),
@@ -44,6 +44,27 @@ mock.module('@archon/core/services/cleanup-service', () => ({
4444
cleanupMergedWorktrees: mockCleanupMergedWorktrees,
4545
}));
4646

47+
const mockListEnvironments = mock(() =>
48+
Promise.resolve({
49+
codebases: [
50+
{
51+
codebaseId: 'cb-1',
52+
defaultCwd: '/test/repo',
53+
repositoryUrl: 'https://github.com/owner/repo',
54+
environments: [],
55+
},
56+
],
57+
totalEnvironments: 0,
58+
ghostsReconciled: 0,
59+
})
60+
);
61+
const mockCleanupMergedEnvironments = mock(() => Promise.resolve({ removed: [], skipped: [] }));
62+
63+
mock.module('@archon/core/operations/isolation-operations', () => ({
64+
listEnvironments: mockListEnvironments,
65+
cleanupMergedEnvironments: mockCleanupMergedEnvironments,
66+
}));
67+
4768
const mockHasUncommittedChanges = mock(() => Promise.resolve(false));
4869
// Default: gh returns empty PR array, git log returns empty string (no commits to report)
4970
const mockExecFileAsync = mock((cmd: string) =>
@@ -358,3 +379,32 @@ describe('isolationCompleteCommand', () => {
358379
expect(consoleLogSpy).toHaveBeenCalledWith('\nComplete: 1 completed, 1 failed, 1 not found');
359380
});
360381
});
382+
383+
describe('isolationCleanupMergedCommand', () => {
384+
let consoleLogSpy: ReturnType<typeof spyOn>;
385+
let consoleErrorSpy: ReturnType<typeof spyOn>;
386+
387+
beforeEach(() => {
388+
consoleLogSpy = spyOn(console, 'log').mockImplementation(() => {});
389+
consoleErrorSpy = spyOn(console, 'error').mockImplementation(() => {});
390+
mockCleanupMergedEnvironments.mockReset();
391+
mockCleanupMergedEnvironments.mockResolvedValue({ removed: [], skipped: [] });
392+
});
393+
394+
afterEach(() => {
395+
consoleLogSpy.mockRestore();
396+
consoleErrorSpy.mockRestore();
397+
});
398+
399+
it('passes includeClosed=true when --include-closed flag is set', async () => {
400+
await isolationCleanupMergedCommand({ includeClosed: true });
401+
expect(mockCleanupMergedEnvironments).toHaveBeenCalledWith('cb-1', '/test/repo', {
402+
includeClosed: true,
403+
});
404+
});
405+
406+
it('defaults to includeClosed=false', async () => {
407+
await isolationCleanupMergedCommand();
408+
expect(mockCleanupMergedEnvironments).toHaveBeenCalledWith('cb-1', '/test/repo', {});
409+
});
410+
});

0 commit comments

Comments
 (0)