Skip to content
Merged
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
12 changes: 6 additions & 6 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@
"email": "hi@okis.dev"
},
"description": "Multi-model orchestration marketplace for Claude Code.",
"version": "0.0.69",
"version": "0.0.70",
"plugins": [
{
"name": "grok",
"source": "./plugins/grok",
"displayName": "Grok Companion",
"description": "Local Grok CLI delegation: task, review, resumable history, best-of-n tournaments, background jobs, stats, and setup health checks.",
"version": "0.0.69",
"version": "0.0.70",
"author": {
"name": "Harry Yep"
},
Expand All @@ -34,7 +34,7 @@
"source": "./plugins/codex",
"displayName": "Codex Companion",
"description": "First party local Codex CLI delegation for tasks, reviews, resumable threads, and durable background jobs.",
"version": "0.0.69",
"version": "0.0.70",
"author": {
"name": "Harry Yep"
},
Expand All @@ -55,7 +55,7 @@
"source": "./plugins/cursor",
"displayName": "Cursor Companion",
"description": "Local Cursor Agent CLI delegation: multi family consults, Composer implementation in owned worktrees, review, resumable history, background jobs, and setup health checks.",
"version": "0.0.69",
"version": "0.0.70",
"author": {
"name": "Harry Yep"
},
Expand All @@ -76,7 +76,7 @@
"source": "./plugins/devin",
"displayName": "Devin Companion",
"description": "Local Devin CLI delegation: multi family consults, SWE implementation in owned worktrees, review, resumable history, background jobs, and setup health checks.",
"version": "0.0.69",
"version": "0.0.70",
"author": {
"name": "Harry Yep"
},
Expand All @@ -97,7 +97,7 @@
"source": "./plugins/fusion",
"displayName": "Fusion Orchestrator",
"description": "Multi-model orchestration: tier agents, routing rules, blind panel, ultra fleet, model config, and drift doctor.",
"version": "0.0.69",
"version": "0.0.70",
"author": {
"name": "Harry Yep"
},
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# changelog

## 0.0.70

- devin jobs record their sessions in the user's own devin session database. the private home held its own `sessions.db`, so every routed devin job was invisible to usage readers and to `devin list`; on two hosts the real database had no session since the engine joined while the private one held 14 and 2. the private home now links `devin/cli/sessions.db` to the real one, as it links the credentials file, and keeps logs and session locks private.
- the first companion call after the upgrade merges the private session database into the real one under a lock, copying only sessions the real database lacks and only the tables and columns both share, then replaces it with the link. a merge that fails or a devin job still running leaves the private store in place and retries on a later call.
- the job configuration also denies agent reads of the session database link.

## 0.0.69

- this release acts on the first day of devin call records on one host: 2 routed jobs, one accepted consult and one panel track lost after 6 minutes of work.
Expand Down
4 changes: 2 additions & 2 deletions docs/devin-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ The companion resolves the CLI as `devin` on `PATH`; `DEVIN_BIN` overrides the p
The Devin CLI reads configuration, rules, skills, hooks, MCP servers, and plugins from the home directory and imports them from other tools: `~/.config/devin`, `~/.config/cognition/skills`, `~/.agents/skills`, `~/.claude`, `~/.claude.json`, `~/.codeium`, `~/.copilot`, `~/.config/opencode`, `~/.config/zed`, and personal plugins and MCP servers from the Devin Cloud account. The companion therefore runs every job with `HOME` and the XDG base directories pointing at directories it owns.

- `HOME` is `<data>/home`, shared by every job and otherwise empty, so no home scoped source exists.
- `XDG_DATA_HOME` is `<data>/home/.local/share`, shared by every job. It holds the CLI's session database (`devin/cli/sessions.db`), logs, and session locks, which is what lets `session/load` resume a session in a later job. `devin/credentials.toml` inside it is a symbolic link to the real user's credentials file (`$XDG_DATA_HOME/devin/credentials.toml`, else `~/.local/share/devin/credentials.toml`). The companion never reads or copies the credential.
- `XDG_DATA_HOME` is `<data>/home/.local/share`, shared by every job. `devin/cli/sessions.db` is a symbolic link to the real user's session database (`$XDG_DATA_HOME/devin/cli/sessions.db` when `XDG_DATA_HOME` is absolute, else `~/.local/share/devin/cli/sessions.db`); logs and session locks stay private. `devin/credentials.toml` is a symbolic link to the real user's credentials file (`$XDG_DATA_HOME/devin/credentials.toml` when `XDG_DATA_HOME` is absolute, else `~/.local/share/devin/credentials.toml`). The companion never reads or copies the credential.
- `XDG_STATE_HOME` and `XDG_CACHE_HOME` are `<data>/home/.local/state` and `<data>/home/.cache`.
- `XDG_CONFIG_HOME` is `<data>/runs/<job-id>/config`, private to the job. It contains `devin/config.json`, generated per mode, and `devin/mcp_config.json` with an empty `mcpServers` object.

The generated `config.json` sets `auto_update` to `false`, `attribution` to `false`, `subagents_enabled` to `false`, `notify` to `"never"`, and `read_config_from` with `agents_standard` `true` and `cursor`, `windsurf`, `claude`, `copilot`, `opencode`, `zed`, and `cloud` all `false`. `permissions.ask` is empty. `permissions.deny` holds `Fetch(*)` and a `Read(<path>/**)` rule for each sensitive directory of the real user's home: `.ssh`, `.gnupg`, `.aws`, `.azure`, `.kube`, `.docker`, `.config`, `.local/share`, `.claude`, `.codex`, `.grok`, `.cursor`, `.devin`, `Library/Keychains`, and `Library/Application Support`, plus `Read(<home>/.netrc)`, `Read(<home>/.npmrc)`, and `Read(<home>/.git-credentials)`, the directory that holds the real credentials file (which differs when `XDG_DATA_HOME` is set), and the credentials link inside the private home. Consult and review add `edit` and `exec` to `permissions.deny`. Write mode sets `permissions.allow` to `["Write(<cwd>/**)"]`, and for a linked worktree also the worktree's own Git directory; the repository's Git common directory stays read only.
The generated `config.json` sets `auto_update` to `false`, `attribution` to `false`, `subagents_enabled` to `false`, `notify` to `"never"`, and `read_config_from` with `agents_standard` `true` and `cursor`, `windsurf`, `claude`, `copilot`, `opencode`, `zed`, and `cloud` all `false`. `permissions.ask` is empty. `permissions.deny` holds `Fetch(*)` and a `Read(<path>/**)` rule for each sensitive directory of the real user's home: `.ssh`, `.gnupg`, `.aws`, `.azure`, `.kube`, `.docker`, `.config`, `.local/share`, `.claude`, `.codex`, `.grok`, `.cursor`, `.devin`, `Library/Keychains`, and `Library/Application Support`, plus `Read(<home>/.netrc)`, `Read(<home>/.npmrc)`, and `Read(<home>/.git-credentials)`, the directory that holds the real credentials file (which differs when `XDG_DATA_HOME` is set), and the credentials and sessions database links inside the private home. Consult and review add `edit` and `exec` to `permissions.deny`. Write mode sets `permissions.allow` to `["Write(<cwd>/**)"]`, and for a linked worktree also the worktree's own Git directory; the repository's Git common directory stays read only.

The child environment inherits only `PATH`, `LANG`, `LANGUAGE`, `TERM`, `COLORTERM`, `TMPDIR`, `TZ`, `USER`, `LOGNAME`, `SHELL`, every `LC_*` variable, `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, `NO_PROXY` (and their lowercase forms), and the toolchain roots listed in the Cursor contract. `DEVIN_MODEL`, `DEVIN_REFUSAL_FALLBACK`, `DEVIN_PERMISSION_MODE`, and every other `DEVIN_*` variable are never inherited, because each would change the model or the permission posture outside the companion's control. The companion sets `RUST_LOG=error` and, in write mode, `DEVIN_SANDBOX=true`.

Expand Down
2 changes: 1 addition & 1 deletion plugins/codex/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "codex",
"displayName": "Codex Companion",
"version": "0.0.69",
"version": "0.0.70",
"description": "First party local Codex CLI delegation for tasks, reviews, resumable threads, and durable background jobs.",
"author": {
"name": "Harry Yep"
Expand Down
2 changes: 1 addition & 1 deletion plugins/cursor/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "cursor",
"displayName": "Cursor Companion",
"version": "0.0.69",
"version": "0.0.70",
"description": "Local Cursor Agent CLI delegation: multi family consults, Composer implementation in owned worktrees, review, resumable history, background jobs, and setup health checks.",
"author": {
"name": "Harry Yep"
Expand Down
2 changes: 1 addition & 1 deletion plugins/devin/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "devin",
"displayName": "Devin Companion",
"version": "0.0.69",
"version": "0.0.70",
"description": "Local Devin CLI delegation: multi family consults, SWE implementation in owned worktrees, review, resumable history, background jobs, and setup health checks.",
"author": {
"name": "Harry Yep"
Expand Down
92 changes: 91 additions & 1 deletion plugins/devin/scripts/lib/devin-home.mjs
Original file line number Diff line number Diff line change
@@ -1,7 +1,73 @@
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import { createRequire } from "node:module";
import os from "node:os";
import path from "node:path";
import { withRecordLock } from "./record-lock.mjs";
import { listAllJobRecords } from "./state.mjs";
import { processGroupAlive, processIdentityMatches, processIsDirectlyAlive, validProcessIdentity } from "./process-identity.mjs";

const SESSION_TABLES = ["sessions", "message_nodes", "tool_call_state", "live_row_times", "subagent_heads", "prompt_history", "rendered_commits"];
const ORDER_KEYS = { message_nodes: "row_id", prompt_history: "id", rendered_commits: "id" };

function jobMayBeLive(record) {
if (record.status !== "running") return false;
const processes = [[record.pid, record.pidIdentity], [record.devinPid, record.devinPidIdentity]]
.filter(([pid]) => Number.isInteger(pid) && pid > 1);
if (processes.some(([pid, identity]) => validProcessIdentity(identity) && processIdentityMatches(pid, identity)
|| processIsDirectlyAlive(pid) || processGroupAlive(pid))) return true;
if (processes.length) return false;
const createdAt = Date.parse(record.createdAt);
return Number.isFinite(createdAt) && Date.now() - createdAt < 15000;
}

function tableColumns(db, schema, table) {
const present = db.prepare(`SELECT 1 FROM ${schema}.sqlite_master WHERE type = 'table' AND name = ?`).get(table);
return present ? db.prepare(`PRAGMA ${schema}.table_info("${table}")`).all().map((column) => column.name) : [];
}

function mergeSessions(privateFile, realFile) {
const { DatabaseSync } = createRequire(import.meta.url)("node:sqlite");
const source = new DatabaseSync(privateFile, { timeout: 5000 });
try {
const checkpoint = source.prepare("PRAGMA wal_checkpoint(TRUNCATE)").get();
if (checkpoint.busy) throw new Error("Private session database checkpoint is busy");
if (!fs.existsSync(realFile)) {
source.prepare("VACUUM INTO ?").run(realFile);
return;
}
} finally {
source.close();
}
const real = new DatabaseSync(realFile, { timeout: 5000 });
try {
real.prepare("ATTACH DATABASE ? AS private_store").run(privateFile);
real.exec("BEGIN IMMEDIATE");
try {
if (!tableColumns(real, "main", "sessions").includes("id") || !tableColumns(real, "private_store", "sessions").includes("id")) throw new Error("Missing sessions table");
real.exec("CREATE TEMP TABLE missing_sessions AS SELECT p.id FROM private_store.sessions AS p WHERE NOT EXISTS (SELECT 1 FROM main.sessions AS r WHERE r.id = p.id)");
for (const table of SESSION_TABLES) {
const sourceColumns = tableColumns(real, "private_store", table);
const targetColumns = tableColumns(real, "main", table);
if (!sourceColumns.length || !targetColumns.length) continue;
if (table !== "sessions" && (!sourceColumns.includes("session_id") || !targetColumns.includes("session_id"))) continue;
const key = ORDER_KEYS[table];
const columns = sourceColumns.filter((column) => targetColumns.includes(column) && column !== key);
if (!columns.length) continue;
const quoted = columns.map((column) => `"${column.replaceAll('"', '""')}"`);
const joinColumn = table === "sessions" ? "id" : "session_id";
const order = key && sourceColumns.includes(key) ? ` ORDER BY p."${key}"` : "";
real.exec(`INSERT INTO main."${table}" (${quoted.join(", ")}) SELECT ${quoted.map((column) => `p.${column}`).join(", ")} FROM private_store."${table}" AS p JOIN missing_sessions AS m ON m.id = p."${joinColumn}"${order}`);
}
real.exec("COMMIT");
} catch (error) {
real.exec("ROLLBACK");
throw error;
}
} finally {
real.close();
}
}

export function realCredentialsPath({ env = process.env, realHome = os.homedir() }) {
const dataHome = path.isAbsolute(env.XDG_DATA_HOME || "") ? env.XDG_DATA_HOME : path.join(realHome, ".local", "share");
Expand All @@ -26,6 +92,30 @@ export function ensurePrivateHome({ dataDir, env = process.env, realHome = os.ho
const stats = fs.lstatSync(link, { throwIfNoEntry: false });
if (stats && (!stats.isSymbolicLink() || fs.readlinkSync(link) !== target)) fs.rmSync(link, { recursive: true, force: true });
if (!fs.lstatSync(link, { throwIfNoEntry: false })) fs.symlinkSync(target, link);
const privateCli = path.join(homeDir, ".local", "share", "devin", "cli");
const privateSessions = path.join(privateCli, "sessions.db");
const realCli = path.join(path.dirname(target), "cli");
const realSessions = path.join(realCli, "sessions.db");
fs.mkdirSync(privateCli, { recursive: true, mode: 0o700 });
fs.chmodSync(privateCli, 0o700);
fs.mkdirSync(realCli, { recursive: true });
try {
withRecordLock(path.join(dataDir, ".devin-session-migration.json"), () => {
const existing = fs.lstatSync(privateSessions, { throwIfNoEntry: false });
if (existing?.isFile()) {
if (listAllJobRecords(dataDir).some(({ record }) => jobMayBeLive(record))) return;
try {
mergeSessions(privateSessions, realSessions);
} catch {
return;
}
for (const suffix of ["", "-wal", "-shm"]) fs.rmSync(`${privateSessions}${suffix}`, { force: true });
} else if (existing && (!existing.isSymbolicLink() || fs.readlinkSync(privateSessions) !== realSessions)) {
fs.rmSync(privateSessions, { recursive: true, force: true });
}
if (!fs.lstatSync(privateSessions, { throwIfNoEntry: false })) fs.symlinkSync(realSessions, privateSessions);
});
} catch {}
return { homeDir };
}

Expand All @@ -45,7 +135,7 @@ export function buildJobConfig({ configDir, mode, cwd, realHome = os.homedir(),
...sensitiveDirs.map((directory) => `Read(${path.join(realHome, directory)}/**)`),
...[".netrc", ".npmrc", ".git-credentials"].map((file) => `Read(${path.join(realHome, file)})`),
`Read(${path.dirname(realCredentialsPath({ env, realHome }))}/**)`,
...(homeDir ? [`Read(${path.join(homeDir, ".local", "share", "devin", "credentials.toml")})`] : [])
...(homeDir ? ["credentials.toml", "cli/sessions.db"].map((file) => `Read(${path.join(homeDir, ".local", "share", "devin", file)})`) : [])
];
if (mode !== "write") deny.push("edit", "exec");
const permissions = { ask: [], deny };
Expand Down
2 changes: 1 addition & 1 deletion plugins/devin/skills/devin-cli-runtime/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Execution rules:
- Every run executes `devin acp` with the workspace as its working directory. The companion sends JSON-RPC `initialize`, `session/new` or `session/load`, the supported `session/set_config_option` requests, then `session/prompt`. The brief appears only in that prompt message. The companion never passes `--cloud`, `--agent-type`, `--model`, or `--refusal-fallback`. Permission requests receive `cancelled`.
- Consult is the default mode, `--write` selects write mode, and `review` is always read only. Consult and review set ACP mode `ask` and allow `read`, `grep`, `glob`, `find_file_by_name`, `notebook_read`, `todo_write`, and `web_search`, which runs on Devin's backend. Write sets `autonomous` and additionally allows `edit`, `write`, `notebook_edit`, `exec`, `get_output`, `kill_shell`, and `write_to_process`. A `tool_call` outside the mode list terminates the process group and fails as `policy`.
- Consult and review deny edit and exec in the generated configuration. Write sets `DEVIN_SANDBOX=true` and allows `Write(<cwd>/**)` plus the linked worktree Git directory. The OS sandbox confines shell writes to these scopes and fails closed when it cannot start. Sensitive real home paths and `Fetch(*)` are denied in every mode; other home reads may remain possible.
- Every job uses shared private `HOME`, `XDG_DATA_HOME`, `XDG_STATE_HOME`, and `XDG_CACHE_HOME` under `<data>/home`, and private `XDG_CONFIG_HOME` at `<data>/runs/<job-id>/config`. The shared data directory holds the CLI session database and a symbolic link to the real user credentials file; the companion never reads or copies the credential. Generated `devin/config.json` disables auto update, attribution, subagents, notifications, and personal imports, and `devin/mcp_config.json` has empty `mcpServers`. The child environment never inherits `DEVIN_*` variables.
- Every job uses shared private `HOME`, `XDG_DATA_HOME`, `XDG_STATE_HOME`, and `XDG_CACHE_HOME` under `<data>/home`, and private `XDG_CONFIG_HOME` at `<data>/runs/<job-id>/config`. The shared data directory links `devin/cli/sessions.db` to the real user's session database and `devin/credentials.toml` to the real user's credentials file; logs and session locks stay private. The companion never reads or copies the credential. Generated `devin/config.json` disables auto update, attribution, subagents, notifications, and personal imports, and `devin/mcp_config.json` has empty `mcpServers`. The child environment never inherits `DEVIN_*` variables.
- Project scoped `AGENTS.md`, `AGENTS.local.md`, `AGENT.md`, `.windsurfrules`, and project skills under `.devin/skills`, `.cognition/skills`, and `.agents/skills` still load. Project `.devin/config.json`, `.devin/config.local.json`, `.devin/mcp_config.json`, and `.devin/mcp_config.local.json` may add permissions, hooks, MCP servers, or imports; the companion warns when they exist. Organization settings still apply. Prompts, workspace content, and results pass through Devin's backend to the serving model vendor, so never delegate secrets or credentials to this lane.
- Fusion routed dispatches always carry `--model` and `--effort` from the routing table. A direct user call without `--model` uses the account default. `models` reads `devin models list --format json`, caches the family catalog for one hour, and `--refresh` reads it again.
- Models are family slugs such as `kimi-k3`, `glm-5.2`, `muse-wonder`, and `swe-2`, their aliases, or variant uids. The companion matches the requested family to a usable session model option and records that option as `resolvedModel`; the option keeps the family's base variant uid whatever effort applies. A missing catalog family fails before job creation as `input`; a family unavailable to the account fails as `setup`. Effort is the selected model's `thought_level`: `default` leaves its default, a supported level is set explicitly, and an unsupported level fails as `input`. The level in effect when the prompt is sent is recorded as `resolvedEffort`. `speed` is set to `standard` when offered. `adaptive` and `fusion-*` pairings are refused as `input`.
Expand Down
2 changes: 1 addition & 1 deletion plugins/fusion/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
"name": "fusion",
"displayName": "Fusion Orchestrator",
"version": "0.0.69",
"version": "0.0.70",
"description": "Multi-model orchestration: tier agents, routing rules, blind panel, ultra fleet, model config, and drift doctor.",
"author": {
"name": "Harry Yep"
Expand Down
Loading
Loading