c0de-agent 1.9.0 → 1.11.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 (109) hide show
  1. package/README.md +25 -8
  2. package/dist/cli/commands/acp.js +5 -3
  3. package/dist/cli/commands/auth.d.ts +17 -0
  4. package/dist/cli/commands/auth.js +73 -0
  5. package/dist/cli/commands/chat.js +10 -0
  6. package/dist/cli/commands/config.d.ts +2 -9
  7. package/dist/cli/commands/config.js +15 -54
  8. package/dist/cli/commands/init.js +4 -2
  9. package/dist/cli/commands/plugin.js +8 -4
  10. package/dist/cli/commands/serve.d.ts +1 -0
  11. package/dist/cli/commands/serve.js +4 -1
  12. package/dist/cli/commands/sessions.d.ts +10 -0
  13. package/dist/cli/commands/sessions.js +35 -0
  14. package/dist/cli/commands/update.js +18 -10
  15. package/dist/cli/deps.d.ts +7 -5
  16. package/dist/cli/deps.js +23 -7
  17. package/dist/cli/index.d.ts +1 -1
  18. package/dist/cli/index.js +96 -37
  19. package/dist/cli/modes/print.d.ts +3 -1
  20. package/dist/cli/modes/print.js +31 -4
  21. package/dist/core/agent.js +3 -4
  22. package/dist/core/config-path.d.ts +9 -0
  23. package/dist/core/config-path.js +48 -0
  24. package/dist/core/config.d.ts +27 -2
  25. package/dist/core/config.js +83 -13
  26. package/dist/core/index.d.ts +1 -1
  27. package/dist/core/index.js +1 -1
  28. package/dist/core/loop.js +14 -4
  29. package/dist/core/slash.js +92 -30
  30. package/dist/core/title.js +15 -0
  31. package/dist/core/types.d.ts +2 -0
  32. package/dist/core/workflows/discovery.d.ts +1 -1
  33. package/dist/core/workflows/index.d.ts +1 -1
  34. package/dist/db/client.d.ts +4 -0
  35. package/dist/db/client.js +5 -0
  36. package/dist/db/schema.d.ts +51 -0
  37. package/dist/db/schema.js +7 -1
  38. package/dist/server/agent-manager.d.ts +20 -0
  39. package/dist/server/agent-manager.js +63 -11
  40. package/dist/server/app.js +12 -1
  41. package/dist/server/auth-manager.d.ts +79 -0
  42. package/dist/server/auth-manager.js +270 -0
  43. package/dist/server/context.js +3 -0
  44. package/dist/server/middleware/auth.d.ts +4 -3
  45. package/dist/server/middleware/auth.js +5 -5
  46. package/dist/server/middleware/cors.d.ts +11 -2
  47. package/dist/server/middleware/cors.js +15 -12
  48. package/dist/server/permission/interactive.d.ts +2 -0
  49. package/dist/server/permission/interactive.js +5 -1
  50. package/dist/server/permission/store.d.ts +4 -0
  51. package/dist/server/permission/store.js +1 -0
  52. package/dist/server/routes/auth.d.ts +4 -0
  53. package/dist/server/routes/auth.js +94 -0
  54. package/dist/server/routes/chat.d.ts +6 -1
  55. package/dist/server/routes/chat.js +294 -153
  56. package/dist/server/routes/config.js +60 -8
  57. package/dist/server/routes/permissions.d.ts +4 -1
  58. package/dist/server/routes/permissions.js +27 -4
  59. package/dist/server/routes/project.js +56 -2
  60. package/dist/server/routes/session.js +37 -9
  61. package/dist/server/routes/terminal.js +29 -2
  62. package/dist/server/routes/update.d.ts +1 -0
  63. package/dist/server/routes/update.js +33 -4
  64. package/dist/server/routes/workflows.js +2 -2
  65. package/dist/server/server.d.ts +24 -1
  66. package/dist/server/server.js +149 -29
  67. package/dist/server/types.d.ts +17 -2
  68. package/dist/session/archive.d.ts +1 -1
  69. package/dist/session/branch.d.ts +11 -2
  70. package/dist/session/branch.js +107 -31
  71. package/dist/session/compaction.js +1 -0
  72. package/dist/session/context.d.ts +3 -2
  73. package/dist/session/context.js +36 -3
  74. package/dist/session/index.d.ts +1 -1
  75. package/dist/session/index.js +1 -1
  76. package/dist/session/session.d.ts +25 -5
  77. package/dist/session/session.js +129 -10
  78. package/dist/session/snapshot.d.ts +9 -3
  79. package/dist/session/snapshot.js +45 -4
  80. package/dist/session/squash.js +1 -0
  81. package/dist/session/types.d.ts +3 -1
  82. package/dist/shared/types/agent.d.ts +11 -0
  83. package/dist/shared/types/config.d.ts +12 -6
  84. package/dist/shared/types/message.d.ts +6 -2
  85. package/dist/shared/utils/path.d.ts +2 -3
  86. package/dist/shared/utils/path.js +20 -4
  87. package/dist/tools/builtin/todo.js +5 -4
  88. package/dist/tools/index.d.ts +14 -3
  89. package/dist/tools/index.js +35 -14
  90. package/dist/update/index.d.ts +2 -2
  91. package/dist/update/index.js +2 -2
  92. package/dist/update/ipc.d.ts +38 -5
  93. package/dist/update/ipc.js +56 -7
  94. package/dist/update/snapshot.d.ts +4 -0
  95. package/dist/update/snapshot.js +8 -0
  96. package/dist/update/updater.d.ts +61 -10
  97. package/dist/update/updater.js +236 -25
  98. package/dist/update/version.d.ts +6 -1
  99. package/dist/update/version.js +18 -3
  100. package/drizzle/0005_snapshot_mtime.sql +1 -0
  101. package/drizzle/0006_sour_susan_delgado.sql +2 -0
  102. package/drizzle/0007_same_wilson_fisk.sql +2 -0
  103. package/drizzle/meta/0005_snapshot.json +855 -0
  104. package/drizzle/meta/0006_snapshot.json +867 -0
  105. package/drizzle/meta/0007_snapshot.json +869 -0
  106. package/drizzle/meta/_journal.json +21 -0
  107. package/package.json +1 -7
  108. package/dist/mcp/index.d.ts +0 -1
  109. package/dist/mcp/index.js +0 -3
@@ -1,14 +1,16 @@
1
1
  import { Hono } from 'hono';
2
2
  import { apiError } from '../middleware/error.js';
3
- /** GET/PUT /api/permissions — 全局授权模式(default 逐个确认 / auto 自动放行 ask 工具)。 */
3
+ /** GET/PUT /api/permissions — 授权模式(default 逐个确认 / auto 自动放行 ask 工具)。
4
+ * - 根路径:默认模式(全局,启动时取 config.permission.defaultMode)
5
+ * - /:sessionId:会话级覆盖(P1-5 按会话隔离 auto 高风险状态)
6
+ */
4
7
  function createPermissionsRoute(ctx) {
5
8
  const app = new Hono();
6
- // GET / — 当前授权模式
9
+ // GET / — 默认授权模式
7
10
  app.get('/', (c) => {
8
11
  return c.json({ mode: ctx.permissionMode });
9
12
  });
10
- // PUT / — 运行时切换授权模式(仅本次运行生效,不回写 config;
11
- // 重启后回到 config.permission.defaultMode 持久化默认值)
13
+ // PUT / — 运行时切换默认模式(仅本次运行生效,不回写 config)
12
14
  app.put('/', async (c) => {
13
15
  const body = (await c.req.json().catch(() => null));
14
16
  const mode = body?.mode;
@@ -18,6 +20,27 @@ function createPermissionsRoute(ctx) {
18
20
  ctx.permissionMode = mode;
19
21
  return c.json({ mode: ctx.permissionMode });
20
22
  });
23
+ // GET /:sessionId — 会话实际生效模式(覆盖优先,回退默认)
24
+ app.get('/:sessionId', (c) => {
25
+ const sessionId = c.req.param('sessionId');
26
+ return c.json({ mode: ctx.sessionPermissionModes.get(sessionId) ?? ctx.permissionMode });
27
+ });
28
+ // PUT /:sessionId — 设置会话级覆盖;不传 mode 则清除覆盖
29
+ app.put('/:sessionId', async (c) => {
30
+ const sessionId = c.req.param('sessionId');
31
+ const body = (await c.req.json().catch(() => null));
32
+ const mode = body?.mode;
33
+ if (mode !== undefined && mode !== 'default' && mode !== 'auto') {
34
+ return apiError(c, 400, 'INVALID_MODE', "mode 必须是 'default' 或 'auto'");
35
+ }
36
+ if (mode === undefined) {
37
+ ctx.sessionPermissionModes.delete(sessionId);
38
+ }
39
+ else {
40
+ ctx.sessionPermissionModes.set(sessionId, mode);
41
+ }
42
+ return c.json({ mode: ctx.sessionPermissionModes.get(sessionId) ?? ctx.permissionMode });
43
+ });
21
44
  return app;
22
45
  }
23
46
  export { createPermissionsRoute };
@@ -1,10 +1,15 @@
1
+ // src/server/routes/project.ts
2
+ import { existsSync } from 'node:fs';
3
+ import { and, eq, isNull } from 'drizzle-orm';
1
4
  import { Hono } from 'hono';
5
+ import { kanbanBoards, projects, sessions } from '../../db/schema.js';
2
6
  import { fromDirectory, getProject, listProjects, resolveProject, updateProjectName, } from '../../project/index.js';
3
7
  import { apiError } from '../middleware/error.js';
4
8
  import { expandPath } from './filesystem.js';
5
- /** 实时取 git 分支(分支随时变,不存 DB)。 */
9
+ /** 实时取 git 分支(分支随时变,不存 DB)。worktree 不存在时标记缺失。 */
6
10
  function withBranch(project) {
7
- return { ...project, gitBranch: resolveProject(project.worktree).gitBranch };
11
+ const worktreeMissing = !existsSync(project.worktree);
12
+ return { ...project, gitBranch: resolveProject(project.worktree).gitBranch, worktreeMissing };
8
13
  }
9
14
  function createProjectRoute(ctx) {
10
15
  const app = new Hono();
@@ -46,6 +51,55 @@ function createProjectRoute(ctx) {
46
51
  return apiError(c, 404, 'NOT_FOUND', 'Project not found');
47
52
  return c.json(withBranch(project));
48
53
  });
54
+ // P1-9 + P2-2:删除项目记录(看板级联删除)。
55
+ // 会话不再留孤儿(FK set null 后在任何项目视图都不可见且 cwd 回退 serve 目录):
56
+ // 删除前把该项目全部未删除会话软删除进回收站(30 天内可恢复),
57
+ // 并把 worktreePath 落盘——恢复后 resolveAgentCwd 仍能解析原工作目录。
58
+ // 有活跃 run 绑定该项目时拒绝删除,避免 agent 工作目录悬空。
59
+ app.delete('/:id', async (c) => {
60
+ const id = c.req.param('id');
61
+ const project = await getProject(ctx.db, id);
62
+ if (!project)
63
+ return apiError(c, 404, 'NOT_FOUND', 'Project not found');
64
+ // 活跃 run 守卫:任一活跃会话归属于该项目 → 拒绝
65
+ const activeSessionIds = new Set();
66
+ for (const run of ctx.agentManager.children(id)) {
67
+ activeSessionIds.add(run.sessionId);
68
+ }
69
+ if (activeSessionIds.size === 0) {
70
+ // 检查该项目下是否有正在进行(含占位)的会话
71
+ const rows = await ctx.db.db
72
+ .select({ id: sessions.id, projectId: sessions.projectId })
73
+ .from(sessions)
74
+ .where(and(eq(sessions.projectId, id)));
75
+ const active = rows.filter((r) => ctx.agentManager.get(r.id));
76
+ if (active.length > 0) {
77
+ return apiError(c, 409, 'PROJECT_HAS_ACTIVE_SESSIONS', `项目下有 ${active.length} 个进行中的对话,请先中止后再删除`);
78
+ }
79
+ }
80
+ let deletedSessions = 0;
81
+ await ctx.db.db.transaction(async (tx) => {
82
+ // 该项目下所有未删除会话:记录 worktreePath(若尚无)并软删除进回收站
83
+ const now = new Date();
84
+ const bound = await tx
85
+ .select({ id: sessions.id, worktreePath: sessions.worktreePath })
86
+ .from(sessions)
87
+ .where(and(eq(sessions.projectId, id), isNull(sessions.deletedAt)));
88
+ for (const s of bound) {
89
+ if (!s.worktreePath) {
90
+ await tx
91
+ .update(sessions)
92
+ .set({ worktreePath: project.worktree })
93
+ .where(eq(sessions.id, s.id));
94
+ }
95
+ await tx.update(sessions).set({ deletedAt: now }).where(eq(sessions.id, s.id));
96
+ deletedSessions += 1;
97
+ }
98
+ await tx.delete(kanbanBoards).where(eq(kanbanBoards.projectId, id));
99
+ await tx.delete(projects).where(eq(projects.id, id));
100
+ });
101
+ return c.json({ ok: true, deletedSessions });
102
+ });
49
103
  return app;
50
104
  }
51
105
  export { createProjectRoute };
@@ -2,9 +2,9 @@ import { Hono } from 'hono';
2
2
  import { createSummarizer, runCompaction } from '../../core/compact.js';
3
3
  import { fromDirectory } from '../../project/index.js';
4
4
  import { archiveOriginalEntries } from '../../session/archive.js';
5
- import { forkSession, getBranches, getTree } from '../../session/branch.js';
5
+ import { BranchPointOutOfRangeError, forkSession, getBranches, getTree, } from '../../session/branch.js';
6
6
  import { deleteEntriesByIds, getMessages, insertEntry } from '../../session/message.js';
7
- import { createSession, deleteSession, getLLMSegments, getSession, listSessions, listSessionsByProject, touchLastOpened, } from '../../session/session.js';
7
+ import { createSession, getLLMSegments, getSession, listDeletedSessions, listSessions, listSessionsByProject, restoreSession, softDeleteSession, touchLastOpened, } from '../../session/session.js';
8
8
  import { applyShakeRegions, collectShakeRegions, DEFAULT_SHAKE_CONFIG, toRegionViews, } from '../../session/shake.js';
9
9
  import { estimateMessageTokens } from '../../session/token.js';
10
10
  import { generateId } from '../../shared/index.js';
@@ -41,6 +41,11 @@ function createSessionRoute(ctx) {
41
41
  const tree = await getTree(ctx.db);
42
42
  return c.json(tree);
43
43
  });
44
+ // 回收站:已软删除的会话列表(必须注册在 /:id 之前,避免被参数路由吞掉)
45
+ app.get('/deleted', async (c) => {
46
+ const sessions = await listDeletedSessions(ctx.db);
47
+ return c.json(sessions);
48
+ });
44
49
  // 获取会话详情
45
50
  app.get('/:id', async (c) => {
46
51
  try {
@@ -54,24 +59,44 @@ function createSessionRoute(ctx) {
54
59
  return apiError(c, 404, 'NOT_FOUND', 'Session not found');
55
60
  }
56
61
  });
57
- // 分支会话
62
+ // 分支会话:未指定 messageIndex 时默认在最新一条消息处分叉(fork=完整副本语义)
58
63
  app.post('/:id/fork', async (c) => {
59
64
  const id = c.req.param('id');
60
65
  const body = await c.req.json().catch(() => ({}));
61
- const messageIndex = body.messageIndex ?? 0;
66
+ let messageIndex = body.messageIndex;
67
+ if (messageIndex === undefined || !Number.isFinite(messageIndex)) {
68
+ const messages = await getMessages(ctx.db, id);
69
+ if (messages.length === 0) {
70
+ return apiError(c, 400, 'EMPTY_SESSION', '空会话无法分支');
71
+ }
72
+ messageIndex = messages.length - 1;
73
+ }
62
74
  try {
63
75
  const forked = await forkSession(ctx.db, id, messageIndex);
64
76
  return c.json(forked, 201);
65
77
  }
66
- catch {
78
+ catch (error) {
79
+ // 分支点越界是客户端索引/分页 bug → 400 并透出明确语义;归 404 会误导排查
80
+ if (error instanceof BranchPointOutOfRangeError) {
81
+ return apiError(c, 400, 'BRANCH_POINT_OUT_OF_RANGE', error.message);
82
+ }
67
83
  return apiError(c, 404, 'NOT_FOUND', 'Session not found');
68
84
  }
69
85
  });
70
- // 删除会话
86
+ // 删除会话(软删除:级联其 fork 后代进入回收站,30 天后物理清除)
71
87
  app.delete('/:id', async (c) => {
72
- await deleteSession(ctx.db, c.req.param('id'));
88
+ const ok = await softDeleteSession(ctx.db, c.req.param('id'));
89
+ if (!ok)
90
+ return apiError(c, 404, 'NOT_FOUND', 'Session not found');
73
91
  return c.body(null, 204);
74
92
  });
93
+ // 恢复会话(从回收站还原;仅还原该会话本身)
94
+ app.post('/:id/restore', async (c) => {
95
+ const ok = await restoreSession(ctx.db, c.req.param('id'));
96
+ if (!ok)
97
+ return apiError(c, 404, 'NOT_FOUND', '会话不存在或未删除');
98
+ return c.json({ ok: true });
99
+ });
75
100
  // 获取消息列表
76
101
  app.get('/:id/messages', async (c) => {
77
102
  const messages = await getMessages(ctx.db, c.req.param('id'));
@@ -113,13 +138,16 @@ function createSessionRoute(ctx) {
113
138
  }
114
139
  });
115
140
  // 获取会话状态:内存有活跃 run → 返回其 status;否则查 DB lastRun。
116
- // lastRun.status='running' 但无活跃 run → 服务重启被中断。
141
+ // lastRun.status='running' 且无活跃 run → 服务重启被中断。
142
+ // lastRun.status='paused' 但无活跃 run(热更新/重启后 run 状态未迁移)→ 同样按中断处理:
143
+ // agent 内存态已丢失,resume 端点无法恢复,唯一可行路径是重发上一条消息。
117
144
  app.get('/:id/status', async (c) => {
118
145
  const run = ctx.agentManager.get(c.req.param('id'));
119
146
  if (run)
120
147
  return c.json(run.state.status);
121
148
  const session = await getSession(ctx.db, c.req.param('id'));
122
- if (session?.metadata.lastRun?.status === 'running') {
149
+ if (session?.metadata.lastRun?.status === 'running' ||
150
+ session?.metadata.lastRun?.status === 'paused') {
123
151
  return c.json({ _tag: 'interrupted' });
124
152
  }
125
153
  return c.json({ _tag: 'idle' });
@@ -1,6 +1,11 @@
1
1
  // src/server/routes/terminal.ts
2
+ import { existsSync } from 'node:fs';
3
+ import { isAbsolute } from 'node:path';
2
4
  import { Hono } from 'hono';
5
+ import { getProject } from '../../project/index.js';
3
6
  import { apiError } from '../middleware/error.js';
7
+ /** 允许的 shell 白名单(P2-6:拒绝路径分隔符与任意二进制)。 */
8
+ const ALLOWED_SHELLS = new Set(['bash', 'zsh', 'fish', 'sh']);
4
9
  /** 创建终端路由(REST 部分;WebSocket 升级在 server.ts 中处理)。 */
5
10
  function createTerminalRoute(ctx) {
6
11
  const app = new Hono();
@@ -12,12 +17,34 @@ function createTerminalRoute(ctx) {
12
17
  // 创建新 PTY 会话
13
18
  app.post('/', async (c) => {
14
19
  const body = await c.req.json().catch(() => ({}));
15
- const cwd = typeof body.cwd === 'string' && body.cwd.length > 0 ? body.cwd : ctx.cwd;
20
+ const projectId = typeof body.projectId === 'string' && body.projectId.length > 0 ? body.projectId : undefined;
21
+ // P2-13:未显式传 cwd 且带 projectId 时,默认目录跟随项目 worktree
22
+ // (与 chat 的 resolveAgentCwd 行为一致;项目缺失/目录失效时回退 ctx.cwd)。
23
+ let defaultCwd = ctx.cwd;
24
+ if (!(typeof body.cwd === 'string' && body.cwd.length > 0) && projectId) {
25
+ try {
26
+ const project = await getProject(ctx.db, projectId);
27
+ if (project && existsSync(project.worktree))
28
+ defaultCwd = project.worktree;
29
+ }
30
+ catch {
31
+ // db 不可用(测试桩/临时故障):保持 ctx.cwd
32
+ }
33
+ }
34
+ const cwd = typeof body.cwd === 'string' && body.cwd.length > 0 ? body.cwd : defaultCwd;
16
35
  const cols = Number.isFinite(body.cols) ? Number(body.cols) : undefined;
17
36
  const rows = Number.isFinite(body.rows) ? Number(body.rows) : undefined;
18
37
  const title = typeof body.title === 'string' ? body.title : undefined;
19
38
  const shell = typeof body.shell === 'string' && body.shell.length > 0 ? body.shell : undefined;
20
- const projectId = typeof body.projectId === 'string' && body.projectId.length > 0 ? body.projectId : undefined;
39
+ // 入参校验(P2-6):shell 白名单;cwd 必须为绝对路径。
40
+ if (shell !== undefined) {
41
+ if (shell.includes('/') || shell.includes('\\') || !ALLOWED_SHELLS.has(shell)) {
42
+ return apiError(c, 400, 'INVALID_SHELL', `shell 必须是 ${[...ALLOWED_SHELLS].join('/')} 之一`);
43
+ }
44
+ }
45
+ if (typeof cwd === 'string' && cwd.length > 0 && !isAbsolute(cwd)) {
46
+ return apiError(c, 400, 'INVALID_CWD', 'cwd 必须是绝对路径');
47
+ }
21
48
  try {
22
49
  const info = mgr.create({ cwd, cols, rows, title, shell, projectId });
23
50
  return c.json(info, 201);
@@ -9,6 +9,7 @@ import type { ServerContext } from '../types.js';
9
9
  * 序列化当前会话 → performHotUpdate(npm install -g + spawn 新实例 + handoffPort)。
10
10
  * 旧实例通过 handoff 端点优雅退出,新实例 restore 快照并接管端口。
11
11
  * 响应立即返回(不阻塞 spawn),实际接管由 handoff IPC 异步完成。
12
+ * dev(vite)模式无 handoff server,返回 409——热更新仅在独立 serve 进程可用。
12
13
  */
13
14
  declare function createUpdateRoute(ctx: ServerContext): Hono;
14
15
  export { createUpdateRoute };
@@ -1,5 +1,5 @@
1
1
  import { Hono } from 'hono';
2
- import { performHotUpdate, serializeSessions } from '../../update/index.js';
2
+ import { getCurrentVersion, performHotUpdate, serializeSessions } from '../../update/index.js';
3
3
  import { apiError } from '../middleware/error.js';
4
4
  /**
5
5
  * GET /api/update — 返回后台调度器缓存的版本检查结果(spec §18.1)。
@@ -10,36 +10,65 @@ import { apiError } from '../middleware/error.js';
10
10
  * 序列化当前会话 → performHotUpdate(npm install -g + spawn 新实例 + handoffPort)。
11
11
  * 旧实例通过 handoff 端点优雅退出,新实例 restore 快照并接管端口。
12
12
  * 响应立即返回(不阻塞 spawn),实际接管由 handoff IPC 异步完成。
13
+ * dev(vite)模式无 handoff server,返回 409——热更新仅在独立 serve 进程可用。
13
14
  */
14
15
  function createUpdateRoute(ctx) {
15
16
  const app = new Hono();
16
17
  app.get('/', (c) => {
18
+ // P2-8:update.enabled=false 时无 handoff server,apply 必然 409。
19
+ // 不再触发 checkNow,也不返回 hasUpdate,避免横幅出现一个点了必失败的应用按钮。
20
+ if (ctx.config.update.enabled === false) {
21
+ const v = getCurrentVersion();
22
+ return c.json({ hasUpdate: false, disabled: true, currentVersion: v, latestVersion: v });
23
+ }
17
24
  const cached = ctx.updateScheduler.getLastResult();
18
25
  if (cached)
19
26
  return c.json(cached);
20
27
  // 无缓存(首次启动延迟未到):同步触发一次,避免前端首屏空。
21
28
  // 不 await——保持 GET 语义非阻塞;前端下次轮询拿到结果。
22
29
  void ctx.updateScheduler.checkNow();
30
+ const v = getCurrentVersion();
23
31
  return c.json({
24
32
  hasUpdate: false,
25
- currentVersion: '0.0.0',
26
- latestVersion: '0.0.0',
33
+ currentVersion: v,
34
+ latestVersion: v,
27
35
  });
28
36
  });
29
37
  app.post('/apply', async (c) => {
38
+ if (!ctx.handoff) {
39
+ return apiError(c, 409, 'HOT_UPDATE_UNAVAILABLE', '热更新仅在独立 serve 进程可用(dev 模式请手动更新)');
40
+ }
30
41
  const result = await ctx.updateScheduler.checkNow();
31
42
  if (!result.hasUpdate) {
32
43
  return apiError(c, 409, 'NO_UPDATE', '已是最新版本,无需热更新');
33
44
  }
45
+ // P1-8:热更新前先暂停所有活跃 run,等其到达安全暂停点(原子操作完成)。
46
+ // 暂停状态已持久化(lastRun.status='paused'),新实例接管后可恢复继续执行。
47
+ const pauseTimeoutMs = ctx.config.update.pauseTimeoutMs ?? 30_000;
48
+ const pauseResult = await ctx.agentManager.pauseAll(pauseTimeoutMs);
34
49
  const snapshot = await serializeSessions(ctx.db, ctx.config);
35
50
  const r = await performHotUpdate(snapshot, {
36
- handoffPort: ctx.handoff?.port,
51
+ handoffPort: ctx.handoff.port,
52
+ port: ctx.port,
53
+ // P2-16:传当前 bootstrap(轮换后 ctx.authToken 可能已过期);
54
+ // 旧实例 handoff 端点经 verifyHandoff 接受当前/历史 bootstrap 与设备 token。
55
+ authToken: ctx.authManager?.bootstrap ?? ctx.authToken,
37
56
  });
38
57
  if (r._tag === 'success') {
39
58
  return c.json({
40
59
  ok: true,
41
60
  snapshotPath: r.snapshotPath,
42
61
  latestVersion: result.latestVersion,
62
+ installMethod: r.installMethod,
63
+ pausedSessions: pauseResult.paused,
64
+ forcedAbort: pauseResult.forcedAbort,
65
+ });
66
+ }
67
+ if (r._tag === 'manual_install_required') {
68
+ // P0-2:无法自动安装(未知安装方式/自动安装失败/等待超时)→ 引导手动更新
69
+ return apiError(c, 409, 'MANUAL_UPDATE_REQUIRED', r.error, {
70
+ command: r.command,
71
+ snapshotPath: r.snapshotPath,
43
72
  });
44
73
  }
45
74
  return apiError(c, 500, 'HOT_UPDATE_FAILED', `${r._tag}: ${'error' in r ? r.error : 'unknown'}`);
@@ -2,9 +2,9 @@ import { unlink } from 'node:fs/promises';
2
2
  import { Hono } from 'hono';
3
3
  import { streamSSE } from 'hono/streaming';
4
4
  import { createAgent } from '../../core/agent.js';
5
- import { executeWorkflow } from '../../core/workflows/runtime.js';
6
- import { reloadRegistry } from '../../core/workflows/registry.js';
7
5
  import { discoverWorkflows, saveWorkflow } from '../../core/workflows/discovery.js';
6
+ import { reloadRegistry } from '../../core/workflows/registry.js';
7
+ import { executeWorkflow } from '../../core/workflows/runtime.js';
8
8
  import { getProject } from '../../project/project.js';
9
9
  import { createSession } from '../../session/session.js';
10
10
  import { autoAllowChecker } from '../../tools/permission.js';
@@ -23,6 +23,8 @@ type StartServerOptions = {
23
23
  type RunningServer = {
24
24
  app: Hono;
25
25
  port: number;
26
+ /** 认证 token(authEnabled=false 时为 undefined);serve 命令据此打印带 token 的 URL。 */
27
+ authToken?: string;
26
28
  close(): Promise<void>;
27
29
  };
28
30
  type BootstrappedServer = {
@@ -43,6 +45,15 @@ declare function syncRegistryFromConfig(registry: Registry, config: Config): voi
43
45
  * 历史上数据库放在 <cwd>/.c0de/pglite(项目级),但项目 DB schema 通过 projectId
44
46
  * 区分项目,设计本意是全局共享单库。旧路径已在 migrateLegacyPglite 中自动迁移。 */
45
47
  declare function resolveDbDir(): string;
48
+ /**
49
+ * 认证 token 解析(P0 安全修复):
50
+ * 1. security.authEnabled 显式为 false → 无认证(用户显式选择)。
51
+ * 2. 用户配置 security.token → 使用。
52
+ * 3. 环境变量 C0DE_AUTH_TOKEN(热更新新实例经 updater 传入)→ 采用并落盘。
53
+ * 4. dataDir 下 auth-token 文件(跨重启/热更新稳定)→ 复用。
54
+ * 5. 都没有 → 生成随机 token 并写入 auth-token(0600)。
55
+ */
56
+ declare function resolveAuthToken(config: Config, dataDir: string): string | undefined;
46
57
  /**
47
58
  * 围绕已有 DB handle 组装 ServerContext(不建/不关闭 DB)。
48
59
  *
@@ -54,6 +65,18 @@ declare function buildServerContext(db: DB, opts?: StartServerOptions): Promise<
54
65
  ctx: ServerContext;
55
66
  dispose: () => Promise<void>;
56
67
  }>;
68
+ /**
69
+ * Cross-process guard for PGLite dataDir.
70
+ *
71
+ * PGLite is single-writer WASM Postgres — two processes on the same dataDir
72
+ * always abort (`RuntimeError: Aborted()`). This lock prevents silent
73
+ * corruption when multiple dev servers (e.g. different ports) target the
74
+ * same project.
75
+ *
76
+ * - Live PID in lock → throw clear error instead of cryptic WASM abort.
77
+ * - Dead PID in lock → stale: remove `.dev.lock` + `postmaster.pid`, proceed.
78
+ */
79
+ declare function acquireDevDbLock(dataDir: string): void;
57
80
  /** Release the dev DB lock (best-effort, stale-detection covers missed calls). */
58
81
  declare function releaseDevDbLock(dataDir: string): void;
59
82
  /** dev 专用:创建 + migrate PGLite,跨热重载复用(单写者,只建一次)。 */
@@ -62,4 +85,4 @@ declare function createDevDb(cwd: string): Promise<DB>;
62
85
  declare function bootstrapServerContext(opts?: StartServerOptions): Promise<BootstrappedServer>;
63
86
  declare function startServer(opts?: StartServerOptions): Promise<RunningServer>;
64
87
  export type { BootstrappedServer, RunningServer, StartServerOptions };
65
- export { bootstrapServerContext, buildRegistryFromConfig, buildServerContext, createDevDb, releaseDevDbLock, resolveDbDir, startServer, syncRegistryFromConfig, };
88
+ export { acquireDevDbLock, bootstrapServerContext, buildRegistryFromConfig, buildServerContext, createDevDb, releaseDevDbLock, resolveAuthToken, resolveDbDir, startServer, syncRegistryFromConfig, };