@staix/agent-hub 0.12.44 → 0.13.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 +40 -0
- package/README.md +83 -116
- package/THIRD_PARTY_NOTICES.md +29 -0
- package/docs/adding-an-ai-tool.md +102 -14
- package/docs/agent-notes/README.md +15 -0
- package/docs/agent-notes/adapters.md +63 -46
- package/docs/agent-notes/budget.md +7 -2
- package/docs/agent-notes/bus.md +6 -4
- package/docs/agent-notes/daemon.md +15 -8
- package/docs/agent-notes/local-worker.md +6 -4
- package/docs/agent-notes/tasks.md +18 -5
- package/docs/agent-notes/tests.md +11 -9
- package/docs/dogfood.md +92 -0
- package/docs/events.md +14 -1
- package/docs/operations.md +349 -26
- package/docs/profiles.md +2 -2
- package/docs/quickstart.md +12 -2
- package/docs/sdlc.md +61 -13
- package/docs/security.md +24 -12
- package/docs/smoke.md +9 -4
- package/docs/specs/2026-09-19-agent-hub-design.md +873 -13
- package/docs/verification/2026-10-19-reviewer-isolation.md +2 -0
- package/docs/verification/2026-10-21-469-r1-dashboard-tokens.md +43 -0
- package/docs/verification/2026-10-21-469-r2-dashboard-shell.md +54 -0
- package/docs/verification/2026-10-21-469-r3-dashboard-board.md +51 -0
- package/docs/verification/2026-10-21-469-r4-dashboard-views.md +77 -0
- package/docs/verification/2026-10-21-469-r6-dashboard-motion-states.md +82 -0
- package/docs/verification/2026-10-22-0.12.45.md +60 -0
- package/docs/verification/2026-10-22-code-facts.md +78 -0
- package/docs/verification/2026-10-22-codex-reviewer-isolation.md +47 -0
- package/docs/verification/2026-10-22-kimi-qwen-compat.md +79 -0
- package/docs/verification/2026-10-23-0.13.0.md +89 -0
- package/docs/verification/assets/469-r1-board-1440-dark.png +0 -0
- package/docs/verification/assets/469-r1-board-1440-light.png +0 -0
- package/docs/verification/assets/469-r1-board-390-dark.png +0 -0
- package/docs/verification/assets/469-r1-board-390-light.png +0 -0
- package/docs/verification/assets/469-r2-agents-1440-dark.png +0 -0
- package/docs/verification/assets/469-r2-agents-768-dark.png +0 -0
- package/docs/verification/assets/469-r2-board-1024-light.png +0 -0
- package/docs/verification/assets/469-r2-board-1440-dark.png +0 -0
- package/docs/verification/assets/469-r2-board-1440-light.png +0 -0
- package/docs/verification/assets/469-r2-board-390-dark.png +0 -0
- package/docs/verification/assets/469-r2-board-390-light.png +0 -0
- package/docs/verification/assets/469-r2-board-768-light.png +0 -0
- package/docs/verification/assets/469-r3-board-1440-dark.png +0 -0
- package/docs/verification/assets/469-r3-board-1440-light.png +0 -0
- package/docs/verification/assets/469-r3-board-390-light.png +0 -0
- package/docs/verification/assets/469-r3-detail-1440-dark.png +0 -0
- package/docs/verification/assets/469-r3-detail-1440-light.png +0 -0
- package/docs/verification/assets/469-r3-detail-390-dark.png +0 -0
- package/docs/verification/assets/469-r3-plan-1440-light.png +0 -0
- package/docs/verification/assets/469-r3-plan-390-light.png +0 -0
- package/docs/verification/assets/469-r4-agents-1440-dark.png +0 -0
- package/docs/verification/assets/469-r4-agents-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-agents-390-dark.png +0 -0
- package/docs/verification/assets/469-r4-memory-1440-dark.png +0 -0
- package/docs/verification/assets/469-r4-memory-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-memory-390-dark.png +0 -0
- package/docs/verification/assets/469-r4-message-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-office-1440-dark.png +0 -0
- package/docs/verification/assets/469-r4-office-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-settings-1440-dark.png +0 -0
- package/docs/verification/assets/469-r4-settings-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-stream-1440-light.png +0 -0
- package/docs/verification/assets/469-r4-stream-390-light.png +0 -0
- package/docs/verification/assets/469-r6-agents-empty-390-dark.png +0 -0
- package/docs/verification/assets/469-r6-agents-empty-390-light.png +0 -0
- package/docs/verification/assets/469-r6-board-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-board-390-dark.png +0 -0
- package/docs/verification/assets/469-r6-board-empty-1440-dark.png +0 -0
- package/docs/verification/assets/469-r6-board-empty-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-disconnected-1440-dark.png +0 -0
- package/docs/verification/assets/469-r6-disconnected-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-disconnected-390-dark.png +0 -0
- package/docs/verification/assets/469-r6-skeleton-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-skeleton-390-dark.png +0 -0
- package/docs/verification/assets/469-r6-toasts-1440-dark.png +0 -0
- package/docs/verification/assets/469-r6-toasts-1440-light.png +0 -0
- package/docs/verification/assets/469-r6-toasts-390-light.png +0 -0
- package/docs/verification/dogfood/2026-10-22.md +3109 -0
- package/docs/verified.json +19 -16
- package/package.json +5 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +10120 -9623
- package/src/adapters/adapter-api.ts +2 -0
- package/src/adapters/agy/definition.ts +3 -1
- package/src/adapters/agy/entry.ts +5 -4
- package/src/adapters/claude-channel.ts +32 -12
- package/src/adapters/codex-appserver.ts +19 -2
- package/src/adapters/codex-review-isolation.ts +12 -5
- package/src/adapters/compat.ts +140 -0
- package/src/adapters/conformance/acp.ts +114 -0
- package/src/adapters/conformance/codex.ts +191 -0
- package/src/adapters/conformance/enrollment.ts +43 -0
- package/src/adapters/conformance/factories.ts +17 -0
- package/src/adapters/conformance/fake-paths.ts +12 -0
- package/src/adapters/conformance/fakes/acp-server.ts +267 -0
- package/src/adapters/conformance/fakes/acp-watchdog-agent.ts +10 -0
- package/src/adapters/conformance/fakes/app-server.ts +119 -0
- package/src/adapters/conformance/fakes/codex-bin.ts +15 -0
- package/src/adapters/conformance/fakes/jcode-agent.ts +91 -0
- package/src/adapters/conformance/fakes/model-server.ts +39 -0
- package/src/adapters/conformance/fakes/pi-rpc.ts +32 -0
- package/src/adapters/conformance/index.ts +11 -0
- package/src/adapters/conformance/kit.ts +50 -0
- package/src/adapters/conformance/local-pi.ts +100 -0
- package/src/adapters/conformance/owned-acp.ts +44 -0
- package/src/adapters/definitions/builtin-metadata.ts +25 -7
- package/src/adapters/definitions/native-cli-doctor.ts +12 -5
- package/src/adapters/definitions/native-cli-entry-jcode.ts +9 -6
- package/src/adapters/definitions/native-cli-entry-kimi.ts +7 -1
- package/src/adapters/definitions/native-cli-entry.ts +7 -19
- package/src/adapters/definitions/native-cli-jcode-mcp.ts +9 -2
- package/src/adapters/definitions/native-cli-preview-common.ts +2 -0
- package/src/adapters/definitions/native-cli-preview-jcode.ts +8 -2
- package/src/adapters/definitions/native-cli-preview-qwen.ts +2 -1
- package/src/adapters/definitions/native-cli-preview.ts +2 -11
- package/src/adapters/definitions/qwen-native.ts +7 -3
- package/src/adapters/definitions/qwen.ts +5 -1
- package/src/adapters/external-metadata.ts +48 -0
- package/src/adapters/external-module-scan.ts +126 -0
- package/src/adapters/external-modules.ts +240 -0
- package/src/adapters/jcode.ts +72 -31
- package/src/adapters/kimi-review.ts +6 -3
- package/src/adapters/pi.ts +2 -0
- package/src/adapters/public.ts +7 -0
- package/src/adapters/registry-types.ts +13 -1
- package/src/adapters/registry.ts +52 -15
- package/src/cli/agents.ts +40 -0
- package/src/cli/console-review.ts +54 -0
- package/src/cli/console-state.ts +124 -29
- package/src/cli/console-symbols.ts +74 -0
- package/src/cli/console-usage.ts +89 -0
- package/src/cli/console.ts +26 -1
- package/src/cli/help.ts +36 -21
- package/src/cli/identity.ts +30 -7
- package/src/cli/init.ts +109 -11
- package/src/cli/main.ts +145 -16
- package/src/cli/output.ts +44 -1
- package/src/cli/preview.ts +1 -1
- package/src/cli/sdlc-release-guard.ts +82 -0
- package/src/cli/upgrade-labels.ts +1 -1
- package/src/cli/upgrade-presentation.ts +1 -1
- package/src/cli/upgrade-restore-state.ts +3 -2
- package/src/cli/upgrade-runtime.ts +4 -1
- package/src/hub/action-class.ts +362 -0
- package/src/hub/action-policy.ts +106 -0
- package/src/hub/board.ts +72 -11
- package/src/hub/budget.ts +71 -2
- package/src/hub/bus.ts +43 -3
- package/src/hub/checks.ts +80 -31
- package/src/hub/code-facts.ts +223 -0
- package/src/hub/config-apply.ts +28 -11
- package/src/hub/config-reload.ts +8 -5
- package/src/hub/config-trust.ts +7 -4
- package/src/hub/control-client.ts +4 -1
- package/src/hub/daemon.ts +332 -61
- package/src/hub/dogfood-read.ts +199 -0
- package/src/hub/dogfood.ts +163 -0
- package/src/hub/envelope.ts +1 -1
- package/src/hub/events.ts +11 -2
- package/src/hub/facts.ts +76 -13
- package/src/hub/hub-tools.ts +45 -7
- package/src/hub/inbox-polling.ts +25 -0
- package/src/hub/instance-config.ts +17 -5
- package/src/hub/instance-execution.ts +20 -8
- package/src/hub/instances.ts +75 -2
- package/src/hub/peers.ts +9 -0
- package/src/hub/plan-conformance.ts +83 -0
- package/src/hub/profiles.ts +7 -2
- package/src/hub/release-worktree.ts +56 -0
- package/src/hub/review-brief.ts +36 -0
- package/src/hub/review-findings.ts +104 -0
- package/src/hub/roster.ts +31 -5
- package/src/hub/sdlc-queue.ts +48 -11
- package/src/hub/sdlc-release-authorization.ts +173 -0
- package/src/hub/sdlc-release.ts +16 -1
- package/src/hub/sdlc.ts +71 -18
- package/src/hub/settings.ts +10 -3
- package/src/hub/snapshots.ts +101 -14
- package/src/hub/symbol-normalizer.ts +26 -0
- package/src/hub/task-code-symbols.ts +90 -0
- package/src/hub/task-sweep.ts +3 -2
- package/src/hub/tasks.ts +556 -88
- package/src/hub/test-integrity.ts +162 -0
- package/src/hub/ui.ts +2 -2
- package/src/hub/usage-rollup.ts +361 -0
- package/src/local/sandbox.ts +5 -3
- package/src/ui/index.html +560 -78
- package/src/ui/office-model.ts +3 -1
- package/src/ui/peer-palette.ts +44 -0
- package/templates/AGENTS.block.md +1 -1
- package/templates/profiles/dev.json +6 -1
- package/templates/profiles/general.json +6 -1
- package/templates/profiles/research.json +6 -1
- package/templates/profiles/writing.json +6 -1
- package/templates/release-guard.block.md +6 -0
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
import { runClaudeEntry } from "./native-cli-entry-claude.ts";
|
|
2
|
+
import { runCodexEntry } from "./native-cli-entry-codex.ts";
|
|
3
|
+
import { runKimiEntry } from "./native-cli-entry-kimi.ts";
|
|
4
|
+
import { runPiEntry } from "./native-cli-entry-pi.ts";
|
|
5
|
+
import { runLocalEntry } from "./native-cli-entry-local.ts";
|
|
6
|
+
import { runJcodeEntry } from "./native-cli-entry-jcode.ts";
|
|
7
|
+
import { previewClaude } from "./native-cli-preview-claude.ts";
|
|
8
|
+
import { previewCodex } from "./native-cli-preview-codex.ts";
|
|
9
|
+
import { previewKimi } from "./native-cli-preview-kimi.ts";
|
|
10
|
+
import { previewPi } from "./native-cli-preview-pi.ts";
|
|
11
|
+
import { previewJcode } from "./native-cli-preview-jcode.ts";
|
|
1
12
|
import type { PermissionMode } from "../../hub/permission-mode.ts";
|
|
2
13
|
import { claudeReviewerBackend, CODEX_REVIEWER_VARIANT, KIMI_REVIEWER_VARIANT } from "./reviewer-variants.ts";
|
|
3
14
|
import { kimiAcpCommand } from "./native-daemon-kimi.ts";
|
|
@@ -73,6 +84,7 @@ const CLAUDE_REVIEWER_VARIANT: AdapterDefinition = {
|
|
|
73
84
|
// daemon.ts runtime mappings/stop policy, budget.ts exemptions, and ui/peer-palette.ts CSS foregrounds.
|
|
74
85
|
export const NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN: readonly AdapterDefinition[] = Object.freeze([
|
|
75
86
|
{
|
|
87
|
+
projection: { roleRank: 0, helpRank: 0, plannerEligible: true },
|
|
76
88
|
api: 1, kind: "claude", displayName: "Claude Code", family: "claude", transport: "claude-channel",
|
|
77
89
|
roles: Object.freeze(["planner", "reviewer"]), defaultOnRoster: true, trust: { onPrem: false, hubSandboxed: false },
|
|
78
90
|
start: { forms: Object.freeze(["tui"] as const), defaultForm: "tui", launch: claudeNativeLaunch, version: claudeObservedVersion, ...CLAUDE_LAUNCH_POLICY, conductorStartable: true,
|
|
@@ -84,11 +96,12 @@ export const NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN: readonly AdapterDefinition
|
|
|
84
96
|
"never-ask": "every tool runs without asking" } },
|
|
85
97
|
session: { idField: "sessionId", currentId: sessionId("sessionId"), configDirEnv: "CLAUDE_CONFIG_DIR", zeroTurnFresh: true, launchKind: "claude-channel", launchRecord: true, ...CLAUDE_RECOVERY_SESSION, crashPlan: claudeCrashPlan, unrecordedTerminal: "always" },
|
|
86
98
|
turnEnd: "native-hook", budget: { account: "claude", checkpoint: true, reader: { source: "claude status line", read: claudeWindows }, context: { source: "claude_statusline", read: claudeContext } },
|
|
87
|
-
cli: { command: "claude", retainedTerminalNotice: claudeRetainedTerminalNotice, stopNotice: () => "this peer belongs to a native terminal; end it in its terminal", startWords: () => ["claude"] }, display: CLAUDE_DISPLAY, hooks: { recallPlatform: "claude", recallSource: "claude", recallInjected: true, pluginInventory: claudePluginInventory },
|
|
99
|
+
cli: { command: "claude", entry: { lifecycleAdmission: true, execute: runClaudeEntry }, preview: previewClaude, usage: [["ahub claude [--print-command] [--unattended] [args...]", "launch Claude Code with the hub channel"]], retainedTerminalNotice: claudeRetainedTerminalNotice, stopNotice: () => "this peer belongs to a native terminal; end it in its terminal", startWords: () => ["claude"] }, display: CLAUDE_DISPLAY, hooks: { recallPlatform: "claude", recallSource: "claude", recallInjected: true, pluginInventory: claudePluginInventory },
|
|
88
100
|
reviewerVariant: CLAUDE_REVIEWER_VARIANT,
|
|
89
101
|
},
|
|
90
102
|
{
|
|
91
103
|
reviewerVariant: CODEX_REVIEWER_VARIANT,
|
|
104
|
+
projection: { roleRank: 1, startRank: 1, helpRank: 1, plannerEligible: true },
|
|
92
105
|
api: 1, kind: "codex", displayName: "Codex", family: "codex", transport: "codex-appserver",
|
|
93
106
|
roles: Object.freeze(["implementer"]), defaultOnRoster: true, trust: { onPrem: false, hubSandboxed: false },
|
|
94
107
|
start: { create: createCodex, daemonPort: "codex-appserver", forms: dualForms, defaultForm: "tui", launch: codexNativeLaunch, version: codexObservedVersion, ...CODEX_LAUNCH_POLICY, conductorStartable: true,
|
|
@@ -97,10 +110,11 @@ export const NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN: readonly AdapterDefinition
|
|
|
97
110
|
permission: { trackUnattended: true, 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" } },
|
|
98
111
|
session: { nativeRevert: true, idField: "threadId", currentId: sessionId("threadId"), configDirEnv: "CODEX_HOME", zeroTurnFresh: true, launchKind: "codex", launchRecord: true, ...CODEX_RECOVERY_SESSION, crashPlan: codexCrashPlan, unrecordedTerminal: "always" },
|
|
99
112
|
turnEnd: "adapter", budget: { account: "codex", checkpoint: true, reader: { source: "codex rateLimits", read: codexWindows }, context: { source: "codex_token_usage", read: codexContext } },
|
|
100
|
-
cli: { command: "codex", launchConfigKeys: ["codex_bin"], retainedTerminalNotice: codexRetainedTerminalNotice, rosterReturnNotice: codexRosterReturnNotice, stopNotice: () => "this peer belongs to a native terminal; end it in its terminal", startWords: () => ["codex"] }, display: CODEX_DISPLAY, hooks: { recallPlatform: "codex", recallSource: "codex", recallInjected: true, pluginInventory: codexPluginInventory, classifyTool: codexToolSemantic },
|
|
113
|
+
cli: { command: "codex", entry: { lifecycleAdmission: true, execute: runCodexEntry }, preview: previewCodex, usage: [["ahub codex [--print-command] [--unattended] [args...]", "start the Codex adapter and attach the TUI"]], launchConfigKeys: ["codex_bin"], retainedTerminalNotice: codexRetainedTerminalNotice, rosterReturnNotice: codexRosterReturnNotice, stopNotice: () => "this peer belongs to a native terminal; end it in its terminal", startWords: () => ["codex"] }, display: CODEX_DISPLAY, hooks: { recallPlatform: "codex", recallSource: "codex", recallInjected: true, pluginInventory: codexPluginInventory, classifyTool: codexToolSemantic },
|
|
101
114
|
},
|
|
102
115
|
{
|
|
103
116
|
reviewerVariant: KIMI_REVIEWER_VARIANT,
|
|
117
|
+
projection: { roleRank: 2, startRank: 2, helpRank: 2, plannerEligible: true },
|
|
104
118
|
api: 1, kind: "kimi", displayName: "Kimi Code", family: "kimi", transport: "acp",
|
|
105
119
|
roles: implementerRoles, defaultOnRoster: true, trust: { onPrem: false, hubSandboxed: false },
|
|
106
120
|
start: { create: createKimi, acpCommand: kimiAcpCommand, explicitHeadlessMode: true, planCommand: () => "ahub kimi", daemonPort: "acp", forms: dualForms, defaultForm: "tui", conductorStartable: true, hubSwitches: [], ownedFlags: [] },
|
|
@@ -112,9 +126,10 @@ export const NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN: readonly AdapterDefinition
|
|
|
112
126
|
unmanagedForms: Object.freeze(["tui"] as const), unmanagedStatusNote: "Kimi approves tools in its own TUI; no hub permission mode", unmanagedNote: "Kimi approves tools and selects its permission mode in its own TUI; no hub permission mode" },
|
|
113
127
|
session: { idField: "sessionId", currentId: sessionId("sessionId"), freshOnRestore: true, launchKind: "acp", ...KIMI_RECOVERY_SESSION, crashPlan: kimiCrashPlan },
|
|
114
128
|
turnEnd: "adapter", budget: { account: "kimi", resetSoftTokensOnResume: true, context: { source: "acp_usage_update", read: acpContextValue } }, capabilities: { watch: true }, hooks: { recallPlatform: "kimi", recallSource: "kimi" },
|
|
115
|
-
cli: { command: "kimi", launchConfigKeys: ["kimi_cmd", "peers.kimi.start_mode"], stopNotice: form => form === "tui" ? "Kimi belongs to its own TUI terminal; end it in that terminal" : "", startWords: form => ["kimi", "--mode", form] }, display: KIMI_DISPLAY,
|
|
129
|
+
cli: { command: "kimi", entry: { lifecycleAdmission: true, execute: runKimiEntry }, preview: previewKimi, usage: [["ahub kimi [--print-command] [--mode tui|headless] [--model <alias>]", "default: Kimi's own TUI, pull-only via hub_inbox; no push, steer, watchdog, console approvals or automatic quota handoff. Select headless for ACP"]], launchConfigKeys: ["kimi_cmd", "peers.kimi.start_mode"], stopNotice: form => form === "tui" ? "Kimi belongs to its own TUI terminal; end it in that terminal" : "", startWords: form => ["kimi", "--mode", form] }, display: KIMI_DISPLAY,
|
|
116
130
|
},
|
|
117
131
|
{
|
|
132
|
+
projection: { roleRank: 4, startRank: 0, helpRank: 6, plannerEligible: false },
|
|
118
133
|
api: 1, kind: "pi", enabledSwitchStartOnly: true, displayName: "Pi", family: "pi", transport: "hub-native",
|
|
119
134
|
roles: implementerRoles, defaultOnRoster: false, trust: { onPrem: false, hubSandboxed: true },
|
|
120
135
|
start: { create: createPi, exclusiveStartRefusal: "Pi start is in progress; inspect status before retrying", refreshTools: true, explicitHeadlessMode: true, daemonPort: "pi-rpc", forms: dualForms, defaultForm: "tui", conductorStartable: true, autoStart: "pi.auto_start", conductorStartArgs: piConductorStartArgs, hubSwitches: [], ownedFlags: [] },
|
|
@@ -124,9 +139,10 @@ export const NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN: readonly AdapterDefinition
|
|
|
124
139
|
session: { idField: "sessionId", currentId: sessionId("sessionId"), restoreOnStart: true, launchKind: "pi", launchRecord: true, ...PI_RECOVERY_SESSION, crashPlan: piCrashPlan, unrecordedTerminal: "tui", continuityBookmark: piRecoveryContinuity, continuity: { key: "pi", parse: parsePiContinuity } },
|
|
125
140
|
turnEnd: "adapter", budget: { account: "pi", instrumented: true }, reporting: { tokensNotReported: true }, configuration: { schema: piConfigSchema, routingSchema: piRoutingSchema, settings: PI_SETTINGS, restartReasons: PI_RESTART_REASONS, validateEffective: piEffectiveConfig },
|
|
126
141
|
routing: { localAllowedRefusal: "local_allowed = false also excludes pi for this class", longContext: "backend-limit", preferredTier: true, handoffPriority: 0, trace: "backend", backendLabel: "pi" },
|
|
127
|
-
cli: { command: "pi", launchConfigKeys: ["pi.cmd", "pi.backend", "pi.dgx_coding", "pi.dgx_fast", "pi.max_steps", "routing.pi", "peers.pi.start_mode"], previewArgs: ["--mode", "tui"], retainedTerminalNotice: piRetainedTerminalNotice, personOnlyFlags: ["--replace-session"], startWords: form => ["pi", "--mode", form] }, display: PI_DISPLAY,
|
|
142
|
+
cli: { command: "pi", entry: { lifecycleAdmission: false, execute: runPiEntry }, preview: previewPi, usage: [["ahub pi [--print-command] [--mode headless|tui | --headless] [--backend auto|dgx|mlx] [--model <alias>] [--session-id <id>] [--session-file <path>] [--replace-session]", "start Pi: its TUI in this terminal unless --headless or the start mode setting (peers.pi.start_mode) says headless; without a terminal the hub opens one or says how"]], launchConfigKeys: ["pi.cmd", "pi.backend", "pi.dgx_coding", "pi.dgx_fast", "pi.max_steps", "routing.pi", "peers.pi.start_mode"], previewArgs: ["--mode", "tui"], retainedTerminalNotice: piRetainedTerminalNotice, personOnlyFlags: ["--replace-session"], startWords: form => ["pi", "--mode", form] }, display: PI_DISPLAY,
|
|
128
143
|
},
|
|
129
144
|
{
|
|
145
|
+
projection: { roleRank: 3, helpRank: 7, plannerEligible: false },
|
|
130
146
|
api: 1, kind: "local", displayName: "Local worker", family: "local", transport: "hub-native",
|
|
131
147
|
roles: implementerRoles, defaultOnRoster: true, trust: { onPrem: true, hubSandboxed: true },
|
|
132
148
|
start: { create: createLocal, reconfigureWhileAttached: true, refreshTools: true, daemonPort: "local-worker", forms: headlessForms, defaultForm: "headless", conductorStartable: true, hubSwitches: [], ownedFlags: [] },
|
|
@@ -136,18 +152,20 @@ export const NATIVE_BUILTIN_DEFINITIONS_WITHOUT_QWEN: readonly AdapterDefinition
|
|
|
136
152
|
session: { idField: "sessionId", currentId: sessionId("sessionId"), freshOnRestore: true, launchKind: "local", ...LOCAL_RECOVERY_SESSION, crashPlan: localCrashPlan },
|
|
137
153
|
turnEnd: "adapter", budget: { account: "local", quotaExempt: true, instrumented: true }, configuration: { schema: localConfigSchema, routingSchema: localRoutingSchema, restartReasons: LOCAL_RESTART_REASONS },
|
|
138
154
|
routing: { localAllowedRefusal: "local_allowed = false for this class", longContext: "skip-owner", preferredTier: true, handoffPriority: 1, trace: "route" },
|
|
139
|
-
cli: { command: "local", launchConfigKeys: ["local.deny", "local.bash_network", "local.network_allow", "local.max_steps", "local.read_allow", "routing.local"], startWords: () => ["local"] }, display: LOCAL_DISPLAY,
|
|
155
|
+
cli: { command: "local", entry: { lifecycleAdmission: false, execute: runLocalEntry }, usage: [["ahub local [--route <id> | --model <id>]", "start the hub-native worker on the self-hosted models (routing.toml)"]], launchConfigKeys: ["local.deny", "local.bash_network", "local.network_allow", "local.max_steps", "local.read_allow", "routing.local"], startWords: () => ["local"] }, display: LOCAL_DISPLAY,
|
|
140
156
|
},
|
|
141
157
|
{
|
|
158
|
+
projection: { roleRank: 6, startRank: 3, helpRank: 3, plannerEligible: true },
|
|
142
159
|
api: 1, kind: "jcode", displayName: "Jcode", family: "jcode", transport: "acp",
|
|
143
160
|
roles: implementerRoles, optional: true, defaultOnRoster: false, trust: { onPrem: false, hubSandboxed: false },
|
|
144
161
|
start: { create: createJcode, preflight: jcodeStartPreflight, daemonPort: "owned-acp", forms: dualForms, defaultForm: "tui", explicitHeadlessMode: true, conductorStartable: true, hubSwitches: [], ownedFlags: [] },
|
|
145
|
-
|
|
162
|
+
// #685: an extra Jcode is a headless instance with its own native home, socket and state (instances.<id>.jcode_home).
|
|
163
|
+
identity: { mcp: "project-file" }, instances: { mode: "headless", nativeHome: true },
|
|
146
164
|
permission: { unmanaged: "Jcode native policy is unmanaged; no hub mode mapping", grants: {
|
|
147
165
|
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" } },
|
|
148
166
|
session: { idField: "sessionId", currentId: sessionId("sessionId"), launchKind: "jcode", ...JCODE_RECOVERY_SESSION, crashPlan: jcodeCrashPlan }, turnEnd: "adapter", budget: { account: "jcode" },
|
|
149
167
|
// Jcode has no dedicated CSS swatch today: retain peer-palette.ts's existing 'other' fallback.
|
|
150
|
-
cli: { command: "jcode", authority: "person", launchConfigKeys: ["jcode_cmd", "jcode_home", "jcode_provider_profile"], startWords: mode => ["jcode", "--mode", mode] }, display: JCODE_DISPLAY, hooks: { recallPlatform: "jcode" },
|
|
168
|
+
cli: { command: "jcode", entry: { lifecycleAdmission: true, execute: runJcodeEntry }, preview: previewJcode, usage: [["ahub jcode [--mode tui|headless] [--model <alias>] [--provider-profile <name>] [--session-id <id>] [--print-command]", "optional Jcode (roster.jcode, default off): its TUI on the hub-owned ACP session, opened once that session is saved; --mode headless for ACP only; native policy unmanaged, no hub sandbox"]], authority: "person", launchConfigKeys: ["jcode_cmd", "jcode_home", "jcode_provider_profile"], startWords: mode => ["jcode", "--mode", mode] }, display: JCODE_DISPLAY, hooks: { recallPlatform: "jcode" },
|
|
151
169
|
},
|
|
152
170
|
] satisfies AdapterDefinition[]);
|
|
153
171
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { CompatId } from "../compat.ts";
|
|
1
2
|
import type { DoctorCheck } from "../../cli/output.ts";
|
|
2
3
|
|
|
3
4
|
// Fixed builtin doctor policy only. These projections neither execute probes nor extend AdapterDefinition.
|
|
@@ -33,17 +34,23 @@ export function nativeDoctorRecoveryIdentity(peer: { id: string; state: string;
|
|
|
33
34
|
export type NativeDoctorOptionalProbe =
|
|
34
35
|
| { kind: "source-identity"; name: string; command: string[]; detail: string }
|
|
35
36
|
| { kind: "jcode-mcp"; name: string; home: string }
|
|
36
|
-
| { kind: "pinned-version"; name: string; command: string[];
|
|
37
|
+
| { kind: "pinned-version"; name: string; command: string[]; compat: CompatId; confirmed: string; unconfirmed: string };
|
|
37
38
|
|
|
38
39
|
/** The host runs these existing checks in order; no process, filesystem or caller callback enters this planner. */
|
|
39
|
-
export function nativeDoctorOptionalProbes(config: { roster: Readonly<Record<string, boolean | undefined>>; jcode_cmd: string[]; qwen_cmd: string[]; jcode_home?: string
|
|
40
|
+
export function nativeDoctorOptionalProbes(config: { roster: Readonly<Record<string, boolean | undefined>>; jcode_cmd: string[]; qwen_cmd: string[]; jcode_home?: string;
|
|
41
|
+
/** Declared extra instances (#685): a rostered Jcode instance's own home is inventoried like the base home. */
|
|
42
|
+
instances?: Readonly<Record<string, { kind: string; jcode_home?: string }>> }): NativeDoctorOptionalProbe[] {
|
|
40
43
|
const probes: NativeDoctorOptionalProbe[] = [];
|
|
41
44
|
if (config.roster.jcode === true) probes.push({ kind: "source-identity", name: "jcode", command: config.jcode_cmd,
|
|
42
45
|
detail: "CLI/source identity only; provider, project MCP and native teardown require separate receipts; native policy unmanaged" });
|
|
43
46
|
// #572 AC4: jcode cannot be told which MCP servers to load, so the host lists the ones its config files name.
|
|
44
47
|
if (config.roster.jcode === true && config.jcode_home) probes.push({ kind: "jcode-mcp", name: "jcode MCP servers", home: config.jcode_home });
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
+
// #685: each extra instance has its own home, hence its own inventory. Names and homes only: no model source, URL or token.
|
|
49
|
+
for (const [id, declaration] of Object.entries(config.instances ?? {})) {
|
|
50
|
+
if (declaration.kind === "jcode" && declaration.jcode_home && config.roster[id] === true) probes.push({ kind: "jcode-mcp", name: `${id} MCP servers`, home: declaration.jcode_home });
|
|
51
|
+
}
|
|
52
|
+
if (config.roster.qwen === true) probes.push({ kind: "pinned-version", name: "qwen", command: config.qwen_cmd, compat: "qwen",
|
|
53
|
+
confirmed: "CLI version verified; native ACP identity/modes and provider access require a separate live check",
|
|
54
|
+
unconfirmed: "Qwen Code executable not confirmed; provider and ACP readiness unknown" });
|
|
48
55
|
return probes;
|
|
49
56
|
}
|
|
@@ -51,7 +51,10 @@ export async function runJcodeEntry(ctx: NativeCliEntryContext): Promise<void> {
|
|
|
51
51
|
if (rest.length || (one["--mode"] !== undefined && !["headless", "tui"].includes(one["--mode"]))) fail("usage: ahub jcode [--mode tui|headless] [--model <alias>] [--provider-profile <name>] [--session-id <id>]");
|
|
52
52
|
if (one["--session-id"] !== undefined && !qwenSessionId(one["--session-id"])) fail("invalid Jcode session identity");
|
|
53
53
|
const control = readControl(stateDir);
|
|
54
|
+
// #685: `--as <id>` selects an extra Jcode instance; every request below names it, never the base peer.
|
|
55
|
+
const peer = ctx.instance ?? "jcode";
|
|
54
56
|
const mode = one["--mode"] ?? (ctx.instance ? "headless" : control?.startModes?.jcode ?? startModeOf(ctx.projectConfig().peers, "jcode"));
|
|
57
|
+
if (ctx.instance && mode === "tui") fail(`${peer} is an extra Jcode instance: it runs headless on its own private server (ahub jcode --as ${peer}); the TUI belongs to the base jcode`);
|
|
55
58
|
if (print && (mode === "headless" || !control?.features?.includes("native_tui_launch"))) {
|
|
56
59
|
try { return console.log(JSON.stringify(launcherPreview("jcode", args, cwd, stateDir, unattendedEnv), null, 2)); }
|
|
57
60
|
catch { return fail("cannot preview Jcode launch: invalid arguments or configuration (details withheld)"); }
|
|
@@ -66,10 +69,10 @@ export async function runJcodeEntry(ctx: NativeCliEntryContext): Promise<void> {
|
|
|
66
69
|
hub.onClose = (code, reason) => { if (!abort.signal.aborted) abort.abort(); oldClose?.(code, reason); };
|
|
67
70
|
try {
|
|
68
71
|
const snapshot = (await hub.request({ t: "status" })).status;
|
|
69
|
-
const rostered = rosterInStatus(snapshot,
|
|
70
|
-
if (rostered === undefined) fail(
|
|
71
|
-
if (!rostered) fail(
|
|
72
|
-
const warning = retainedLaunchWarning(snapshot,
|
|
72
|
+
const rostered = rosterInStatus(snapshot, peer);
|
|
73
|
+
if (rostered === undefined) fail(`cannot read ${peer}'s roster membership from the running hub; upgrade or restart it, then retry (nothing was started)`);
|
|
74
|
+
if (!rostered) fail(`${peer} is off roster; add it with ahub roster add ${peer}`);
|
|
75
|
+
const warning = retainedLaunchWarning(snapshot, peer, ctx.definition); if (warning) console.error(warning);
|
|
73
76
|
const options = { model: one["--model"], providerProfile: one["--provider-profile"], sessionId: one["--session-id"] };
|
|
74
77
|
if (print) {
|
|
75
78
|
if (!hub.features.includes("native_tui_launch") || mode !== "tui") return console.log(JSON.stringify(launcherPreview("jcode", args, cwd, stateDir, unattendedEnv), null, 2));
|
|
@@ -89,10 +92,10 @@ export async function runJcodeEntry(ctx: NativeCliEntryContext): Promise<void> {
|
|
|
89
92
|
}
|
|
90
93
|
const written = configureJcodeMcp(cwd, stateDir, bundleInStatus(snapshot) ?? resolve(cliDir, "../../plugins/agent-hub/server.js"));
|
|
91
94
|
if (written.tracked) console.error(".jcode/mcp.json is tracked; review this local machine-path change before committing it");
|
|
92
|
-
const reply = await hub.request({ t: "start", peer
|
|
95
|
+
const reply = await hub.request({ t: "start", peer, args: { mode, ...options }, operationId: process.env.AGENTHUB_RECOVERY_OPERATION }, 65_000);
|
|
93
96
|
if (interruptedWait()) return;
|
|
94
97
|
if (!reply.ok) fail(reply.error);
|
|
95
|
-
if (mode === "headless") { console.log(reply.already ?
|
|
98
|
+
if (mode === "headless") { console.log(reply.already ? `${peer} is already attached` : `${peer} attached (headless); native policy unmanaged, NO hub sandbox`); return; }
|
|
96
99
|
const ready = await waitForJcodeTui(hub, fail, abort.signal, options);
|
|
97
100
|
if (interruptedWait() || !ready) return;
|
|
98
101
|
// No managed terminal record: Jcode's controlled recovery is refused at plan time, so, like Kimi's TUI, it has
|
|
@@ -45,7 +45,13 @@ export async function runKimiEntry(ctx: NativeCliEntryContext): Promise<void> {
|
|
|
45
45
|
let result: ReturnType<typeof spawnSync>;
|
|
46
46
|
try {
|
|
47
47
|
// Inside the try: a kimi_cmd with no TUI form throws here, and the admission must still be released (#549).
|
|
48
|
-
|
|
48
|
+
// The running hub's value wins over the file's while a reload is retained (#654): when it has no TUI form, say whose it is, so the
|
|
49
|
+
// error is not read as a fault in the file. The words themselves are never echoed.
|
|
50
|
+
const running = Array.isArray(runningCommand) && runningCommand.every((arg: unknown) => typeof arg === "string");
|
|
51
|
+
try { launch = buildKimiTuiLaunch(running ? runningCommand : projectConfig().kimi_cmd, model); }
|
|
52
|
+
catch (error) {
|
|
53
|
+
throw running ? new Error(`${(error as Error).message} (the command is the running hub's kimi_cmd: the value in the config file is retained until ahub settings reload)`) : error;
|
|
54
|
+
}
|
|
49
55
|
// The daemon's own bundle, so the MCP server matches the hub it talks to (#502); this CLI's copy only for an older hub.
|
|
50
56
|
const written = configureKimiMcp(cwd, stateDir, bundleInStatus(snapshot) ?? resolve(cliDir, "../../plugins/agent-hub/server.js"), "tui");
|
|
51
57
|
if (written.tracked) console.error(".kimi-code/mcp.json is tracked; the agent-hub entry contains machine paths. Review this local change before committing it.");
|
|
@@ -1,21 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { AdapterDefinition } from "../registry-types.ts";
|
|
2
2
|
import type { BuiltinCliEntry } from "./native-cli-entry-types.ts";
|
|
3
|
-
import { runClaudeEntry } from "./native-cli-entry-claude.ts";
|
|
4
|
-
import { runCodexEntry } from "./native-cli-entry-codex.ts";
|
|
5
|
-
import { runJcodeEntry } from "./native-cli-entry-jcode.ts";
|
|
6
|
-
import { runQwenEntry } from "./native-cli-entry-qwen.ts";
|
|
7
|
-
import { runKimiEntry } from "./native-cli-entry-kimi.ts";
|
|
8
|
-
import { runPiEntry } from "./native-cli-entry-pi.ts";
|
|
9
|
-
import { runLocalEntry } from "./native-cli-entry-local.ts";
|
|
10
3
|
|
|
11
|
-
|
|
12
|
-
export
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
{ command: "qwen", lifecycleAdmission: true, execute: runQwenEntry },
|
|
18
|
-
{ command: "kimi", lifecycleAdmission: true, execute: runKimiEntry },
|
|
19
|
-
{ command: "pi", lifecycleAdmission: false, execute: runPiEntry },
|
|
20
|
-
{ command: "local", lifecycleAdmission: false, execute: runLocalEntry },
|
|
21
|
-
]);
|
|
4
|
+
/** Project only the trusted definitions supplied by the host, preserving their order. */
|
|
5
|
+
export function builtinCliEntries(definitions: readonly AdapterDefinition[]): readonly BuiltinCliEntry[] {
|
|
6
|
+
return Object.freeze(definitions.flatMap(definition => definition.cli.entry
|
|
7
|
+
? [Object.freeze({ ...definition.cli.entry, command: definition.cli.command })]
|
|
8
|
+
: []));
|
|
9
|
+
}
|
|
@@ -18,10 +18,17 @@ function document(file: string): Document {
|
|
|
18
18
|
return value;
|
|
19
19
|
}
|
|
20
20
|
const map = (doc: Document): Document => (doc.mcpServers ?? doc.servers ?? {}) as Document;
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
|
+
* The project's `.jcode/mcp.json` entry is shared by every Jcode in it, so it names no peer (#685): jcode gives an entry's own env
|
|
23
|
+
* precedence over the process env (probed on 0.99.0), so a hardcoded `AGENTHUB_PEER_ID: "jcode"` would make an extra instance's
|
|
24
|
+
* hub tools act as the base peer. Each owned launch path supplies its own id instead (peerChildEnv for serve and ACP, the TUI launch env).
|
|
25
|
+
* An entry written before this still hardcodes the id: `jcodeMcpReady` is false for it, and the launcher rewrites it.
|
|
26
|
+
*/
|
|
27
|
+
const owned = (entry: unknown, cwd: string): entry is Document => object(entry) && object(entry.env) && entry.env.AGENTHUB_MODE === "tools"
|
|
28
|
+
&& (entry.env.AGENTHUB_PEER_ID === undefined || entry.env.AGENTHUB_PEER_ID === "jcode") && entry.env.AGENTHUB_PROJECT_DIR === cwd;
|
|
22
29
|
|
|
23
30
|
export function jcodeMcpEntry(cwd: string, stateDir: string, bundle: string) {
|
|
24
|
-
return { command: "bun", args: ["run", bundle], shared: false, env: { AGENTHUB_MODE: "tools", AGENTHUB_DELIVERY_ONLY: "true",
|
|
31
|
+
return { command: "bun", args: ["run", bundle], shared: false, env: { AGENTHUB_MODE: "tools", AGENTHUB_DELIVERY_ONLY: "true", AGENTHUB_STATE_DIR: stateDir, AGENTHUB_PROJECT_DIR: cwd } };
|
|
25
32
|
}
|
|
26
33
|
/** The native loader reads later project files last. Never shadow a foreign definition. */
|
|
27
34
|
function inspect(cwd: string): Document {
|
|
@@ -23,6 +23,8 @@ export interface NativePreviewHelpers {
|
|
|
23
23
|
export interface NativePreviewContext {
|
|
24
24
|
tool: string; args: string[]; cwd: string; stateDir: string; cliDir: string; unattended: boolean;
|
|
25
25
|
config: ReturnType<typeof loadConfig>; definition: AdapterDefinition; baseEnvNames: string[]; helpers: NativePreviewHelpers;
|
|
26
|
+
/** The declared extra instance a `--as <id>` launch selected; its own model fields belong in the preview (#685). */
|
|
27
|
+
instance?: string;
|
|
26
28
|
}
|
|
27
29
|
export interface NativePreviewMetadata { mode: "tui" | "headless"; mcp: { file: string; action: string; changed: boolean; tracked: boolean } }
|
|
28
30
|
export type NativePreviewBuilder = (ctx: NativePreviewContext) => NativePreview;
|
|
@@ -2,7 +2,7 @@ import type { Launch } from "../../cli/launch.ts";
|
|
|
2
2
|
import { REDACTED, UNRESOLVED, nativeCommand, previewState, finishPreview, type NativePreviewContext, type NativePreview } from "./native-cli-preview-common.ts";
|
|
3
3
|
|
|
4
4
|
export function previewJcode(ctx: NativePreviewContext): NativePreview {
|
|
5
|
-
const { args, config } = ctx;
|
|
5
|
+
const { args, config, instance } = ctx;
|
|
6
6
|
const { jcodeCommand, jcodeTuiCommand, qwenSessionId, startModeOf } = ctx.helpers;
|
|
7
7
|
const state = previewState(args, ctx.baseEnvNames), { unresolved, value } = state;
|
|
8
8
|
let envNames = state.envNames;
|
|
@@ -11,7 +11,13 @@ export function previewJcode(ctx: NativePreviewContext): NativePreview {
|
|
|
11
11
|
if (mode !== "headless" && mode !== "tui") throw new Error("invalid Jcode start mode");
|
|
12
12
|
for (let i = 0; i < args.length; i += 2) if (!["--model", "--mode", "--provider-profile", "--session-id"].includes(args[i]!) || !args[i + 1]) throw new Error("unknown Jcode option");
|
|
13
13
|
if (session !== undefined && !qwenSessionId(session)) throw new Error("invalid Jcode session identity");
|
|
14
|
-
|
|
14
|
+
// The daemon starts an instance with its own declared provider, model and profile (a launch flag still wins, as in the start branch):
|
|
15
|
+
// preview the same flags. Values stay redacted, as the base Jcode's model and profile already are.
|
|
16
|
+
const declared = instance ? config.instances?.[instance] : undefined;
|
|
17
|
+
const provider = declared?.provider ? REDACTED : undefined, modelWord = model || declared?.model ? REDACTED : undefined;
|
|
18
|
+
const profileWord = profile || declared?.provider_profile || config.jcode_provider_profile ? REDACTED : undefined;
|
|
19
|
+
const cmd = mode === "tui" ? jcodeTuiCommand(nativeCommand(config.jcode_cmd, ["jcode"]), UNRESOLVED, UNRESOLVED, modelWord, profileWord, provider)
|
|
20
|
+
: jcodeCommand(nativeCommand(config.jcode_cmd, ["jcode"]), "acp", UNRESOLVED, UNRESOLVED, modelWord, profileWord, provider, instance ?? "jcode");
|
|
15
21
|
launch = { cmd: cmd[0]!, args: cmd.slice(1) };
|
|
16
22
|
unresolved.push({ field: "ownedDaemon", reason: "private daemon/socket and source-pinned native identity verified at start; preview starts nothing" });
|
|
17
23
|
unresolved.push({ field: "hubTools", reason: "project MCP bridge required; native tool-call receipt remains unverified" });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describe } from "../compat.ts";
|
|
1
2
|
import { join } from "node:path";
|
|
2
3
|
import type { Launch } from "../../cli/launch.ts";
|
|
3
4
|
import { REDACTED, UNRESOLVED, nativeCommand, previewState, finishPreview, type NativePreviewContext } from "./native-cli-preview-common.ts";
|
|
@@ -16,7 +17,7 @@ export function previewQwen(ctx: NativePreviewContext) {
|
|
|
16
17
|
if (sessionId) unresolved.push({ field: "sessionId", reason: "requested through ACP session/load at start; preview does not load or expose it" });
|
|
17
18
|
launch = adapter.start.launch!({ instance: tool, form: "headless", projectRoot: cwd, stateDir, entrypoint: join(cliDir, "main.js"),
|
|
18
19
|
userArgs: [], unattended: false, env: {}, preview: true, nativeCommand: nativeCommand(config.qwen_cmd, ["qwen", "--acp"]), model: model ? REDACTED : undefined });
|
|
19
|
-
unresolved.push({ field: "nativeIdentity", reason:
|
|
20
|
+
unresolved.push({ field: "nativeIdentity", reason: `Qwen Code (${describe("qwen")}) identity and permission modes are checked at ACP initialization; preview starts nothing` });
|
|
20
21
|
|
|
21
22
|
return finishPreview(ctx, launch, envNames, unresolved, undefined);
|
|
22
23
|
}
|
|
@@ -1,16 +1,7 @@
|
|
|
1
1
|
import type { AdapterDefinition } from "../registry-types.ts";
|
|
2
2
|
import type { NativePreviewBuilder } from "./native-cli-preview-common.ts";
|
|
3
|
-
import { previewClaude } from "./native-cli-preview-claude.ts";
|
|
4
|
-
import { previewCodex } from "./native-cli-preview-codex.ts";
|
|
5
|
-
import { previewKimi } from "./native-cli-preview-kimi.ts";
|
|
6
|
-
import { previewPi } from "./native-cli-preview-pi.ts";
|
|
7
|
-
import { previewQwen } from "./native-cli-preview-qwen.ts";
|
|
8
|
-
import { previewJcode } from "./native-cli-preview-jcode.ts";
|
|
9
3
|
|
|
10
|
-
|
|
11
|
-
const previews: Readonly<Record<string, NativePreviewBuilder>> = Object.freeze({
|
|
12
|
-
claude: previewClaude, codex: previewCodex, kimi: previewKimi, pi: previewPi, qwen: previewQwen, jcode: previewJcode,
|
|
13
|
-
});
|
|
4
|
+
/** The host admits builtin-only preview ports before calling this projection. */
|
|
14
5
|
export function builtinPreview(definition: AdapterDefinition): NativePreviewBuilder | undefined {
|
|
15
|
-
return
|
|
6
|
+
return definition.cli.preview;
|
|
16
7
|
}
|
|
@@ -1,16 +1,20 @@
|
|
|
1
|
+
import { assessVersion } from "../compat.ts";
|
|
1
2
|
import type { PermissionMode } from "../../hub/permission-mode.ts";
|
|
2
3
|
import type { Launch } from "../../cli/launch.ts";
|
|
3
4
|
|
|
4
5
|
export const QWEN_MODE_IDS: Readonly<Record<PermissionMode, string>> = { ask: "default", "ask-when-needed": "auto-edit", "never-ask": "yolo" };
|
|
5
6
|
|
|
6
7
|
export class QwenIdentityError extends Error {
|
|
7
|
-
constructor() { super("Qwen Code
|
|
8
|
+
constructor(reason?: string) { super(reason ? `Qwen Code native identity is not verified: ${reason}` : "Qwen Code native identity is not verified; check the executable and its advertised ACP version"); }
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export function qwenNativeIdentity(instance: string, production: boolean | undefined, init: unknown, kind?: string): boolean {
|
|
11
12
|
const native = init as { agentInfo?: { name?: unknown; version?: unknown } } | null | undefined;
|
|
12
|
-
const
|
|
13
|
-
|
|
13
|
+
const identity = production === true && (kind ?? instance) === "qwen" && native?.agentInfo?.name === "qwen-code";
|
|
14
|
+
// #691: a version refusal carries the compat reason (observed and verified versions, the trust command).
|
|
15
|
+
const version = identity ? assessVersion("qwen", native?.agentInfo?.version) : undefined;
|
|
16
|
+
if (production && !version?.ok) throw new QwenIdentityError(version && !version.ok ? version.reason : undefined);
|
|
17
|
+
const verified = !!version?.ok;
|
|
14
18
|
return verified;
|
|
15
19
|
}
|
|
16
20
|
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { describe } from "../compat.ts";
|
|
2
|
+
import { runQwenEntry } from "./native-cli-entry-qwen.ts";
|
|
3
|
+
import { previewQwen } from "./native-cli-preview-qwen.ts";
|
|
1
4
|
import { QWEN_DISPLAY } from "./native-display.ts";
|
|
2
5
|
import { QWEN_RECOVERY_SESSION } from "./native-recovery.ts";
|
|
3
6
|
import { AcpPeer } from "../acp.ts";
|
|
@@ -9,6 +12,7 @@ import { shellQuote } from "../../cli/shell-quote.ts";
|
|
|
9
12
|
export const QWEN_DEFINITION: AdapterDefinition = {
|
|
10
13
|
api: 1,
|
|
11
14
|
kind: "qwen",
|
|
15
|
+
projection: { roleRank: 5, helpRank: 5, plannerEligible: true },
|
|
12
16
|
displayName: "Qwen Code",
|
|
13
17
|
family: "qwen",
|
|
14
18
|
transport: "acp",
|
|
@@ -64,7 +68,7 @@ export const QWEN_DEFINITION: AdapterDefinition = {
|
|
|
64
68
|
turnEnd: "adapter",
|
|
65
69
|
budget: { account: "qwen" },
|
|
66
70
|
capabilities: { watch: true },
|
|
67
|
-
cli: { command: "qwen", launchConfigKeys: ["qwen_cmd"], startWords: () => ["qwen"] },
|
|
71
|
+
cli: { command: "qwen", entry: { lifecycleAdmission: true, execute: runQwenEntry }, preview: previewQwen, usage: [["ahub qwen [--mode headless] [--model <alias>] [--session-id <id>] [--print-command]", `start optional Qwen Code ACP (${describe("qwen")}; roster.qwen, default off)`]], launchConfigKeys: ["qwen_cmd"], startWords: () => ["qwen"] },
|
|
68
72
|
display: QWEN_DISPLAY,
|
|
69
73
|
hooks: { recallPlatform: "qwen" },
|
|
70
74
|
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { types } from "node:util";
|
|
2
|
+
import { ADAPTER_API } from "./adapter-api.ts";
|
|
3
|
+
import type { AdapterDefinition } from "./registry-types.ts";
|
|
4
|
+
export interface ExternalAdapterMetadata { readonly kind: string; readonly displayName: string }
|
|
5
|
+
const MAX_MODULES = 16;
|
|
6
|
+
const fail = (reason: string): never => { throw new Error(`external adapter: ${reason}`); };
|
|
7
|
+
export function moduleDataObject(value: unknown): Record<string, unknown> {
|
|
8
|
+
if (!value || typeof value !== "object" || types.isProxy(value) || Array.isArray(value)
|
|
9
|
+
|| ![Object.prototype, null].includes(Object.getPrototypeOf(value)) || Object.getOwnPropertySymbols(value).length) return fail("plain own-data object required");
|
|
10
|
+
const fields: Record<string, PropertyDescriptor> = Object.assign(Object.create(null), Object.getOwnPropertyDescriptors(value));
|
|
11
|
+
if (Object.values(fields).some(field => !Object.hasOwn(field, "value") || !field.enumerable)) return fail("accessors and hidden fields are unsupported");
|
|
12
|
+
return Object.fromEntries(Object.entries(fields).map(([key, field]) => [key, field.value]));
|
|
13
|
+
}
|
|
14
|
+
export function moduleText(value: unknown, max: number): string {
|
|
15
|
+
if (typeof value !== "string" || !value.trim() || Buffer.byteLength(value) > max || /[\p{Cc}\p{Cf}]/u.test(value)) return fail("bounded control-free text required");
|
|
16
|
+
return value;
|
|
17
|
+
}
|
|
18
|
+
export function moduleArray(value: unknown, max: number): unknown[] {
|
|
19
|
+
if (!Array.isArray(value) || types.isProxy(value) || Object.getPrototypeOf(value) !== Array.prototype || value.length > max
|
|
20
|
+
|| Object.getOwnPropertySymbols(value).length) return fail("bounded array required");
|
|
21
|
+
const fields: Record<string, PropertyDescriptor> = Object.assign(Object.create(null), Object.getOwnPropertyDescriptors(value));
|
|
22
|
+
if (Object.keys(fields).length !== value.length + 1) return fail("dense array required");
|
|
23
|
+
return Array.from({ length: value.length }, (_, index) => {
|
|
24
|
+
const field = fields[String(index)];
|
|
25
|
+
if (!field || !Object.hasOwn(field, "value") || !field.enumerable) return fail("array data fields required");
|
|
26
|
+
return field.value;
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/** Bounded inert metadata, usable by CLI/config readers without executing module callbacks. */
|
|
30
|
+
export function externalAdapterMetadata(raw: unknown): readonly ExternalAdapterMetadata[] {
|
|
31
|
+
if (raw === undefined) return Object.freeze([]);
|
|
32
|
+
const rows = moduleArray(raw, MAX_MODULES).map(value => {
|
|
33
|
+
const row = moduleDataObject(value), kind = row.kind;
|
|
34
|
+
if (Object.keys(row).some(key => !["kind", "displayName"].includes(key)) || typeof kind !== "string" || !/^[a-z][a-z0-9-]{0,31}$/.test(kind)) return fail("invalid external display metadata");
|
|
35
|
+
return Object.freeze({ kind, displayName: moduleText(row.displayName, 256) });
|
|
36
|
+
});
|
|
37
|
+
if (new Set(rows.map(row => row.kind)).size !== rows.length) return fail("duplicate external display metadata");
|
|
38
|
+
return Object.freeze(rows);
|
|
39
|
+
}
|
|
40
|
+
export function externalMetadataDefinitions(raw: unknown): readonly AdapterDefinition[] {
|
|
41
|
+
return Object.freeze(externalAdapterMetadata(raw).map(({ kind, displayName }): AdapterDefinition => ({
|
|
42
|
+
api: ADAPTER_API, kind, family: kind, displayName, transport: "acp", roles: [], optional: true, defaultOnRoster: false,
|
|
43
|
+
trust: { onPrem: false, hubSandboxed: false }, identity: { mcp: "acp-session" }, permission: { genericAcp: {}, unmanaged: "external adapter metadata; authority belongs to the running daemon" },
|
|
44
|
+
start: { forms: ["headless"], defaultForm: "headless", conductorStartable: true, hubSwitches: [], ownedFlags: [] },
|
|
45
|
+
session: { idField: "sessionId", currentId: () => undefined }, turnEnd: "adapter",
|
|
46
|
+
cli: { command: kind, startWords: () => [kind] }, display: { palette: { fg: "white", bg: "black" } },
|
|
47
|
+
})));
|
|
48
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
interface Token { kind: "word" | "string" | "punct"; value: string }
|
|
2
|
+
const forbiddenNames = new Set(["require", "createRequire", "eval", "Function"]);
|
|
3
|
+
const globalNames = new Set(["globalThis", "global", "self", "window"]);
|
|
4
|
+
const regexAfterWords = new Set(["return", "throw", "case", "delete", "typeof", "void", "yield", "await", "in", "of", "else", "do"]);
|
|
5
|
+
const regexAfterPunctuation = new Set(["=", "(", "[", "{", ":", ",", ";", "!", "?", "&&", "||", "??", "=>", "+", "-", "*", "%", "&", "|", "^", "~", "<", ">"]);
|
|
6
|
+
const fail = (): never => { throw new Error("external adapter: module source scan refused (details withheld)"); };
|
|
7
|
+
|
|
8
|
+
/** Lex normalized JS, retaining literal tokens only for direct computed global/member access. */
|
|
9
|
+
function tokensOf(source: string): Token[] {
|
|
10
|
+
const tokens: Token[] = [];
|
|
11
|
+
let index = 0;
|
|
12
|
+
const controlParens: boolean[] = [];
|
|
13
|
+
let regexAllowed = true;
|
|
14
|
+
const push = (kind: Token["kind"], value: string) => {
|
|
15
|
+
const previous = tokens.at(-1);
|
|
16
|
+
if (kind === "punct" && value === "(") controlParens.push(previous?.kind === "word" && ["if", "while", "for", "with", "switch", "catch"].includes(previous.value));
|
|
17
|
+
const controlEnd = kind === "punct" && value === ")" ? controlParens.pop() === true : false;
|
|
18
|
+
tokens.push({ kind, value });
|
|
19
|
+
regexAllowed = controlEnd || (kind === "word" ? regexAfterWords.has(value) : kind === "punct" && regexAfterPunctuation.has(value));
|
|
20
|
+
};
|
|
21
|
+
const wordStart = (char: string) => /[$_\p{ID_Start}]/u.test(char);
|
|
22
|
+
const wordPart = (char: string) => /[$_\p{ID_Continue}]/u.test(char);
|
|
23
|
+
const quoted = (quote: string) => {
|
|
24
|
+
index++;
|
|
25
|
+
let value = "";
|
|
26
|
+
while (index < source.length) {
|
|
27
|
+
const char = source[index++]!;
|
|
28
|
+
if (char === quote) { push("string", value); return; }
|
|
29
|
+
if (char === "\\") {
|
|
30
|
+
const escaped = source[index++];
|
|
31
|
+
// Unknown escapes are not a proof of a statically named member. Transform normally canonicalizes them.
|
|
32
|
+
value += escaped === quote || escaped === "\\" ? escaped : "?";
|
|
33
|
+
} else value += char;
|
|
34
|
+
}
|
|
35
|
+
fail();
|
|
36
|
+
};
|
|
37
|
+
const regex = () => {
|
|
38
|
+
index++;
|
|
39
|
+
let characterClass = false;
|
|
40
|
+
while (index < source.length) {
|
|
41
|
+
const char = source[index++]!;
|
|
42
|
+
if (char === "\\") { index++; continue; }
|
|
43
|
+
if (char === "[") characterClass = true;
|
|
44
|
+
else if (char === "]") characterClass = false;
|
|
45
|
+
else if (char === "/" && !characterClass) {
|
|
46
|
+
while (index < source.length && /[a-z]/i.test(source[index]!)) index++;
|
|
47
|
+
push("string", "");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
fail();
|
|
52
|
+
};
|
|
53
|
+
const template = (depth: number) => {
|
|
54
|
+
if (depth > 128) fail();
|
|
55
|
+
index++;
|
|
56
|
+
let text = "";
|
|
57
|
+
while (index < source.length) {
|
|
58
|
+
const char = source[index]!;
|
|
59
|
+
if (source.charCodeAt(index) === 96) { index++; push("string", text); return; }
|
|
60
|
+
if (char === "\\") { index += 2; text += "?"; continue; }
|
|
61
|
+
if (char === "$" && source[index + 1] === "{") {
|
|
62
|
+
push("string", text); text = ""; index += 2;
|
|
63
|
+
push("punct", "(");
|
|
64
|
+
code(true, depth + 1);
|
|
65
|
+
push("punct", ")");
|
|
66
|
+
} else { text += char; index++; }
|
|
67
|
+
}
|
|
68
|
+
fail();
|
|
69
|
+
};
|
|
70
|
+
const code = (stopAtBrace: boolean, depth: number) => {
|
|
71
|
+
if (depth > 128) fail();
|
|
72
|
+
let braces = 0;
|
|
73
|
+
while (index < source.length) {
|
|
74
|
+
const char = source[index]!, next = source[index + 1];
|
|
75
|
+
if (/\s/u.test(char)) { index++; continue; }
|
|
76
|
+
if (char === "/" && next === "/") {
|
|
77
|
+
index += 2; while (index < source.length && source[index] !== "\n") index++;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (char === "/" && next === "*") {
|
|
81
|
+
const end = source.indexOf("*/", index + 2);
|
|
82
|
+
if (end < 0) fail(); index = end + 2; continue;
|
|
83
|
+
}
|
|
84
|
+
if (char === "'" || char === '"') { quoted(char); continue; }
|
|
85
|
+
if (source.charCodeAt(index) === 96) { template(depth + 1); continue; }
|
|
86
|
+
if (char === "}" && stopAtBrace && braces === 0) { index++; return; }
|
|
87
|
+
if (char === "{") braces++;
|
|
88
|
+
if (char === "}") braces--;
|
|
89
|
+
if (char === "/" && regexAllowed) { regex(); continue; }
|
|
90
|
+
if (wordStart(char)) {
|
|
91
|
+
const start = index++;
|
|
92
|
+
while (index < source.length && wordPart(source[index]!)) index++;
|
|
93
|
+
push("word", source.slice(start, index)); continue;
|
|
94
|
+
}
|
|
95
|
+
const pair = char + (next ?? "");
|
|
96
|
+
if (["=>", "&&", "||", "??", "++", "--", "?."].includes(pair)) { push("punct", pair); index += 2; }
|
|
97
|
+
else { push("punct", char); index++; }
|
|
98
|
+
}
|
|
99
|
+
if (stopAtBrace) fail();
|
|
100
|
+
};
|
|
101
|
+
code(false, 0);
|
|
102
|
+
return tokens;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Conservative ESM dependency/code-loading inspection, not a sandbox or proof against obfuscated privileged code.
|
|
107
|
+
* Literal messages/comments are inert. Direct code-loading names are refused even when aliased or shadowed.
|
|
108
|
+
*/
|
|
109
|
+
export function scanModuleSource(source: string): readonly Bun.Import[] {
|
|
110
|
+
try {
|
|
111
|
+
const transpiler = new Bun.Transpiler({ loader: "js", minifyWhitespace: true });
|
|
112
|
+
const imports = transpiler.scanImports(source);
|
|
113
|
+
if (imports.some(record => ["dynamic-import", "require-call", "require-resolve"].includes(record.kind))) fail();
|
|
114
|
+
const tokens = tokensOf(transpiler.transformSync(source));
|
|
115
|
+
for (const [index, token] of tokens.entries()) {
|
|
116
|
+
if (token.kind === "word" && forbiddenNames.has(token.value)) fail();
|
|
117
|
+
if (token.kind === "word" && token.value === "import" && tokens[index + 1]?.value === "(") fail();
|
|
118
|
+
if (token.kind === "string" && forbiddenNames.has(token.value) && tokens[index - 1]?.value === "[" && tokens[index + 1]?.value === "]") {
|
|
119
|
+
const owner = tokens[index - 2];
|
|
120
|
+
const importMeta = owner?.value === "meta" && tokens[index - 3]?.value === "." && tokens[index - 4]?.value === "import";
|
|
121
|
+
if (owner?.kind === "word" && globalNames.has(owner.value) || importMeta) fail();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return Object.freeze(imports.map(record => Object.freeze({ ...record })));
|
|
125
|
+
} catch { return fail(); }
|
|
126
|
+
}
|