@steerable/agent-shell 0.6.32 → 0.6.34
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/dist/host/runtime.js +10 -8
- package/dist/host/visible-terminal-exec.js +4 -0
- package/dist/host-tools-runtime.d.ts +10 -0
- package/dist/host-tools-runtime.js +44 -0
- package/dist/host-tools.d.ts +73 -0
- package/dist/host-tools.js +198 -0
- package/dist/local-backend/router.js +27 -16
- package/dist/local-backend/skill-loader.d.ts +32 -0
- package/dist/local-backend/skill-loader.js +36 -2
- package/dist/local-backend/task-service.js +2 -8
- package/dist/main.js +36 -16
- package/dist/mcp-server-registry.d.ts +2 -0
- package/dist/mcp-server-registry.js +8 -0
- package/dist/product-config.d.ts +63 -0
- package/dist/product-config.js +17 -0
- package/dist/server/http-server.js +7 -0
- package/dist/storage/index.js +93 -76
- package/dist/storage/llm-settings.d.ts +15 -0
- package/dist/storage/llm-settings.js +28 -0
- package/dist/tool-router.js +9 -2
- package/package.json +8 -4
package/dist/host/runtime.js
CHANGED
|
@@ -17,6 +17,7 @@ import { TerminalManager } from '../terminal-manager.js';
|
|
|
17
17
|
import { ToolRouter } from '../tool-router.js';
|
|
18
18
|
import { McpServerRegistry, } from '../mcp-server-registry.js';
|
|
19
19
|
import { ProjectRegistry } from '../project-registry.js';
|
|
20
|
+
import { getProductConfig } from '../product-config.js';
|
|
20
21
|
import { LocalBackendRouter } from '../local-backend/router.js';
|
|
21
22
|
import { WorktreeService } from '../local-backend/worktree-service.js';
|
|
22
23
|
import { TaskService } from '../local-backend/task-service.js';
|
|
@@ -31,6 +32,7 @@ import { getChatAttachmentsDir } from '../attachments.js';
|
|
|
31
32
|
import { ensureChatWorkspace } from '../project-home.js';
|
|
32
33
|
import { recordInsightTurn } from '../insights/record.js';
|
|
33
34
|
import { llmService } from '../llm/index.js';
|
|
35
|
+
import { isProductLlmLocked, resolveRuntimeLlmSettings } from '../host-tools-runtime.js';
|
|
34
36
|
import { getPackAssemblies, } from './pack-assembly.js';
|
|
35
37
|
export async function createHostRuntime(options) {
|
|
36
38
|
const { broadcast, hasWindow, onLog } = options;
|
|
@@ -65,15 +67,14 @@ export async function createHostRuntime(options) {
|
|
|
65
67
|
// 生效,这里负责进程重启后的恢复)。
|
|
66
68
|
{
|
|
67
69
|
const persistedLlmSettings = await defaultStore.getLlmSettings();
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
const runtimeLlmSettings = resolveRuntimeLlmSettings(persistedLlmSettings);
|
|
71
|
+
if (isProductLlmLocked()) {
|
|
72
|
+
await defaultStore.setLlmSettings(runtimeLlmSettings);
|
|
73
|
+
}
|
|
74
|
+
setDefaultExecTimeoutMs(runtimeLlmSettings.execTimeoutSeconds
|
|
75
|
+
? runtimeLlmSettings.execTimeoutSeconds * 1000
|
|
70
76
|
: null);
|
|
71
|
-
llmService.initialize(
|
|
72
|
-
provider: 'ollama',
|
|
73
|
-
model: 'llama3.1:8b',
|
|
74
|
-
baseUrl: 'http://127.0.0.1:11434',
|
|
75
|
-
temperature: 0.3,
|
|
76
|
-
}, {
|
|
77
|
+
llmService.initialize(runtimeLlmSettings, {
|
|
77
78
|
setLlmSettings: (settings) => defaultStore.setLlmSettings(settings),
|
|
78
79
|
});
|
|
79
80
|
}
|
|
@@ -85,6 +86,7 @@ export async function createHostRuntime(options) {
|
|
|
85
86
|
name: 'agent-mcp-servers',
|
|
86
87
|
defaults: { mcpServers: [] },
|
|
87
88
|
}));
|
|
89
|
+
mcpServerRegistry.seedProductServers(getProductConfig().builtinMcp);
|
|
88
90
|
const projectRegistry = new ProjectRegistry(createJsonStore({
|
|
89
91
|
name: 'agent-projects',
|
|
90
92
|
defaults: { projects: [] },
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import log from 'electron-log';
|
|
7
7
|
import { GUI_LAUNCH_WAIT_MS, getConfiguredExecTimeoutMs, isGuiLaunchCommand, rewriteExeCommandIfNeeded, } from '../local-executor.js';
|
|
8
|
+
import { getResolvedHostTools } from '../host-tools-runtime.js';
|
|
8
9
|
/**
|
|
9
10
|
* Map a raw PTY shell binary name (e.g. `powershell.exe`, `cmd.exe`, `zsh`) to
|
|
10
11
|
* the {@link LocalExecResult} `shell` enum so the agent sees the *real* dialect
|
|
@@ -33,6 +34,9 @@ export function mapSessionShell(shell) {
|
|
|
33
34
|
export function createVisibleTerminalExec(deps) {
|
|
34
35
|
const { localExecutor, terminalManager } = deps;
|
|
35
36
|
return async function maybeExecInTerminal(req) {
|
|
37
|
+
if (!getResolvedHostTools().terminal.chrome) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
36
40
|
if (req.command) {
|
|
37
41
|
req.command = rewriteExeCommandIfNeeded(req.command);
|
|
38
42
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { buildHostApproval, type ChatModeId, type ResolvedHostTools } from './host-tools.js';
|
|
2
|
+
import { type LlmSettings } from './storage/llm-settings.js';
|
|
3
|
+
export declare function getResolvedHostTools(): ResolvedHostTools;
|
|
4
|
+
export declare function isProductApprovalEnabled(): boolean;
|
|
5
|
+
export declare function resolveTurnApproval(storePath?: string): ReturnType<typeof buildHostApproval>;
|
|
6
|
+
export declare function assertHostIpcAllowed(channel: string): void;
|
|
7
|
+
export declare function resolveTurnChatMode(requested: unknown): ChatModeId;
|
|
8
|
+
export declare function isProductLlmLocked(): boolean;
|
|
9
|
+
/** 设置页开着走已存/出厂默认;关掉则钉死 product.json 的 llm。 */
|
|
10
|
+
export declare function resolveRuntimeLlmSettings(persisted: LlmSettings | null): LlmSettings;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 把产品注入的 hostTools / approval 接到解析器。
|
|
3
|
+
* host-tools.ts 保持无 node 依赖,供测试与渲染层复用解析规则。
|
|
4
|
+
*/
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { getProductConfig } from './product-config.js';
|
|
8
|
+
import { buildHostApproval, clampChatMode, isHostIpcAllowed, resolveChatModes, resolveHostTools, resolveSettingsChrome, } from './host-tools.js';
|
|
9
|
+
import { DEFAULT_LLM_SETTINGS, resolveLockedLlmSettings, } from './storage/llm-settings.js';
|
|
10
|
+
export function getResolvedHostTools() {
|
|
11
|
+
return resolveHostTools(getProductConfig().hostTools);
|
|
12
|
+
}
|
|
13
|
+
export function isProductApprovalEnabled() {
|
|
14
|
+
return (buildHostApproval({
|
|
15
|
+
productApproval: getProductConfig().approval,
|
|
16
|
+
envApproval: process.env.STEERABLE_APPROVAL,
|
|
17
|
+
storePath: 'unused',
|
|
18
|
+
}) !== undefined);
|
|
19
|
+
}
|
|
20
|
+
export function resolveTurnApproval(storePath = path.join(os.homedir(), '.steerable', 'approvals.json')) {
|
|
21
|
+
return buildHostApproval({
|
|
22
|
+
productApproval: getProductConfig().approval,
|
|
23
|
+
envApproval: process.env.STEERABLE_APPROVAL,
|
|
24
|
+
storePath,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export function assertHostIpcAllowed(channel) {
|
|
28
|
+
if (!isHostIpcAllowed(channel, getResolvedHostTools())) {
|
|
29
|
+
throw new Error(`${channel} is disabled for this product`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export function resolveTurnChatMode(requested) {
|
|
33
|
+
return clampChatMode(requested, resolveChatModes(getProductConfig().chatModes));
|
|
34
|
+
}
|
|
35
|
+
export function isProductLlmLocked() {
|
|
36
|
+
return !resolveSettingsChrome(getProductConfig().settings).llm;
|
|
37
|
+
}
|
|
38
|
+
/** 设置页开着走已存/出厂默认;关掉则钉死 product.json 的 llm。 */
|
|
39
|
+
export function resolveRuntimeLlmSettings(persisted) {
|
|
40
|
+
if (!isProductLlmLocked()) {
|
|
41
|
+
return persisted ?? DEFAULT_LLM_SETTINGS;
|
|
42
|
+
}
|
|
43
|
+
return resolveLockedLlmSettings(getProductConfig().llm, process.env, persisted);
|
|
44
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 宿主工具族:产品声明引入哪些工具,服务端按同一份配置钳死围栏 / 路由 / 工具面。
|
|
3
|
+
* 渲染层只投影 chrome;缺省(中性 shell、未声明的族)全开,兼容存量产品。
|
|
4
|
+
*/
|
|
5
|
+
export declare const HOST_TOOL_FAMILY_IDS: readonly ["terminal", "local-fs", "projects", "background-tasks", "mcp", "web", "plugins"];
|
|
6
|
+
export type HostToolFamilyId = (typeof HOST_TOOL_FAMILY_IDS)[number];
|
|
7
|
+
export interface HostToolFamilySurface {
|
|
8
|
+
/** 模型可见工具 + 围栏 / 服务端能力。 */
|
|
9
|
+
capability: boolean;
|
|
10
|
+
/** 用户入口:按钮、菜单、项目分组、点路径打开、可见 PTY。会话附件跟 capability 走。 */
|
|
11
|
+
chrome: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** 产品声明:`true` 两边开,`false` 两边关,对象可拆 chrome / capability。 */
|
|
14
|
+
export type HostToolsConfig = Partial<Record<HostToolFamilyId, boolean | {
|
|
15
|
+
capability?: boolean;
|
|
16
|
+
chrome?: boolean;
|
|
17
|
+
}>>;
|
|
18
|
+
export type ResolvedHostTools = Record<HostToolFamilyId, HostToolFamilySurface>;
|
|
19
|
+
export type ApprovalMode = 'host' | 'off';
|
|
20
|
+
export declare const CHAT_MODE_IDS: readonly ["agent", "plan"];
|
|
21
|
+
export type ChatModeId = (typeof CHAT_MODE_IDS)[number];
|
|
22
|
+
/** 产品声明的对话模式。缺省 Agent + Plan;空/非法回落 Agent。 */
|
|
23
|
+
export declare function resolveChatModes(value?: unknown): ChatModeId[];
|
|
24
|
+
export declare function clampChatMode(requested: unknown, modes?: readonly ChatModeId[]): ChatModeId;
|
|
25
|
+
/** 设置入口:侧栏页 + 综合设置分段。缺省全开;`false` 只藏入口。 */
|
|
26
|
+
export declare const SETTINGS_ITEM_IDS: readonly ["agents", "skills", "mcp", "appearance", "llm", "web-search", "usage", "diagnose", "security", "insights", "telemetry"];
|
|
27
|
+
export type SettingsItemId = (typeof SETTINGS_ITEM_IDS)[number];
|
|
28
|
+
export declare const GENERAL_SETTINGS_ITEM_IDS: readonly ["appearance", "llm", "web-search", "usage", "diagnose", "security", "insights", "telemetry"];
|
|
29
|
+
export type SettingsChromeConfig = Partial<Record<SettingsItemId, boolean>>;
|
|
30
|
+
export type ResolvedSettingsChrome = Record<SettingsItemId, boolean>;
|
|
31
|
+
/**
|
|
32
|
+
* 产品 `settings`:未声明的项全开。关了的宿主工具族会一并藏对应设置段
|
|
33
|
+
* (skills←plugins、mcp、web-search←web)。
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveSettingsChrome(value?: unknown, tools?: ResolvedHostTools): ResolvedSettingsChrome;
|
|
36
|
+
export declare function hasGeneralSettingsChrome(chrome?: ResolvedSettingsChrome): boolean;
|
|
37
|
+
export declare const LOCAL_FS_TOOL_NAMES: readonly ["local_exec_shell", "local_read_file", "local_write_file", "local_edit_file", "local_open_path", "local_run_snippet", "local_list_scripts", "local_run_script"];
|
|
38
|
+
export declare function resolveHostToolSurface(value: boolean | {
|
|
39
|
+
capability?: boolean;
|
|
40
|
+
chrome?: boolean;
|
|
41
|
+
} | undefined): HostToolFamilySurface;
|
|
42
|
+
/**
|
|
43
|
+
* 对象声明必须显式 `chrome: true` 才开入口。
|
|
44
|
+
* `capability` 缺省为 true——只关入口写成 `{ chrome: false }` 即可。
|
|
45
|
+
*/
|
|
46
|
+
export declare function resolveHostTools(config?: HostToolsConfig | null): ResolvedHostTools;
|
|
47
|
+
export declare function resolveApprovalMode(value: unknown): ApprovalMode;
|
|
48
|
+
/**
|
|
49
|
+
* 产品 `approval: "off"` 或 STEERABLE_APPROVAL=0 时本轮不挂 host 审批。
|
|
50
|
+
* 环境变量是调试逃生口;产品字段是装配真源。
|
|
51
|
+
*/
|
|
52
|
+
export declare function isApprovalEnabled(input?: {
|
|
53
|
+
productApproval?: unknown;
|
|
54
|
+
envApproval?: string;
|
|
55
|
+
}): boolean;
|
|
56
|
+
export declare function clampExecPolicy(requested: unknown, tools?: ResolvedHostTools): 'workspace' | 'full';
|
|
57
|
+
export declare function familyForHostToolName(name: string): HostToolFamilyId | null;
|
|
58
|
+
export declare function isHostToolCapabilityEnabled(name: string, tools?: ResolvedHostTools): boolean;
|
|
59
|
+
export declare function isHostRouteAllowed(pathname: string, tools?: ResolvedHostTools): boolean;
|
|
60
|
+
export declare function sanitizeRightPanelKind(kind: string | null | undefined, tools?: ResolvedHostTools): string | null;
|
|
61
|
+
/** Electron IPC 通道与 HTTP `/host/*` 用同一套 chrome / capability 规则。 */
|
|
62
|
+
export declare function isHostIpcAllowed(channel: string, tools?: ResolvedHostTools): boolean;
|
|
63
|
+
/** 本轮是否挂 host 审批对象;关掉时返回 undefined(sidecar 不弹询问)。 */
|
|
64
|
+
export declare function buildHostApproval(input: {
|
|
65
|
+
productApproval?: unknown;
|
|
66
|
+
envApproval?: string;
|
|
67
|
+
storePath: string;
|
|
68
|
+
timeoutMs?: number;
|
|
69
|
+
}): {
|
|
70
|
+
mode: 'host';
|
|
71
|
+
timeoutMs: number;
|
|
72
|
+
storePath: string;
|
|
73
|
+
} | undefined;
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 宿主工具族:产品声明引入哪些工具,服务端按同一份配置钳死围栏 / 路由 / 工具面。
|
|
3
|
+
* 渲染层只投影 chrome;缺省(中性 shell、未声明的族)全开,兼容存量产品。
|
|
4
|
+
*/
|
|
5
|
+
export const HOST_TOOL_FAMILY_IDS = [
|
|
6
|
+
'terminal',
|
|
7
|
+
'local-fs',
|
|
8
|
+
'projects',
|
|
9
|
+
'background-tasks',
|
|
10
|
+
'mcp',
|
|
11
|
+
'web',
|
|
12
|
+
'plugins',
|
|
13
|
+
];
|
|
14
|
+
export const CHAT_MODE_IDS = ['agent', 'plan'];
|
|
15
|
+
const DEFAULT_CHAT_MODES = ['agent', 'plan'];
|
|
16
|
+
/** 产品声明的对话模式。缺省 Agent + Plan;空/非法回落 Agent。 */
|
|
17
|
+
export function resolveChatModes(value) {
|
|
18
|
+
if (!Array.isArray(value))
|
|
19
|
+
return [...DEFAULT_CHAT_MODES];
|
|
20
|
+
const allowed = CHAT_MODE_IDS.filter((id) => value.includes(id));
|
|
21
|
+
return allowed.length > 0 ? allowed : ['agent'];
|
|
22
|
+
}
|
|
23
|
+
export function clampChatMode(requested, modes = resolveChatModes()) {
|
|
24
|
+
return requested === 'plan' && modes.includes('plan') ? 'plan' : 'agent';
|
|
25
|
+
}
|
|
26
|
+
/** 设置入口:侧栏页 + 综合设置分段。缺省全开;`false` 只藏入口。 */
|
|
27
|
+
export const SETTINGS_ITEM_IDS = [
|
|
28
|
+
'agents',
|
|
29
|
+
'skills',
|
|
30
|
+
'mcp',
|
|
31
|
+
'appearance',
|
|
32
|
+
'llm',
|
|
33
|
+
'web-search',
|
|
34
|
+
'usage',
|
|
35
|
+
'diagnose',
|
|
36
|
+
'security',
|
|
37
|
+
'insights',
|
|
38
|
+
'telemetry',
|
|
39
|
+
];
|
|
40
|
+
export const GENERAL_SETTINGS_ITEM_IDS = [
|
|
41
|
+
'appearance',
|
|
42
|
+
'llm',
|
|
43
|
+
'web-search',
|
|
44
|
+
'usage',
|
|
45
|
+
'diagnose',
|
|
46
|
+
'security',
|
|
47
|
+
'insights',
|
|
48
|
+
'telemetry',
|
|
49
|
+
];
|
|
50
|
+
function settingsFollowsHostTool(id, tools) {
|
|
51
|
+
if (id === 'skills')
|
|
52
|
+
return tools.plugins.chrome;
|
|
53
|
+
if (id === 'mcp')
|
|
54
|
+
return tools.mcp.chrome;
|
|
55
|
+
if (id === 'web-search')
|
|
56
|
+
return tools.web.chrome;
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* 产品 `settings`:未声明的项全开。关了的宿主工具族会一并藏对应设置段
|
|
61
|
+
* (skills←plugins、mcp、web-search←web)。
|
|
62
|
+
*/
|
|
63
|
+
export function resolveSettingsChrome(value, tools = resolveHostTools()) {
|
|
64
|
+
const config = value && typeof value === 'object' && !Array.isArray(value)
|
|
65
|
+
? value
|
|
66
|
+
: {};
|
|
67
|
+
const out = {};
|
|
68
|
+
for (const id of SETTINGS_ITEM_IDS) {
|
|
69
|
+
out[id] = config[id] !== false && settingsFollowsHostTool(id, tools);
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
export function hasGeneralSettingsChrome(chrome = resolveSettingsChrome()) {
|
|
74
|
+
return GENERAL_SETTINGS_ITEM_IDS.some((id) => chrome[id]);
|
|
75
|
+
}
|
|
76
|
+
export const LOCAL_FS_TOOL_NAMES = [
|
|
77
|
+
'local_exec_shell',
|
|
78
|
+
'local_read_file',
|
|
79
|
+
'local_write_file',
|
|
80
|
+
'local_edit_file',
|
|
81
|
+
'local_open_path',
|
|
82
|
+
'local_run_snippet',
|
|
83
|
+
'local_list_scripts',
|
|
84
|
+
'local_run_script',
|
|
85
|
+
];
|
|
86
|
+
const FAMILY_TOOL_PREFIX = {
|
|
87
|
+
terminal: [],
|
|
88
|
+
projects: [],
|
|
89
|
+
'local-fs': LOCAL_FS_TOOL_NAMES,
|
|
90
|
+
'background-tasks': [
|
|
91
|
+
'task_run',
|
|
92
|
+
'task_send',
|
|
93
|
+
'task_status',
|
|
94
|
+
'task_result',
|
|
95
|
+
'worktree_create',
|
|
96
|
+
'worktree_list',
|
|
97
|
+
'worktree_remove',
|
|
98
|
+
],
|
|
99
|
+
mcp: ['mcp_list_tools', 'mcp_tool_exec'],
|
|
100
|
+
web: ['web_search', 'web_fetch'],
|
|
101
|
+
plugins: ['plugin_list', 'plugin_enable', 'plugin_disable', 'plugin_reload'],
|
|
102
|
+
};
|
|
103
|
+
const ALL_ON = { capability: true, chrome: true };
|
|
104
|
+
const ALL_OFF = { capability: false, chrome: false };
|
|
105
|
+
export function resolveHostToolSurface(value) {
|
|
106
|
+
if (value === undefined)
|
|
107
|
+
return { ...ALL_ON };
|
|
108
|
+
if (value === true)
|
|
109
|
+
return { ...ALL_ON };
|
|
110
|
+
if (value === false)
|
|
111
|
+
return { ...ALL_OFF };
|
|
112
|
+
return {
|
|
113
|
+
capability: value.capability !== false,
|
|
114
|
+
chrome: value.chrome === true,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* 对象声明必须显式 `chrome: true` 才开入口。
|
|
119
|
+
* `capability` 缺省为 true——只关入口写成 `{ chrome: false }` 即可。
|
|
120
|
+
*/
|
|
121
|
+
export function resolveHostTools(config) {
|
|
122
|
+
const out = {};
|
|
123
|
+
for (const id of HOST_TOOL_FAMILY_IDS) {
|
|
124
|
+
out[id] = resolveHostToolSurface(config?.[id]);
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
export function resolveApprovalMode(value) {
|
|
129
|
+
return value === 'off' ? 'off' : 'host';
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* 产品 `approval: "off"` 或 STEERABLE_APPROVAL=0 时本轮不挂 host 审批。
|
|
133
|
+
* 环境变量是调试逃生口;产品字段是装配真源。
|
|
134
|
+
*/
|
|
135
|
+
export function isApprovalEnabled(input = {}) {
|
|
136
|
+
if (input.envApproval === '0')
|
|
137
|
+
return false;
|
|
138
|
+
return resolveApprovalMode(input.productApproval) === 'host';
|
|
139
|
+
}
|
|
140
|
+
export function clampExecPolicy(requested, tools = resolveHostTools()) {
|
|
141
|
+
if (!tools['local-fs'].chrome)
|
|
142
|
+
return 'workspace';
|
|
143
|
+
return requested === 'full' ? 'full' : 'workspace';
|
|
144
|
+
}
|
|
145
|
+
export function familyForHostToolName(name) {
|
|
146
|
+
if (name.startsWith('mcp__'))
|
|
147
|
+
return 'mcp';
|
|
148
|
+
for (const id of HOST_TOOL_FAMILY_IDS) {
|
|
149
|
+
if (FAMILY_TOOL_PREFIX[id].includes(name))
|
|
150
|
+
return id;
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
export function isHostToolCapabilityEnabled(name, tools = resolveHostTools()) {
|
|
155
|
+
const family = familyForHostToolName(name);
|
|
156
|
+
return family == null ? true : tools[family].capability;
|
|
157
|
+
}
|
|
158
|
+
export function isHostRouteAllowed(pathname, tools = resolveHostTools()) {
|
|
159
|
+
if (pathname.startsWith('/host/terminal'))
|
|
160
|
+
return tools.terminal.chrome;
|
|
161
|
+
// 会话附件:公文关 chrome(不露访达 / 完整权限)仍要上传材料。
|
|
162
|
+
if (pathname === '/host/attachments/save')
|
|
163
|
+
return tools['local-fs'].capability;
|
|
164
|
+
if (pathname === '/host/local/open-path')
|
|
165
|
+
return tools['local-fs'].chrome;
|
|
166
|
+
if (pathname.startsWith('/host/local/'))
|
|
167
|
+
return tools['local-fs'].capability;
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
export function sanitizeRightPanelKind(kind, tools = resolveHostTools()) {
|
|
171
|
+
if (!kind)
|
|
172
|
+
return null;
|
|
173
|
+
if (kind === 'terminal')
|
|
174
|
+
return tools.terminal.chrome ? 'terminal' : null;
|
|
175
|
+
return kind;
|
|
176
|
+
}
|
|
177
|
+
/** Electron IPC 通道与 HTTP `/host/*` 用同一套 chrome / capability 规则。 */
|
|
178
|
+
export function isHostIpcAllowed(channel, tools = resolveHostTools()) {
|
|
179
|
+
if (channel.startsWith('terminal:'))
|
|
180
|
+
return tools.terminal.chrome;
|
|
181
|
+
if (channel === 'attachments:save')
|
|
182
|
+
return tools['local-fs'].capability;
|
|
183
|
+
if (channel === 'local:open-path')
|
|
184
|
+
return tools['local-fs'].chrome;
|
|
185
|
+
if (channel.startsWith('local:'))
|
|
186
|
+
return tools['local-fs'].capability;
|
|
187
|
+
return true;
|
|
188
|
+
}
|
|
189
|
+
/** 本轮是否挂 host 审批对象;关掉时返回 undefined(sidecar 不弹询问)。 */
|
|
190
|
+
export function buildHostApproval(input) {
|
|
191
|
+
if (!isApprovalEnabled(input))
|
|
192
|
+
return undefined;
|
|
193
|
+
return {
|
|
194
|
+
mode: 'host',
|
|
195
|
+
timeoutMs: input.timeoutMs ?? 120_000,
|
|
196
|
+
storePath: input.storePath,
|
|
197
|
+
};
|
|
198
|
+
}
|
|
@@ -22,6 +22,8 @@ import { sanitizeCompatOverrides, sanitizePresetsChoice, sanitizeLlmProvider, sa
|
|
|
22
22
|
import { setDefaultExecTimeoutMs } from '../local-executor.js';
|
|
23
23
|
import { allowEgressForBaseUrl, getActiveEgressBroker, getEgressPosture } from '../sidecar/egress-proxy.js';
|
|
24
24
|
import { buildExecSandbox, parseExecPolicy } from '../sidecar/exec-sandbox.js';
|
|
25
|
+
import { clampExecPolicy } from '../host-tools.js';
|
|
26
|
+
import { getResolvedHostTools, isProductLlmLocked, resolveRuntimeLlmSettings, resolveTurnApproval, resolveTurnChatMode, } from '../host-tools-runtime.js';
|
|
25
27
|
import { SidecarSupervisor } from '../sidecar/index.js';
|
|
26
28
|
import { diagnoseLlmConnection } from './llm-diagnose.js';
|
|
27
29
|
import { brandSkillVars, buildSystemPrompt, buildForcedSkillMessage, conditionsFromTools, } from './prompt-builder.js';
|
|
@@ -38,7 +40,7 @@ import { detectInterruptedTurn } from './interrupted-helper.js';
|
|
|
38
40
|
import { dropCurrentUserMessage } from './history-helper.js';
|
|
39
41
|
import { parseImageAttachments, processImageAttachments } from '../image-attachment.js';
|
|
40
42
|
import { loadProjectRuleFiles } from '../project-rules.js';
|
|
41
|
-
import { allocateProjectHome, ensureChatWorkspace, ensureProjectHome, } from '../project-home.js';
|
|
43
|
+
import { allocateProjectHome, ensureChatWorkspace, ensureProjectHome, expandUserPath, } from '../project-home.js';
|
|
42
44
|
import { registerLiveStream, getLiveStream, removeLiveStream } from './live-stream.js';
|
|
43
45
|
import { beginPackTurnObservers, collectPackExecWritableRoots, collectPackForcedSkillVars, collectPackWorldState, } from './pack-turn-hooks.js';
|
|
44
46
|
import { matchPackBackendRoute } from './pack-backend-routes.js';
|
|
@@ -48,7 +50,9 @@ import { getAuthProvider } from '../auth/index.js';
|
|
|
48
50
|
function parseSourceFolders(value) {
|
|
49
51
|
if (!Array.isArray(value))
|
|
50
52
|
return [];
|
|
51
|
-
return value
|
|
53
|
+
return value
|
|
54
|
+
.filter((item) => typeof item === 'string' && item.trim() !== '')
|
|
55
|
+
.map(expandUserPath);
|
|
52
56
|
}
|
|
53
57
|
/**
|
|
54
58
|
* 把 CoreLoop/LLM 的底层错误翻译成适合直接显示在助手消息里的中文提示。
|
|
@@ -391,6 +395,9 @@ export class LocalBackendRouter {
|
|
|
391
395
|
const projectId = typeof payload.projectId === 'string' && payload.projectId.trim()
|
|
392
396
|
? payload.projectId.trim()
|
|
393
397
|
: null;
|
|
398
|
+
if (projectId && !getResolvedHostTools().projects.capability) {
|
|
399
|
+
return { status: 403, data: { detail: 'Projects are disabled for this product' } };
|
|
400
|
+
}
|
|
394
401
|
if (projectId) {
|
|
395
402
|
const project = this.toolRouter.projectRegistry?.get(projectId);
|
|
396
403
|
if (!project) {
|
|
@@ -426,6 +433,10 @@ export class LocalBackendRouter {
|
|
|
426
433
|
// ───── 项目模式:projects CRUD ─────
|
|
427
434
|
// 项目记录存 electron-store(agent-projects.json),chat.project_id 存
|
|
428
435
|
// SQLite。删除项目不删会话——会话降级为无项目对话。
|
|
436
|
+
if ((pathname === '/api/v2/projects' || pathname.startsWith('/api/v2/projects/')) &&
|
|
437
|
+
!getResolvedHostTools().projects.capability) {
|
|
438
|
+
return { status: 403, data: { error: 'Projects are disabled for this product' } };
|
|
439
|
+
}
|
|
429
440
|
if (pathname === '/api/v2/projects') {
|
|
430
441
|
const registry = this.toolRouter.projectRegistry;
|
|
431
442
|
if (!registry) {
|
|
@@ -439,7 +450,7 @@ export class LocalBackendRouter {
|
|
|
439
450
|
try {
|
|
440
451
|
const name = String(payload.name || '');
|
|
441
452
|
const sourceFolders = parseSourceFolders(payload.sourceFolders);
|
|
442
|
-
let folderPath = typeof payload.folderPath === 'string' ? payload.folderPath
|
|
453
|
+
let folderPath = typeof payload.folderPath === 'string' ? expandUserPath(payload.folderPath) : '';
|
|
443
454
|
if (!folderPath) {
|
|
444
455
|
folderPath = allocateProjectHome(name);
|
|
445
456
|
ensureProjectHome(folderPath);
|
|
@@ -471,7 +482,7 @@ export class LocalBackendRouter {
|
|
|
471
482
|
try {
|
|
472
483
|
const project = registry.update(projectId, {
|
|
473
484
|
name: typeof payload.name === 'string' ? payload.name : undefined,
|
|
474
|
-
folderPath: typeof payload.folderPath === 'string' ? payload.folderPath : undefined,
|
|
485
|
+
folderPath: typeof payload.folderPath === 'string' ? expandUserPath(payload.folderPath) : undefined,
|
|
475
486
|
sourceFolders: payload.sourceFolders === undefined
|
|
476
487
|
? undefined
|
|
477
488
|
: parseSourceFolders(payload.sourceFolders),
|
|
@@ -679,6 +690,9 @@ export class LocalBackendRouter {
|
|
|
679
690
|
// (必须真实存在,防止把会话挂到幽灵项目上)。
|
|
680
691
|
let projectIdUpdate;
|
|
681
692
|
if ('projectId' in payload) {
|
|
693
|
+
if (!getResolvedHostTools().projects.capability) {
|
|
694
|
+
return { status: 403, data: { detail: 'Projects are disabled for this product' } };
|
|
695
|
+
}
|
|
682
696
|
if (payload.projectId === null) {
|
|
683
697
|
projectIdUpdate = null;
|
|
684
698
|
}
|
|
@@ -1463,10 +1477,13 @@ export class LocalBackendRouter {
|
|
|
1463
1477
|
if (method === 'GET') {
|
|
1464
1478
|
return {
|
|
1465
1479
|
status: 200,
|
|
1466
|
-
data: await this.store.getLlmSettings(),
|
|
1480
|
+
data: resolveRuntimeLlmSettings(await this.store.getLlmSettings()),
|
|
1467
1481
|
};
|
|
1468
1482
|
}
|
|
1469
1483
|
if (method === 'POST') {
|
|
1484
|
+
if (isProductLlmLocked()) {
|
|
1485
|
+
return { status: 403, data: { error: 'LLM settings are product-locked' } };
|
|
1486
|
+
}
|
|
1470
1487
|
const payload = this.toRecord(request.body);
|
|
1471
1488
|
const saved = await llmService.setSettings({
|
|
1472
1489
|
provider: sanitizeLlmProvider(payload.provider),
|
|
@@ -2020,7 +2037,7 @@ export class LocalBackendRouter {
|
|
|
2020
2037
|
}
|
|
2021
2038
|
// Plan 模式(类 Cursor "先出计划"):仅暴露只读工具给模型,引导它先调研、
|
|
2022
2039
|
// 再输出结构化计划,不做任何写操作/执行。其余值一律视为常规 agent 模式。
|
|
2023
|
-
const chatMode = payload.mode
|
|
2040
|
+
const chatMode = resolveTurnChatMode(payload.mode);
|
|
2024
2041
|
// 本轮生效的智能体:人设、技能勾选、工具策略同源。必须先解析——工具
|
|
2025
2042
|
// 策略决定工具列表,工具列表又决定技能的触发条件。
|
|
2026
2043
|
const turnAgents = await this.resolveTurnAgents(chatId, payload, mentionedAgentIds);
|
|
@@ -2910,19 +2927,13 @@ export class LocalBackendRouter {
|
|
|
2910
2927
|
// 工作区)。
|
|
2911
2928
|
...collectPackExecWritableRoots(chatId),
|
|
2912
2929
|
workspaceRoot,
|
|
2913
|
-
], { policy: parseExecPolicy(payload.execPolicy) });
|
|
2930
|
+
], { policy: clampExecPolicy(parseExecPolicy(payload.execPolicy), getResolvedHostTools()) });
|
|
2914
2931
|
// W4-1: approval algebra, host mode — every tool call is gated by the
|
|
2915
2932
|
// Electron approval UI over the reverse channel. The durable scope
|
|
2916
2933
|
// (allow_always / deny_always) persists per tool category under
|
|
2917
|
-
// ~/.steerable (writable inside the layer-1 sandbox).
|
|
2918
|
-
//
|
|
2919
|
-
const approval =
|
|
2920
|
-
? undefined
|
|
2921
|
-
: {
|
|
2922
|
-
mode: 'host',
|
|
2923
|
-
timeoutMs: 120_000,
|
|
2924
|
-
storePath: path.join(os.homedir(), '.steerable', 'approvals.json'),
|
|
2925
|
-
};
|
|
2934
|
+
// ~/.steerable (writable inside the layer-1 sandbox). 产品
|
|
2935
|
+
// approval:"off" 或 STEERABLE_APPROVAL=0 本轮不挂审批。
|
|
2936
|
+
const approval = resolveTurnApproval();
|
|
2926
2937
|
let assistantText = '';
|
|
2927
2938
|
const executedActions = [];
|
|
2928
2939
|
const timeline = [];
|
|
@@ -14,6 +14,38 @@
|
|
|
14
14
|
* readable by both. The sidecar is the only chat path and auto-restarts, so
|
|
15
15
|
* it is available both in-turn (prompt assembly) and for the management UI.
|
|
16
16
|
*/
|
|
17
|
+
/** shell 随包分发的内置技能目录。产品经 product.json `builtinSkills` 按需引入。 */
|
|
18
|
+
export declare const SHELL_BUILTIN_SKILLS: readonly [{
|
|
19
|
+
readonly dir: "00-identity";
|
|
20
|
+
readonly name: "identity";
|
|
21
|
+
}, {
|
|
22
|
+
readonly dir: "10-goal";
|
|
23
|
+
readonly name: "goal";
|
|
24
|
+
}, {
|
|
25
|
+
readonly dir: "11-loop";
|
|
26
|
+
readonly name: "loop";
|
|
27
|
+
}, {
|
|
28
|
+
readonly dir: "12-create-skill";
|
|
29
|
+
readonly name: "create-skill";
|
|
30
|
+
}, {
|
|
31
|
+
readonly dir: "70-plan-mode";
|
|
32
|
+
readonly name: "plan-mode";
|
|
33
|
+
}, {
|
|
34
|
+
readonly dir: "80-tool-usage";
|
|
35
|
+
readonly name: "tool-usage";
|
|
36
|
+
}, {
|
|
37
|
+
readonly dir: "81-anti-deferred";
|
|
38
|
+
readonly name: "anti-deferred-execution";
|
|
39
|
+
}, {
|
|
40
|
+
readonly dir: "82-data-grounding";
|
|
41
|
+
readonly name: "data-grounding";
|
|
42
|
+
}, {
|
|
43
|
+
readonly dir: "85-local-exec";
|
|
44
|
+
readonly name: "local-exec";
|
|
45
|
+
}, {
|
|
46
|
+
readonly dir: "86-proactive-coding";
|
|
47
|
+
readonly name: "proactive-coding";
|
|
48
|
+
}];
|
|
17
49
|
/** builtin = 随应用分发; user = 设置页导入; workspace = 项目/工作区 `skills/`. */
|
|
18
50
|
export type SkillOrigin = 'builtin' | 'user' | 'workspace';
|
|
19
51
|
export declare function getUserSkillsDir(): string;
|
|
@@ -17,10 +17,38 @@
|
|
|
17
17
|
import fs from 'node:fs';
|
|
18
18
|
import path from 'node:path';
|
|
19
19
|
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { isShellBuiltinSkillEnabled } from '../product-config.js';
|
|
20
21
|
import { getAppRootDir, getUserDataDir } from '../runtime.js';
|
|
21
22
|
import { getSidecarSupervisor, whenSidecarSupervisor } from '../sidecar/handle.js';
|
|
22
23
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
24
|
const DEFAULT_SKILLS_DIR = path.resolve(__dirname, 'skills');
|
|
25
|
+
/** shell 随包分发的内置技能目录。产品经 product.json `builtinSkills` 按需引入。 */
|
|
26
|
+
export const SHELL_BUILTIN_SKILLS = [
|
|
27
|
+
{ dir: '00-identity', name: 'identity' },
|
|
28
|
+
{ dir: '10-goal', name: 'goal' },
|
|
29
|
+
{ dir: '11-loop', name: 'loop' },
|
|
30
|
+
{ dir: '12-create-skill', name: 'create-skill' },
|
|
31
|
+
{ dir: '70-plan-mode', name: 'plan-mode' },
|
|
32
|
+
{ dir: '80-tool-usage', name: 'tool-usage' },
|
|
33
|
+
{ dir: '81-anti-deferred', name: 'anti-deferred-execution' },
|
|
34
|
+
{ dir: '82-data-grounding', name: 'data-grounding' },
|
|
35
|
+
{ dir: '85-local-exec', name: 'local-exec' },
|
|
36
|
+
{ dir: '86-proactive-coding', name: 'proactive-coding' },
|
|
37
|
+
];
|
|
38
|
+
function productWantsShellBuiltinSkills() {
|
|
39
|
+
return SHELL_BUILTIN_SKILLS.some((skill) => isShellBuiltinSkillEnabled(skill.dir));
|
|
40
|
+
}
|
|
41
|
+
function disabledShellBuiltinSkillNames() {
|
|
42
|
+
if (!productWantsShellBuiltinSkills())
|
|
43
|
+
return [];
|
|
44
|
+
const names = [];
|
|
45
|
+
for (const skill of SHELL_BUILTIN_SKILLS) {
|
|
46
|
+
if (!isShellBuiltinSkillEnabled(skill.dir)) {
|
|
47
|
+
names.push(skill.dir, skill.name);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return names;
|
|
51
|
+
}
|
|
24
52
|
export function getUserSkillsDir() {
|
|
25
53
|
const dir = path.join(getUserDataDir(), 'skills');
|
|
26
54
|
if (!fs.existsSync(dir)) {
|
|
@@ -82,7 +110,9 @@ export function listSkillRoots(skillsDir) {
|
|
|
82
110
|
const user = path.resolve(getUserSkillsDir());
|
|
83
111
|
const pack = packSkillsDir && fs.existsSync(packSkillsDir) ? [path.resolve(packSkillsDir)] : [];
|
|
84
112
|
const extras = uniqueExisting(workspaceSkillRootsProvider?.() ?? []).filter((dir) => !samePath(dir, builtin) && !samePath(dir, user) && !pack.some((p) => samePath(dir, p)));
|
|
85
|
-
return
|
|
113
|
+
return productWantsShellBuiltinSkills()
|
|
114
|
+
? [builtin, ...pack, ...extras, user]
|
|
115
|
+
: [...pack, ...extras, user];
|
|
86
116
|
}
|
|
87
117
|
export function classifySkillOrigin(skillsDir) {
|
|
88
118
|
const resolved = path.resolve(skillsDir);
|
|
@@ -109,10 +139,14 @@ export async function loadSkills(options = {}) {
|
|
|
109
139
|
return [];
|
|
110
140
|
}
|
|
111
141
|
try {
|
|
142
|
+
const exclude = [
|
|
143
|
+
...disabledShellBuiltinSkillNames(),
|
|
144
|
+
...(options.excludeSkillNames ? Array.from(options.excludeSkillNames) : []),
|
|
145
|
+
];
|
|
112
146
|
return await supervisor.listSkills({
|
|
113
147
|
roots: listSkillRoots(options.skillsDir),
|
|
114
148
|
conditions: options.conditions ? Array.from(options.conditions) : undefined,
|
|
115
|
-
exclude:
|
|
149
|
+
exclude: exclude.length > 0 ? exclude : undefined,
|
|
116
150
|
ignoreConditions: options.ignoreConditions,
|
|
117
151
|
});
|
|
118
152
|
}
|
|
@@ -24,12 +24,12 @@
|
|
|
24
24
|
* 没有 `task_run`,任务不能再派生任务(任务里仍可 delegate_subagent)。
|
|
25
25
|
*/
|
|
26
26
|
import path from 'node:path';
|
|
27
|
-
import os from 'node:os';
|
|
28
27
|
import { llmService, getSidecarSupervisor } from '../llm/index.js';
|
|
29
28
|
import { sidecarWireProvider } from '../storage/llm-settings.js';
|
|
30
29
|
import { buildWorldState, streamCoreLoopTurn, } from './coreloop-stream.js';
|
|
31
30
|
import { builtinSubagentParam } from './subagent-profiles.js';
|
|
32
31
|
import { buildExecSandbox } from '../sidecar/exec-sandbox.js';
|
|
32
|
+
import { resolveTurnApproval } from '../host-tools-runtime.js';
|
|
33
33
|
import { readSidecarHistoryEntries, timelineFromHistoryEntries, } from './task-process.js';
|
|
34
34
|
import { appendTimelineDelta, syncTimelineTools, } from './turn-timeline.js';
|
|
35
35
|
/** 任务回合的系统提示词:自包含、汇报结果、不假装执行。 */
|
|
@@ -279,13 +279,7 @@ export class TaskService {
|
|
|
279
279
|
? await this.deps.resolveChatWorkspaceRoot(chatId)
|
|
280
280
|
: null);
|
|
281
281
|
const execSandbox = buildExecSandbox(fenceRoot ? [fenceRoot] : []);
|
|
282
|
-
const approval =
|
|
283
|
-
? undefined
|
|
284
|
-
: {
|
|
285
|
-
mode: 'host',
|
|
286
|
-
timeoutMs: 120_000,
|
|
287
|
-
storePath: path.join(os.homedir(), '.steerable', 'approvals.json'),
|
|
288
|
-
};
|
|
282
|
+
const approval = resolveTurnApproval();
|
|
289
283
|
// depth-1:任务回合没有 task_run(不能再派生任务),其余工具面与
|
|
290
284
|
// 普通回合一致;task_status / task_result 保留(任务可以自查/互查)。
|
|
291
285
|
const tools = this.deps.toolRouter
|
package/dist/main.js
CHANGED
|
@@ -11,6 +11,7 @@ import log from 'electron-log';
|
|
|
11
11
|
import { getBrand } from './brand.js';
|
|
12
12
|
import { getPreloadPath } from './runtime.js';
|
|
13
13
|
import { getProductConfig } from './product-config.js';
|
|
14
|
+
import { assertHostIpcAllowed, getResolvedHostTools } from './host-tools-runtime.js';
|
|
14
15
|
import { createJsonStore } from './json-store.js';
|
|
15
16
|
import { getUserDataDir } from './runtime.js';
|
|
16
17
|
import { saveAttachmentFiles } from './attachments.js';
|
|
@@ -428,19 +429,23 @@ function createMenu() {
|
|
|
428
429
|
{
|
|
429
430
|
label: '视图',
|
|
430
431
|
submenu: [
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
432
|
+
...(getResolvedHostTools().terminal.chrome
|
|
433
|
+
? [
|
|
434
|
+
{
|
|
435
|
+
label: '打开终端',
|
|
436
|
+
accelerator: 'CmdOrCtrl+T',
|
|
437
|
+
click: () => {
|
|
438
|
+
// 终端内嵌在主窗口的 /terminal 路由——通知 renderer 导航过去。
|
|
439
|
+
if (mainWindow && !mainWindow.isDestroyed()) {
|
|
440
|
+
mainWindow.show();
|
|
441
|
+
mainWindow.focus();
|
|
442
|
+
mainWindow.webContents.send('menu:open-terminal');
|
|
443
|
+
}
|
|
444
|
+
},
|
|
445
|
+
},
|
|
446
|
+
{ type: 'separator' },
|
|
447
|
+
]
|
|
448
|
+
: []),
|
|
444
449
|
{ role: 'reload', label: '重新加载' },
|
|
445
450
|
{ role: 'forceReload', label: '强制重新加载' },
|
|
446
451
|
{ type: 'separator' },
|
|
@@ -558,6 +563,7 @@ function setupIpcHandlers() {
|
|
|
558
563
|
});
|
|
559
564
|
// 会话附件:把渲染进程给出的源路径(Electron)或 base64 字节(BS)落盘。
|
|
560
565
|
ipcMain.handle('attachments:save', async (_event, input) => {
|
|
566
|
+
assertHostIpcAllowed('attachments:save');
|
|
561
567
|
const chatId = typeof input?.chatId === 'string' ? input.chatId : '';
|
|
562
568
|
const files = Array.isArray(input?.files)
|
|
563
569
|
? input.files.filter((f) => f && (typeof f.path === 'string' || typeof f.data === 'string'))
|
|
@@ -612,7 +618,10 @@ function setupIpcHandlers() {
|
|
|
612
618
|
});
|
|
613
619
|
ipcMain.handle('local:read-file', async (_event, request) => localExecutor.readLocalFile(request));
|
|
614
620
|
ipcMain.handle('local:write-file', async (_event, request) => localExecutor.writeLocalFile(request));
|
|
615
|
-
ipcMain.handle('local:open-path', async (_event, request) =>
|
|
621
|
+
ipcMain.handle('local:open-path', async (_event, request) => {
|
|
622
|
+
assertHostIpcAllowed('local:open-path');
|
|
623
|
+
return localExecutor.openLocalTarget(request);
|
|
624
|
+
});
|
|
616
625
|
ipcMain.handle('local:list-scripts', async () => localScriptRegistry.list());
|
|
617
626
|
ipcMain.handle('local:add-script', async (_event, input) => localScriptRegistry.create(input));
|
|
618
627
|
ipcMain.handle('local:update-script', async (_event, payload) => localScriptRegistry.update(payload.id, payload.updates));
|
|
@@ -748,11 +757,16 @@ function setupIpcHandlers() {
|
|
|
748
757
|
// ---------------------------------------------------------------------------
|
|
749
758
|
// Terminal IPC: visible PTY embedded in the main window's /terminal route
|
|
750
759
|
// ---------------------------------------------------------------------------
|
|
751
|
-
ipcMain.handle('terminal:list', async () =>
|
|
760
|
+
ipcMain.handle('terminal:list', async () => {
|
|
761
|
+
assertHostIpcAllowed('terminal:list');
|
|
762
|
+
return terminalManager.list();
|
|
763
|
+
});
|
|
752
764
|
ipcMain.handle('terminal:spawn', async (_event, options = {}) => {
|
|
765
|
+
assertHostIpcAllowed('terminal:spawn');
|
|
753
766
|
return terminalManager.spawn(options);
|
|
754
767
|
});
|
|
755
768
|
ipcMain.handle('terminal:ensure', async (event, options = {}) => {
|
|
769
|
+
assertHostIpcAllowed('terminal:ensure');
|
|
756
770
|
const session = terminalManager.ensurePrimary(options);
|
|
757
771
|
// Replay any output buffered before the renderer was attached, so the
|
|
758
772
|
// window doesn't look empty if the agent kicked off a command before the
|
|
@@ -770,13 +784,19 @@ function setupIpcHandlers() {
|
|
|
770
784
|
return session;
|
|
771
785
|
});
|
|
772
786
|
ipcMain.handle('terminal:write', async (_event, payload) => {
|
|
787
|
+
assertHostIpcAllowed('terminal:write');
|
|
773
788
|
return terminalManager.write(payload.id, payload.data);
|
|
774
789
|
});
|
|
775
790
|
ipcMain.handle('terminal:resize', async (_event, payload) => {
|
|
791
|
+
assertHostIpcAllowed('terminal:resize');
|
|
776
792
|
return terminalManager.resize(payload.id, payload.cols, payload.rows);
|
|
777
793
|
});
|
|
778
|
-
ipcMain.handle('terminal:kill', async (_event, id) =>
|
|
794
|
+
ipcMain.handle('terminal:kill', async (_event, id) => {
|
|
795
|
+
assertHostIpcAllowed('terminal:kill');
|
|
796
|
+
return terminalManager.kill(id);
|
|
797
|
+
});
|
|
779
798
|
ipcMain.handle('terminal:exec', async (_event, payload) => {
|
|
799
|
+
assertHostIpcAllowed('terminal:exec');
|
|
780
800
|
let id = payload.id;
|
|
781
801
|
if (!id) {
|
|
782
802
|
const session = terminalManager.ensurePrimary();
|
|
@@ -70,6 +70,8 @@ export declare class McpServerRegistry {
|
|
|
70
70
|
constructor(store: McpServerKvStore);
|
|
71
71
|
list(): McpServerEntry[];
|
|
72
72
|
get(idOrName: string): McpServerEntry | null;
|
|
73
|
+
/** 产品预置 MCP:缺省不引入;已有同名服务不覆盖。 */
|
|
74
|
+
seedProductServers(specs: CreateMcpServerInput[] | undefined): void;
|
|
73
75
|
create(input: CreateMcpServerInput): McpServerEntry;
|
|
74
76
|
update(id: string, updates: Partial<CreateMcpServerInput>): McpServerEntry;
|
|
75
77
|
delete(id: string): boolean;
|
|
@@ -33,6 +33,14 @@ export class McpServerRegistry {
|
|
|
33
33
|
const needle = idOrName.trim().toLowerCase();
|
|
34
34
|
return (this.list().find((s) => s.id === idOrName || s.name.toLowerCase() === needle) ?? null);
|
|
35
35
|
}
|
|
36
|
+
/** 产品预置 MCP:缺省不引入;已有同名服务不覆盖。 */
|
|
37
|
+
seedProductServers(specs) {
|
|
38
|
+
for (const spec of specs ?? []) {
|
|
39
|
+
if (this.get(spec.name))
|
|
40
|
+
continue;
|
|
41
|
+
this.create(spec);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
36
44
|
create(input) {
|
|
37
45
|
const name = input.name.trim();
|
|
38
46
|
const command = input.command.trim();
|
package/dist/product-config.d.ts
CHANGED
|
@@ -33,7 +33,68 @@ export interface ProductConfig {
|
|
|
33
33
|
* 数据;中性 shell 缺省 'agent-shell.db'。
|
|
34
34
|
*/
|
|
35
35
|
dbFileName?: string;
|
|
36
|
+
/**
|
|
37
|
+
* 宿主工具族:产品声明引入哪些工具。缺省全开。
|
|
38
|
+
* 形状由 host-tools 解析;本模块只存声明、不依赖解析器。
|
|
39
|
+
*/
|
|
40
|
+
hostTools?: Record<string, boolean | {
|
|
41
|
+
capability?: boolean;
|
|
42
|
+
chrome?: boolean;
|
|
43
|
+
}>;
|
|
44
|
+
/**
|
|
45
|
+
* 命令安全询问:host = 弹宿主审批;off = 本轮不挂审批。缺省 host。
|
|
46
|
+
* STEERABLE_APPROVAL=0 仍是调试逃生口。
|
|
47
|
+
*/
|
|
48
|
+
approval?: 'host' | 'off';
|
|
49
|
+
/**
|
|
50
|
+
* 对话模式。缺省 `['agent','plan']`。只留一种时渲染层不显示切换。
|
|
51
|
+
*/
|
|
52
|
+
chatModes?: Array<'agent' | 'plan'>;
|
|
53
|
+
/**
|
|
54
|
+
* 设置入口。缺省全开。`false` 藏对应侧栏页或综合设置分段。
|
|
55
|
+
*/
|
|
56
|
+
settings?: Record<string, boolean>;
|
|
57
|
+
/**
|
|
58
|
+
* 产品钉死的大模型。`settings.llm === false` 时必填,运行时用这份,
|
|
59
|
+
* 不再读设置页。密钥用 `apiKeyEnv` 指向环境变量,不要把 key 写进仓库。
|
|
60
|
+
*/
|
|
61
|
+
llm?: {
|
|
62
|
+
provider?: string;
|
|
63
|
+
vendorId?: string;
|
|
64
|
+
model?: string;
|
|
65
|
+
baseUrl?: string;
|
|
66
|
+
apiKey?: string;
|
|
67
|
+
apiKeyEnv?: string;
|
|
68
|
+
temperature?: number;
|
|
69
|
+
maxTotalTokens?: number;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* shell 内置智能体。按需引入,缺省全关。
|
|
73
|
+
* `true` 才种子;未声明或 `false` 不写入,已有行归档。
|
|
74
|
+
*/
|
|
75
|
+
builtinAgents?: Partial<Record<'local-assistant' | 'all-round-assistant', boolean>>;
|
|
76
|
+
/**
|
|
77
|
+
* shell 内置技能。按需引入,缺省全关。
|
|
78
|
+
* `true` 引入全部;对象里 `true` 的目录才引入。设置页入口不受影响。
|
|
79
|
+
*/
|
|
80
|
+
builtinSkills?: boolean | Partial<Record<string, boolean>>;
|
|
81
|
+
/**
|
|
82
|
+
* 产品预置 MCP 服务。按需引入,缺省空。已有同名服务不覆盖。
|
|
83
|
+
* 设置页入口不受影响。
|
|
84
|
+
*/
|
|
85
|
+
builtinMcp?: Array<{
|
|
86
|
+
name: string;
|
|
87
|
+
command: string;
|
|
88
|
+
args?: string[];
|
|
89
|
+
env?: Record<string, string>;
|
|
90
|
+
cwd?: string;
|
|
91
|
+
enabled?: boolean;
|
|
92
|
+
}>;
|
|
36
93
|
}
|
|
94
|
+
/** shell 内置智能体:只有产品显式 `true` 才开。 */
|
|
95
|
+
export declare function isShellBuiltinAgentEnabled(id: 'local-assistant' | 'all-round-assistant', config?: ProductConfig): boolean;
|
|
96
|
+
/** shell 内置技能:`true` 全开;否则只有对象里显式 `true` 的目录开。 */
|
|
97
|
+
export declare function isShellBuiltinSkillEnabled(id: string, config?: ProductConfig): boolean;
|
|
37
98
|
/**
|
|
38
99
|
* 注入产品配置(产品组装根在第一个 import 时调用)。重复注入抛错
|
|
39
100
|
* (组装期笔误,fail fast——与 setProductBrand 同语义)。
|
|
@@ -41,3 +102,5 @@ export interface ProductConfig {
|
|
|
41
102
|
export declare function setProductConfig(config: ProductConfig): void;
|
|
42
103
|
/** 读取已注入的产品配置;未注入返回空对象(中性框架行为)。 */
|
|
43
104
|
export declare function getProductConfig(): ProductConfig;
|
|
105
|
+
/** 测试用:清掉已注入的产品配置,避免用例互相污染。 */
|
|
106
|
+
export declare function resetProductConfigForTests(): void;
|
package/dist/product-config.js
CHANGED
|
@@ -9,6 +9,19 @@
|
|
|
9
9
|
*
|
|
10
10
|
* 本模块只允许依赖 node 内置模块(与 brand.ts 同约束)。
|
|
11
11
|
*/
|
|
12
|
+
/** shell 内置智能体:只有产品显式 `true` 才开。 */
|
|
13
|
+
export function isShellBuiltinAgentEnabled(id, config = getProductConfig()) {
|
|
14
|
+
return config.builtinAgents?.[id] === true;
|
|
15
|
+
}
|
|
16
|
+
/** shell 内置技能:`true` 全开;否则只有对象里显式 `true` 的目录开。 */
|
|
17
|
+
export function isShellBuiltinSkillEnabled(id, config = getProductConfig()) {
|
|
18
|
+
const value = config.builtinSkills;
|
|
19
|
+
if (value === true)
|
|
20
|
+
return true;
|
|
21
|
+
if (!value || typeof value !== 'object')
|
|
22
|
+
return false;
|
|
23
|
+
return value[id] === true;
|
|
24
|
+
}
|
|
12
25
|
let productConfig = null;
|
|
13
26
|
/**
|
|
14
27
|
* 注入产品配置(产品组装根在第一个 import 时调用)。重复注入抛错
|
|
@@ -24,3 +37,7 @@ export function setProductConfig(config) {
|
|
|
24
37
|
export function getProductConfig() {
|
|
25
38
|
return productConfig ?? {};
|
|
26
39
|
}
|
|
40
|
+
/** 测试用:清掉已注入的产品配置,避免用例互相污染。 */
|
|
41
|
+
export function resetProductConfigForTests() {
|
|
42
|
+
productConfig = null;
|
|
43
|
+
}
|
|
@@ -36,6 +36,8 @@ import { getBrand } from '../brand.js';
|
|
|
36
36
|
import { saveAttachmentFiles } from '../attachments.js';
|
|
37
37
|
import { selectNativeDirectory } from '../native-folder-dialog.js';
|
|
38
38
|
import { getAuthProvider } from '../auth/index.js';
|
|
39
|
+
import { isHostRouteAllowed } from '../host-tools.js';
|
|
40
|
+
import { getResolvedHostTools } from '../host-tools-runtime.js';
|
|
39
41
|
/** 与 router.handleStream 内部的路由正则保持一致——只有这些路径是流式。 */
|
|
40
42
|
const STREAM_PATH_PATTERNS = [
|
|
41
43
|
/^\/api\/v2\/chats\/[^/]+\/(send|run|agent)$/,
|
|
@@ -153,6 +155,10 @@ export function createBsServer(deps) {
|
|
|
153
155
|
async function handleHost(req, res, pathname) {
|
|
154
156
|
const body = asRecord(await readJsonBody(req));
|
|
155
157
|
const method = req.method ?? 'GET';
|
|
158
|
+
if (!isHostRouteAllowed(pathname, getResolvedHostTools())) {
|
|
159
|
+
sendJson(res, 403, { detail: 'Host route is disabled for this product' });
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
156
162
|
if (method === 'GET' && pathname === '/host/info') {
|
|
157
163
|
const brand = getBrand();
|
|
158
164
|
sendJson(res, 200, {
|
|
@@ -330,6 +336,7 @@ export function createBsServer(deps) {
|
|
|
330
336
|
sendJson(res, 200, await saveAttachmentFiles(chatId, files));
|
|
331
337
|
return;
|
|
332
338
|
}
|
|
339
|
+
log.warn(`[bs] ${method} ${pathname} -> 404`);
|
|
333
340
|
sendJson(res, 404, { detail: `Unknown host endpoint: ${method} ${pathname}` });
|
|
334
341
|
}
|
|
335
342
|
/** 静态托管 + index.html 注入 BS 标记(前端据此切换 http-bridge)。 */
|
package/dist/storage/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomUUID } from 'crypto';
|
|
2
2
|
import { ALL_ROUND_ASSISTANT_AGENT_ID, LOCAL_ASSISTANT_AGENT_ID, getBrand, } from '../brand.js';
|
|
3
|
+
import { isShellBuiltinAgentEnabled } from '../product-config.js';
|
|
3
4
|
import { DEFAULT_LLM_SETTINGS, llmSettingsCarryExpiredBakedKey, llmSettingsMatchOllamaDefaults, mergeLlmSettings, } from './llm-settings.js';
|
|
4
5
|
import { MESSAGE_ORDER_DESC } from './message-order.js';
|
|
5
6
|
import { mergeTelemetrySettings, } from './telemetry-settings.js';
|
|
@@ -473,92 +474,108 @@ export class SqliteScopedStore {
|
|
|
473
474
|
@sortOrder, @createdAt, @updatedAt
|
|
474
475
|
)
|
|
475
476
|
`);
|
|
476
|
-
if (
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
.get(this.scope.tenantId, this.scope.userId, LOCAL_ASSISTANT_AGENT_ID);
|
|
508
|
-
if (local?.role_prompt === '你是本地离线助手,回答时清晰、可执行。') {
|
|
509
|
-
this.db
|
|
510
|
-
.prepare(`UPDATE chat_agents SET role_prompt = ?
|
|
477
|
+
if (isShellBuiltinAgentEnabled(LOCAL_ASSISTANT_AGENT_ID)) {
|
|
478
|
+
if (!hasBuiltin) {
|
|
479
|
+
insertAgent.run({
|
|
480
|
+
tenantId: this.scope.tenantId,
|
|
481
|
+
userId: this.scope.userId,
|
|
482
|
+
id: LOCAL_ASSISTANT_AGENT_ID,
|
|
483
|
+
slug: LOCAL_ASSISTANT_AGENT_ID,
|
|
484
|
+
name: '电脑操作员',
|
|
485
|
+
icon: 'Cpu',
|
|
486
|
+
color: '#4f46e5',
|
|
487
|
+
description: '默认本地助手,可调用 shell、文件与 MCP 工具。',
|
|
488
|
+
rolePrompt: '你是 **电脑操作员**,本地离线助手,回答时清晰、可执行。',
|
|
489
|
+
forbiddenPrompt: null,
|
|
490
|
+
skillIds: JSON.stringify([]),
|
|
491
|
+
toolPolicy: JSON.stringify({ mode: 'all', tools: [] }),
|
|
492
|
+
allowExternalSkills: 1,
|
|
493
|
+
loadAllSkills: 0,
|
|
494
|
+
isBuiltin: 1,
|
|
495
|
+
isArchived: 0,
|
|
496
|
+
// 激活包有主打种子(sortOrder 0)时让位到 1。
|
|
497
|
+
sortOrder: hasHeroSeed ? 1 : 0,
|
|
498
|
+
createdAt: now,
|
|
499
|
+
updatedAt: now,
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
this.db.prepare(`UPDATE chat_agents SET name = ?
|
|
504
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?`)
|
|
505
|
+
.run('电脑操作员', this.scope.tenantId, this.scope.userId, LOCAL_ASSISTANT_AGENT_ID);
|
|
506
|
+
const local = this.db
|
|
507
|
+
.prepare(`SELECT role_prompt FROM chat_agents
|
|
511
508
|
WHERE tenant_id = ? AND user_id = ? AND id = ?`)
|
|
512
|
-
.
|
|
509
|
+
.get(this.scope.tenantId, this.scope.userId, LOCAL_ASSISTANT_AGENT_ID);
|
|
510
|
+
if (local?.role_prompt === '你是本地离线助手,回答时清晰、可执行。') {
|
|
511
|
+
this.db
|
|
512
|
+
.prepare(`UPDATE chat_agents SET role_prompt = ?
|
|
513
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?`)
|
|
514
|
+
.run('你是 **电脑操作员**,本地离线助手,回答时清晰、可执行。', this.scope.tenantId, this.scope.userId, LOCAL_ASSISTANT_AGENT_ID);
|
|
515
|
+
}
|
|
513
516
|
}
|
|
514
517
|
}
|
|
518
|
+
else if (hasBuiltin) {
|
|
519
|
+
this.db
|
|
520
|
+
.prepare(`UPDATE chat_agents SET is_archived = 1, updated_at = ?
|
|
521
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ? AND is_archived = 0`)
|
|
522
|
+
.run(now, this.scope.tenantId, this.scope.userId, LOCAL_ASSISTANT_AGENT_ID);
|
|
523
|
+
}
|
|
515
524
|
const hasAllRound = this.db
|
|
516
525
|
.prepare(`SELECT id FROM chat_agents WHERE tenant_id = ? AND user_id = ? AND id = ?`)
|
|
517
526
|
.get(this.scope.tenantId, this.scope.userId, ALL_ROUND_ASSISTANT_AGENT_ID);
|
|
518
|
-
if (
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
else {
|
|
548
|
-
const agent = this.db.prepare(`SELECT role_prompt FROM chat_agents
|
|
549
|
-
WHERE tenant_id = ? AND user_id = ? AND id = ?`).get(this.scope.tenantId, this.scope.userId, ALL_ROUND_ASSISTANT_AGENT_ID);
|
|
550
|
-
if (agent) {
|
|
551
|
-
const updatedPrompt = agent.role_prompt.replace(/全能专家助手/g, '智能助手');
|
|
552
|
-
this.db.prepare(`UPDATE chat_agents SET name = ?, role_prompt = ?
|
|
553
|
-
WHERE tenant_id = ? AND user_id = ? AND id = ?`)
|
|
554
|
-
.run('智能助手', updatedPrompt, this.scope.tenantId, this.scope.userId, ALL_ROUND_ASSISTANT_AGENT_ID);
|
|
527
|
+
if (isShellBuiltinAgentEnabled(ALL_ROUND_ASSISTANT_AGENT_ID)) {
|
|
528
|
+
if (!hasAllRound) {
|
|
529
|
+
insertAgent.run({
|
|
530
|
+
tenantId: this.scope.tenantId,
|
|
531
|
+
userId: this.scope.userId,
|
|
532
|
+
id: ALL_ROUND_ASSISTANT_AGENT_ID,
|
|
533
|
+
slug: ALL_ROUND_ASSISTANT_AGENT_ID,
|
|
534
|
+
name: '智能助手',
|
|
535
|
+
icon: '🔮',
|
|
536
|
+
color: '#a855f7',
|
|
537
|
+
description: '全能智能体,自动加载全部已安装本地技能和工具,无需限定条件。',
|
|
538
|
+
rolePrompt: [
|
|
539
|
+
'你是 **智能助手**,一个具备高权限且集成了全部本地技能(Skills)的行动体。',
|
|
540
|
+
'你具备完整的本地命令执行(local_exec_shell)、文件读写能力,以及当前产品安装的全部场景工具。',
|
|
541
|
+
'你的系统提示词中已经无条件加载了所有已安装的本地技能与工具。',
|
|
542
|
+
'请根据用户的需求,直接、高效地调用最合适的本地工具或执行脚本来解决任务。'
|
|
543
|
+
].join('\n'),
|
|
544
|
+
forbiddenPrompt: null,
|
|
545
|
+
skillIds: JSON.stringify([]),
|
|
546
|
+
toolPolicy: JSON.stringify({ mode: 'all', tools: [] }),
|
|
547
|
+
allowExternalSkills: 1,
|
|
548
|
+
// 「智能助手」的定位就是无条件加载全部技能(此前是 router 硬编码)。
|
|
549
|
+
loadAllSkills: 1,
|
|
550
|
+
isBuiltin: 1,
|
|
551
|
+
isArchived: 0,
|
|
552
|
+
sortOrder: 2,
|
|
553
|
+
createdAt: now,
|
|
554
|
+
updatedAt: now,
|
|
555
|
+
});
|
|
555
556
|
}
|
|
556
557
|
else {
|
|
557
|
-
this.db.prepare(`
|
|
558
|
-
|
|
559
|
-
|
|
558
|
+
const agent = this.db.prepare(`SELECT role_prompt FROM chat_agents
|
|
559
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?`).get(this.scope.tenantId, this.scope.userId, ALL_ROUND_ASSISTANT_AGENT_ID);
|
|
560
|
+
if (agent) {
|
|
561
|
+
const updatedPrompt = agent.role_prompt.replace(/全能专家助手/g, '智能助手');
|
|
562
|
+
this.db.prepare(`UPDATE chat_agents SET name = ?, role_prompt = ?
|
|
563
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?`)
|
|
564
|
+
.run('智能助手', updatedPrompt, this.scope.tenantId, this.scope.userId, ALL_ROUND_ASSISTANT_AGENT_ID);
|
|
565
|
+
}
|
|
566
|
+
else {
|
|
567
|
+
this.db.prepare(`UPDATE chat_agents SET name = ?
|
|
568
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ?`)
|
|
569
|
+
.run('智能助手', this.scope.tenantId, this.scope.userId, ALL_ROUND_ASSISTANT_AGENT_ID);
|
|
570
|
+
}
|
|
560
571
|
}
|
|
561
572
|
}
|
|
573
|
+
else if (hasAllRound) {
|
|
574
|
+
this.db
|
|
575
|
+
.prepare(`UPDATE chat_agents SET is_archived = 1, updated_at = ?
|
|
576
|
+
WHERE tenant_id = ? AND user_id = ? AND id = ? AND is_archived = 0`)
|
|
577
|
+
.run(now, this.scope.tenantId, this.scope.userId, ALL_ROUND_ASSISTANT_AGENT_ID);
|
|
578
|
+
}
|
|
562
579
|
// ─── 场景包智能体种子(0.3d) ───
|
|
563
580
|
// 激活包:不存在则种子;命中历史代文案(用户从未定制)则静默升级;
|
|
564
581
|
// 被归档但未定制的行重新露出。未激活包:未定制的内置行归档(用户改
|
|
@@ -81,6 +81,21 @@ export declare function llmSettingsCarryExpiredBakedKey(s: LlmSettings): boolean
|
|
|
81
81
|
export declare const OLLAMA_DEFAULTS: LlmSettings;
|
|
82
82
|
export declare function sanitizeLlmProvider(raw: unknown): LlmProvider;
|
|
83
83
|
export declare function sanitizeVendorId(raw: unknown): string | undefined;
|
|
84
|
+
export interface ProductLlmConfig {
|
|
85
|
+
provider?: string;
|
|
86
|
+
vendorId?: string;
|
|
87
|
+
model?: string;
|
|
88
|
+
baseUrl?: string;
|
|
89
|
+
apiKey?: string;
|
|
90
|
+
apiKeyEnv?: string;
|
|
91
|
+
temperature?: number;
|
|
92
|
+
maxTotalTokens?: number;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* `settings.llm` 关掉后,产品必须声明 `llm.model`。密钥优先 product.apiKey,
|
|
96
|
+
* 其次 `apiKeyEnv` 指向的环境变量,再回落已存 key(升级前用户填过的)。
|
|
97
|
+
*/
|
|
98
|
+
export declare function resolveLockedLlmSettings(value: unknown, env?: Record<string, string | undefined>, persisted?: LlmSettings | null): LlmSettings;
|
|
84
99
|
/** Sidecar `agent.chat.stream` 的 provider 字段。 */
|
|
85
100
|
export declare function sidecarWireProvider(provider: LlmProvider): string;
|
|
86
101
|
export declare function usesOpenAiCompatExtras(provider: LlmProvider): boolean;
|
|
@@ -67,6 +67,34 @@ export function sanitizeVendorId(raw) {
|
|
|
67
67
|
const id = raw.trim();
|
|
68
68
|
return id || undefined;
|
|
69
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* `settings.llm` 关掉后,产品必须声明 `llm.model`。密钥优先 product.apiKey,
|
|
72
|
+
* 其次 `apiKeyEnv` 指向的环境变量,再回落已存 key(升级前用户填过的)。
|
|
73
|
+
*/
|
|
74
|
+
export function resolveLockedLlmSettings(value, env = {}, persisted = null) {
|
|
75
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
76
|
+
throw new Error('[product-llm] settings.llm is off; product.json must declare llm');
|
|
77
|
+
}
|
|
78
|
+
const raw = value;
|
|
79
|
+
const model = typeof raw.model === 'string' ? raw.model.trim() : '';
|
|
80
|
+
if (!model) {
|
|
81
|
+
throw new Error('[product-llm] llm.model is required when settings.llm is off');
|
|
82
|
+
}
|
|
83
|
+
const fromProduct = typeof raw.apiKey === 'string' && raw.apiKey.trim() ? raw.apiKey.trim() : undefined;
|
|
84
|
+
const envName = typeof raw.apiKeyEnv === 'string' ? raw.apiKeyEnv.trim() : '';
|
|
85
|
+
const fromEnv = envName ? env[envName]?.trim() || undefined : undefined;
|
|
86
|
+
const fromPersisted = persisted?.apiKey?.trim() || undefined;
|
|
87
|
+
return mergeLlmSettings({
|
|
88
|
+
provider: sanitizeLlmProvider(raw.provider ?? persisted?.provider ?? 'openai-compat'),
|
|
89
|
+
vendorId: sanitizeVendorId(raw.vendorId),
|
|
90
|
+
model,
|
|
91
|
+
baseUrl: typeof raw.baseUrl === 'string' ? raw.baseUrl : persisted?.baseUrl,
|
|
92
|
+
apiKey: fromProduct || fromEnv || fromPersisted,
|
|
93
|
+
temperature: typeof raw.temperature === 'number' ? raw.temperature : undefined,
|
|
94
|
+
maxTotalTokens: typeof raw.maxTotalTokens === 'number' ? raw.maxTotalTokens : persisted?.maxTotalTokens,
|
|
95
|
+
execTimeoutSeconds: persisted?.execTimeoutSeconds,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
70
98
|
/** Sidecar `agent.chat.stream` 的 provider 字段。 */
|
|
71
99
|
export function sidecarWireProvider(provider) {
|
|
72
100
|
switch (provider) {
|
package/dist/tool-router.js
CHANGED
|
@@ -5,6 +5,8 @@ import { mcpExecutor } from './mcp-executor.js';
|
|
|
5
5
|
import { rankTools, resolveMaxResults, TOOL_SEARCH_DEFAULT_MAX_RESULTS, } from './tool-search-rank.js';
|
|
6
6
|
import { maybeAutoInstallWrittenSkill } from './local-backend/skill-install.js';
|
|
7
7
|
import { filterToolsByPolicy, isToolAllowed, } from './local-backend/agent-capability.js';
|
|
8
|
+
import { isHostToolCapabilityEnabled } from './host-tools.js';
|
|
9
|
+
import { getResolvedHostTools } from './host-tools-runtime.js';
|
|
8
10
|
/** 已注册 MCP 服务的动态工具名前缀:`mcp__<serverKey>__<toolName>`。 */
|
|
9
11
|
export const MCP_DYNAMIC_TOOL_PREFIX = 'mcp__';
|
|
10
12
|
/**
|
|
@@ -608,7 +610,8 @@ export class ToolRouter {
|
|
|
608
610
|
*/
|
|
609
611
|
listModelSchemas(policy) {
|
|
610
612
|
const effective = policy ?? { mode: 'all', tools: [] };
|
|
611
|
-
const
|
|
613
|
+
const tools = getResolvedHostTools();
|
|
614
|
+
const all = filterToolsByPolicy(this.listSchemas(), effective).filter((s) => isHostToolCapabilityEnabled(s.name, tools));
|
|
612
615
|
const hasDeferred = all.some(s => s.exposure === 'deferred');
|
|
613
616
|
return all.filter(s => {
|
|
614
617
|
if ((s.exposure ?? 'direct') !== 'direct')
|
|
@@ -630,6 +633,9 @@ export class ToolRouter {
|
|
|
630
633
|
if (toolPolicy && !isToolAllowed(toolPolicy, call.name)) {
|
|
631
634
|
throw new Error(`工具 ${call.name} 被当前智能体的工具策略拒绝(智能体管理 → 工具权限)`);
|
|
632
635
|
}
|
|
636
|
+
if (!isHostToolCapabilityEnabled(call.name, getResolvedHostTools())) {
|
|
637
|
+
throw new Error(`工具 ${call.name} 未在本产品引入`);
|
|
638
|
+
}
|
|
633
639
|
if (call.name.startsWith(MCP_DYNAMIC_TOOL_PREFIX)) {
|
|
634
640
|
return await this.executeRegisteredMcpTool(call.name, args);
|
|
635
641
|
}
|
|
@@ -883,7 +889,8 @@ export class ToolRouter {
|
|
|
883
889
|
*/
|
|
884
890
|
executeToolSearch(args, policy) {
|
|
885
891
|
const cap = resolveMaxResults(args.max_results);
|
|
886
|
-
const
|
|
892
|
+
const tools = getResolvedHostTools();
|
|
893
|
+
const deferred = filterToolsByPolicy(this.listSchemas().filter(s => s.exposure === 'deferred'), policy ?? { mode: 'all', tools: [] }).filter((s) => isHostToolCapabilityEnabled(s.name, tools));
|
|
887
894
|
const ranked = rankTools(deferred, String(args.query ?? ''));
|
|
888
895
|
const matches = ranked.slice(0, cap).map(schema => ({
|
|
889
896
|
name: schema.name,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steerable/agent-shell",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.34",
|
|
4
4
|
"description": "Steerable framework — product-neutral desktop/headless agent host (Tier 5). Electron main + preload + headless HTTP server (BS) sharing one storage/sidecar/tooling core; scenario packs extend it through the @steerable/pack-sdk contract and are composed at build time by the consuming product.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://steerableframework.com/",
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
"type": "module",
|
|
16
16
|
"main": "dist/runtime.js",
|
|
17
17
|
"exports": {
|
|
18
|
+
"./host-tools": {
|
|
19
|
+
"types": "./dist/host-tools.d.ts",
|
|
20
|
+
"default": "./dist/host-tools.js"
|
|
21
|
+
},
|
|
18
22
|
"./auth": {
|
|
19
23
|
"types": "./dist/auth/index.d.ts",
|
|
20
24
|
"default": "./dist/auth/index.js"
|
|
@@ -49,9 +53,9 @@
|
|
|
49
53
|
"electron-store": "^8.1.0",
|
|
50
54
|
"he": "^1.2.0",
|
|
51
55
|
"node-pty": "^1.1.0",
|
|
52
|
-
"@steerable/agent-
|
|
53
|
-
"@steerable/pack-sdk": "0.6.
|
|
54
|
-
"@steerable/agent-
|
|
56
|
+
"@steerable/agent-harness": "0.6.34",
|
|
57
|
+
"@steerable/pack-sdk": "0.6.34",
|
|
58
|
+
"@steerable/agent-protocol": "0.6.34"
|
|
55
59
|
},
|
|
56
60
|
"devDependencies": {
|
|
57
61
|
"@types/better-sqlite3": "^7.6.13",
|