@staix/agent-hub 0.12.31 → 0.12.32
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 +13 -1
- package/README.md +13 -7
- package/docs/agent-notes/adapters.md +7 -4
- package/docs/agent-notes/bus.md +1 -0
- package/docs/agent-notes/daemon.md +10 -7
- package/docs/agent-notes/local-worker.md +1 -1
- package/docs/agent-notes/tasks.md +3 -2
- package/docs/agent-notes/tests.md +1 -0
- package/docs/cooperbench.md +1 -1
- package/docs/gsd-collaboration-analysis.md +1 -1
- package/docs/memory.md +1 -1
- package/docs/operations.md +114 -22
- package/docs/quickstart.md +10 -6
- package/docs/security.md +12 -8
- package/docs/smoke.md +34 -0
- package/docs/specs/2026-09-19-agent-hub-design.md +204 -0
- package/docs/specs/2026-09-20-upgrade-recovery-design.md +191 -0
- package/docs/verification/2026-10-18-0.12.32.md +54 -0
- package/docs/verified.json +20 -15
- package/package.json +2 -2
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +1296 -26
- package/src/adapters/acp.ts +5 -6
- package/src/adapters/definitions/builtin-metadata.ts +121 -0
- package/src/adapters/definitions/kimi-native.ts +4 -0
- package/src/adapters/definitions/qwen-native.ts +24 -0
- package/src/adapters/definitions/qwen.ts +71 -0
- package/src/adapters/pi.ts +6 -2
- package/src/adapters/registry-types.ts +174 -0
- package/src/adapters/registry.ts +36 -0
- package/src/cli/console-state.ts +74 -5
- package/src/cli/console.ts +35 -2
- package/src/cli/help.ts +3 -1
- package/src/cli/identity.ts +13 -3
- package/src/cli/launch.ts +1 -6
- package/src/cli/main.ts +200 -39
- package/src/cli/output.ts +2 -0
- package/src/cli/preview.ts +8 -5
- package/src/cli/recovery-guidance.ts +137 -0
- package/src/cli/recovery-presentation-options.ts +8 -0
- package/src/cli/recovery-session-file.ts +18 -0
- package/src/cli/terminal-recovery.ts +48 -0
- package/src/cli/upgrade-interactive.ts +129 -75
- package/src/cli/upgrade-labels.ts +30 -0
- package/src/cli/upgrade-presentation-data.ts +41 -0
- package/src/cli/upgrade-presentation-flags.ts +31 -0
- package/src/cli/upgrade-presentation.ts +217 -0
- package/src/cli/upgrade-restore-state.ts +6 -2
- package/src/cli/upgrade-runtime.ts +345 -194
- package/src/cli/upgrade.ts +276 -25
- package/src/hub/bus.ts +11 -0
- package/src/hub/cohorts.ts +2 -2
- package/src/hub/config-apply.ts +242 -0
- package/src/hub/config-reload.ts +120 -0
- package/src/hub/config-trust.ts +11 -2
- package/src/hub/crash.ts +8 -6
- package/src/hub/daemon.ts +559 -170
- package/src/hub/events.ts +2 -2
- package/src/hub/manager.ts +1 -1
- package/src/hub/peer-catalog.ts +10 -13
- package/src/hub/peers.ts +12 -2
- package/src/hub/permission-mode.ts +5 -3
- package/src/hub/recovery-continuity.ts +152 -0
- package/src/hub/recovery-store.ts +10 -4
- package/src/hub/sdlc.ts +5 -1
- package/src/hub/settings.ts +50 -36
- package/src/hub/settled-roles.ts +35 -0
- package/src/hub/supervision.ts +3 -2
- package/src/hub/tasks.ts +28 -8
- package/src/hub/ui.ts +1 -1
- package/src/memory/brief.ts +6 -5
- package/src/pi/session.ts +23 -4
- package/src/ui/index.html +23 -7
package/src/adapters/acp.ts
CHANGED
|
@@ -3,7 +3,10 @@ import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
|
3
3
|
import { createInterface } from "node:readline";
|
|
4
4
|
import { renderDigest, replyAudience, replyParent, sanitize, type Envelope, type PeerId, type PeerState } from "../hub/envelope.ts";
|
|
5
5
|
import { acpQuotaRefusal, type ACPQuotaRefusal } from "./acp-quota.ts";
|
|
6
|
-
import {
|
|
6
|
+
import type { PermissionMode } from "../hub/permission-mode.ts";
|
|
7
|
+
import { KIMI_MODE_IDS } from "./definitions/kimi-native.ts";
|
|
8
|
+
import { QWEN_MODE_IDS, qwenNativeIdentity } from "./definitions/qwen-native.ts";
|
|
9
|
+
export { QwenIdentityError } from "./definitions/qwen-native.ts";
|
|
7
10
|
import { BasePeer } from "../hub/peers.ts";
|
|
8
11
|
import { peerChildEnv, stopOwnedProcess, trackGroup } from "../hub/child-process.ts";
|
|
9
12
|
|
|
@@ -115,9 +118,6 @@ export function canonicalMcpToolName(announcedTitle: string | undefined, serverN
|
|
|
115
118
|
}
|
|
116
119
|
|
|
117
120
|
class RpcTimeoutError extends Error {}
|
|
118
|
-
export class QwenIdentityError extends Error {
|
|
119
|
-
constructor() { super("Qwen Code 0.25.0 native identity is not verified; check the executable and its advertised ACP version"); }
|
|
120
|
-
}
|
|
121
121
|
|
|
122
122
|
type Pending = { resolve: (v: any) => void; reject: (e: Error) => void };
|
|
123
123
|
|
|
@@ -241,8 +241,7 @@ export class AcpPeer extends BasePeer {
|
|
|
241
241
|
// Installed Kimi Code CLI's ACP initialize advertises this exact name.
|
|
242
242
|
this.kimiAgent = init?.agentInfo?.name === "Kimi Code CLI";
|
|
243
243
|
if (this.opts.expectedAgent && (init?.agentInfo?.name !== this.opts.expectedAgent.name || init?.agentInfo?.version !== this.opts.expectedAgent.version)) throw new Error("native ACP identity is not verified for this production adapter");
|
|
244
|
-
this.qwenAgent = this.opts.qwen025
|
|
245
|
-
if (this.opts.qwen025 && !this.qwenAgent) throw new QwenIdentityError();
|
|
244
|
+
this.qwenAgent = qwenNativeIdentity(this.id, this.opts.qwen025, init);
|
|
246
245
|
const resume = this.opts.resumeSessionId;
|
|
247
246
|
if (!resume) return this.request("session/new", { cwd: this.opts.cwd, mcpServers: this.opts.mcpServers ?? [] });
|
|
248
247
|
// The agent replays the session as updates while it loads; they arrive before the peer is idle, so none of
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import type { AdapterDefinition, LaunchContext, LaunchPlan, StartForm } from "../registry-types.ts";
|
|
2
|
+
import type { PeerAdapter } from "../../hub/peers.ts";
|
|
3
|
+
import { shellQuote } from "../../cli/terminal-recovery.ts";
|
|
4
|
+
|
|
5
|
+
type CompatibilityKind = "claude" | "codex" | "kimi" | "pi" | "local" | "jcode";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* ponytail: A1 compatibility definitions delegate to the existing CLI; the existing daemon remains their runtime
|
|
9
|
+
* authority until A2 migrates each native path. These launch methods are not a second native launcher or factory.
|
|
10
|
+
*/
|
|
11
|
+
function compatibilityLaunch(kind: CompatibilityKind, ctx: LaunchContext): LaunchPlan {
|
|
12
|
+
const mode = kind === "kimi" || kind === "pi" ? ["--mode", ctx.form] : [];
|
|
13
|
+
const unattended = ctx.unattended && (kind === "claude" || kind === "codex") ? ["--unattended"] : [];
|
|
14
|
+
const args = [ctx.entrypoint, "--project", ctx.projectRoot, kind, ...mode, ...unattended,
|
|
15
|
+
...(ctx.model && !ctx.userArgs.includes("--model") ? ["--model", ctx.model] : []), ...ctx.userArgs];
|
|
16
|
+
const argv = [process.execPath, ...args];
|
|
17
|
+
const env = {
|
|
18
|
+
...(ctx.env.CODEX_HOME !== undefined ? { CODEX_HOME: ctx.env.CODEX_HOME } : {}),
|
|
19
|
+
...(ctx.env.CLAUDE_CONFIG_DIR !== undefined ? { CLAUDE_CONFIG_DIR: ctx.env.CLAUDE_CONFIG_DIR } : {}),
|
|
20
|
+
};
|
|
21
|
+
const prefix = Object.entries(env).map(([name, value]) => `${name}=${shellQuote(value)}`);
|
|
22
|
+
return { cmd: process.execPath, args, ...(unattended.length ? { unattended: true } : {}),
|
|
23
|
+
metadata: { packageEntrypoint: ctx.entrypoint, argv, env, command: [...prefix, ...argv.map(shellQuote)].join(" ") } };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sessionId(field: "sessionId" | "threadId"): (peer: PeerAdapter) => string | undefined {
|
|
27
|
+
return peer => {
|
|
28
|
+
const value = peer.recoveryMetadata?.()[field];
|
|
29
|
+
return typeof value === "string" && value ? value : undefined;
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const terminal = (form: StartForm) => form === "tui";
|
|
33
|
+
const headless = () => false;
|
|
34
|
+
const switches = Object.freeze(["--unattended", "--safe", "--new"]);
|
|
35
|
+
const implementerRoles = Object.freeze(["implementer", "verifier"]);
|
|
36
|
+
const dualForms = Object.freeze(["tui", "headless"] as const);
|
|
37
|
+
const headlessForms = Object.freeze(["headless"] as const);
|
|
38
|
+
const palette = (fg: string) => ({ palette: { fg, bg: "transparent" } });
|
|
39
|
+
const hubGrants = {
|
|
40
|
+
ask: "every write, edit and shell command asks",
|
|
41
|
+
"ask-when-needed": "write and edit inside the project run without asking; shell and git writes still ask",
|
|
42
|
+
"never-ask": "every tool runs without asking, inside the hub sandbox and path guard",
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Sources: peer-catalog.ts roles, roster.ts defaults, start-mode.ts forms, permission-mode.ts grants,
|
|
46
|
+
// daemon.ts runtime mappings/stop policy, budget.ts exemptions, and ui/peer-palette.ts CSS foregrounds.
|
|
47
|
+
export const BUILTIN_DEFINITIONS_WITHOUT_QWEN: readonly AdapterDefinition[] = Object.freeze([
|
|
48
|
+
{
|
|
49
|
+
api: 1, kind: "claude", displayName: "Claude Code", family: "claude", transport: "claude-channel",
|
|
50
|
+
roles: Object.freeze(["planner", "reviewer"]), defaultOnRoster: true, trust: { onPrem: false, hubSandboxed: false },
|
|
51
|
+
start: { forms: Object.freeze(["tui"] as const), defaultForm: "tui", ownsTerminal: terminal, launch: ctx => compatibilityLaunch("claude", ctx), conductorStartable: true,
|
|
52
|
+
unattended: true, hubSwitches: switches, ownedFlags: Object.freeze(["--dangerously-load-development-channels", "--dangerously-skip-permissions"]) },
|
|
53
|
+
identity: { mcp: "channel-plugin" },
|
|
54
|
+
// ponytail: A1 compatibility, existing daemon permission hooks remain authority until A2; absence is not unmanaged or a refusal.
|
|
55
|
+
permission: { modes: {}, grants: { ask: "Claude's own permission rules decide",
|
|
56
|
+
"ask-when-needed": "file tools on project files run without asking, outside .agenthub, .git and agent configuration; other tools keep Claude's own rules",
|
|
57
|
+
"never-ask": "every tool runs without asking" } },
|
|
58
|
+
session: { idField: "sessionId", currentId: sessionId("sessionId"), configDirEnv: "CLAUDE_CONFIG_DIR", zeroTurnFresh: true, launchKind: "claude-channel", launchRecord: true },
|
|
59
|
+
turnEnd: "native-hook", budget: { account: "claude", checkpoint: true },
|
|
60
|
+
cli: { command: "claude", startWords: () => ["claude"] }, display: palette("#6495ed"), hooks: { recallPlatform: "claude" },
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
api: 1, kind: "codex", displayName: "Codex", family: "codex", transport: "codex-appserver",
|
|
64
|
+
roles: Object.freeze(["implementer"]), defaultOnRoster: true, trust: { onPrem: false, hubSandboxed: false },
|
|
65
|
+
start: { forms: dualForms, defaultForm: "tui", ownsTerminal: terminal, launch: ctx => compatibilityLaunch("codex", ctx), conductorStartable: true,
|
|
66
|
+
unattended: true, hubSwitches: switches, ownedFlags: Object.freeze(["--remote", "--yolo", "--dangerously-bypass-approvals-and-sandbox"]) },
|
|
67
|
+
identity: { mcp: "env" },
|
|
68
|
+
permission: { modes: { ask: { kind: "native-only", note: "restore the session's captured native approval policy" },
|
|
69
|
+
"ask-when-needed": { kind: "codex-approval", policy: "on-request" }, "never-ask": { kind: "codex-approval", policy: "never" } },
|
|
70
|
+
grants: { ask: "the session's own approval policy applies again", "ask-when-needed": "approval policy on-request on every turn", "never-ask": "approval policy never on every turn: nothing asks" } },
|
|
71
|
+
session: { idField: "threadId", currentId: sessionId("threadId"), configDirEnv: "CODEX_HOME", zeroTurnFresh: true, launchKind: "codex", launchRecord: true },
|
|
72
|
+
turnEnd: "adapter", budget: { account: "codex", checkpoint: true },
|
|
73
|
+
cli: { command: "codex", startWords: () => ["codex"] }, display: palette("#22b8cf"), hooks: { recallPlatform: "codex" },
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
api: 1, kind: "kimi", displayName: "Kimi Code", family: "kimi", transport: "acp",
|
|
77
|
+
roles: implementerRoles, defaultOnRoster: true, trust: { onPrem: false, hubSandboxed: false },
|
|
78
|
+
start: { forms: dualForms, defaultForm: "tui", ownsTerminal: terminal, launch: ctx => compatibilityLaunch("kimi", ctx), conductorStartable: true, hubSwitches: [], ownedFlags: [] },
|
|
79
|
+
// ACP sessions carry headless identity; the existing CLI alone owns the TUI's project MCP entry.
|
|
80
|
+
identity: { mcp: "acp-session" }, attach: { pullOnlyForms: Object.freeze(["tui"] as const) },
|
|
81
|
+
permission: { modes: { ask: { kind: "acp-mode", modeId: "default" }, "ask-when-needed": { kind: "acp-mode", modeId: "yolo" }, "never-ask": { kind: "acp-mode", modeId: "auto" } },
|
|
82
|
+
grants: { ask: "Kimi asks as it does on its own", "ask-when-needed": "Kimi's yolo mode: it asks only where it judges it must", "never-ask": "Kimi's auto mode: nothing asks" },
|
|
83
|
+
unmanagedForms: Object.freeze(["tui"] as const), unmanagedNote: "Kimi approves tools and selects its permission mode in its own TUI; no hub permission mode" },
|
|
84
|
+
session: { idField: "sessionId", currentId: sessionId("sessionId"), freshOnRestore: true, launchKind: "acp" },
|
|
85
|
+
turnEnd: "adapter", budget: { account: "kimi" }, capabilities: { watch: true }, init: { mcpFile: ".kimi-code/mcp.json" },
|
|
86
|
+
cli: { command: "kimi", startWords: form => ["kimi", "--mode", form] }, display: palette("#bc73ca"),
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
api: 1, kind: "pi", displayName: "Pi", family: "pi", transport: "hub-native",
|
|
90
|
+
roles: implementerRoles, defaultOnRoster: false, trust: { onPrem: false, hubSandboxed: true },
|
|
91
|
+
start: { forms: dualForms, defaultForm: "tui", ownsTerminal: terminal, launch: ctx => compatibilityLaunch("pi", ctx), conductorStartable: true, autoStart: "pi.auto_start", hubSwitches: [], ownedFlags: [] },
|
|
92
|
+
identity: { mcp: "env" },
|
|
93
|
+
// ponytail: A1 compatibility, existing daemon permission hooks remain authority until A2; absence is not unmanaged or a refusal.
|
|
94
|
+
permission: { modes: {}, grants: { ...hubGrants } },
|
|
95
|
+
session: { idField: "sessionId", currentId: sessionId("sessionId"), launchKind: "pi", launchRecord: true },
|
|
96
|
+
turnEnd: "adapter", budget: { account: "pi" },
|
|
97
|
+
cli: { command: "pi", startWords: form => ["pi", "--mode", form] }, display: palette("#c79936"),
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
api: 1, kind: "local", displayName: "Local worker", family: "local", transport: "hub-native",
|
|
101
|
+
roles: implementerRoles, defaultOnRoster: true, trust: { onPrem: true, hubSandboxed: true },
|
|
102
|
+
start: { forms: headlessForms, defaultForm: "headless", ownsTerminal: headless, launch: ctx => compatibilityLaunch("local", ctx), conductorStartable: true, hubSwitches: [], ownedFlags: [] },
|
|
103
|
+
identity: { mcp: "env" },
|
|
104
|
+
// ponytail: A1 compatibility, existing daemon permission hooks remain authority until A2; absence is not unmanaged or a refusal.
|
|
105
|
+
permission: { modes: {}, grants: { ...hubGrants } },
|
|
106
|
+
session: { idField: "sessionId", currentId: sessionId("sessionId"), freshOnRestore: true, launchKind: "local" },
|
|
107
|
+
turnEnd: "adapter",
|
|
108
|
+
cli: { command: "local", startWords: () => ["local"] }, display: palette("#4caa77"),
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
api: 1, kind: "jcode", displayName: "Jcode", family: "jcode", transport: "acp",
|
|
112
|
+
roles: implementerRoles, optional: true, defaultOnRoster: false, trust: { onPrem: false, hubSandboxed: false },
|
|
113
|
+
start: { forms: headlessForms, defaultForm: "headless", ownsTerminal: headless, launch: ctx => compatibilityLaunch("jcode", ctx), conductorStartable: true, hubSwitches: [], ownedFlags: [] },
|
|
114
|
+
identity: { mcp: "project-file" },
|
|
115
|
+
permission: { modes: {}, unmanaged: "Jcode native policy is unmanaged; no hub mode mapping", grants: {
|
|
116
|
+
ask: "no verified hub mode mapping; native policy only", "ask-when-needed": "no verified hub mode mapping; native policy only", "never-ask": "no verified hub mode mapping; native policy only" } },
|
|
117
|
+
session: { idField: "sessionId", currentId: sessionId("sessionId"), launchKind: "jcode" }, turnEnd: "adapter", budget: { account: "jcode" },
|
|
118
|
+
// Jcode has no dedicated CSS swatch today: retain peer-palette.ts's existing 'other' fallback.
|
|
119
|
+
cli: { command: "jcode", startWords: () => ["jcode"] }, display: palette("#8a8f99"),
|
|
120
|
+
},
|
|
121
|
+
] satisfies AdapterDefinition[]);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { PermissionMode } from "../../hub/permission-mode.ts";
|
|
2
|
+
|
|
3
|
+
/** Shared native mode ids stay independent of the registry's factories. */
|
|
4
|
+
export const KIMI_MODE_IDS: Readonly<Record<PermissionMode, string>> = { ask: "default", "ask-when-needed": "yolo", "never-ask": "auto" };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { PermissionMode } from "../../hub/permission-mode.ts";
|
|
2
|
+
import type { Launch } from "../../cli/launch.ts";
|
|
3
|
+
|
|
4
|
+
export const QWEN_MODE_IDS: Readonly<Record<PermissionMode, string>> = { ask: "default", "ask-when-needed": "auto-edit", "never-ask": "yolo" };
|
|
5
|
+
|
|
6
|
+
export class QwenIdentityError extends Error {
|
|
7
|
+
constructor() { super("Qwen Code 0.25.0 native identity is not verified; check the executable and its advertised ACP version"); }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function qwenNativeIdentity(instance: string, production: boolean | undefined, init: unknown): boolean {
|
|
11
|
+
const native = init as { agentInfo?: { name?: unknown; version?: unknown } } | null | undefined;
|
|
12
|
+
const verified = production === true && instance === "qwen" && native?.agentInfo?.name === "qwen-code" && native?.agentInfo?.version === "0.25.0";
|
|
13
|
+
if (production && !verified) throw new QwenIdentityError();
|
|
14
|
+
return verified;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Qwen flags follow the configured executable argv, including wrapper/script arguments. */
|
|
18
|
+
export function buildQwenLaunch(command: readonly string[], model?: string): Launch {
|
|
19
|
+
const [cmd, ...args] = command;
|
|
20
|
+
if (!cmd) throw new Error("qwen_cmd must name an executable");
|
|
21
|
+
return { cmd, args: [...args, ...(model ? ["--model", model] : [])] };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const qwenSessionId = (value: unknown): value is string => typeof value === "string" && /^[A-Za-z0-9][A-Za-z0-9._:-]{0,255}$/.test(value);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { AcpPeer } from "../acp.ts";
|
|
2
|
+
import type { AdapterDefinition } from "../registry-types.ts";
|
|
3
|
+
import { buildQwenLaunch, qwenNativeIdentity, qwenSessionId, QWEN_MODE_IDS } from "./qwen-native.ts";
|
|
4
|
+
import { shellQuote } from "../../cli/terminal-recovery.ts";
|
|
5
|
+
|
|
6
|
+
/** Production Qwen policy. Legacy AcpPeer callers opt in separately through qwen025. */
|
|
7
|
+
export const QWEN_DEFINITION: AdapterDefinition = {
|
|
8
|
+
api: 1,
|
|
9
|
+
kind: "qwen",
|
|
10
|
+
displayName: "Qwen Code",
|
|
11
|
+
family: "qwen",
|
|
12
|
+
transport: "acp",
|
|
13
|
+
roles: ["implementer", "verifier"],
|
|
14
|
+
optional: true,
|
|
15
|
+
defaultOnRoster: false,
|
|
16
|
+
trust: { onPrem: false, hubSandboxed: false },
|
|
17
|
+
start: {
|
|
18
|
+
forms: ["headless"],
|
|
19
|
+
defaultForm: "headless",
|
|
20
|
+
ownsTerminal: () => false,
|
|
21
|
+
conductorStartable: true,
|
|
22
|
+
hubSwitches: [],
|
|
23
|
+
ownedFlags: [],
|
|
24
|
+
preflight: request => request.form !== undefined && request.form !== "headless"
|
|
25
|
+
? "Qwen TUI is not offered before its native verification; use ahub qwen --mode headless" : undefined,
|
|
26
|
+
launch: context => {
|
|
27
|
+
if (context.form !== "headless") throw new Error("Qwen TUI is not offered before its native verification; use ahub qwen --mode headless");
|
|
28
|
+
const launch = buildQwenLaunch(context.nativeCommand ?? ["qwen", "--acp"], context.model);
|
|
29
|
+
const argv = [launch.cmd, ...launch.args];
|
|
30
|
+
return { ...launch, metadata: { packageEntrypoint: context.entrypoint, argv, command: argv.map(shellQuote).join(" "), env: {} } };
|
|
31
|
+
},
|
|
32
|
+
create: context => {
|
|
33
|
+
if (!("acp" in context.daemon)) throw new Error("Qwen requires its ACP transport options");
|
|
34
|
+
if (context.permission === "read-only") throw new Error("Qwen has no verified read-only permission mapping");
|
|
35
|
+
return new AcpPeer(context.instance, { ...context.daemon.acp, qwen025: true, permissionMode: context.permission, mcpServers: context.mcpServers });
|
|
36
|
+
},
|
|
37
|
+
conductorStartArgs: args => args.length ? new Error("Qwen start takes no conductor overrides") : {},
|
|
38
|
+
},
|
|
39
|
+
identity: { mcp: "acp-session" },
|
|
40
|
+
permission: {
|
|
41
|
+
modes: {
|
|
42
|
+
ask: { kind: "acp-mode", modeId: QWEN_MODE_IDS.ask },
|
|
43
|
+
"ask-when-needed": { kind: "acp-mode", modeId: QWEN_MODE_IDS["ask-when-needed"] },
|
|
44
|
+
"never-ask": { kind: "acp-mode", modeId: QWEN_MODE_IDS["never-ask"] },
|
|
45
|
+
},
|
|
46
|
+
grants: {
|
|
47
|
+
ask: "every write, edit and shell command asks",
|
|
48
|
+
"ask-when-needed": "Qwen auto-edit approves file edits; shell commands ask; NO hub sandbox",
|
|
49
|
+
"never-ask": "Qwen yolo approves every tool; NO hub sandbox",
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
session: {
|
|
53
|
+
idField: "sessionId",
|
|
54
|
+
currentId: peer => {
|
|
55
|
+
const sessionId = peer.recoveryMetadata?.().sessionId;
|
|
56
|
+
return typeof sessionId === "string" ? sessionId : undefined;
|
|
57
|
+
},
|
|
58
|
+
launchKind: "acp",
|
|
59
|
+
restoreOnStart: true,
|
|
60
|
+
crashIdentity: row => {
|
|
61
|
+
const sessionId = row && typeof row === "object" ? (row as { sessionId?: unknown }).sessionId : undefined;
|
|
62
|
+
return qwenSessionId(sessionId) ? { sessionId } : {};
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
turnEnd: "adapter",
|
|
66
|
+
budget: { account: "qwen" },
|
|
67
|
+
capabilities: { watch: true },
|
|
68
|
+
cli: { command: "qwen", startWords: () => ["qwen"] },
|
|
69
|
+
display: { palette: { fg: "#8a8f99", bg: "transparent" } },
|
|
70
|
+
hooks: { agentIdentity: init => { qwenNativeIdentity("qwen", true, init); }, recallPlatform: "qwen" },
|
|
71
|
+
};
|
package/src/adapters/pi.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { renderDigest, replyAudience, replyParent, type Envelope, type PeerId }
|
|
|
7
7
|
import { BasePeer } from "../hub/peers.ts";
|
|
8
8
|
import { stopOwnedProcess, trackGroup } from "../hub/child-process.ts";
|
|
9
9
|
import { processLiveness, processSignature } from "../pi/process-signature.ts";
|
|
10
|
-
import { validatePiSession } from "../pi/session.ts";
|
|
10
|
+
import { PiSessionBookmarkMismatch, validatePiSession } from "../pi/session.ts";
|
|
11
11
|
import { ApprovalWaitStop, toolResultFailed, type ApprovalProvenance } from "../local/tools.ts";
|
|
12
12
|
import { piToolStepCeiling, type PiToolStepCeiling } from "../pi/ceiling.ts";
|
|
13
13
|
import type { ExecutionBudgetDecision, ExecutionUnit } from "../hub/execution-budget.ts";
|
|
@@ -346,7 +346,11 @@ export class PiPeer extends BasePeer {
|
|
|
346
346
|
const sessions = join(this.opts.stateDir, "pi-sessions");
|
|
347
347
|
mkdirSync(sessions, { recursive: true, mode: 0o700 });
|
|
348
348
|
if (this.opts.sessionFile) {
|
|
349
|
-
validatePiSession(this.opts.stateDir, this.opts.cwd, this.opts.sessionFile, this.opts.sessionId);
|
|
349
|
+
try { validatePiSession(this.opts.stateDir, this.opts.cwd, this.opts.sessionFile, this.opts.sessionId); }
|
|
350
|
+
catch (error) {
|
|
351
|
+
if (error instanceof PiSessionBookmarkMismatch) throw new PiSessionBookmarkMismatch("Pi session header id does not match the requested session", { cause: error });
|
|
352
|
+
throw error;
|
|
353
|
+
}
|
|
350
354
|
}
|
|
351
355
|
const token = randomUUID();
|
|
352
356
|
this.tuiExit = new Promise((resolvePromise) => { this.resolveTuiExit = resolvePromise; });
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
import type { Launch, StatusLineTee, FactsHook } from "../cli/launch.ts";
|
|
2
|
+
import type { LaunchMetadata, TerminalBinding } from "../cli/terminal-recovery.ts";
|
|
3
|
+
import type { UsageWindow } from "../hub/budget.ts";
|
|
4
|
+
import type { ContextReading } from "../hub/context-window.ts";
|
|
5
|
+
import type { PeerAdapter } from "../hub/peers.ts";
|
|
6
|
+
import type { PermissionMode } from "../hub/permission-mode.ts";
|
|
7
|
+
import type { AcpOptions } from "./acp.ts";
|
|
8
|
+
import type { CodexOptions } from "./codex-appserver.ts";
|
|
9
|
+
import type { JcodeOptions } from "./jcode.ts";
|
|
10
|
+
import type { LocalOptions } from "./local-worker.ts";
|
|
11
|
+
import type { PiOptions } from "./pi.ts";
|
|
12
|
+
|
|
13
|
+
export type StartForm = "tui" | "headless";
|
|
14
|
+
|
|
15
|
+
/** Existing buildLaunch inputs plus the startup-bound instance and project identity. */
|
|
16
|
+
export interface LaunchContext {
|
|
17
|
+
instance: string;
|
|
18
|
+
form: StartForm;
|
|
19
|
+
projectRoot: string;
|
|
20
|
+
stateDir: string;
|
|
21
|
+
entrypoint: string;
|
|
22
|
+
userArgs: string[];
|
|
23
|
+
unattended: boolean;
|
|
24
|
+
proxyUrl?: string;
|
|
25
|
+
codexBin?: string;
|
|
26
|
+
statusLine?: StatusLineTee;
|
|
27
|
+
facts?: FactsHook;
|
|
28
|
+
preview?: boolean;
|
|
29
|
+
model?: string;
|
|
30
|
+
fresh?: boolean;
|
|
31
|
+
env: Readonly<Record<string, string>>;
|
|
32
|
+
/** Existing validated config argv (or the existing redacted preview), never model input. */
|
|
33
|
+
nativeCommand?: readonly string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type LaunchPlan = Launch & { metadata: LaunchMetadata; mcpFile?: { path: string; entry: unknown } };
|
|
37
|
+
|
|
38
|
+
/** A factory receives exactly its existing transport options, with no cross-transport callback widening. */
|
|
39
|
+
export type DaemonPorts =
|
|
40
|
+
| { codex: CodexOptions }
|
|
41
|
+
| { acp: AcpOptions }
|
|
42
|
+
| { jcode: JcodeOptions }
|
|
43
|
+
| { pi: PiOptions }
|
|
44
|
+
| { local: LocalOptions };
|
|
45
|
+
|
|
46
|
+
export interface CreateContext<Ports extends DaemonPorts = DaemonPorts> {
|
|
47
|
+
instance: string;
|
|
48
|
+
projectRoot: string;
|
|
49
|
+
stateDir: string;
|
|
50
|
+
kindConfig: unknown;
|
|
51
|
+
permission: PermissionMode | "read-only";
|
|
52
|
+
mcpServers: NonNullable<AcpOptions["mcpServers"]>;
|
|
53
|
+
daemon: Ports;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface StartRequest {
|
|
57
|
+
instance: string;
|
|
58
|
+
form?: StartForm;
|
|
59
|
+
model?: string;
|
|
60
|
+
route?: string;
|
|
61
|
+
unattended?: boolean;
|
|
62
|
+
replaceSession?: boolean;
|
|
63
|
+
existing?: PeerAdapter;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface VersionContext {
|
|
67
|
+
instance: string;
|
|
68
|
+
peer?: PeerAdapter;
|
|
69
|
+
env: Readonly<Record<string, string>>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export type NativeModeSpec =
|
|
73
|
+
| { kind: "acp-mode"; modeId: string }
|
|
74
|
+
| { kind: "codex-approval"; policy: string }
|
|
75
|
+
| { kind: "launch-flag"; argv: string[] }
|
|
76
|
+
| { kind: "hub-gate"; allow: readonly ("read" | "search" | "think")[] }
|
|
77
|
+
| { kind: "native-only"; note: string };
|
|
78
|
+
|
|
79
|
+
export interface QuotaReader {
|
|
80
|
+
source: string;
|
|
81
|
+
read(peer: PeerAdapter): UsageWindow[] | undefined;
|
|
82
|
+
resetOnWindow?: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ContextReader {
|
|
86
|
+
source: ContextReading["source"];
|
|
87
|
+
read(peer: PeerAdapter): ContextReading | undefined;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface CapabilityManifest {
|
|
91
|
+
capabilities: {
|
|
92
|
+
id: string;
|
|
93
|
+
kind: "command" | "skill" | "mcp_tool";
|
|
94
|
+
name: string;
|
|
95
|
+
description: string;
|
|
96
|
+
inputSchema?: unknown;
|
|
97
|
+
sideEffect: "read" | "write" | "exec" | "external";
|
|
98
|
+
}[];
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Only the frozen native seams are extensible; a definition cannot add an arbitrary daemon callback. */
|
|
102
|
+
export interface NativeHooks {
|
|
103
|
+
agentIdentity(init: unknown): void;
|
|
104
|
+
onAttach(instance: string): void;
|
|
105
|
+
onDetach(instance: string): void;
|
|
106
|
+
factsHook: (msg: unknown) => unknown;
|
|
107
|
+
conversationRevert(turnId: string): Promise<void>;
|
|
108
|
+
replaceSession(req: StartRequest): Promise<unknown>;
|
|
109
|
+
pluginInventory(env: Readonly<Record<string, string>>): string[];
|
|
110
|
+
recallPlatform: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Adapter API 1, including the merged revision-3 members frozen in issue #451. */
|
|
114
|
+
export interface AdapterDefinition {
|
|
115
|
+
api: 1;
|
|
116
|
+
kind: string;
|
|
117
|
+
displayName: string;
|
|
118
|
+
family: string;
|
|
119
|
+
transport: "acp" | "codex-appserver" | "claude-channel" | "ws-pull" | "hub-native";
|
|
120
|
+
roles: readonly string[];
|
|
121
|
+
optional?: boolean;
|
|
122
|
+
defaultOnRoster?: boolean;
|
|
123
|
+
trust: { onPrem: boolean; hubSandboxed: boolean };
|
|
124
|
+
start: {
|
|
125
|
+
forms: readonly StartForm[];
|
|
126
|
+
defaultForm: StartForm;
|
|
127
|
+
ownsTerminal(form: StartForm): boolean;
|
|
128
|
+
launch(ctx: LaunchContext): LaunchPlan;
|
|
129
|
+
create?(ctx: CreateContext): PeerAdapter;
|
|
130
|
+
preflight?(ctx: StartRequest): string | undefined;
|
|
131
|
+
version?(ctx: VersionContext): string | undefined;
|
|
132
|
+
conductorStartable: boolean;
|
|
133
|
+
autoStart?: "pi.auto_start";
|
|
134
|
+
unattended?: boolean;
|
|
135
|
+
hubSwitches: readonly string[];
|
|
136
|
+
ownedFlags: readonly string[];
|
|
137
|
+
conductorStartArgs?(args: string[]): { mode?: StartForm } | Error;
|
|
138
|
+
};
|
|
139
|
+
identity: { mcp: "acp-session" | "project-file" | "env" | "channel-plugin" };
|
|
140
|
+
attach?: { pullOnlyForms?: readonly StartForm[] };
|
|
141
|
+
permission: {
|
|
142
|
+
modes: Partial<Record<PermissionMode, NativeModeSpec>>;
|
|
143
|
+
grants?: Partial<Record<PermissionMode, string>>;
|
|
144
|
+
readOnly?: NativeModeSpec;
|
|
145
|
+
unmanaged?: string;
|
|
146
|
+
unmanagedForms?: readonly StartForm[];
|
|
147
|
+
unmanagedNote?: string;
|
|
148
|
+
};
|
|
149
|
+
session: {
|
|
150
|
+
idField: "sessionId" | "threadId";
|
|
151
|
+
currentId(peer: PeerAdapter): string | undefined;
|
|
152
|
+
configDirEnv?: "CODEX_HOME" | "CLAUDE_CONFIG_DIR";
|
|
153
|
+
resumeArgv?(binding: TerminalBinding, fresh: boolean): string[];
|
|
154
|
+
transcriptPath?(binding: TerminalBinding): string | undefined;
|
|
155
|
+
transcriptExists?(binding: TerminalBinding): boolean | "unknown";
|
|
156
|
+
zeroTurnFresh?: boolean;
|
|
157
|
+
freshOnRestore?: boolean;
|
|
158
|
+
restoreOnStart?: boolean;
|
|
159
|
+
launchKind?: string;
|
|
160
|
+
launchRecord?: boolean;
|
|
161
|
+
crashIdentity?(row: unknown): { sessionId?: string; sessionFile?: string };
|
|
162
|
+
};
|
|
163
|
+
turnEnd: "adapter" | "native-hook";
|
|
164
|
+
budget?: { account: string; reader?: QuotaReader; context?: ContextReader; checkpoint?: boolean };
|
|
165
|
+
capabilities?: { manifest?(peer: PeerAdapter): Promise<CapabilityManifest>; watch?: boolean };
|
|
166
|
+
init?: { templateFragment?: string; mcpFile?: string };
|
|
167
|
+
cli: {
|
|
168
|
+
command: string;
|
|
169
|
+
startWords(form: StartForm): string[];
|
|
170
|
+
stopNotice?(form: StartForm, owner: PeerAdapter | undefined): string;
|
|
171
|
+
};
|
|
172
|
+
display: { palette: { fg: string; bg: string } };
|
|
173
|
+
hooks?: Partial<NativeHooks>;
|
|
174
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { BUILTIN_DEFINITIONS_WITHOUT_QWEN } from "./definitions/builtin-metadata.ts";
|
|
2
|
+
import { QWEN_DEFINITION } from "./definitions/qwen.ts";
|
|
3
|
+
import type { AdapterDefinition } from "./registry-types.ts";
|
|
4
|
+
|
|
5
|
+
export type * from "./registry-types.ts";
|
|
6
|
+
export const ADAPTER_API = 1;
|
|
7
|
+
|
|
8
|
+
function immutable<T>(value: T, seen = new WeakSet<object>()): T {
|
|
9
|
+
if (!value || typeof value !== "object" || seen.has(value)) return value;
|
|
10
|
+
seen.add(value);
|
|
11
|
+
for (const member of Object.values(value)) immutable(member, seen);
|
|
12
|
+
return Object.freeze(value);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Keep the native catalog's order, independently of the legacy default-role object order. */
|
|
16
|
+
export const ADAPTERS: readonly AdapterDefinition[] = immutable([
|
|
17
|
+
...BUILTIN_DEFINITIONS_WITHOUT_QWEN.filter(definition => definition.kind !== "jcode"),
|
|
18
|
+
QWEN_DEFINITION,
|
|
19
|
+
...BUILTIN_DEFINITIONS_WITHOUT_QWEN.filter(definition => definition.kind === "jcode"),
|
|
20
|
+
]);
|
|
21
|
+
const definitions = new Map<string, AdapterDefinition>();
|
|
22
|
+
for (const definition of ADAPTERS) {
|
|
23
|
+
if (definition.api !== ADAPTER_API || definitions.has(definition.kind)) throw new Error("invalid builtin adapter registry");
|
|
24
|
+
definitions.set(definition.kind, definition);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** A1 registers kinds only. Undeclared suffixes never acquire a native kind or admission. */
|
|
28
|
+
export function kindOf(id: string): string | undefined {
|
|
29
|
+
return definitions.has(id) ? id : undefined;
|
|
30
|
+
}
|
|
31
|
+
export function definitionOf(id: string): AdapterDefinition | undefined {
|
|
32
|
+
return definitions.get(id);
|
|
33
|
+
}
|
|
34
|
+
export function familyOf(id: string): string | undefined {
|
|
35
|
+
return definitionOf(id)?.family;
|
|
36
|
+
}
|