@staix/agent-hub 0.19.0 → 0.20.0
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/CHANGELOG.md +11 -0
- package/README.md +2 -2
- package/docs/agent-notes/adapters.md +3 -2
- package/docs/agent-notes/bus.md +1 -1
- package/docs/agent-notes/daemon.md +6 -2
- package/docs/agent-notes/local-worker.md +1 -0
- package/docs/agent-notes/tasks.md +4 -2
- package/docs/agent-notes/tests.md +4 -3
- package/docs/dogfood.md +4 -1
- package/docs/memory.md +1 -1
- package/docs/operations.md +40 -8
- package/docs/quickstart.md +7 -3
- package/docs/sdlc.md +12 -0
- package/docs/security.md +4 -4
- package/docs/specs/2026-09-19-agent-hub-design.md +85 -3
- package/docs/verification/2026-10-27-0.20.0.md +75 -0
- package/docs/verified.json +24 -16
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +103 -7
- package/src/adapters/acp.ts +8 -0
- package/src/adapters/channel-peer.ts +7 -5
- package/src/adapters/claude-implementer.ts +1 -0
- package/src/adapters/codex-appserver.ts +11 -1
- package/src/adapters/conformance/enrollment.ts +4 -4
- package/src/adapters/conformance/factories.ts +2 -1
- package/src/adapters/conformance/kit.ts +10 -0
- package/src/adapters/local-worker.ts +2 -0
- package/src/adapters/pi.ts +7 -0
- package/src/adapters/reviewer-print.ts +1 -0
- package/src/hub/ci-watch.ts +140 -0
- package/src/hub/claude-launch-proof.ts +67 -2
- package/src/hub/config-apply.ts +2 -2
- package/src/hub/config-trust.ts +1 -0
- package/src/hub/daemon.ts +86 -79
- package/src/hub/peers.ts +34 -0
- package/src/hub/settings.ts +1 -0
- package/src/hub/task-shell-default.ts +3 -2
- package/src/hub/tasks.ts +139 -7
- package/src/local/deny.ts +21 -0
- package/src/local/sandbox.ts +3 -1
|
@@ -85,7 +85,8 @@ function memoryGitEnv(env = process.env, options = {}) {
|
|
|
85
85
|
}
|
|
86
86
|
function memoryGitSync(args, options) {
|
|
87
87
|
const argv = memoryGitArgv(args);
|
|
88
|
-
|
|
88
|
+
const timeout = gitTimeoutContext?.getStore();
|
|
89
|
+
return childProcess().spawnSync(argv[0], argv.slice(1), { ...timeout !== undefined ? { timeout, killSignal: "SIGKILL" } : {}, ...options, env: memoryGitEnv(options.env ?? process.env) });
|
|
89
90
|
}
|
|
90
91
|
function processSignature(pid) {
|
|
91
92
|
try {
|
|
@@ -121,7 +122,7 @@ var require2, processes, childProcess = () => processes ??= require2("node:child
|
|
|
121
122
|
const parent = dirname(absolute);
|
|
122
123
|
return parent === absolute ? absolute : join(canonical(parent), basename(absolute));
|
|
123
124
|
}
|
|
124
|
-
}, DENY_SEGMENTS, DENY_NAMES, denyNamePatterns, hasSegment = (rel, seg) => `/${rel}/`.includes(`/${seg}/`), SAFE_CONFIG, CREDENTIAL_KEYS, memoryGitArgv = (args, opts = {}) => ["git", "--no-pager", ...SAFE_CONFIG.filter((c) => !opts.credentials || !CREDENTIAL_KEYS.has(c)).flatMap((c) => ["-c", c]), ...args],
|
|
125
|
+
}, DENY_SEGMENTS, DENY_NAMES, denyNamePatterns, hasSegment = (rel, seg) => `/${rel}/`.includes(`/${seg}/`), SAFE_CONFIG, CREDENTIAL_KEYS, memoryGitArgv = (args, opts = {}) => ["git", "--no-pager", ...SAFE_CONFIG.filter((c) => !opts.credentials || !CREDENTIAL_KEYS.has(c)).flatMap((c) => ["-c", c]), ...args], gitTimeoutContext;
|
|
125
126
|
var init_platform = __esm(() => {
|
|
126
127
|
require2 = createRequire(import.meta.url);
|
|
127
128
|
DENY_SEGMENTS = [".maru/secrets", ".agenthub/state", ".agenthub/archive"];
|
|
@@ -1415,6 +1416,7 @@ var init_reviewer_print = __esm(() => {
|
|
|
1415
1416
|
ReviewerPrintPeer = class ReviewerPrintPeer extends BasePeer {
|
|
1416
1417
|
opts;
|
|
1417
1418
|
hubNative = true;
|
|
1419
|
+
hubOwned = true;
|
|
1418
1420
|
generation = 0;
|
|
1419
1421
|
child;
|
|
1420
1422
|
childCleanup;
|
|
@@ -2943,6 +2945,9 @@ var init_acp = __esm(() => {
|
|
|
2943
2945
|
};
|
|
2944
2946
|
AcpPeer = class AcpPeer extends BasePeer {
|
|
2945
2947
|
opts;
|
|
2948
|
+
deliveryOnly = true;
|
|
2949
|
+
hubOwned = true;
|
|
2950
|
+
form = "headless";
|
|
2946
2951
|
permissionMode;
|
|
2947
2952
|
nativePolicy;
|
|
2948
2953
|
modeUnknown = false;
|
|
@@ -3058,6 +3063,10 @@ var init_acp = __esm(() => {
|
|
|
3058
3063
|
recoveryMetadata() {
|
|
3059
3064
|
return { launch: { kind: "acp", ...this.opts.launchModel ? { model: this.opts.launchModel } : {} }, ...this.sessionId ? { sessionId: this.sessionId } : {} };
|
|
3060
3065
|
}
|
|
3066
|
+
get nativeSession() {
|
|
3067
|
+
const session = this.recoveryMetadata().sessionId;
|
|
3068
|
+
return typeof session === "string" ? session : undefined;
|
|
3069
|
+
}
|
|
3061
3070
|
async start() {
|
|
3062
3071
|
if (this.opts.requireOwnedGroup && this.proc)
|
|
3063
3072
|
throw new Error("Configured ACP still has a process; stop it before starting another");
|
|
@@ -3664,6 +3673,7 @@ var init_codex_appserver = __esm(() => {
|
|
|
3664
3673
|
TRACKED = new Set(["thread/start", "thread/resume"]);
|
|
3665
3674
|
CodexPeer = class CodexPeer extends BasePeer {
|
|
3666
3675
|
opts;
|
|
3676
|
+
frontendProxy;
|
|
3667
3677
|
permissionMode;
|
|
3668
3678
|
actionWaits = new Map;
|
|
3669
3679
|
get actionGated() {
|
|
@@ -3708,6 +3718,17 @@ var init_codex_appserver = __esm(() => {
|
|
|
3708
3718
|
super(id, opts.watchdogMs);
|
|
3709
3719
|
this.opts = opts;
|
|
3710
3720
|
this.permissionMode = opts.permissionMode ?? "ask";
|
|
3721
|
+
const owner = this;
|
|
3722
|
+
this.frontendProxy = {
|
|
3723
|
+
get url() {
|
|
3724
|
+
return owner.proxyUrl;
|
|
3725
|
+
},
|
|
3726
|
+
get attached() {
|
|
3727
|
+
return owner.proxyAttached;
|
|
3728
|
+
},
|
|
3729
|
+
setStartupPermissionMode: this.setStartupPermissionMode.bind(this),
|
|
3730
|
+
clearPermissionMode: this.clearPermissionMode.bind(this)
|
|
3731
|
+
};
|
|
3711
3732
|
}
|
|
3712
3733
|
recoveryMetadata() {
|
|
3713
3734
|
return {
|
|
@@ -3718,6 +3739,9 @@ var init_codex_appserver = __esm(() => {
|
|
|
3718
3739
|
get thread() {
|
|
3719
3740
|
return this.threadId;
|
|
3720
3741
|
}
|
|
3742
|
+
get nativeSession() {
|
|
3743
|
+
return this.threadId || undefined;
|
|
3744
|
+
}
|
|
3721
3745
|
get proxyUrl() {
|
|
3722
3746
|
return `ws://127.0.0.1:${this.server?.port ?? this.opts.proxyPort}`;
|
|
3723
3747
|
}
|
|
@@ -4324,6 +4348,9 @@ ${renderDigest(envs, false)}`;
|
|
|
4324
4348
|
link.up.send(JSON.stringify({ method: "thread/revert", id, params: { threadId: this.threadId, beforeTurnId: turnId } }));
|
|
4325
4349
|
});
|
|
4326
4350
|
}
|
|
4351
|
+
revertTurn(nativeTurn) {
|
|
4352
|
+
return this.revert(nativeTurn);
|
|
4353
|
+
}
|
|
4327
4354
|
readUsage() {
|
|
4328
4355
|
const link = this.link;
|
|
4329
4356
|
if (!this.opts.onUsage || !link || link.up.readyState !== WebSocket.OPEN)
|
|
@@ -5902,6 +5929,15 @@ function hubWriteRegexes(root, gitDirs = [], moduleGit = [], worktreesRoot) {
|
|
|
5902
5929
|
...external
|
|
5903
5930
|
];
|
|
5904
5931
|
}
|
|
5932
|
+
function objectStoreRules(common) {
|
|
5933
|
+
const objects = `${common}/objects`, o = sbplEscape(objects);
|
|
5934
|
+
const re = (s) => `(regex ${sbplString(s)})`;
|
|
5935
|
+
return [
|
|
5936
|
+
`(deny file-write* (subpath ${sbplString(`${objects}/info`)}) (subpath ${sbplString(`${objects}/pack`)}) ${[objects, `${objects}/info`, `${objects}/pack`].map((p) => `(literal ${sbplString(p)})`).join(" ")})`,
|
|
5937
|
+
`(deny file-write-unlink ${re(`^${o}/[0-9a-f][0-9a-f]$`)})`,
|
|
5938
|
+
`(deny file-write-data file-write-unlink file-write-mode file-write-flags ${re(`^${o}/[0-9a-f][0-9a-f]/[0-9a-f]+$`)})`
|
|
5939
|
+
];
|
|
5940
|
+
}
|
|
5905
5941
|
function denyRegexes(root, extra = [], withNames = true) {
|
|
5906
5942
|
const names = DENY_NAMES.map((re) => {
|
|
5907
5943
|
const body = re.startsWith("^") ? re.slice(1) : `[^/]*${re}`;
|
|
@@ -6045,6 +6081,7 @@ function profile(cwd, network, readAllow = [], deny = [], readOnly = []) {
|
|
|
6045
6081
|
"(deny file-write*)",
|
|
6046
6082
|
...readOnly.length ? ['(allow file-write* (literal "/dev/null"))'] : [`(allow file-write* (subpath ${q(root)}) (regex #"^/dev/") ${gitDirs.map((d) => `(subpath ${q(d)})`).join(" ")})`],
|
|
6047
6083
|
`(deny file-write* ${hubWriteRegexes(root, gitDirs, moduleGit, worktrees).join(" ")})`,
|
|
6084
|
+
...worktrees ? objectStoreRules(dirname9(worktrees)) : [],
|
|
6048
6085
|
`(deny file-read* file-write* ${creds.map((c) => `(subpath ${q(join12(home, c))})`).join(" ")})`,
|
|
6049
6086
|
`(deny file-read* file-write* ${denyRegexes(root, deny).join(" ")})`,
|
|
6050
6087
|
...network ? [`(allow file-read* ${CA_BUNDLES.map((p) => `(literal ${q(p)})`).join(" ")} (regex ${q("/certifi/cacert\\.pem$")}))`, `(deny file-read* file-write* ${places})`] : []
|
|
@@ -6464,6 +6501,7 @@ var init_pi = __esm(() => {
|
|
|
6464
6501
|
opts;
|
|
6465
6502
|
preflight;
|
|
6466
6503
|
hubNative = true;
|
|
6504
|
+
hubOwned = true;
|
|
6467
6505
|
proc;
|
|
6468
6506
|
server;
|
|
6469
6507
|
buffer = "";
|
|
@@ -6577,6 +6615,10 @@ var init_pi = __esm(() => {
|
|
|
6577
6615
|
const file = this.sessionFile && (exists || !empty) ? this.sessionFile : undefined;
|
|
6578
6616
|
return { launch: { kind: "pi", cwd: this.opts.cwd, mode: this.opts.mode, backend: this.opts.backend, ...this.opts.model ? { model: this.opts.model } : {}, ...file ? { sessionFile: file } : {} }, ...this.sessionId ? { sessionId: this.sessionId } : {}, ...file ? { sessionFile: file } : {} };
|
|
6579
6617
|
}
|
|
6618
|
+
get nativeSession() {
|
|
6619
|
+
const session = this.recoveryMetadata().sessionId;
|
|
6620
|
+
return typeof session === "string" ? session : undefined;
|
|
6621
|
+
}
|
|
6580
6622
|
async captureResume() {
|
|
6581
6623
|
if (this.state === "offline") {
|
|
6582
6624
|
if (this.canReuseVerifiedEmptyResume())
|
|
@@ -6941,6 +6983,9 @@ var init_pi = __esm(() => {
|
|
|
6941
6983
|
get mode() {
|
|
6942
6984
|
return this.opts.mode;
|
|
6943
6985
|
}
|
|
6986
|
+
get form() {
|
|
6987
|
+
return this.opts.mode;
|
|
6988
|
+
}
|
|
6944
6989
|
async detachForRoster() {
|
|
6945
6990
|
if (this.opts.mode !== "tui")
|
|
6946
6991
|
throw new Error("Pi roster detach requires a personal TUI");
|
|
@@ -10319,6 +10364,8 @@ var init_local_worker = __esm(() => {
|
|
|
10319
10364
|
LocalPeer = class LocalPeer extends BasePeer {
|
|
10320
10365
|
opts;
|
|
10321
10366
|
hubNative = true;
|
|
10367
|
+
deliveryOnly = true;
|
|
10368
|
+
hubOwned = true;
|
|
10322
10369
|
history = [];
|
|
10323
10370
|
sessionId = `agent-hub-local-${randomUUID6()}`;
|
|
10324
10371
|
routes;
|
|
@@ -11015,6 +11062,7 @@ var init_claude_implementer = __esm(() => {
|
|
|
11015
11062
|
ClaudeImplementerPeer = class ClaudeImplementerPeer extends BasePeer {
|
|
11016
11063
|
options;
|
|
11017
11064
|
hubNative = true;
|
|
11065
|
+
hubOwned = true;
|
|
11018
11066
|
generation = 0;
|
|
11019
11067
|
owned;
|
|
11020
11068
|
running;
|
|
@@ -13995,8 +14043,49 @@ var init_lifecycle = __esm(() => {
|
|
|
13995
14043
|
init_shard();
|
|
13996
14044
|
});
|
|
13997
14045
|
|
|
14046
|
+
// node_modules/@staix/agent-mem/src/memory/search-score.ts
|
|
14047
|
+
function sql(expr, rank) {
|
|
14048
|
+
const get = (part) => sql(part, rank);
|
|
14049
|
+
switch (expr.op) {
|
|
14050
|
+
case "rank":
|
|
14051
|
+
return rank;
|
|
14052
|
+
case "tier":
|
|
14053
|
+
return `CASE tier ${Object.entries(TIER_WEIGHTS).map(([name, value]) => `WHEN '${name}' THEN ${real(value)}`).join(" ")} ELSE ${real(DEFAULT_TIER_WEIGHT)} END`;
|
|
14054
|
+
case "age":
|
|
14055
|
+
return `((julianday(?) - julianday(at)) / ${real(MONTH_DAYS)})`;
|
|
14056
|
+
case "constant":
|
|
14057
|
+
return real(expr.value);
|
|
14058
|
+
case "negative":
|
|
14059
|
+
return `(-(${get(expr.value)}))`;
|
|
14060
|
+
case "reciprocal":
|
|
14061
|
+
return `(1.0 / ${get(expr.value)})`;
|
|
14062
|
+
case "add":
|
|
14063
|
+
return `(${get(expr.left)} + ${get(expr.right)})`;
|
|
14064
|
+
case "multiply":
|
|
14065
|
+
return expr.right.op === "reciprocal" ? `(${get(expr.left)} / ${get(expr.right.value)})` : `(${get(expr.left)} * ${get(expr.right)})`;
|
|
14066
|
+
case "max":
|
|
14067
|
+
return `max(${get(expr.left)}, ${get(expr.right)})`;
|
|
14068
|
+
}
|
|
14069
|
+
}
|
|
14070
|
+
function rankedScoreSql(rank = "bm25(docs)") {
|
|
14071
|
+
return sql(SCORE, rank);
|
|
14072
|
+
}
|
|
14073
|
+
var MONTH_DAYS = 30, TIER_WEIGHTS, DEFAULT_TIER_WEIGHT = 0.9, TIER_FACTOR, DENOMINATOR, RECENCY_FACTOR, SCORE, real = (value) => Number.isInteger(value) ? `${value}.0` : String(value);
|
|
14074
|
+
var init_search_score = __esm(() => {
|
|
14075
|
+
TIER_WEIGHTS = { long: 1, capture: 0.5 };
|
|
14076
|
+
TIER_FACTOR = { op: "tier" };
|
|
14077
|
+
DENOMINATOR = { op: "add", left: { op: "constant", value: 1 }, right: { op: "max", left: { op: "constant", value: 0 }, right: { op: "age" } } };
|
|
14078
|
+
RECENCY_FACTOR = { op: "reciprocal", value: DENOMINATOR };
|
|
14079
|
+
SCORE = { op: "multiply", left: { op: "multiply", left: { op: "negative", value: { op: "rank" } }, right: TIER_FACTOR }, right: RECENCY_FACTOR };
|
|
14080
|
+
});
|
|
14081
|
+
|
|
13998
14082
|
// node_modules/@staix/agent-mem/src/memory/search-fts.ts
|
|
13999
|
-
var
|
|
14083
|
+
var SCREENED_SCORE, SCREENED_SUBSTRING_SCORE;
|
|
14084
|
+
var init_search_fts = __esm(() => {
|
|
14085
|
+
init_search_score();
|
|
14086
|
+
SCREENED_SCORE = rankedScoreSql();
|
|
14087
|
+
SCREENED_SUBSTRING_SCORE = rankedScoreSql("-1.0");
|
|
14088
|
+
});
|
|
14000
14089
|
|
|
14001
14090
|
// node_modules/@staix/agent-mem/src/memory/capture-input.ts
|
|
14002
14091
|
var init_capture_input = __esm(() => {
|
|
@@ -14252,6 +14341,7 @@ var init_store = __esm(() => {
|
|
|
14252
14341
|
init_machine();
|
|
14253
14342
|
init_lifecycle();
|
|
14254
14343
|
init_machine();
|
|
14344
|
+
init_search_score();
|
|
14255
14345
|
init_search_fts();
|
|
14256
14346
|
init_shard();
|
|
14257
14347
|
init_capture();
|
|
@@ -21344,7 +21434,8 @@ var MACHINE_LOCAL = [
|
|
|
21344
21434
|
"local.read_allow",
|
|
21345
21435
|
"local.bash_network",
|
|
21346
21436
|
"local.network_allow",
|
|
21347
|
-
"terminal.open"
|
|
21437
|
+
"terminal.open",
|
|
21438
|
+
"tasks.auto_pull"
|
|
21348
21439
|
];
|
|
21349
21440
|
var ALL_MACHINE_LOCAL = [...MACHINE_LOCAL, "action_policy", "protect_instruction_files", "qwen_cmd", "jcode_cmd", "jcode_home", "jcode_provider_profile", "adapters", "adapter_modules", "text_projection", "instances"];
|
|
21350
21441
|
function configRefusal(cwd, name) {
|
|
@@ -21715,7 +21806,7 @@ var tool2 = (name, description, inputSchema, readOnlyHint = true) => ({
|
|
|
21715
21806
|
annotations: { readOnlyHint, destructiveHint: false, openWorldHint: false }
|
|
21716
21807
|
});
|
|
21717
21808
|
var MCP_TOOLS = [
|
|
21718
|
-
tool2("amem_search", "Search screened memory in this project. Capture results are recall, not curated evidence.", object6({ query: string(1000), limit, since: string(40), includeCapture: { type: "boolean" } }, ["query"])),
|
|
21809
|
+
tool2("amem_search", "Search screened memory in this project. Capture results are recall, not curated evidence.", object6({ query: string(1000), limit, since: string(40), includeCapture: { type: "boolean" }, explain: { type: "boolean" } }, ["query"])),
|
|
21719
21810
|
tool2("amem_evidence", "Return a bounded bundle of cited, live curated entries for a question. Untrusted memory data only: no answer or model call. Capture, repo-only notes, pending candidates and model answers are not evidence.", object6({ question: string(1000), limit: { type: "integer", minimum: 1, maximum: 20 }, maxBytes: { type: "integer", minimum: 256, maximum: 8192 } }, ["question"])),
|
|
21720
21811
|
tool2("amem_show", "Show a screened memory entry in this project by its exact id or unique prefix.", object6({ id: string(80) }, ["id"])),
|
|
21721
21812
|
tool2("amem_timeline", "Read screened memory near an entry or time in this project.", object6({ id: string(80), since: string(40), limit })),
|
|
@@ -21874,7 +21965,7 @@ init_sdlc_metrics();
|
|
|
21874
21965
|
// package.json
|
|
21875
21966
|
var package_default = {
|
|
21876
21967
|
name: "@staix/agent-hub",
|
|
21877
|
-
version: "0.
|
|
21968
|
+
version: "0.20.0",
|
|
21878
21969
|
description: "Native multi-agent hub: Claude Code, Codex, Kimi Code, Pi and local inference as peers in one project",
|
|
21879
21970
|
license: "MIT",
|
|
21880
21971
|
type: "module",
|
|
@@ -21891,7 +21982,7 @@ var package_default = {
|
|
|
21891
21982
|
bun: ">=1.4.2"
|
|
21892
21983
|
},
|
|
21893
21984
|
dependencies: {
|
|
21894
|
-
"@staix/agent-mem": "0.
|
|
21985
|
+
"@staix/agent-mem": "0.11.0"
|
|
21895
21986
|
},
|
|
21896
21987
|
devDependencies: {
|
|
21897
21988
|
"@modelcontextprotocol/sdk": "^1.27.1",
|
|
@@ -22007,6 +22098,10 @@ init_registry();
|
|
|
22007
22098
|
// src/hub/task-sweep.ts
|
|
22008
22099
|
var GROOMING_CODES = new Set(["tests", "spec", "roster", "pull-only", "not-attached", "excluded", "owner-admission", "offline", "paused", "failing", "pii", "policy", "cancelled", "unavailable", "runner", "approvals", "quota", "dependency", "no-writes"]);
|
|
22009
22100
|
|
|
22101
|
+
// src/hub/ci-watch.ts
|
|
22102
|
+
init_sdlc();
|
|
22103
|
+
var CI_WATCH_DEADLINE_MS = 45 * 60000;
|
|
22104
|
+
|
|
22010
22105
|
// src/hub/tasks.ts
|
|
22011
22106
|
init_sdlc();
|
|
22012
22107
|
init_routing();
|
|
@@ -22016,6 +22111,7 @@ var QUIET_EVENTS = new Set(["answer", "reviewer changed", "idle sweep", "memory
|
|
|
22016
22111
|
QUIET_EVENTS.add("ready held");
|
|
22017
22112
|
QUIET_EVENTS.add("plan observed");
|
|
22018
22113
|
QUIET_EVENTS.add("planner changed");
|
|
22114
|
+
QUIET_EVENTS.add("shell branch bound");
|
|
22019
22115
|
var OWNERSHIP_EVENTS = new Set(["assigned", "escalated", "reassigned", "unassigned"]);
|
|
22020
22116
|
var GROOM_FYI_WINDOW_MS = 10 * 60000;
|
|
22021
22117
|
|
package/src/adapters/acp.ts
CHANGED
|
@@ -10,6 +10,7 @@ export { QwenIdentityError } from "./definitions/qwen-native.ts";
|
|
|
10
10
|
import { BasePeer } from "../hub/peers.ts";
|
|
11
11
|
import { peerChildEnv, stopOwnedProcess, trackGroup } from "../hub/child-process.ts";
|
|
12
12
|
import { AcpOwnedGroup } from "./acp-owned-group.ts";
|
|
13
|
+
import type { StartForm } from "./registry-types.ts";
|
|
13
14
|
|
|
14
15
|
export interface PermissionOption {
|
|
15
16
|
optionId: string;
|
|
@@ -163,6 +164,9 @@ function jsonObject(text: string | undefined): object | undefined {
|
|
|
163
164
|
|
|
164
165
|
/** ACP client (JSON-RPC 2.0, newline-delimited, over the child's stdio). One session, one prompt in flight. */
|
|
165
166
|
export class AcpPeer extends BasePeer {
|
|
167
|
+
readonly deliveryOnly: boolean = true;
|
|
168
|
+
readonly hubOwned: boolean = true;
|
|
169
|
+
readonly form: StartForm = "headless";
|
|
166
170
|
private permissionMode: PermissionMode;
|
|
167
171
|
private readonly nativePolicy: AcpOptions["nativePolicy"];
|
|
168
172
|
private modeUnknown = false;
|
|
@@ -256,6 +260,10 @@ export class AcpPeer extends BasePeer {
|
|
|
256
260
|
recoveryMetadata(): Record<string, unknown> {
|
|
257
261
|
return { launch: { kind: "acp", ...(this.opts.launchModel ? { model: this.opts.launchModel } : {}) }, ...(this.sessionId ? { sessionId: this.sessionId } : {}) };
|
|
258
262
|
}
|
|
263
|
+
get nativeSession(): string | undefined {
|
|
264
|
+
const session = this.recoveryMetadata().sessionId;
|
|
265
|
+
return typeof session === "string" ? session : undefined;
|
|
266
|
+
}
|
|
259
267
|
|
|
260
268
|
async start(): Promise<void> {
|
|
261
269
|
if (this.opts.requireOwnedGroup && this.proc) throw new Error("Configured ACP still has a process; stop it before starting another");
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BasePeer, type PeerAdapter } from "../hub/peers.ts";
|
|
2
2
|
import type { Envelope } from "../hub/envelope.ts";
|
|
3
3
|
import { definitionOf } from "./registry.ts";
|
|
4
|
+
import type { StartForm } from "./registry-types.ts";
|
|
4
5
|
|
|
5
6
|
/** Only the control socket operations the peer owns; authentication remains in the daemon. */
|
|
6
7
|
export interface ChannelSocket {
|
|
@@ -12,6 +13,7 @@ export interface ChannelSocket {
|
|
|
12
13
|
|
|
13
14
|
/** A peer that lives in another process and attaches over the control WS (the Claude channel plugin). */
|
|
14
15
|
export class WsPeer extends BasePeer {
|
|
16
|
+
readonly form: StartForm = "tui";
|
|
15
17
|
private generation = crypto.randomUUID();
|
|
16
18
|
// Checkpoints become stale at hello/claim, before asynchronous recall finishes and attach changes delivery generation.
|
|
17
19
|
private claimGeneration = crypto.randomUUID();
|
|
@@ -23,7 +25,7 @@ export class WsPeer extends BasePeer {
|
|
|
23
25
|
private delivered = new Set<string>();
|
|
24
26
|
private sock: ChannelSocket | undefined;
|
|
25
27
|
private claimed: ChannelSocket | undefined;
|
|
26
|
-
private
|
|
28
|
+
private nativeTurnSession: string | undefined;
|
|
27
29
|
private nativeActive = false;
|
|
28
30
|
/** The attached session said it cannot show channel pushes (issue #205): the bus keeps its queue for `inbox`. */
|
|
29
31
|
pullOnly = false;
|
|
@@ -66,7 +68,7 @@ export class WsPeer extends BasePeer {
|
|
|
66
68
|
this.sock = sock;
|
|
67
69
|
this.generation = crypto.randomUUID();
|
|
68
70
|
this.delivered.clear();
|
|
69
|
-
this.
|
|
71
|
+
this.nativeTurnSession = undefined;
|
|
70
72
|
this.nativeActive = false;
|
|
71
73
|
this.pullOnly = this.claimedPullOnly; // before idle: the idle transition drains
|
|
72
74
|
// Native TUI readback in #493 proves important channel input can be consumed before the turn ends.
|
|
@@ -112,13 +114,13 @@ export class WsPeer extends BasePeer {
|
|
|
112
114
|
owns(sock: ChannelSocket): boolean { return this.sock === sock; }
|
|
113
115
|
nativeStarted(session: string): void {
|
|
114
116
|
if (!this.sock) return;
|
|
115
|
-
this.
|
|
117
|
+
this.nativeTurnSession = session;
|
|
116
118
|
this.nativeActive = true;
|
|
117
119
|
this.setState("busy");
|
|
118
120
|
}
|
|
119
121
|
nativeStopped(session: string): boolean {
|
|
120
|
-
if (!this.sock || (this.
|
|
121
|
-
this.
|
|
122
|
+
if (!this.sock || (this.nativeTurnSession !== undefined && this.nativeTurnSession !== session)) return false;
|
|
123
|
+
this.nativeTurnSession = session;
|
|
122
124
|
this.nativeActive = false;
|
|
123
125
|
this.setState("idle");
|
|
124
126
|
return true;
|
|
@@ -72,6 +72,7 @@ export async function proveClaudeImplementer(peer: string, command: readonly str
|
|
|
72
72
|
/** One owned print child at a time. Idle holds no native process; task continuity is ids-only on disk. */
|
|
73
73
|
export class ClaudeImplementerPeer extends BasePeer {
|
|
74
74
|
readonly hubNative = true;
|
|
75
|
+
readonly hubOwned: boolean = true;
|
|
75
76
|
private generation = 0;
|
|
76
77
|
private owned: OwnedPrintChild | undefined;
|
|
77
78
|
private running: Promise<void> | undefined;
|
|
@@ -2,7 +2,7 @@ import { spawn, type ChildProcess } from "node:child_process";
|
|
|
2
2
|
import type { Server, ServerWebSocket } from "bun";
|
|
3
3
|
import { renderDigest, replyAudience, replyParent, type Envelope, type PeerId } from "../hub/envelope.ts";
|
|
4
4
|
import type { PermissionMode } from "../hub/permission-mode.ts";
|
|
5
|
-
import { BasePeer } from "../hub/peers.ts";
|
|
5
|
+
import { BasePeer, type FrontendProxyPort } from "../hub/peers.ts";
|
|
6
6
|
import { codexContext, type ContextReading } from "../hub/context-window.ts";
|
|
7
7
|
import { peerChildEnv, stopOwnedProcess, trackGroup } from "../hub/child-process.ts";
|
|
8
8
|
import type { ContextReader } from "./registry-types.ts";
|
|
@@ -82,6 +82,7 @@ const TRACKED = new Set(["thread/start", "thread/resume"]);
|
|
|
82
82
|
* TUI's, and their responses are swallowed.
|
|
83
83
|
*/
|
|
84
84
|
export class CodexPeer extends BasePeer {
|
|
85
|
+
readonly frontendProxy: FrontendProxyPort;
|
|
85
86
|
private permissionMode: PermissionMode;
|
|
86
87
|
private readonly actionWaits = new Map<AbortController, { link: Link; turn: string | undefined }>();
|
|
87
88
|
get actionGated(): boolean { return this.opts.actionGate !== undefined; }
|
|
@@ -136,6 +137,13 @@ export class CodexPeer extends BasePeer {
|
|
|
136
137
|
) {
|
|
137
138
|
super(id, opts.watchdogMs);
|
|
138
139
|
this.permissionMode = opts.permissionMode ?? "ask";
|
|
140
|
+
const owner = this;
|
|
141
|
+
this.frontendProxy = {
|
|
142
|
+
get url() { return owner.proxyUrl; },
|
|
143
|
+
get attached() { return owner.proxyAttached; },
|
|
144
|
+
setStartupPermissionMode: this.setStartupPermissionMode.bind(this),
|
|
145
|
+
clearPermissionMode: this.clearPermissionMode.bind(this),
|
|
146
|
+
};
|
|
139
147
|
}
|
|
140
148
|
|
|
141
149
|
recoveryMetadata(): Record<string, unknown> {
|
|
@@ -149,6 +157,7 @@ export class CodexPeer extends BasePeer {
|
|
|
149
157
|
get thread(): string {
|
|
150
158
|
return this.threadId;
|
|
151
159
|
}
|
|
160
|
+
get nativeSession(): string | undefined { return this.threadId || undefined; }
|
|
152
161
|
|
|
153
162
|
get proxyUrl(): string {
|
|
154
163
|
return `ws://127.0.0.1:${this.server?.port ?? this.opts.proxyPort}`;
|
|
@@ -694,6 +703,7 @@ export class CodexPeer extends BasePeer {
|
|
|
694
703
|
link.up.send(JSON.stringify({ method: "thread/revert", id, params: { threadId: this.threadId, beforeTurnId: turnId } }));
|
|
695
704
|
});
|
|
696
705
|
}
|
|
706
|
+
revertTurn(nativeTurn: string): Promise<void> { return this.revert(nativeTurn); }
|
|
697
707
|
|
|
698
708
|
/** account/rateLimits/read through the TUI's connection, with a hub id so the answer never reaches the TUI. */
|
|
699
709
|
private readUsage(): void {
|
|
@@ -4,7 +4,7 @@ import { isFixedBuiltinDefinition } from "../registry.ts";
|
|
|
4
4
|
/** Stable generic ACP legs; both actual test registration and enrollment validation consume this catalog. */
|
|
5
5
|
export const ACP_LEGS = Object.freeze(["native-approval-rpc", "native-mode-ack", "native-quota", "watchdog-generation"] as const);
|
|
6
6
|
export type AcpLeg = typeof ACP_LEGS[number];
|
|
7
|
-
/**
|
|
7
|
+
/** Mandatory product invariant, asserted for generic and owned ACP factories rather than skipped protocol legs. */
|
|
8
8
|
export const ACP_PORTS_LEG = "restrictive-ports";
|
|
9
9
|
export const NATIVE_PROOF_PORTS_LEG = "native-proof-ports";
|
|
10
10
|
/** No unsupported key or non-generic ACP engine can silently escape an executable catalog leg. */
|
|
@@ -18,8 +18,9 @@ export function assertAcpLegEnrollment(definitions: readonly AdapterDefinition[]
|
|
|
18
18
|
if (!row || !isFixedBuiltinDefinition(row.definition)) throw new Error(`${key}: only fixed builtin proof legs may be nonapplicable`);
|
|
19
19
|
continue;
|
|
20
20
|
}
|
|
21
|
-
if (separator < 1 || !acp.has(kind)
|
|
22
|
-
if (leg === ACP_PORTS_LEG
|
|
21
|
+
if (separator < 1 || !acp.has(kind)) throw new Error(`${key}: stale or unknown unsupported ACP leg`);
|
|
22
|
+
if (leg === ACP_PORTS_LEG) throw new Error(`${key}: mandatory ACP restrictive ports leg cannot be unsupported`);
|
|
23
|
+
if (!(ACP_LEGS as readonly string[]).includes(leg)) throw new Error(`${key}: stale or unknown unsupported ACP leg`);
|
|
23
24
|
}
|
|
24
25
|
for (const definition of acp.values()) if (definition.start.daemonPort !== "acp") {
|
|
25
26
|
for (const leg of ACP_LEGS) {
|
|
@@ -50,7 +51,6 @@ export function assertEnrollment(definitions: readonly AdapterDefinition[], enro
|
|
|
50
51
|
}
|
|
51
52
|
for (const { id, definition } of rows) {
|
|
52
53
|
if (isFixedBuiltinDefinition(definition) && !Object.hasOwn(unsupportedLegs, `${id}:${NATIVE_PROOF_PORTS_LEG}`)) throw new Error(`${id}: native proof ports need an explicit nonapplicable reason`);
|
|
53
|
-
if (definition.transport === "acp" && !Object.hasOwn(unsupportedLegs, `${id}:${ACP_PORTS_LEG}`)) throw new Error(`${id}: ACP restrictive ports require the reserved 2b reason`);
|
|
54
54
|
if (!enrolled.includes(id) && !Object.hasOwn(unsupported, id)) throw new Error(`${id}: no conformance fixture or unsupported reason`);
|
|
55
55
|
if (definition.transport === "acp" && !enrolled.includes(id)) throw new Error(`${id}: every ACP definition must run the ACP legs`);
|
|
56
56
|
if (id.endsWith(":reviewer") && !definition.reviewer) throw new Error(`${id}: reviewer variant lacks reviewer policy`);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AdapterDefinition, CreateContext, DaemonPorts } from "../registry-types.ts";
|
|
2
2
|
import type { PeerAdapter } from "../../hub/peers.ts";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { assertNativeProofPorts } from "./kit.ts";
|
|
4
|
+
import { assertAcpProductPorts, assertNativeProofPorts } from "./kit.ts";
|
|
5
5
|
|
|
6
6
|
/** The supplied definition creates its actual adapter, with only this fixture's transport options. */
|
|
7
7
|
export function createFromDefinition<T extends PeerAdapter>(definition: AdapterDefinition, daemon: DaemonPorts): T {
|
|
@@ -10,6 +10,7 @@ export function createFromDefinition<T extends PeerAdapter>(definition: AdapterD
|
|
|
10
10
|
const product = definition.start.create({ instance: definition.kind, projectRoot: options.cwd, stateDir: options.stateDir ?? join(options.cwd, "state"),
|
|
11
11
|
kindConfig: {}, permission: "ask", mcpServers: options.mcpServers ?? [], daemon }) as T;
|
|
12
12
|
assertNativeProofPorts(definition, product);
|
|
13
|
+
assertAcpProductPorts(definition, product);
|
|
13
14
|
return product;
|
|
14
15
|
}
|
|
15
16
|
/** Explicit watchdog seam on the factory-created implementation, replacing direct test subclasses. */
|
|
@@ -24,6 +24,15 @@ export interface AdapterConformance {
|
|
|
24
24
|
create(): Promise<ConformanceFixture>;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/** Every ACP factory product keeps the transport's restrictive delivery and ownership contract. */
|
|
28
|
+
export function assertAcpProductPorts(definition: AdapterDefinition | undefined, product: PeerAdapter): void {
|
|
29
|
+
if (definition?.transport !== "acp") return;
|
|
30
|
+
for (const port of ["deliveryOnly", "hubOwned"] as const) {
|
|
31
|
+
if (product[port] !== true) throw new Error(`${definition.kind}: ACP restrictive port ${port} must be true`);
|
|
32
|
+
}
|
|
33
|
+
if (product.form !== "headless") throw new Error(`${definition.kind}: ACP restrictive port form must be headless`);
|
|
34
|
+
}
|
|
35
|
+
|
|
27
36
|
/** Presence itself is forbidden on a foreign product: even an undefined native proof port must fail the kit. */
|
|
28
37
|
export function assertNativeProofPorts(definition: AdapterDefinition | undefined, product: PeerAdapter): void {
|
|
29
38
|
if (isFixedBuiltinDefinition(definition)) return;
|
|
@@ -38,6 +47,7 @@ export function registerConformance(adapter: AdapterConformance): void {
|
|
|
38
47
|
const f = await adapter.create();
|
|
39
48
|
try {
|
|
40
49
|
assertNativeProofPorts(adapter.definition, f.peer);
|
|
50
|
+
assertAcpProductPorts(adapter.definition, f.peer);
|
|
41
51
|
expect(f.peer.id).toBe(adapter.kind);
|
|
42
52
|
expect(f.peer.state).toBe("idle");
|
|
43
53
|
const envelope: Envelope = newEnvelope("user", "conformance ping", { to: [adapter.kind] });
|
|
@@ -77,6 +77,8 @@ const system = (cwd: string, preamble = "") =>
|
|
|
77
77
|
/** Hub-native agent loop: the hub owns every model call, so it can choose the model per call (L2) and the gateway (L3). */
|
|
78
78
|
export class LocalPeer extends BasePeer {
|
|
79
79
|
readonly hubNative = true;
|
|
80
|
+
readonly deliveryOnly: boolean = true;
|
|
81
|
+
readonly hubOwned: boolean = true;
|
|
80
82
|
private readonly history: ChatMessage[] = [];
|
|
81
83
|
private readonly sessionId = `agent-hub-local-${randomUUID()}`;
|
|
82
84
|
private readonly routes: HubRouteRuntime;
|
package/src/adapters/pi.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { PiSessionBookmarkMismatch, validatePiSession } from "../pi/session.ts";
|
|
|
12
12
|
import { ApprovalWaitStop, toolResultFailed, type ApprovalProvenance } from "../local/tools.ts";
|
|
13
13
|
import { piToolStepCeiling, type PiToolStepCeiling } from "../pi/ceiling.ts";
|
|
14
14
|
import type { ExecutionBudgetDecision, ExecutionUnit } from "../hub/execution-budget.ts";
|
|
15
|
+
import type { StartForm } from "./registry-types.ts";
|
|
15
16
|
|
|
16
17
|
export interface PiModelDescriptor { id: string; name?: string; contextWindow?: number; maxTokens?: number; reasoning?: boolean; }
|
|
17
18
|
export interface PiToolSchema { name: string; description?: string; parameters: Record<string, unknown>; }
|
|
@@ -99,6 +100,7 @@ function ownerStillAlive(pid: number, signature: string | undefined): boolean {
|
|
|
99
100
|
|
|
100
101
|
export class PiPeer extends BasePeer {
|
|
101
102
|
readonly hubNative = true;
|
|
103
|
+
readonly hubOwned: boolean = true;
|
|
102
104
|
private proc?: ChildProcessWithoutNullStreams;
|
|
103
105
|
private server?: ReturnType<typeof Bun.serve>;
|
|
104
106
|
private buffer = "";
|
|
@@ -202,6 +204,10 @@ export class PiPeer extends BasePeer {
|
|
|
202
204
|
const file = this.sessionFile && (exists || !empty) ? this.sessionFile : undefined;
|
|
203
205
|
return { launch: { kind: "pi", cwd: this.opts.cwd, mode: this.opts.mode, backend: this.opts.backend, ...(this.opts.model ? { model: this.opts.model } : {}), ...(file ? { sessionFile: file } : {}) }, ...(this.sessionId ? { sessionId: this.sessionId } : {}), ...(file ? { sessionFile: file } : {}) };
|
|
204
206
|
}
|
|
207
|
+
get nativeSession(): string | undefined {
|
|
208
|
+
const session = this.recoveryMetadata().sessionId;
|
|
209
|
+
return typeof session === "string" ? session : undefined;
|
|
210
|
+
}
|
|
205
211
|
|
|
206
212
|
/** Verify the live source before choosing ID-only restoration. Never infer emptiness from a missing file. */
|
|
207
213
|
async captureResume(): Promise<Record<string, unknown>> {
|
|
@@ -502,6 +508,7 @@ export class PiPeer extends BasePeer {
|
|
|
502
508
|
return pending.map((call) => call.settled);
|
|
503
509
|
}
|
|
504
510
|
get mode(): "headless" | "tui" { return this.opts.mode; }
|
|
511
|
+
get form(): StartForm { return this.opts.mode; }
|
|
505
512
|
|
|
506
513
|
/** Withdraw hub admission without shutting down a person's Pi TUI or its bridge. */
|
|
507
514
|
async detachForRoster(): Promise<void> {
|
|
@@ -156,6 +156,7 @@ const reviewTask = (env: Envelope): number | undefined => {
|
|
|
156
156
|
|
|
157
157
|
export class ReviewerPrintPeer extends BasePeer {
|
|
158
158
|
readonly hubNative = true;
|
|
159
|
+
readonly hubOwned: boolean = true;
|
|
159
160
|
private generation = 0;
|
|
160
161
|
private child: OwnedPrintChild | undefined;
|
|
161
162
|
private childCleanup: (() => Promise<void>) | undefined;
|