trantor 0.17.56 → 0.17.57
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/bin/balances.mjs +3 -7
- package/bin/catchup.mjs +8 -7
- package/bin/crew-verify.mjs +8 -7
- package/bin/gates.mjs +11 -12
- package/bin/git-backfill.mjs +6 -9
- package/bin/reconcile.mjs +10 -11
- package/bin/recost.mjs +7 -9
- package/bin/relay-watch.mjs +13 -11
- package/bin/statusline.mjs +7 -8
- package/bin/sweep.mjs +10 -7
- package/hooks/inbox-deliver.mjs +2 -2
- package/hooks/sessionstart.mjs +8 -5
- package/hooks/stop-inbox.mjs +3 -3
- package/mcp.mjs +2 -2
- package/package.json +1 -1
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"name": "trantor",
|
|
14
14
|
"source": "./",
|
|
15
15
|
"description": "The hub-world for AI agent crews. Say \"fire up the crew\" and Claude becomes the architect: a plan-aware Advisor routes the work (solo / cheap inline calls / live crew of Codex, GLM, Kimi & DeepSeek in their own terminal windows), a Kanban/flow command center with a testing gate tracks it, and an economics brain (Scrooge) keeps the receipts. Includes the relay MCP, a SessionStart auto-discovery hook, and a PreCompact context-handoff so a fresh session can take over a full window instead of compacting.",
|
|
16
|
-
"version": "0.17.
|
|
16
|
+
"version": "0.17.57",
|
|
17
17
|
"author": {
|
|
18
18
|
"name": "Sasha Bogojevic"
|
|
19
19
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "trantor",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.57",
|
|
4
4
|
"description": "Trantor — the hub-world for AI agent crews: live message bus, presence, project Kanban/flow board + crew orchestration for independent AI coding agents (Claude, Codex, Gemini, Kimi, DeepSeek)",
|
|
5
5
|
"mcpServers": {
|
|
6
6
|
"relay": {
|
package/bin/balances.mjs
CHANGED
|
@@ -18,11 +18,8 @@ const noPush = args.includes("--no-push");
|
|
|
18
18
|
// Only check providers the user configured in `trantor profile` — never stray keys in the ambient env.
|
|
19
19
|
const configured = Object.keys(loadProfile().providers || {});
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
try { const c = join(homedir(), ".agent-bus", "config.json"); if (existsSync(c)) { const u = JSON.parse(readFileSync(c, "utf8")).url; if (u) return u; } } catch {}
|
|
24
|
-
return "http://127.0.0.1:4477";
|
|
25
|
-
}
|
|
21
|
+
// Signed via the shared client (2026-07-31, agent-UX audit): unsigned POST rejected under enforce.
|
|
22
|
+
import { signedPost } from "../hooks/lib/api.mjs";
|
|
26
23
|
let _qpct = DEFAULT_LOW_QUOTA_PCT;
|
|
27
24
|
function thresholds() {
|
|
28
25
|
try { const c = JSON.parse(readFileSync(join(homedir(), ".agent-bus", "config.json"), "utf8")); if (typeof c.lowQuotaPct === "number") _qpct = c.lowQuotaPct; if (c.lowBalance && typeof c.lowBalance === "object") return { ...DEFAULT_LOW, ...c.lowBalance }; } catch {}
|
|
@@ -35,8 +32,7 @@ const low = thresholds();
|
|
|
35
32
|
// push the snapshot to the hub (best-effort) so the dashboard + warning line can use it
|
|
36
33
|
if (!noPush) {
|
|
37
34
|
try {
|
|
38
|
-
await
|
|
39
|
-
body: JSON.stringify({ balances, ts: Date.now() }), signal: AbortSignal.timeout(2500) });
|
|
35
|
+
await signedPost("/balances", { balances, ts: Date.now() }, { timeoutMs: 2500 });
|
|
40
36
|
} catch {}
|
|
41
37
|
}
|
|
42
38
|
|
package/bin/catchup.mjs
CHANGED
|
@@ -10,19 +10,20 @@ import { homedir } from "node:os";
|
|
|
10
10
|
import { execSync } from "node:child_process";
|
|
11
11
|
import { resolveProject } from "../lib/project.mjs";
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
return "http://127.0.0.1:4477";
|
|
17
|
-
}
|
|
13
|
+
// Signed read via the shared client (2026-07-31, agent-UX audit): hand-rolled relayUrl missed the
|
|
14
|
+
// per-project hubs map; unsigned reads 401 under enforce. signedGet resolves + signs per call.
|
|
15
|
+
import { relayUrl, signedGet } from "../hooks/lib/api.mjs";
|
|
18
16
|
const haveScrooge = () => { try { execSync("command -v scrooge", { stdio: "ignore" }); return true; } catch { return false; } };
|
|
19
17
|
|
|
20
18
|
const dir = process.cwd();
|
|
21
19
|
const project = resolveProject(dir);
|
|
22
|
-
const url = relayUrl();
|
|
20
|
+
const url = relayUrl(project);
|
|
23
21
|
|
|
24
22
|
let cu = null;
|
|
25
|
-
|
|
23
|
+
{
|
|
24
|
+
const r = await signedGet(`/catchup?project=${encodeURIComponent(project)}`, { timeoutMs: 4000 });
|
|
25
|
+
if (r.ok) cu = r.json; else console.error(`could not reach hub at ${url} (status ${r.status})`);
|
|
26
|
+
}
|
|
26
27
|
let gitlog = "";
|
|
27
28
|
try { gitlog = execSync(`git -C ${JSON.stringify(dir)} log --oneline -12 2>/dev/null`, { encoding: "utf8" }).trim(); } catch {}
|
|
28
29
|
|
package/bin/crew-verify.mjs
CHANGED
|
@@ -24,12 +24,13 @@ const SINCE = si >= 0 ? Number(args.splice(si, 2)[1]) : NaN;
|
|
|
24
24
|
const [PROJ, ...AGENTS] = args;
|
|
25
25
|
if (!PROJ || !AGENTS.length) { console.error("usage: crew-verify.mjs <project> <agent...> [--timeout 30] [--since <ms>]"); process.exit(2); }
|
|
26
26
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
const HUB =
|
|
27
|
+
// Signed + per-project hub (2026-07-31, agent-UX audit): unsigned /peers is a dead 401 under
|
|
28
|
+
// enforce — crew verification would report every seat missing while the crew was fine.
|
|
29
|
+
import { resolveHub, hostId } from "../lib/project.mjs";
|
|
30
|
+
import { loadOrCreate } from "../lib/identity.mjs";
|
|
31
|
+
import { sfetchJson } from "../lib/signed-fetch.mjs";
|
|
32
|
+
const HUB = process.env.RELAY_URL || resolveHub(PROJ);
|
|
33
|
+
const VERIFY_ID = loadOrCreate(process.env.RELAY_SESSION || `${hostId()}:${PROJ}`, "agent");
|
|
33
34
|
// Two distinct clocks, deliberately separate:
|
|
34
35
|
// - FRESH_SINCE: the freshness threshold. A registration counts only if lastSeen >= this.
|
|
35
36
|
// Prefer the launcher's pre-spawn epoch (so an early "booting" beat counts); else our start.
|
|
@@ -43,7 +44,7 @@ const FRESH_SINCE = Number.isFinite(SINCE) ? SINCE : Date.now();
|
|
|
43
44
|
const up = new Set();
|
|
44
45
|
while (Date.now() < DEADLINE && up.size < want.size) {
|
|
45
46
|
try {
|
|
46
|
-
const { peers } = await (await
|
|
47
|
+
const { peers } = await (await sfetchJson(`${HUB}/peers`, { method: "GET", identity: VERIFY_ID })).json();
|
|
47
48
|
for (const p of peers) if (want.has(p.session) && p.lastSeen >= FRESH_SINCE) up.add(p.session);
|
|
48
49
|
} catch {}
|
|
49
50
|
if (up.size < want.size) await new Promise(s => setTimeout(s, 1500));
|
package/bin/gates.mjs
CHANGED
|
@@ -11,21 +11,20 @@ const args = process.argv.slice(2);
|
|
|
11
11
|
const all = args.includes("--all");
|
|
12
12
|
const asJson = args.includes("--json");
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
14
|
+
// Signed read via the shared client (2026-07-31, agent-UX audit): the hand-rolled relayUrl here
|
|
15
|
+
// missed the per-project hubs map AND sent unsigned (401 under enforce). relayUrl/signedGet from
|
|
16
|
+
// hooks/lib/api.mjs resolve the cwd project's hub and sign as this session.
|
|
17
|
+
import { relayUrl, signedGet } from "../hooks/lib/api.mjs";
|
|
19
18
|
|
|
20
19
|
const project = resolveProject(process.cwd());
|
|
21
|
-
const url = `${relayUrl()}/verify-gates?project=${encodeURIComponent(project)}${all ? "&all=1" : ""}`;
|
|
22
20
|
let gates = [];
|
|
23
|
-
|
|
24
|
-
const r = await
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
{
|
|
22
|
+
const r = await signedGet(`/verify-gates?project=${encodeURIComponent(project)}${all ? "&all=1" : ""}`, { timeoutMs: 2500 });
|
|
23
|
+
if (!r.ok) {
|
|
24
|
+
console.error(`could not reach the hub at ${relayUrl(project)} (status ${r.status}) — is it running? (trantor setup / trantor hub)`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
gates = r.json?.gates || [];
|
|
29
28
|
}
|
|
30
29
|
|
|
31
30
|
if (asJson) { process.stdout.write(JSON.stringify(gates, null, 2) + "\n"); process.exit(0); }
|
package/bin/git-backfill.mjs
CHANGED
|
@@ -11,18 +11,16 @@ import { join } from "node:path";
|
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { resolveProject, hostId } from "../lib/project.mjs";
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return "http://127.0.0.1:4477";
|
|
18
|
-
}
|
|
14
|
+
// Signed via the shared client (2026-07-31, agent-UX audit): unsigned POST /task was rejected
|
|
15
|
+
// under enforce; hand-rolled relayUrl missed the per-project hubs map.
|
|
16
|
+
import { relayUrl, signedGet, signedPost } from "../hooks/lib/api.mjs";
|
|
19
17
|
const args = process.argv.slice(2);
|
|
20
18
|
const arg = (name, def) => { const i = args.indexOf("--" + name); return i >= 0 ? args[i + 1] : def; };
|
|
21
19
|
const dir = process.cwd();
|
|
22
20
|
const project = arg("project", resolveProject(dir));
|
|
23
21
|
const since = arg("since", "14 days ago");
|
|
24
22
|
const dry = args.includes("--dry-run");
|
|
25
|
-
const url = relayUrl();
|
|
23
|
+
const url = relayUrl(project);
|
|
26
24
|
const me = `${hostId()}:${project}`;
|
|
27
25
|
|
|
28
26
|
const themeOf = (s) => {
|
|
@@ -53,7 +51,7 @@ for (const r of rows) {
|
|
|
53
51
|
}
|
|
54
52
|
|
|
55
53
|
let existing = new Set();
|
|
56
|
-
try { const
|
|
54
|
+
try { const r = await signedGet(`/tasks?project=${encodeURIComponent(project)}`, { session: me }); const t = r.json?.tasks || []; existing = new Set(t.map(x => x.title)); } catch {}
|
|
57
55
|
|
|
58
56
|
const ents = [...groups.entries()].sort((a, b) => a[1].latest - b[1].latest);
|
|
59
57
|
let posted = 0, skipped = 0;
|
|
@@ -68,8 +66,7 @@ for (const [theme, g] of ents) {
|
|
|
68
66
|
if (existing.has(title)) { skipped++; continue; }
|
|
69
67
|
if (dry) { console.log(`+ [${new Date(g.latest).toISOString().slice(0, 10)}] ${theme.padEnd(20)} ${g.commits.length}c ${title.slice(0, 64)}`); posted++; continue; }
|
|
70
68
|
try {
|
|
71
|
-
await
|
|
72
|
-
body: JSON.stringify({ project, title, status: "done", phase: theme, source: "git", ts: g.latest, assignee: me, by: me }) });
|
|
69
|
+
await signedPost("/task", { project, title, status: "done", phase: theme, source: "git", ts: g.latest, assignee: me, by: me }, { session: me });
|
|
73
70
|
posted++;
|
|
74
71
|
} catch (e) { console.error(`post failed for "${title}": ${e.message}`); }
|
|
75
72
|
}
|
package/bin/reconcile.mjs
CHANGED
|
@@ -14,11 +14,10 @@ import { homedir } from "node:os";
|
|
|
14
14
|
import { execSync, spawnSync } from "node:child_process";
|
|
15
15
|
import { resolveProject } from "../lib/project.mjs";
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
}
|
|
17
|
+
// Signed via the shared client (2026-07-31, agent-UX audit): the unsigned POST /task/update was
|
|
18
|
+
// rejected outright under enforce and merely flagged under warn; the hand-rolled relayUrl missed
|
|
19
|
+
// the per-project hubs map. signedGet/signedPost resolve the cwd project's hub + sign per call.
|
|
20
|
+
import { relayUrl, signedGet, signedPost } from "../hooks/lib/api.mjs";
|
|
22
21
|
function parseDur(s, def) {
|
|
23
22
|
if (!s) return def;
|
|
24
23
|
const m = String(s).match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)?$/i);
|
|
@@ -38,16 +37,16 @@ const doIt = has("--yes", "-y");
|
|
|
38
37
|
const difficulty = ["easy", "medium", "hard"].includes(val("--difficulty", "-d")) ? val("--difficulty", "-d") : "medium";
|
|
39
38
|
const dir = process.cwd();
|
|
40
39
|
const project = resolveProject(dir);
|
|
41
|
-
const url = relayUrl();
|
|
40
|
+
const url = relayUrl(project);
|
|
42
41
|
|
|
43
42
|
async function tasks() {
|
|
44
|
-
const r = await
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
const r = await signedGet(`/tasks?project=${encodeURIComponent(project)}`, { timeoutMs: 6000 });
|
|
44
|
+
if (!r.ok) return [];
|
|
45
|
+
const j = r.json;
|
|
46
|
+
return Array.isArray(j) ? j : (j?.tasks || j?.cards || []);
|
|
47
47
|
}
|
|
48
48
|
async function move(id, status) {
|
|
49
|
-
await
|
|
50
|
-
body: JSON.stringify({ id, status, by: "reconcile" }), signal: AbortSignal.timeout(4000) }).catch(() => {});
|
|
49
|
+
await signedPost("/task/update", { id, status, by: "reconcile" }, { timeoutMs: 4000 }).catch(() => {});
|
|
51
50
|
}
|
|
52
51
|
// the memory record for THIS project (Claude Code stores it per encoded-cwd); optional context.
|
|
53
52
|
function memoryExcerpt() {
|
package/bin/recost.mjs
CHANGED
|
@@ -10,11 +10,8 @@ import { join } from "node:path";
|
|
|
10
10
|
import { homedir } from "node:os";
|
|
11
11
|
import { scanSubagentCosts } from "../lib/subagent-scan.mjs";
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
try { const c = join(homedir(), ".agent-bus", "config.json"); if (existsSync(c)) { const u = JSON.parse(readFileSync(c, "utf8")).url; if (u) return u; } } catch {}
|
|
16
|
-
return "http://127.0.0.1:4477";
|
|
17
|
-
}
|
|
13
|
+
// Signed via the shared client (2026-07-31, agent-UX audit): unsigned POST rejected under enforce.
|
|
14
|
+
import { relayUrl, signedPost } from "../hooks/lib/api.mjs";
|
|
18
15
|
|
|
19
16
|
const dry = process.argv.includes("--dry-run");
|
|
20
17
|
const asJson = process.argv.includes("--json");
|
|
@@ -35,10 +32,11 @@ if (dry) {
|
|
|
35
32
|
}
|
|
36
33
|
|
|
37
34
|
let result;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
35
|
+
{
|
|
36
|
+
const r = await signedPost("/subagent-recost", { entries: allEntries }, { timeoutMs: 15000 });
|
|
37
|
+
if (!r.ok) { console.error(`recost failed: hub ${r.status} at ${relayUrl()}`); process.exit(1); }
|
|
38
|
+
result = r.json;
|
|
39
|
+
}
|
|
42
40
|
|
|
43
41
|
const projs = (result?.projects || []);
|
|
44
42
|
const seeded = projs.filter(p => !p.skipped);
|
package/bin/relay-watch.mjs
CHANGED
|
@@ -2,22 +2,24 @@
|
|
|
2
2
|
// relay-watch — a live feed of the bus via SSE (true push, no polling). Run in a terminal
|
|
3
3
|
// to watch sessions talk in real time, or to monitor a presence/status board.
|
|
4
4
|
// node bin/relay-watch.mjs [session] (default: "all" — see every message)
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
// Signed + per-project hub (2026-07-31, agent-UX audit): the hand-rolled relayUrl() here read
|
|
6
|
+
// only the global config `url` (wrong hub for a migrated project) and fetched unsigned (dead
|
|
7
|
+
// 401 under RELAY_AUTH=enforce). Now: canonical resolver + this session's keypair on every call.
|
|
8
|
+
import { resolveProject, hostId, resolveHub } from "../lib/project.mjs";
|
|
9
|
+
import { loadOrCreate } from "../lib/identity.mjs";
|
|
10
|
+
import { sfetch, sfetchJson } from "../lib/signed-fetch.mjs";
|
|
8
11
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const URL_BASE = relayUrl();
|
|
12
|
+
const PROJECT = resolveProject(process.cwd());
|
|
13
|
+
const ME = process.env.RELAY_SESSION
|
|
14
|
+
|| (process.env.RELAY_AGENT ? `${process.env.RELAY_AGENT}:${PROJECT}` : `${hostId()}:${PROJECT}`);
|
|
15
|
+
const identity = loadOrCreate(ME, "agent");
|
|
16
|
+
const URL_BASE = resolveHub(PROJECT);
|
|
15
17
|
const SESSION = process.argv[2] || "all";
|
|
16
18
|
const t = () => new Date().toLocaleTimeString();
|
|
17
19
|
|
|
18
20
|
async function showPeers() {
|
|
19
21
|
try {
|
|
20
|
-
const { peers } = await (await
|
|
22
|
+
const { peers } = await (await sfetchJson(`${URL_BASE}/peers`, { method: "GET", identity })).json();
|
|
21
23
|
const live = peers.filter(p => p.online);
|
|
22
24
|
console.log(`\n live sessions (${live.length}):`);
|
|
23
25
|
for (const p of live) console.log(` 🟢 ${p.session}${p.status ? ` — ${p.status}` : ""}`);
|
|
@@ -30,7 +32,7 @@ async function watch() {
|
|
|
30
32
|
await showPeers();
|
|
31
33
|
for (;;) {
|
|
32
34
|
try {
|
|
33
|
-
const r = await
|
|
35
|
+
const r = await sfetch(`${URL_BASE}/stream?session=${encodeURIComponent(SESSION)}`, { headers: { accept: "text/event-stream" } }, identity);
|
|
34
36
|
if (!r.ok || !r.body) throw new Error(`stream ${r.status}`);
|
|
35
37
|
let buf = "";
|
|
36
38
|
const dec = new TextDecoder();
|
package/bin/statusline.mjs
CHANGED
|
@@ -8,19 +8,18 @@ import { readFileSync, existsSync } from "node:fs";
|
|
|
8
8
|
import { join, basename } from "node:path";
|
|
9
9
|
import { homedir, hostname } from "node:os";
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
return "http://127.0.0.1:4477";
|
|
15
|
-
}
|
|
11
|
+
// Signed read via the shared client (2026-07-31, agent-UX audit): unsigned /peers is a dead 401
|
|
12
|
+
// under enforce, which painted "trantor offline" while the hub was fine.
|
|
13
|
+
import { sessionContext, signedGet } from "../hooks/lib/api.mjs";
|
|
16
14
|
async function main() {
|
|
17
15
|
let stdin = ""; try { for await (const c of process.stdin) stdin += c; } catch {}
|
|
18
16
|
let cwd = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
19
17
|
try { const j = JSON.parse(stdin || "{}"); cwd = j.cwd || j.workspace?.current_dir || cwd; } catch {}
|
|
20
|
-
const me = process.env.RELAY_SESSION ||
|
|
18
|
+
const me = process.env.RELAY_SESSION || sessionContext(cwd).session;
|
|
21
19
|
try {
|
|
22
|
-
const r = await
|
|
23
|
-
|
|
20
|
+
const r = await signedGet("/peers", { timeoutMs: 800, session: me });
|
|
21
|
+
if (!r.ok) throw new Error(`hub ${r.status}`);
|
|
22
|
+
const { peers } = r.json;
|
|
24
23
|
const live = peers.filter(p => p.online && p.session !== me).length;
|
|
25
24
|
process.stdout.write(`\x1b[38;5;43m● trantor\x1b[0m \x1b[2m· ${live} other${live === 1 ? "" : "s"} live\x1b[0m`);
|
|
26
25
|
} catch {
|
package/bin/sweep.mjs
CHANGED
|
@@ -12,11 +12,11 @@ import { join } from "node:path";
|
|
|
12
12
|
import { homedir } from "node:os";
|
|
13
13
|
import { resolveProject } from "../lib/project.mjs";
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
15
|
+
// Signed as the OWNER (2026-07-31, agent-UX audit): /sweep is an owner endpoint — an unsigned
|
|
16
|
+
// POST is rejected under enforce. Sign with config.ownerIdentity, same pattern as bin/policy.mjs.
|
|
17
|
+
import { relayUrl } from "../hooks/lib/api.mjs";
|
|
18
|
+
import { loadOrCreate } from "../lib/identity.mjs";
|
|
19
|
+
import { sfetchJson } from "../lib/signed-fetch.mjs";
|
|
20
20
|
function parseDur(s, def) {
|
|
21
21
|
if (!s) return def;
|
|
22
22
|
const m = String(s).match(/^(\d+(?:\.\d+)?)\s*(ms|s|m|h|d)?$/i);
|
|
@@ -33,12 +33,15 @@ const olderMs = parseDur(val("--older", "-o"), 30 * 60 * 1000);
|
|
|
33
33
|
const doIt = has("--yes", "-y");
|
|
34
34
|
const allProjects = has("--all-projects", "--all");
|
|
35
35
|
const project = allProjects ? null : resolveProject(process.cwd());
|
|
36
|
-
const url = relayUrl();
|
|
36
|
+
const url = relayUrl(project || undefined);
|
|
37
|
+
let ownerName = "admin";
|
|
38
|
+
try { ownerName = JSON.parse(readFileSync(join(homedir(), ".agent-bus", "config.json"), "utf8")).ownerIdentity || "admin"; } catch {}
|
|
39
|
+
const ownerId = loadOrCreate(ownerName, "human");
|
|
37
40
|
|
|
38
41
|
async function sweep(dryRun) {
|
|
39
42
|
const body = { olderMs, dryRun, by: "cli-sweep" };
|
|
40
43
|
if (project) body.project = project;
|
|
41
|
-
const r = await
|
|
44
|
+
const r = await sfetchJson(`${url}/sweep`, { identity: ownerId, payload: body, signal: AbortSignal.timeout(6000) });
|
|
42
45
|
return r.json();
|
|
43
46
|
}
|
|
44
47
|
|
package/hooks/inbox-deliver.mjs
CHANGED
|
@@ -24,7 +24,7 @@ import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
|
24
24
|
import { join } from "node:path";
|
|
25
25
|
import { homedir } from "node:os";
|
|
26
26
|
import { resolveProject, hostId } from "../lib/project.mjs";
|
|
27
|
-
import {
|
|
27
|
+
import { signedGet } from "./lib/api.mjs"; // signed: enforce hubs 401 unsigned reads — unsigned, T1 delivery is silently dead
|
|
28
28
|
|
|
29
29
|
const POLL_MS = Number(process.env.RELAY_INBOX_POLL_MS || 4000);
|
|
30
30
|
const FETCH_TIMEOUT_MS = Number(process.env.RELAY_INBOX_TIMEOUT_MS || 1500);
|
|
@@ -34,7 +34,7 @@ const FETCH_TIMEOUT_MS = Number(process.env.RELAY_INBOX_TIMEOUT_MS || 1500);
|
|
|
34
34
|
function sanitize(s) { return String(s == null ? "" : s).replace(/[\x00-\x1f\x7f-\x9f]/g, " "); }
|
|
35
35
|
|
|
36
36
|
async function getInbox(session, since) {
|
|
37
|
-
const { ok, json } = await
|
|
37
|
+
const { ok, json } = await signedGet(`/inbox?session=${encodeURIComponent(session)}&since=${since}`, { timeoutMs: FETCH_TIMEOUT_MS, session });
|
|
38
38
|
if (!ok || !json) throw new Error("hub unreachable");
|
|
39
39
|
return json; // { messages: [...], cursor }
|
|
40
40
|
}
|
package/hooks/sessionstart.mjs
CHANGED
|
@@ -71,11 +71,14 @@ function readStdin() {
|
|
|
71
71
|
process.stdin.on("data", c => (d += c)); process.stdin.on("end", () => res(d));
|
|
72
72
|
setTimeout(() => res(d), 100); });
|
|
73
73
|
}
|
|
74
|
-
// Hub reads/writes through the shared client (TDD §8)
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
|
|
74
|
+
// Hub reads/writes through the shared client (TDD §8), ALL signed (Ed25519). Writes via signedPost
|
|
75
|
+
// close the self-asserted `from` hole; reads via signedGet survive RELAY_AUTH=enforce, which 401s
|
|
76
|
+
// unsigned reads (unsigned, the roster/handoff injection was silently dead on the remote hub — the
|
|
77
|
+
// 2026-07-30 agent-UX gap). The hub scope-filters signed reads to this identity's grants; for a
|
|
78
|
+
// session reading its own project + declared links that is the intended shape. Each resolves to
|
|
79
|
+
// {ok,status,json}; a down hub returns {ok:false,json:null} and the caller's catch keeps the
|
|
80
|
+
// session alive (fail-open contract, acceptance §9 #10).
|
|
81
|
+
async function jget(u, session) { const r = await signedGet(u, { timeoutMs: 2500, session }); return r.ok ? (r.json || {}) : {}; }
|
|
79
82
|
async function jpost(u, b, session) { return (await signedPost(u, b, { session, timeoutMs: 2500 })).ok; }
|
|
80
83
|
|
|
81
84
|
// Strip control chars from untrusted injected text so the hook's JSON stdout (which
|
package/hooks/stop-inbox.mjs
CHANGED
|
@@ -25,7 +25,7 @@ import { readFileSync, writeFileSync, existsSync } from "node:fs";
|
|
|
25
25
|
import { join } from "node:path";
|
|
26
26
|
import { homedir } from "node:os";
|
|
27
27
|
import { resolveProject, hostId } from "../lib/project.mjs";
|
|
28
|
-
import {
|
|
28
|
+
import { signedGet } from "./lib/api.mjs"; // signed: enforce hubs 401 unsigned reads — unsigned, T2 delivery is silently dead
|
|
29
29
|
|
|
30
30
|
const FETCH_TIMEOUT_MS = Number(process.env.RELAY_STOP_TIMEOUT_MS || 1500);
|
|
31
31
|
|
|
@@ -75,7 +75,7 @@ async function main() {
|
|
|
75
75
|
let messages = [];
|
|
76
76
|
try {
|
|
77
77
|
// PEEK: look without claiming delivery. We may yet decide to let the stop through.
|
|
78
|
-
const peek = await
|
|
78
|
+
const peek = await signedGet(`/inbox?session=${encodeURIComponent(session)}&since=${cursor}&peek=1`, { timeoutMs: FETCH_TIMEOUT_MS, session });
|
|
79
79
|
if (!peek.ok) return allow();
|
|
80
80
|
messages = peek.json?.messages || [];
|
|
81
81
|
} catch { return allow(); } // hub down — never trap the session
|
|
@@ -86,7 +86,7 @@ async function main() {
|
|
|
86
86
|
// Committed now: claim delivery for real so neither inbox-deliver nor the deferred waker repeats it.
|
|
87
87
|
let next = cursor;
|
|
88
88
|
try {
|
|
89
|
-
const claim = await
|
|
89
|
+
const claim = await signedGet(`/inbox?session=${encodeURIComponent(session)}&since=${cursor}`, { timeoutMs: FETCH_TIMEOUT_MS, session });
|
|
90
90
|
if (claim.ok) next = claim.json?.cursor || cursor;
|
|
91
91
|
} catch {}
|
|
92
92
|
try { writeFileSync(cursorFile, String(next)); } catch {}
|
package/mcp.mjs
CHANGED
|
@@ -21,7 +21,7 @@ const PROJECT = resolveProject(process.env.CLAUDE_PROJECT_DIR || process.cwd());
|
|
|
21
21
|
// Hub URL is PER-PROJECT (TDD §12.1): RELAY_URL env → config.json hubs[PROJECT] → legacy
|
|
22
22
|
// global `url` → local default. A project lives on exactly one hub; codependent projects
|
|
23
23
|
// must share one, so both are pinned to the same hub via `trantor hub set`.
|
|
24
|
-
const URL_BASE = resolveHub(PROJECT);
|
|
24
|
+
const URL_BASE = resolveHub(PROJECT); // boot-time snapshot: startup log only — every api() call re-resolves
|
|
25
25
|
// Identity: RELAY_SESSION wins; else RELAY_AGENT ("codex", "kimi", …) brands the session per-project
|
|
26
26
|
// (set it once in the CLI's global MCP config — works in every project); else hostname:project.
|
|
27
27
|
const SESSION = process.env.RELAY_SESSION
|
|
@@ -63,7 +63,7 @@ const server = new McpServer({ name: "trantor", version: "0.1.0" });
|
|
|
63
63
|
|
|
64
64
|
server.tool("relay_whoami", "Show this session's relay identity, project, and the hub URL.", {}, async () => {
|
|
65
65
|
await api("POST", "/register", { session: SESSION, project: PROJECT }).catch(() => {});
|
|
66
|
-
return { content: [{ type: "text", text: `session=${SESSION}\nproject=${PROJECT}\nhub=${
|
|
66
|
+
return { content: [{ type: "text", text: `session=${SESSION}\nproject=${PROJECT}\nhub=${resolveHub(PROJECT)}` }] };
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
server.tool("relay_task_add", "Add a Kanban card to a project's board on the dashboard (what you're about to work on). Defaults: THIS project, assigned to you, status 'todo'. Pass `project` to target another board — e.g. when you orchestrate a crew that runs in a different directory than the one you launched Claude from. Keep the team's progress visible.",
|