c0de-agent 1.11.0 → 1.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.
Files changed (177) hide show
  1. package/README.md +63 -16
  2. package/dist/cli/commands/acp.js +26 -8
  3. package/dist/cli/commands/chat.js +151 -1
  4. package/dist/cli/commands/config.d.ts +2 -0
  5. package/dist/cli/commands/config.js +63 -3
  6. package/dist/cli/commands/serve.js +11 -2
  7. package/dist/cli/commands/sessions.js +109 -3
  8. package/dist/cli/commands/trust.d.ts +11 -0
  9. package/dist/cli/commands/trust.js +31 -0
  10. package/dist/cli/commands/update.js +4 -0
  11. package/dist/cli/deps.d.ts +9 -6
  12. package/dist/cli/deps.js +66 -19
  13. package/dist/cli/index.d.ts +4 -1
  14. package/dist/cli/index.js +110 -11
  15. package/dist/cli/modes/acp.js +39 -8
  16. package/dist/cli/modes/print.d.ts +3 -0
  17. package/dist/cli/modes/print.js +78 -8
  18. package/dist/core/agent.d.ts +1 -1
  19. package/dist/core/agent.js +9 -4
  20. package/dist/core/agents/builtin.js +2 -2
  21. package/dist/core/agents/index.d.ts +0 -1
  22. package/dist/core/agents/index.js +0 -1
  23. package/dist/core/compact.d.ts +2 -0
  24. package/dist/core/compact.js +1 -0
  25. package/dist/core/config.d.ts +53 -2
  26. package/dist/core/config.js +235 -19
  27. package/dist/core/loop/compaction.d.ts +2 -1
  28. package/dist/core/loop/compaction.js +19 -6
  29. package/dist/core/loop/segment.d.ts +3 -1
  30. package/dist/core/loop/segment.js +24 -1
  31. package/dist/core/loop/stream-collect.js +9 -1
  32. package/dist/core/loop/subagent.js +88 -37
  33. package/dist/core/loop.d.ts +7 -0
  34. package/dist/core/loop.js +90 -6
  35. package/dist/core/redact.d.ts +11 -0
  36. package/dist/core/redact.js +49 -0
  37. package/dist/core/slash.d.ts +10 -0
  38. package/dist/core/slash.js +237 -65
  39. package/dist/core/title.js +7 -1
  40. package/dist/core/types.d.ts +22 -1
  41. package/dist/core/workflows/context.js +44 -4
  42. package/dist/core/workflows/discovery.d.ts +2 -2
  43. package/dist/core/workflows/discovery.js +5 -7
  44. package/dist/core/workflows/index.d.ts +2 -1
  45. package/dist/core/workflows/index.js +2 -1
  46. package/dist/core/workflows/registry.d.ts +13 -3
  47. package/dist/core/workflows/registry.js +14 -9
  48. package/dist/core/workflows/resolve.d.ts +22 -0
  49. package/dist/core/workflows/resolve.js +23 -0
  50. package/dist/core/workflows/runtime.d.ts +6 -1
  51. package/dist/core/workflows/runtime.js +19 -2
  52. package/dist/db/schema.d.ts +373 -1
  53. package/dist/db/schema.js +60 -4
  54. package/dist/kanban/index.d.ts +2 -1
  55. package/dist/kanban/index.js +1 -1
  56. package/dist/kanban/store.d.ts +87 -1
  57. package/dist/kanban/store.js +429 -22
  58. package/dist/llm/index.d.ts +1 -1
  59. package/dist/llm/index.js +1 -1
  60. package/dist/llm/provider.js +1 -1
  61. package/dist/llm/registry.d.ts +7 -1
  62. package/dist/llm/registry.js +7 -1
  63. package/dist/llm/retry.d.ts +5 -2
  64. package/dist/llm/retry.js +6 -5
  65. package/dist/llm/routing.d.ts +24 -1
  66. package/dist/llm/routing.js +29 -1
  67. package/dist/plugins/init.d.ts +7 -0
  68. package/dist/plugins/init.js +22 -1
  69. package/dist/plugins/loader.d.ts +7 -1
  70. package/dist/plugins/loader.js +12 -4
  71. package/dist/project/index.d.ts +1 -1
  72. package/dist/project/index.js +1 -1
  73. package/dist/project/project.d.ts +37 -0
  74. package/dist/project/project.js +142 -1
  75. package/dist/project/trust.d.ts +65 -0
  76. package/dist/project/trust.js +328 -0
  77. package/dist/server/agent-manager.d.ts +15 -2
  78. package/dist/server/agent-manager.js +18 -3
  79. package/dist/server/app.js +45 -30
  80. package/dist/server/auth-manager.d.ts +26 -9
  81. package/dist/server/auth-manager.js +78 -42
  82. package/dist/server/context.js +14 -1
  83. package/dist/server/index.d.ts +1 -1
  84. package/dist/server/index.js +1 -1
  85. package/dist/server/middleware/auth.d.ts +11 -1
  86. package/dist/server/middleware/auth.js +25 -3
  87. package/dist/server/permission/interactive.d.ts +11 -3
  88. package/dist/server/permission/interactive.js +23 -4
  89. package/dist/server/permission/store.d.ts +14 -2
  90. package/dist/server/permission/store.js +39 -3
  91. package/dist/server/registry-config.d.ts +14 -0
  92. package/dist/server/registry-config.js +44 -0
  93. package/dist/server/routes/auth.js +66 -10
  94. package/dist/server/routes/chat.d.ts +21 -0
  95. package/dist/server/routes/chat.js +685 -52
  96. package/dist/server/routes/commands.d.ts +4 -2
  97. package/dist/server/routes/commands.js +9 -4
  98. package/dist/server/routes/config.js +180 -23
  99. package/dist/server/routes/files.js +142 -28
  100. package/dist/server/routes/kanban.js +218 -26
  101. package/dist/server/routes/permissions.d.ts +0 -4
  102. package/dist/server/routes/permissions.js +140 -7
  103. package/dist/server/routes/project.js +132 -29
  104. package/dist/server/routes/provider.js +54 -3
  105. package/dist/server/routes/session.js +463 -23
  106. package/dist/server/routes/tool.js +24 -5
  107. package/dist/server/routes/update.d.ts +11 -5
  108. package/dist/server/routes/update.js +145 -29
  109. package/dist/server/routes/usage.d.ts +4 -0
  110. package/dist/server/routes/usage.js +133 -0
  111. package/dist/server/routes/workflows.js +476 -87
  112. package/dist/server/server.d.ts +16 -13
  113. package/dist/server/server.js +364 -107
  114. package/dist/server/terminal/pty-manager.d.ts +12 -0
  115. package/dist/server/terminal/pty-manager.js +63 -7
  116. package/dist/server/types.d.ts +16 -0
  117. package/dist/session/archive.d.ts +3 -1
  118. package/dist/session/archive.js +13 -1
  119. package/dist/session/branch.d.ts +2 -1
  120. package/dist/session/branch.js +92 -9
  121. package/dist/session/context.js +28 -2
  122. package/dist/session/import.d.ts +44 -0
  123. package/dist/session/import.js +83 -0
  124. package/dist/session/index.d.ts +1 -1
  125. package/dist/session/index.js +1 -1
  126. package/dist/session/jobs.d.ts +12 -0
  127. package/dist/session/jobs.js +53 -0
  128. package/dist/session/message.d.ts +7 -1
  129. package/dist/session/message.js +21 -1
  130. package/dist/session/session.d.ts +186 -12
  131. package/dist/session/session.js +577 -33
  132. package/dist/session/snapshot.d.ts +5 -2
  133. package/dist/session/snapshot.js +10 -2
  134. package/dist/session/types.d.ts +13 -1
  135. package/dist/session/usage.d.ts +62 -0
  136. package/dist/session/usage.js +194 -0
  137. package/dist/shared/types/agent.d.ts +29 -2
  138. package/dist/shared/types/config.d.ts +64 -5
  139. package/dist/shared/types/kanban.d.ts +17 -2
  140. package/dist/shared/types/message.d.ts +23 -0
  141. package/dist/tools/builtin/kanban.js +13 -2
  142. package/dist/tools/index.d.ts +14 -3
  143. package/dist/tools/index.js +23 -5
  144. package/dist/tools/websearch/index.d.ts +5 -1
  145. package/dist/tools/websearch/index.js +46 -7
  146. package/dist/tools/websearch/providers/brave.js +2 -2
  147. package/dist/tools/websearch/providers/duckduckgo.js +2 -1
  148. package/dist/tools/websearch/providers/tavily.js +2 -2
  149. package/dist/tools/websearch/types.d.ts +5 -1
  150. package/dist/tools/websearch/types.js +7 -1
  151. package/dist/update/index.d.ts +3 -3
  152. package/dist/update/index.js +2 -2
  153. package/dist/update/ipc.d.ts +13 -2
  154. package/dist/update/ipc.js +36 -2
  155. package/dist/update/scheduler.js +5 -2
  156. package/dist/update/snapshot.d.ts +18 -3
  157. package/dist/update/snapshot.js +5 -2
  158. package/dist/update/updater.d.ts +17 -1
  159. package/dist/update/updater.js +47 -18
  160. package/dist/update/version.d.ts +3 -0
  161. package/dist/update/version.js +9 -3
  162. package/drizzle/0008_tranquil_lily_hollister.sql +1 -0
  163. package/drizzle/0010_usage_events_ledger.sql +17 -0
  164. package/drizzle/0011_slow_slapstick.sql +12 -0
  165. package/drizzle/0012_chief_timeslip.sql +1 -0
  166. package/drizzle/0013_swift_zeigeist.sql +1 -0
  167. package/drizzle/0014_flimsy_legion.sql +1 -0
  168. package/drizzle/meta/0008_snapshot.json +875 -0
  169. package/drizzle/meta/0010_snapshot.json +1010 -0
  170. package/drizzle/meta/0011_snapshot.json +1055 -0
  171. package/drizzle/meta/0012_snapshot.json +1062 -0
  172. package/drizzle/meta/0013_snapshot.json +1068 -0
  173. package/drizzle/meta/0014_snapshot.json +1075 -0
  174. package/drizzle/meta/_journal.json +42 -0
  175. package/package.json +42 -42
  176. package/dist/core/agents/discovery.d.ts +0 -6
  177. package/dist/core/agents/discovery.js +0 -96
@@ -20,6 +20,7 @@ const runWithFallback = async (registry, chain, run) => {
20
20
  const result = await withRetry(() => run(target.provider, target.model), {
21
21
  maxRetries: chain.maxRetries,
22
22
  sleep: chain.sleep,
23
+ initialDelayMs: chain.retryDelay,
23
24
  });
24
25
  return { result, provider: target.provider, model: target.model };
25
26
  }
@@ -50,4 +51,31 @@ const shouldFallOver = (error) => {
50
51
  return true;
51
52
  return false;
52
53
  };
53
- export { runWithFallback, shouldFallOver };
54
+ /**
55
+ * 从 config.fallback 构建回退链(主 chat 循环/压缩/标题等 LLM 调用的接线点)。
56
+ * - enabled=false → undefined(保持内置默认:3 次重试、无跨 provider 回退)。
57
+ * - enabled=true → 按配置 maxRetries/retryDelay 重试,并对「声明了同一模型」的
58
+ * 其他 provider 依次回退。无合格回退目标时 fallbacks 为空——重试参数仍然生效。
59
+ * 回退只接受声明了该模型的 provider:对未声明的 provider 盲目回退会在其 API 处
60
+ * 得到 InvalidRequest 并中断整条链(shouldFallOver 不放过非回退错误)。
61
+ */
62
+ function buildFallbackChain(config, provider, model) {
63
+ const fb = config.fallback;
64
+ if (!fb.enabled)
65
+ return undefined;
66
+ const fallbacks = config.providers
67
+ .filter((p) => {
68
+ const name = p.name || p._tag;
69
+ if (!name || name === provider)
70
+ return false;
71
+ return p.models !== undefined && model in p.models;
72
+ })
73
+ .map((p) => ({ provider: (p.name || p._tag), model }));
74
+ return {
75
+ primary: { provider, model },
76
+ fallbacks,
77
+ maxRetries: fb.maxRetries,
78
+ retryDelay: fb.retryDelay,
79
+ };
80
+ }
81
+ export { buildFallbackChain, runWithFallback, shouldFallOver };
@@ -7,6 +7,11 @@ type InitPluginsOptions = {
7
7
  toolRegistry: unknown;
8
8
  /** LLM 注册表;插件通过 registerProvider 往这里注入。 */
9
9
  llmRegistry: unknown;
10
+ /**
11
+ * P0-2:项目已显式信任(projects.trustedAt 非空)时才加载项目 .c0de/plugins。
12
+ * 缺省 false(fail-closed):克隆仓库自带插件不会在 serve/chat 时静默执行。
13
+ */
14
+ projectTrusted?: boolean;
10
15
  };
11
16
  type InitPluginsResult = {
12
17
  pluginRegistry: PluginRegistry;
@@ -17,6 +22,8 @@ type InitPluginsResult = {
17
22
  *
18
23
  * 失败的外部插件被静默跳过(loader 已处理),单个坏插件不影响其余加载。
19
24
  * 内置插件(tool-audit-log / write-guard)始终激活。
25
+ * P0-2:项目插件仅在项目被显式信任后加载(跳过时若目录非空则告警并给出
26
+ * 信任途径);全局插件是用户本机显式放置,始终加载。
20
27
  */
21
28
  declare function initPlugins(opts: InitPluginsOptions): Promise<InitPluginsResult>;
22
29
  export type { InitPluginsOptions, InitPluginsResult };
@@ -1,3 +1,8 @@
1
+ // 插件系统初始化编排(spec §7):创建 hookRunner + pluginRegistry,激活内置插件,
2
+ // 发现并加载外部插件(.c0de/plugins + ~/.c0de/plugins)。供 server bootstrap 与
3
+ // CLI buildAgentDeps 复用——两处启动路径此前都未接入,导致插件框架存在却永不加载。
4
+ import { existsSync, readdirSync } from 'node:fs';
5
+ import { join } from 'node:path';
1
6
  import { registerBuiltinHooks } from './builtin.js';
2
7
  import { createHookRunner } from './hooks.js';
3
8
  import { activatePlugin } from './lifecycle.js';
@@ -8,6 +13,8 @@ import { createPluginRegistry, registerPlugin } from './registry.js';
8
13
  *
9
14
  * 失败的外部插件被静默跳过(loader 已处理),单个坏插件不影响其余加载。
10
15
  * 内置插件(tool-audit-log / write-guard)始终激活。
16
+ * P0-2:项目插件仅在项目被显式信任后加载(跳过时若目录非空则告警并给出
17
+ * 信任途径);全局插件是用户本机显式放置,始终加载。
11
18
  */
12
19
  async function initPlugins(opts) {
13
20
  const hookRunner = createHookRunner();
@@ -20,7 +27,21 @@ async function initPlugins(opts) {
20
27
  // 内置插件先激活:它们注册 tool:before/after hook,对后续外部插件同样生效。
21
28
  await registerBuiltinHooks(pluginRegistry, services);
22
29
  // 发现并加载外部插件(项目 .c0de/plugins 与全局 ~/.c0de/plugins)。
23
- const discovered = await discoverPlugins(opts.cwd);
30
+ const projectPluginsDir = join(opts.cwd, '.c0de', 'plugins');
31
+ if (opts.projectTrusted !== true) {
32
+ try {
33
+ if (existsSync(projectPluginsDir) && readdirSync(projectPluginsDir).length > 0) {
34
+ console.warn(`[plugin] 项目插件目录 ${projectPluginsDir} 存在插件,但项目尚未被信任(或信任后风险配置/插件代码已变更)——已跳过加载。\n` +
35
+ ` 信任/重新确认方式:c0de trust(CLI),或在 Web 界面遇到信任确认时点击「信任项目」;信任后重启 serve 生效。`);
36
+ }
37
+ }
38
+ catch {
39
+ // 目录读取失败不影响启动
40
+ }
41
+ }
42
+ const discovered = await discoverPlugins(opts.cwd, {
43
+ includeProject: opts.projectTrusted === true,
44
+ });
24
45
  for (const { plugin } of discovered) {
25
46
  registerPlugin(pluginRegistry, plugin);
26
47
  await activatePlugin(pluginRegistry, plugin, services);
@@ -8,7 +8,13 @@ type ValidationResult = {
8
8
  };
9
9
  declare function validatePluginModule(mod: unknown): ValidationResult;
10
10
  declare function loadPlugin(path: string): Promise<Plugin>;
11
- declare function discoverPlugins(projectDir: string): Promise<{
11
+ declare function discoverPlugins(projectDir: string,
12
+ /** P0-2:includeProject=false 时跳过项目 .c0de/plugins(未信任项目不加载,
13
+ * 信任后重启生效);全局插件目录(默认 ~/.c0de/plugins,C0DE_CONFIG_DIR 可重定向)
14
+ * 始终加载(用户本机显式放置)。 */
15
+ opts?: {
16
+ includeProject?: boolean;
17
+ }): Promise<{
12
18
  name: string;
13
19
  path: string;
14
20
  plugin: Plugin;
@@ -1,7 +1,7 @@
1
1
  // src/plugins/loader.ts
2
2
  import { existsSync, readdirSync, statSync } from 'node:fs';
3
- import { homedir } from 'node:os';
4
3
  import { join } from 'node:path';
4
+ import { resolveGlobalConfigDir } from '../core/config.js';
5
5
  function validatePluginModule(mod) {
6
6
  const candidate = mod?.default ?? mod;
7
7
  if (typeof candidate !== 'object' || candidate === null) {
@@ -49,10 +49,18 @@ function scanPluginDir(dir) {
49
49
  }
50
50
  return plugins;
51
51
  }
52
- async function discoverPlugins(projectDir) {
52
+ async function discoverPlugins(projectDir,
53
+ /** P0-2:includeProject=false 时跳过项目 .c0de/plugins(未信任项目不加载,
54
+ * 信任后重启生效);全局插件目录(默认 ~/.c0de/plugins,C0DE_CONFIG_DIR 可重定向)
55
+ * 始终加载(用户本机显式放置)。 */
56
+ opts = {}) {
53
57
  const projectPluginsDir = join(projectDir, '.c0de', 'plugins');
54
- const globalPluginsDir = join(homedir(), '.c0de', 'plugins');
55
- const discovered = [...scanPluginDir(projectPluginsDir), ...scanPluginDir(globalPluginsDir)];
58
+ // 全局插件根(默认 ~/.c0de/plugins,C0DE_CONFIG_DIR 可重定向)。
59
+ const globalPluginsDir = join(resolveGlobalConfigDir(), 'plugins');
60
+ const discovered = [
61
+ ...(opts.includeProject === false ? [] : scanPluginDir(projectPluginsDir)),
62
+ ...scanPluginDir(globalPluginsDir),
63
+ ];
56
64
  const results = [];
57
65
  for (const entry of discovered) {
58
66
  try {
@@ -1,4 +1,4 @@
1
1
  export type { Project } from './project.js';
2
- export { fromDirectory, getByDirectory, getProject, listProjects, updateProjectName, } from './project.js';
2
+ export { enforceProjectTrust, fromDirectory, getByDirectory, getProject, listProjects, relocateProject, trustProject, updateProjectName, } from './project.js';
3
3
  export type { ResolvedProject } from './resolve.js';
4
4
  export { resolveProject } from './resolve.js';
@@ -1,2 +1,2 @@
1
- export { fromDirectory, getByDirectory, getProject, listProjects, updateProjectName, } from './project.js';
1
+ export { enforceProjectTrust, fromDirectory, getByDirectory, getProject, listProjects, relocateProject, trustProject, updateProjectName, } from './project.js';
2
2
  export { resolveProject } from './resolve.js';
@@ -1,4 +1,5 @@
1
1
  import type { DB } from '../db/client.js';
2
+ import type { Config } from '../shared/types/config.js';
2
3
  export type Project = {
3
4
  id: string;
4
5
  worktree: string;
@@ -7,6 +8,10 @@ export type Project = {
7
8
  gitRemote: string | null;
8
9
  createdAt: number;
9
10
  updatedAt: number;
11
+ /** 用户显式信任该项目作用域配置/插件的时间戳;null=未信任(P0-2 信任边界)。 */
12
+ trustedAt: number | null;
13
+ /** 信任时的风险配置指纹;null=历史记录(无指纹,见 schema 注释)。 */
14
+ riskFingerprint: string | null;
10
15
  };
11
16
  /**
12
17
  * 解析目录 + upsert 项目记录。返回 Project。
@@ -21,3 +26,35 @@ export declare function listProjects(handle: DB): Promise<Project[]>;
21
26
  export declare function getProject(handle: DB, id: string): Promise<Project | null>;
22
27
  export declare function getByDirectory(handle: DB, directory: string): Promise<Project | null>;
23
28
  export declare function updateProjectName(handle: DB, id: string, name: string): Promise<Project | null>;
29
+ /**
30
+ * P0-2:显式信任项目——用户在信任确认弹窗中批准项目作用域配置/插件后落盘。
31
+ * 信任是「克隆即信任」防线的落点;未信任项目携带风险配置时聊天入口被门禁拦截。
32
+ * 一次性动作:信任后不再拦截(用户可随时在设置中收回项目级配置)。
33
+ * P1:信任时同时落盘「风险配置指纹」(当前项目作用域的 summarizeProjectRisk
34
+ * 快照)——此后若仓库 git pull 新增 auto 权限/插件/MCP 等风险键,指纹漂移会
35
+ * 重新触发门禁复检,而非永久信任。
36
+ */
37
+ export declare function trustProject(handle: DB, id: string): Promise<Project | null>;
38
+ /**
39
+ * P0 CLI 项目信任门禁:agent 执行路径(c0de chat / c0de acp)在无 Web 确认弹窗的
40
+ * 情况下,若目录项目未信任(或信任后未 config 漂移)且项目作用域/全局权限配置含
41
+ * 风险项,直接抛错引导 `c0de trust <dir>`——与 Web 聊天入口 409 TRUST_REQUIRED 用
42
+ * 同一套 projectTrustNeeded 判定,杜绝「Web 拦、CLI 裸奔」的不对称。
43
+ * 无风险项时不拦;trust/sessions 等非 agent 命令由调用方跳过此门禁。
44
+ * `globalRaw` 参数仅供测试注入确定性全局配置(生产走本机 ~/.c0de/config.json)。
45
+ */
46
+ export declare function enforceProjectTrust(handle: DB, cwd: string, globalRaw?: Partial<Config> | undefined): Promise<void>;
47
+ /**
48
+ * 项目重新定位(A1:目录被移动/重命名后的恢复通道)。
49
+ *
50
+ * 项目身份 = sha256(worktree),目录移动后旧项目记录的 worktree 失效且不可改 id。
51
+ * 此前唯一恢复路径是「删除项目」:会话打散进孤儿回收站逐条恢复,看板永久丢失。
52
+ * 本函数在单事务内把项目整体迁移到新目录身份(新 id),保住看板与会话归属:
53
+ * 1. 以新目录解析出的 id upsert 项目行(沿用旧名称);
54
+ * 2. 会话 projectId/worktreePath 迁移到新身份;
55
+ * 3. 看板 projectId 迁移(目标为新身份,唯一约束无冲突);
56
+ * 4. 删除旧项目行。
57
+ * 同一 id(如 git 仓库根未变、仅路径符号变化)→ 仅更新 worktree,不换身份。
58
+ * 目标目录已注册为另一项目时抛 TARGET_OCCUPIED(调用方转 409),绝不静默合并。
59
+ */
60
+ export declare function relocateProject(handle: DB, id: string, directory: string): Promise<Project>;
@@ -1,7 +1,9 @@
1
1
  import { basename } from 'node:path';
2
2
  import { eq } from 'drizzle-orm';
3
- import { projects, sessions } from '../db/schema.js';
3
+ import { loadConfigScopes } from '../core/config.js';
4
+ import { kanbanBoards, projects, sessions } from '../db/schema.js';
4
5
  import { resolveProject } from './resolve.js';
6
+ import { computeProjectRiskFingerprint, projectTrustNeeded } from './trust.js';
5
7
  function rowToProject(row) {
6
8
  return {
7
9
  id: row.id,
@@ -11,6 +13,12 @@ function rowToProject(row) {
11
13
  gitRemote: row.gitRemote,
12
14
  createdAt: row.createdAt instanceof Date ? row.createdAt.getTime() : new Date(row.createdAt).getTime(),
13
15
  updatedAt: row.updatedAt instanceof Date ? row.updatedAt.getTime() : new Date(row.updatedAt).getTime(),
16
+ trustedAt: row.trustedAt instanceof Date
17
+ ? row.trustedAt.getTime()
18
+ : row.trustedAt
19
+ ? new Date(row.trustedAt).getTime()
20
+ : null,
21
+ riskFingerprint: row.riskFingerprint,
14
22
  };
15
23
  }
16
24
  /**
@@ -85,3 +93,136 @@ export async function updateProjectName(handle, id, name) {
85
93
  .returning();
86
94
  return row ? rowToProject(row) : null;
87
95
  }
96
+ /**
97
+ * P0-2:显式信任项目——用户在信任确认弹窗中批准项目作用域配置/插件后落盘。
98
+ * 信任是「克隆即信任」防线的落点;未信任项目携带风险配置时聊天入口被门禁拦截。
99
+ * 一次性动作:信任后不再拦截(用户可随时在设置中收回项目级配置)。
100
+ * P1:信任时同时落盘「风险配置指纹」(当前项目作用域的 summarizeProjectRisk
101
+ * 快照)——此后若仓库 git pull 新增 auto 权限/插件/MCP 等风险键,指纹漂移会
102
+ * 重新触发门禁复检,而非永久信任。
103
+ */
104
+ export async function trustProject(handle, id) {
105
+ const existing = await getProject(handle, id);
106
+ if (!existing)
107
+ return null;
108
+ const scope = loadConfigScopes(existing.worktree).project;
109
+ // 指纹含 MCP 参数与插件文件内容(trust.ts computeProjectRiskFingerprint)——
110
+ // 信任批准的是「当时的完整风险面」,此后任何漂移(含插件代码变更)都会复检。
111
+ const riskFingerprint = computeProjectRiskFingerprint(scope, { projectDir: existing.worktree });
112
+ const rows = await handle.db
113
+ .update(projects)
114
+ .set({ trustedAt: new Date(), riskFingerprint })
115
+ .where(eq(projects.id, id))
116
+ .returning();
117
+ const row = rows[0];
118
+ return row ? rowToProject(row) : null;
119
+ }
120
+ /**
121
+ * P0 CLI 项目信任门禁:agent 执行路径(c0de chat / c0de acp)在无 Web 确认弹窗的
122
+ * 情况下,若目录项目未信任(或信任后未 config 漂移)且项目作用域/全局权限配置含
123
+ * 风险项,直接抛错引导 `c0de trust <dir>`——与 Web 聊天入口 409 TRUST_REQUIRED 用
124
+ * 同一套 projectTrustNeeded 判定,杜绝「Web 拦、CLI 裸奔」的不对称。
125
+ * 无风险项时不拦;trust/sessions 等非 agent 命令由调用方跳过此门禁。
126
+ * `globalRaw` 参数仅供测试注入确定性全局配置(生产走本机 ~/.c0de/config.json)。
127
+ */
128
+ export async function enforceProjectTrust(handle, cwd, globalRaw) {
129
+ const scopes = loadConfigScopes(cwd);
130
+ const effectiveGlobal = globalRaw === undefined ? scopes.global : globalRaw;
131
+ let trustedAt = null;
132
+ let riskFingerprint = null;
133
+ try {
134
+ const project = await getByDirectory(handle, cwd);
135
+ trustedAt = project?.trustedAt ?? null;
136
+ riskFingerprint = project?.riskFingerprint ?? null;
137
+ }
138
+ catch {
139
+ // 信任状态查询失败 → 按未信任处理(宁可拦截,不静默放行)
140
+ }
141
+ const risks = projectTrustNeeded(scopes.project, effectiveGlobal, trustedAt, riskFingerprint, cwd);
142
+ if (risks.length === 0)
143
+ return;
144
+ const globalKinds = new Set(['permission-auto', 'permission-timeout-deny']);
145
+ const globalOnly = risks.every((r) => globalKinds.has(r.kind));
146
+ const sourceNote = globalOnly
147
+ ? '(风险来自全局配置 permission;如有意为之可执行 c0de trust 一次性放行本项目)'
148
+ : '';
149
+ throw new Error(`项目目录 ${cwd} 的配置含需要你确认的风险项。\n` +
150
+ ` 请先审查后执行 c0de trust <目录> --yes 显式信任;或改用 c0de serve 在浏览器确认。\n` +
151
+ ` 风险项:${risks.map((r) => r.kind).join('、')}${sourceNote}`);
152
+ }
153
+ /**
154
+ * 项目重新定位(A1:目录被移动/重命名后的恢复通道)。
155
+ *
156
+ * 项目身份 = sha256(worktree),目录移动后旧项目记录的 worktree 失效且不可改 id。
157
+ * 此前唯一恢复路径是「删除项目」:会话打散进孤儿回收站逐条恢复,看板永久丢失。
158
+ * 本函数在单事务内把项目整体迁移到新目录身份(新 id),保住看板与会话归属:
159
+ * 1. 以新目录解析出的 id upsert 项目行(沿用旧名称);
160
+ * 2. 会话 projectId/worktreePath 迁移到新身份;
161
+ * 3. 看板 projectId 迁移(目标为新身份,唯一约束无冲突);
162
+ * 4. 删除旧项目行。
163
+ * 同一 id(如 git 仓库根未变、仅路径符号变化)→ 仅更新 worktree,不换身份。
164
+ * 目标目录已注册为另一项目时抛 TARGET_OCCUPIED(调用方转 409),绝不静默合并。
165
+ */
166
+ export async function relocateProject(handle, id, directory) {
167
+ const current = await getProject(handle, id);
168
+ if (!current)
169
+ throw new Error('PROJECT_NOT_FOUND');
170
+ const resolved = resolveProject(directory);
171
+ if (resolved.id === id) {
172
+ // 身份不变:仅刷新 worktree 与 git 元数据;会话 worktreePath 同步。
173
+ await handle.db.transaction(async (tx) => {
174
+ await tx
175
+ .update(projects)
176
+ .set({
177
+ worktree: resolved.worktree,
178
+ vcs: resolved.vcs,
179
+ gitRemote: resolved.gitRemote,
180
+ updatedAt: new Date(),
181
+ })
182
+ .where(eq(projects.id, id));
183
+ await tx
184
+ .update(sessions)
185
+ .set({ worktreePath: resolved.worktree })
186
+ .where(eq(sessions.projectId, id));
187
+ });
188
+ }
189
+ else {
190
+ const occupied = await getProject(handle, resolved.id);
191
+ if (occupied) {
192
+ throw new Error(`TARGET_OCCUPIED: 目标目录已注册为项目「${occupied.name ?? '未命名项目'}」`);
193
+ }
194
+ await handle.db.transaction(async (tx) => {
195
+ await tx
196
+ .insert(projects)
197
+ .values({
198
+ id: resolved.id,
199
+ worktree: resolved.worktree,
200
+ vcs: resolved.vcs,
201
+ name: current.name ?? basename(resolved.worktree),
202
+ gitRemote: resolved.gitRemote,
203
+ })
204
+ .onConflictDoUpdate({
205
+ target: projects.id,
206
+ set: {
207
+ worktree: resolved.worktree,
208
+ vcs: resolved.vcs,
209
+ gitRemote: resolved.gitRemote,
210
+ updatedAt: new Date(),
211
+ },
212
+ });
213
+ await tx
214
+ .update(sessions)
215
+ .set({ projectId: resolved.id, worktreePath: resolved.worktree })
216
+ .where(eq(sessions.projectId, id));
217
+ await tx
218
+ .update(kanbanBoards)
219
+ .set({ projectId: resolved.id })
220
+ .where(eq(kanbanBoards.projectId, id));
221
+ await tx.delete(projects).where(eq(projects.id, id));
222
+ });
223
+ }
224
+ const result = await getProject(handle, resolved.id);
225
+ if (!result)
226
+ throw new Error(`Relocate failed for ${directory}`);
227
+ return result;
228
+ }
@@ -0,0 +1,65 @@
1
+ import type { Config } from '../shared/types/config.js';
2
+ /** 单个风险项:kind 供前端图标/文案映射,detail 为人类可读说明。
3
+ * 'trust-drift' 为展示专用(仅 projectTrustNeeded 在漂移时返回),
4
+ * 不参与 summarizeProjectRisk 与指纹计算。 */
5
+ export type TrustRiskItem = {
6
+ kind: 'permission-auto' | 'permission-timeout-deny' | 'plugins-enabled' | 'mcp-enabled' | 'provider-rerouting' | 'workflows-enabled' | 'trust-drift';
7
+ detail: string;
8
+ };
9
+ /** 从全局配置中提取「会作用于本项目的权限风险」项(auto 放行 / 超时后继续自主执行)。
10
+ * 仅这两类参与门禁与可视化——全局插件/MCP 是用户本机显式安装(需手改 ~/.c0de),
11
+ * 不属仓库自带风险面,不纳入项目信任门禁。 */
12
+ export declare function globalPermissionRiskItems(globalRaw: Partial<Config> | undefined): TrustRiskItem[];
13
+ /**
14
+ * 把「全局配置中会作用于本项目的权限风险」并入项目风险列表(作为可见上下文)。
15
+ * 目的:用户在做信任决策时能看到完整生效的权限状态,避免「信任了项目却发现全局
16
+ * 早已 auto」的困惑。复用现有 kind(前端无需新增映射);同类 kind 已存在则跳过。
17
+ */
18
+ export declare function enrichProjectRiskWithGlobal(risks: TrustRiskItem[], globalRaw: Partial<Config> | undefined): TrustRiskItem[];
19
+ /** 汇总项目作用域原始配置中的风险项。无风险返回空数组。
20
+ * 宽松形状:项目 JSON 可能字段漂移,非法值一律忽略(fail-closed 由
21
+ * 「未信任 + 无风险项 = 不拦截」与「有风险项必拦截」共同保证——解析不出
22
+ * 风险的配置也不含可信风险)。
23
+ * projectDir 提供时额外检测 `.c0de/workflows/*.js`(项目工作流是 dynamic
24
+ * import 的任意代码执行面,与 .c0de/plugins 同级,纯文件系统、无配置键)。
25
+ */
26
+ export declare function summarizeProjectRisk(raw: Partial<Config> | undefined, projectDir?: string): TrustRiskItem[];
27
+ /**
28
+ * 计算项目作用域风险配置的指纹。
29
+ *
30
+ * P0(代码面覆盖):指纹不再只是配置键的 hash——已信任仓库 `git pull` 修改
31
+ * 插件代码(同名)或 MCP `args`(同名)此前不会触发复检,下次 serve 重启即
32
+ * 静默加载执行新代码。现在指纹由三部分组成:
33
+ * 1. 风险项 canonical 行(kind:detail,配置键漂移);
34
+ * 2. MCP 服务器 canonical 参数(name+command+args+transport+url 全量,
35
+ * 防「同名改 args」绕过);
36
+ * 3. 项目插件目录全部文件内容 hash(插件可 import 同目录其它文件,
37
+ * 内容面覆盖整个插件目录,防「同名改代码」绕过)。
38
+ * 无风险项、无 MCP、无插件 → ''(空串)。
39
+ * 升级后旧指纹(仅配置键)与含插件/MCP 的新口径不一致 → 一次性重新确认,
40
+ * 属预期行为(漂移复检)。
41
+ */
42
+ export declare function computeProjectRiskFingerprint(raw: Partial<Config> | undefined, opts?: {
43
+ projectDir?: string;
44
+ }): string;
45
+ /**
46
+ * 判定项目是否需要(重新)信任:返回「需要用户确认的风险项」数组,空数组 = 放行。
47
+ * 门禁条件:
48
+ * - 未信任:项目风险 或 全局权限风险(auto / timeoutAction=deny)任一存在即拦。
49
+ * 全局权限风险兜底「仓库未携带 .c0de 但本机已全局 auto」的裸奔场景。
50
+ * - 已信任:仅当项目作用域指纹漂移(git pull 新增风险键 / MCP 参数变更 /
51
+ * 插件或工作流代码变更)时复检——返回项目风险项 + 前置 trust-drift 说明项,
52
+ * 用户能看到「为什么又要确认」;全局配置是用户本机显式选择,不因漂移重新门禁。
53
+ * projectDir 提供时插件/工作流文件内容纳入指纹(serve 启动的插件加载门禁与
54
+ * 聊天入口使用同一口径)。
55
+ * 供 Web 聊天入口(返回 409)与 CLI agent 路径(抛错引导 c0de trust)共用,
56
+ * 保证两处「什么时候拦」判定完全一致,不再各自为政。
57
+ */
58
+ export declare function projectTrustNeeded(raw: Partial<Config> | undefined, globalRaw: Partial<Config> | undefined, trustedAt: number | null | undefined, riskFingerprint: string | null | undefined, projectDir?: string): TrustRiskItem[];
59
+ /**
60
+ * 项目是否「当前可信任」(插件加载门禁用):已信任且指纹与信任时一致。
61
+ * 漂移(fingerprint 不匹配)或未信任 → false(fail-closed,项目插件不加载)。
62
+ * 供 server bootstrap 与 CLI deps 复用——插件在启动时加载,必须与聊天门禁
63
+ * 同口径校验,否则「先重启、后门禁」的窗口里漂移代码已执行。
64
+ */
65
+ export declare function projectTrustCurrent(raw: Partial<Config> | undefined, trustedAt: number | null | undefined, riskFingerprint: string | null | undefined, projectDir: string): boolean;