@tt-a1i/hive 1.7.0 → 2.0.2

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 (251) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.en.md +73 -11
  3. package/README.md +41 -8
  4. package/dist/src/cli/hive-remote.d.ts +46 -0
  5. package/dist/src/cli/hive-remote.js +257 -0
  6. package/dist/src/cli/hive-update.js +7 -2
  7. package/dist/src/cli/hive.d.ts +6 -0
  8. package/dist/src/cli/hive.js +64 -0
  9. package/dist/src/cli/team.d.ts +22 -0
  10. package/dist/src/cli/team.js +255 -5
  11. package/dist/src/server/agent-command-resolver.js +10 -3
  12. package/dist/src/server/agent-exit-classification.d.ts +6 -0
  13. package/dist/src/server/agent-exit-classification.js +6 -0
  14. package/dist/src/server/agent-manager-support.d.ts +2 -1
  15. package/dist/src/server/agent-manager-support.js +59 -15
  16. package/dist/src/server/agent-manager.d.ts +3 -0
  17. package/dist/src/server/agent-manager.js +22 -7
  18. package/dist/src/server/agent-run-bootstrap.d.ts +14 -0
  19. package/dist/src/server/agent-run-bootstrap.js +11 -4
  20. package/dist/src/server/agent-run-exit-handler.js +14 -8
  21. package/dist/src/server/agent-run-starter.d.ts +3 -1
  22. package/dist/src/server/agent-run-starter.js +22 -5
  23. package/dist/src/server/agent-run-sync.js +13 -5
  24. package/dist/src/server/agent-runtime-types.d.ts +1 -0
  25. package/dist/src/server/agent-runtime.d.ts +2 -1
  26. package/dist/src/server/agent-runtime.js +9 -2
  27. package/dist/src/server/agent-startup-instructions.d.ts +2 -1
  28. package/dist/src/server/agent-startup-instructions.js +8 -4
  29. package/dist/src/server/agent-stdin-dispatcher.d.ts +4 -2
  30. package/dist/src/server/agent-stdin-dispatcher.js +35 -3
  31. package/dist/src/server/command-preset-defaults.d.ts +6 -1
  32. package/dist/src/server/command-preset-defaults.js +56 -0
  33. package/dist/src/server/fs-browse.d.ts +2 -0
  34. package/dist/src/server/fs-browse.js +165 -31
  35. package/dist/src/server/fs-pick-folder.js +6 -69
  36. package/dist/src/server/fs-sandbox.d.ts +5 -3
  37. package/dist/src/server/fs-sandbox.js +5 -3
  38. package/dist/src/server/hive-team-guidance.js +18 -6
  39. package/dist/src/server/machine-name.d.ts +2 -0
  40. package/dist/src/server/machine-name.js +13 -0
  41. package/dist/src/server/open-target-commands.d.ts +1 -0
  42. package/dist/src/server/open-target-commands.js +4 -1
  43. package/dist/src/server/orchestrator-autostart.js +1 -1
  44. package/dist/src/server/platform-path.d.ts +1 -0
  45. package/dist/src/server/platform-path.js +14 -1
  46. package/dist/src/server/post-start-input-writer.js +50 -13
  47. package/dist/src/server/preset-launch-support.js +1 -0
  48. package/dist/src/server/recovery-summary.d.ts +2 -1
  49. package/dist/src/server/recovery-summary.js +2 -1
  50. package/dist/src/server/remote-audit-store.d.ts +51 -0
  51. package/dist/src/server/remote-audit-store.js +108 -0
  52. package/dist/src/server/remote-config-keys.d.ts +17 -0
  53. package/dist/src/server/remote-config-keys.js +27 -0
  54. package/dist/src/server/remote-control-constants.d.ts +30 -0
  55. package/dist/src/server/remote-control-constants.js +29 -0
  56. package/dist/src/server/remote-device-session.d.ts +40 -0
  57. package/dist/src/server/remote-device-session.js +22 -0
  58. package/dist/src/server/remote-device-store.d.ts +36 -0
  59. package/dist/src/server/remote-device-store.js +67 -0
  60. package/dist/src/server/remote-frame-bridge.d.ts +102 -0
  61. package/dist/src/server/remote-frame-bridge.js +791 -0
  62. package/dist/src/server/remote-gateway-client.d.ts +14 -0
  63. package/dist/src/server/remote-gateway-client.js +36 -0
  64. package/dist/src/server/remote-loopback-auth.d.ts +6 -0
  65. package/dist/src/server/remote-loopback-auth.js +112 -0
  66. package/dist/src/server/remote-pairing-tunnel.d.ts +59 -0
  67. package/dist/src/server/remote-pairing-tunnel.js +146 -0
  68. package/dist/src/server/remote-pairing.d.ts +58 -0
  69. package/dist/src/server/remote-pairing.js +237 -0
  70. package/dist/src/server/remote-tunnel.d.ts +113 -0
  71. package/dist/src/server/remote-tunnel.js +514 -0
  72. package/dist/src/server/restart-policy-support.d.ts +4 -1
  73. package/dist/src/server/restart-policy-support.js +3 -1
  74. package/dist/src/server/restart-policy.d.ts +1 -1
  75. package/dist/src/server/restart-policy.js +19 -3
  76. package/dist/src/server/route-types.d.ts +1 -1
  77. package/dist/src/server/routes-dispatches.js +1 -1
  78. package/dist/src/server/routes-fs.js +3 -3
  79. package/dist/src/server/routes-marketplace.js +2 -2
  80. package/dist/src/server/routes-open-workspace.js +1 -1
  81. package/dist/src/server/routes-remote.d.ts +2 -0
  82. package/dist/src/server/routes-remote.js +166 -0
  83. package/dist/src/server/routes-runtime.js +6 -6
  84. package/dist/src/server/routes-settings.js +16 -16
  85. package/dist/src/server/routes-tasks.js +2 -2
  86. package/dist/src/server/routes-team-memory.d.ts +2 -0
  87. package/dist/src/server/routes-team-memory.js +154 -0
  88. package/dist/src/server/routes-team-recall.d.ts +2 -0
  89. package/dist/src/server/routes-team-recall.js +119 -0
  90. package/dist/src/server/routes-team.js +31 -9
  91. package/dist/src/server/routes-ui.js +11 -1
  92. package/dist/src/server/routes-workflow-schedules.js +3 -3
  93. package/dist/src/server/routes-workflows.js +5 -5
  94. package/dist/src/server/routes-workspace-memory-dreams.d.ts +2 -0
  95. package/dist/src/server/routes-workspace-memory-dreams.js +105 -0
  96. package/dist/src/server/routes-workspace-memory.d.ts +2 -0
  97. package/dist/src/server/routes-workspace-memory.js +215 -0
  98. package/dist/src/server/routes-workspaces.js +9 -9
  99. package/dist/src/server/routes.js +10 -0
  100. package/dist/src/server/runtime-database.d.ts +1 -0
  101. package/dist/src/server/runtime-database.js +27 -2
  102. package/dist/src/server/runtime-restart-policy.d.ts +3 -1
  103. package/dist/src/server/runtime-restart-policy.js +2 -1
  104. package/dist/src/server/runtime-store-contract.d.ts +37 -0
  105. package/dist/src/server/runtime-store-dream.d.ts +23 -0
  106. package/dist/src/server/runtime-store-dream.js +16 -0
  107. package/dist/src/server/runtime-store-helpers.d.ts +20 -0
  108. package/dist/src/server/runtime-store-helpers.js +81 -7
  109. package/dist/src/server/runtime-store-memory.d.ts +33 -0
  110. package/dist/src/server/runtime-store-memory.js +37 -0
  111. package/dist/src/server/runtime-store-remote.d.ts +5 -0
  112. package/dist/src/server/runtime-store-remote.js +45 -0
  113. package/dist/src/server/runtime-store-workflows.js +2 -0
  114. package/dist/src/server/runtime-store.js +14 -3
  115. package/dist/src/server/session-capture-claude.d.ts +1 -1
  116. package/dist/src/server/session-capture-claude.js +7 -4
  117. package/dist/src/server/session-capture-codex.js +4 -5
  118. package/dist/src/server/session-capture-gemini.js +4 -5
  119. package/dist/src/server/session-capture-opencode.d.ts +4 -4
  120. package/dist/src/server/session-capture-opencode.js +20 -12
  121. package/dist/src/server/session-capture-qwen.d.ts +5 -0
  122. package/dist/src/server/session-capture-qwen.js +104 -0
  123. package/dist/src/server/session-capture.d.ts +17 -0
  124. package/dist/src/server/session-capture.js +16 -0
  125. package/dist/src/server/sqlite-schema-v23.d.ts +2 -0
  126. package/dist/src/server/sqlite-schema-v23.js +43 -0
  127. package/dist/src/server/sqlite-schema-v24.d.ts +2 -0
  128. package/dist/src/server/sqlite-schema-v24.js +34 -0
  129. package/dist/src/server/sqlite-schema-v25.d.ts +2 -0
  130. package/dist/src/server/sqlite-schema-v25.js +127 -0
  131. package/dist/src/server/sqlite-schema-v26.d.ts +2 -0
  132. package/dist/src/server/sqlite-schema-v26.js +56 -0
  133. package/dist/src/server/sqlite-schema-v27.d.ts +6 -0
  134. package/dist/src/server/sqlite-schema-v27.js +92 -0
  135. package/dist/src/server/sqlite-schema-v28.d.ts +2 -0
  136. package/dist/src/server/sqlite-schema-v28.js +19 -0
  137. package/dist/src/server/sqlite-schema-v29.d.ts +2 -0
  138. package/dist/src/server/sqlite-schema-v29.js +27 -0
  139. package/dist/src/server/sqlite-schema-v30.d.ts +2 -0
  140. package/dist/src/server/sqlite-schema-v30.js +27 -0
  141. package/dist/src/server/sqlite-schema-v31.d.ts +2 -0
  142. package/dist/src/server/sqlite-schema-v31.js +30 -0
  143. package/dist/src/server/sqlite-schema.d.ts +1 -1
  144. package/dist/src/server/sqlite-schema.js +49 -1
  145. package/dist/src/server/startup-command-parser.js +5 -1
  146. package/dist/src/server/tasks-file-watcher.d.ts +2 -0
  147. package/dist/src/server/tasks-file-watcher.js +15 -6
  148. package/dist/src/server/tasks-file.js +30 -5
  149. package/dist/src/server/tasks-websocket-server.js +4 -0
  150. package/dist/src/server/team-authz.d.ts +1 -1
  151. package/dist/src/server/team-authz.js +13 -1
  152. package/dist/src/server/team-list-enrichment.js +3 -1
  153. package/dist/src/server/team-memory-digest.d.ts +52 -0
  154. package/dist/src/server/team-memory-digest.js +200 -0
  155. package/dist/src/server/team-memory-dream-applier.d.ts +5 -0
  156. package/dist/src/server/team-memory-dream-applier.js +234 -0
  157. package/dist/src/server/team-memory-dream-http-serializers.d.ts +13 -0
  158. package/dist/src/server/team-memory-dream-http-serializers.js +12 -0
  159. package/dist/src/server/team-memory-dream-ops.d.ts +40 -0
  160. package/dist/src/server/team-memory-dream-ops.js +153 -0
  161. package/dist/src/server/team-memory-dream-reverter.d.ts +22 -0
  162. package/dist/src/server/team-memory-dream-reverter.js +221 -0
  163. package/dist/src/server/team-memory-dream-run-store.d.ts +23 -0
  164. package/dist/src/server/team-memory-dream-run-store.js +211 -0
  165. package/dist/src/server/team-memory-dream-runner.d.ts +37 -0
  166. package/dist/src/server/team-memory-dream-runner.js +178 -0
  167. package/dist/src/server/team-memory-dream-scheduler.d.ts +32 -0
  168. package/dist/src/server/team-memory-dream-scheduler.js +115 -0
  169. package/dist/src/server/team-memory-dream-store.d.ts +19 -0
  170. package/dist/src/server/team-memory-dream-store.js +16 -0
  171. package/dist/src/server/team-memory-dream-types.d.ts +104 -0
  172. package/dist/src/server/team-memory-dream-types.js +23 -0
  173. package/dist/src/server/team-memory-export.d.ts +22 -0
  174. package/dist/src/server/team-memory-export.js +220 -0
  175. package/dist/src/server/team-memory-feature.d.ts +12 -0
  176. package/dist/src/server/team-memory-feature.js +12 -0
  177. package/dist/src/server/team-memory-http-serializers.d.ts +102 -0
  178. package/dist/src/server/team-memory-http-serializers.js +46 -0
  179. package/dist/src/server/team-memory-injection.d.ts +31 -0
  180. package/dist/src/server/team-memory-injection.js +49 -0
  181. package/dist/src/server/team-memory-store.d.ts +116 -0
  182. package/dist/src/server/team-memory-store.js +513 -0
  183. package/dist/src/server/team-operations.d.ts +5 -1
  184. package/dist/src/server/team-operations.js +46 -16
  185. package/dist/src/server/team-recall-store.d.ts +38 -0
  186. package/dist/src/server/team-recall-store.js +205 -0
  187. package/dist/src/server/terminal-input-profile.d.ts +1 -1
  188. package/dist/src/server/terminal-input-profile.js +18 -0
  189. package/dist/src/server/terminal-ws-server.js +6 -0
  190. package/dist/src/server/ui-auth-helpers.d.ts +1 -1
  191. package/dist/src/server/ui-auth-helpers.js +7 -1
  192. package/dist/src/server/ui-auth.d.ts +3 -0
  193. package/dist/src/server/ui-auth.js +21 -1
  194. package/dist/src/server/workflow-cli-policy.d.ts +2 -3
  195. package/dist/src/server/workflow-cli-policy.js +3 -3
  196. package/dist/src/server/workflow-runner.d.ts +1 -0
  197. package/dist/src/server/workflow-runner.js +9 -4
  198. package/dist/src/server/workspace-path-validation.js +6 -2
  199. package/dist/src/server/workspace-store.d.ts +1 -1
  200. package/dist/src/server/workspace-store.js +35 -9
  201. package/dist/src/shared/fs-browse.d.ts +1 -0
  202. package/dist/src/shared/fs-browse.js +1 -0
  203. package/dist/src/shared/path-input.d.ts +12 -0
  204. package/dist/src/shared/path-input.js +22 -0
  205. package/dist/src/shared/remote-bridge-routing.d.ts +19 -0
  206. package/dist/src/shared/remote-bridge-routing.js +141 -0
  207. package/dist/src/shared/remote-crypto.d.ts +138 -0
  208. package/dist/src/shared/remote-crypto.js +427 -0
  209. package/dist/src/shared/remote-pairing-code.d.ts +7 -0
  210. package/dist/src/shared/remote-pairing-code.js +47 -0
  211. package/dist/src/shared/remote-protocol.d.ts +160 -0
  212. package/dist/src/shared/remote-protocol.js +526 -0
  213. package/dist/src/shared/team-memory.d.ts +11 -0
  214. package/dist/src/shared/team-memory.js +10 -0
  215. package/dist/src/shared/team-recall.d.ts +1 -0
  216. package/dist/src/shared/team-recall.js +1 -0
  217. package/dist/src/shared/types.d.ts +4 -5
  218. package/package.json +12 -5
  219. package/scripts/postinstall-native-artifacts.mjs +113 -0
  220. package/web/dist/assets/AddWorkerDialog-CbV75qUX.js +2 -0
  221. package/web/dist/assets/AddWorkspaceFlow-CwV-7wPx.js +1 -0
  222. package/web/dist/assets/FirstRunWizard-a6PWIK3x.js +1 -0
  223. package/web/dist/assets/MarketplaceDrawer-Dd8WIA8T.js +67 -0
  224. package/web/dist/assets/TaskGraphDrawer-Bk5WFIk_.js +1 -0
  225. package/web/dist/assets/{WhatsNewDialog-CHkZeINH.js → WhatsNewDialog-C2VZaip0.js} +1 -1
  226. package/web/dist/assets/WorkerModal-DucW-9YT.js +1 -0
  227. package/web/dist/assets/WorkflowsDrawer-Bjf4olbR.js +1 -0
  228. package/web/dist/assets/WorkspaceMemoryDrawer-DglCy_5f.js +1 -0
  229. package/web/dist/assets/WorkspaceTaskDrawer-BIWwISvA.js +1 -0
  230. package/web/dist/assets/index-BAiLYajK.css +1 -0
  231. package/web/dist/assets/index-BV2k9Dts.js +73 -0
  232. package/web/dist/assets/search-Bk2HQvO7.js +1 -0
  233. package/web/dist/assets/square-terminal-D93m9hfY.js +1 -0
  234. package/web/dist/cli-icons/agy.png +0 -0
  235. package/web/dist/cli-icons/cursor.ico +0 -0
  236. package/web/dist/cli-icons/grok.ico +0 -0
  237. package/web/dist/cli-icons/qwen.png +0 -0
  238. package/web/dist/index.html +8 -3
  239. package/web/dist/sw.js +1 -1
  240. package/scripts/fix-runtime-artifacts.mjs +0 -33
  241. package/web/dist/assets/AddWorkerDialog-BRUxpa3f.js +0 -2
  242. package/web/dist/assets/AddWorkspaceDialog-D56x5JCb.js +0 -1
  243. package/web/dist/assets/FirstRunWizard-BFVaMIsE.js +0 -1
  244. package/web/dist/assets/MarketplaceDrawer-DeEZ35dN.js +0 -76
  245. package/web/dist/assets/WorkerModal-BBCuMLIa.js +0 -1
  246. package/web/dist/assets/WorkspaceTaskDrawer-CpZHAcj1.js +0 -1
  247. package/web/dist/assets/WorkspaceTerminalPanels-7If2mDyp.js +0 -1
  248. package/web/dist/assets/WorkspaceTerminalPanels-DDGTF8rc.css +0 -1
  249. package/web/dist/assets/index-5zh61jMg.css +0 -1
  250. package/web/dist/assets/index-CxNL0O-C.js +0 -73
  251. package/web/dist/assets/path-join-7MR1s7b1.js +0 -1
@@ -2,6 +2,7 @@ import { FEATURE_FLAGS_ALL_OFF } from './feature-flags.js';
2
2
  import { buildRecoverySummary } from './recovery-summary.js';
3
3
  import { findPreviousRun, writeSystemMessage, } from './restart-policy-support.js';
4
4
  import { createSystemRecoverySummaryMessage } from './runtime-message-builders.js';
5
+ import { buildMemoryDigestSafely, logMemoryDigestInjection, rollbackMemoryDigestInjection, } from './team-memory-injection.js';
5
6
  const RECOVERY_WINDOW_MS = 60 * 60 * 1000;
6
7
  export const createNoopRestartPolicy = () => ({
7
8
  injectPostStartMessage() {
@@ -9,7 +10,7 @@ export const createNoopRestartPolicy = () => ({
9
10
  },
10
11
  markUserStopped() { },
11
12
  });
12
- export const createRestartPolicy = ({ deleteMessage, getWorkspaceSnapshot, insertMessage, listAgentRuns, listMessagesForRecovery, readTasks, getFlags, }) => {
13
+ export const createRestartPolicy = ({ deleteMessage, getWorkspaceSnapshot, insertMessage, listAgentRuns, listMessagesForRecovery, memoryInjection, readTasks, getFlags, }) => {
13
14
  // Runs the user killed via the Stop button. A deliberate stop is otherwise
14
15
  // byte-identical to a crash (both end status 'error'), so without this a
15
16
  // stop+Restart would inject the "could not recover" handover with stale open
@@ -33,14 +34,28 @@ export const createRestartPolicy = ({ deleteMessage, getWorkspaceSnapshot, inser
33
34
  return false;
34
35
  const workers = snapshot.agents.filter((item) => item.role !== 'orchestrator' && item.id !== agentId);
35
36
  const tasksContent = readTasks(snapshot.summary.path);
36
- if (startConfig.resumedSessionId)
37
- return true;
38
37
  // Deliberate stop + restart: start fresh, no crash handover.
39
38
  if (wasUserStopped)
40
39
  return false;
40
+ if (startConfig.resumedSessionId)
41
+ return true;
42
+ const memoryDigest = buildMemoryDigestSafely({
43
+ contextType: 'recovery',
44
+ memoryInjection,
45
+ workspaceId: workspace.id,
46
+ });
47
+ const injectionIds = logMemoryDigestInjection({
48
+ agentId,
49
+ contextType: 'recovery',
50
+ memoryDigest,
51
+ memoryInjection,
52
+ workspaceId: workspace.id,
53
+ });
54
+ const auditedMemoryDigest = injectionIds ? memoryDigest : null;
41
55
  const text = buildRecoverySummary({
42
56
  agent,
43
57
  allTaskMessages: listMessagesForRecovery(workspace.id, 0),
58
+ memoryDigest: auditedMemoryDigest?.text,
44
59
  messages: listMessagesForRecovery(workspace.id, Date.now() - RECOVERY_WINDOW_MS),
45
60
  tasksContent,
46
61
  workers,
@@ -54,6 +69,7 @@ export const createRestartPolicy = ({ deleteMessage, getWorkspaceSnapshot, inser
54
69
  runId,
55
70
  text,
56
71
  writeToRun,
72
+ onWriteFailure: () => rollbackMemoryDigestInjection({ injectionIds, memoryInjection }),
57
73
  });
58
74
  return true;
59
75
  },
@@ -62,7 +62,7 @@ export interface ReportTaskBody {
62
62
  from_agent_id: string;
63
63
  token?: string;
64
64
  result: string;
65
- status?: string;
65
+ status?: 'success' | 'failed';
66
66
  artifacts?: unknown[];
67
67
  }
68
68
  export interface CancelTaskBody {
@@ -25,7 +25,7 @@ export const dispatchRoutes = [
25
25
  if (!workspaceId) {
26
26
  return;
27
27
  }
28
- requireUiTokenFromRequest(request, store.validateUiToken);
28
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
29
29
  const url = new URL(request.url ?? '/', 'http://127.0.0.1');
30
30
  if (url.searchParams.has('status')) {
31
31
  sendJson(response, 400, { error: 'Use state instead of status for dispatch filtering' });
@@ -7,17 +7,17 @@ const readPathParam = (request) => {
7
7
  };
8
8
  export const fsRoutes = [
9
9
  route('GET', '/api/fs/browse', async ({ request, response, store }) => {
10
- requireUiTokenFromRequest(request, store.validateUiToken);
10
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
11
11
  const body = await browseDirectory(readPathParam(request));
12
12
  sendJson(response, body.ok ? 200 : 400, body);
13
13
  }),
14
14
  route('GET', '/api/fs/probe', async ({ request, response, store }) => {
15
- requireUiTokenFromRequest(request, store.validateUiToken);
15
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
16
16
  const body = await probeDirectory(readPathParam(request));
17
17
  sendJson(response, 200, body);
18
18
  }),
19
19
  route('POST', '/api/fs/pick-folder', async ({ pickFolderService, request, response, store }) => {
20
- requireUiTokenFromRequest(request, store.validateUiToken);
20
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
21
21
  const body = await pickFolderService();
22
22
  sendJson(response, 200, body);
23
23
  }),
@@ -11,7 +11,7 @@ const readPathParam = (request) => {
11
11
  };
12
12
  export const marketplaceRoutes = [
13
13
  route('GET', '/api/marketplace/manifest', ({ request, response, store }) => {
14
- requireUiTokenFromRequest(request, store.validateUiToken);
14
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
15
15
  const lang = readLanguageParam(request);
16
16
  if (!isMarketplaceLanguage(lang)) {
17
17
  sendJson(response, 400, { error: 'Invalid or missing lang parameter (expected en|zh)' });
@@ -29,7 +29,7 @@ export const marketplaceRoutes = [
29
29
  }
30
30
  }),
31
31
  route('GET', '/api/marketplace/agent', ({ request, response, store }) => {
32
- requireUiTokenFromRequest(request, store.validateUiToken);
32
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
33
33
  const lang = readLanguageParam(request);
34
34
  if (!isMarketplaceLanguage(lang)) {
35
35
  sendJson(response, 400, { error: 'Invalid or missing lang parameter (expected en|zh)' });
@@ -6,7 +6,7 @@ export const openWorkspaceRoutes = [
6
6
  const workspaceId = getRequiredParam(response, params, 'workspaceId', 'Workspace id is required');
7
7
  if (!workspaceId)
8
8
  return;
9
- requireUiTokenFromRequest(request, store.validateUiToken);
9
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
10
10
  const body = await readJsonBody(request);
11
11
  if (!isOpenTargetId(body.target_id)) {
12
12
  sendJson(response, 400, { error: 'Unknown open target', target_id: body.target_id });
@@ -0,0 +1,2 @@
1
+ import type { RouteDefinition } from './route-types.js';
2
+ export declare const remoteRoutes: RouteDefinition[];
@@ -0,0 +1,166 @@
1
+ import { ForbiddenError } from './http-errors.js';
2
+ import { REMOTE_DAEMON_TOKEN_KEY, REMOTE_ENABLED_KEY, REMOTE_GATEWAY_URL_KEY, } from './remote-config-keys.js';
3
+ import { getRequiredParam, readJsonBody, route, sendJson } from './route-helpers.js';
4
+ import { requireUiTokenFromRequest } from './ui-auth-helpers.js';
5
+ // Remote-access device-management + pairing routes (M4). Two gate classes:
6
+ // - gateUi: the standard local-OR-tunnel gate every equal-authority route uses. A paired phone may
7
+ // reach these (list devices, revoke, status, audit) exactly like the desktop.
8
+ // - gateLocalDesktopOnly: the TRUST ROOT (Authority Model). Pairing begin/confirm/reject can ONLY be
9
+ // driven from the local desktop cookie path; a tunnel-tagged (phone) request is refused with 403.
10
+ // This is a pairing-ceremony invariant, NOT a feature permission — a phone can never self-approve
11
+ // a new device. Defense in depth: these three paths are ALSO hard-denied on the bridge
12
+ // (remote-bridge-routing DENIED pairing matcher), so even a bypassed gate gets Reset there.
13
+ // Standard local-OR-tunnel gate. A request with no secret header falls to the cookie path (a browser);
14
+ // a tunnel-stamped request short-circuits as authorized. Used by the equal-authority endpoints.
15
+ const gateUi = ({ request, store }) => {
16
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
17
+ };
18
+ // TRUST-ROOT gate: local desktop ONLY. A tunnel-tagged request (authorizeRemoteTunnelRequest === true)
19
+ // is a phone; refuse it. We audit the forbidden attempt with the concrete reason BEFORE throwing
20
+ // (HARDEN D0.3): the audit spec lists "被拒请求及原因" as a required audited action, and the route is
21
+ // the only layer that can attribute a 403 here to 'pairing_confirm_forbidden' (the bridge's onEnd
22
+ // audits the loopback request status-agnostically). Then we pass NO tunnel authorizer to the token
23
+ // check, so even the short-circuit can't admit a tunnel request that slipped a cookie too.
24
+ const gateLocalDesktopOnly = ({ request, store }) => {
25
+ if (store.authorizeRemoteTunnelRequest(request)) {
26
+ store.getRemoteAuditStore().enqueue({
27
+ action: 'reject',
28
+ result: 'rejected',
29
+ rejectReason: 'pairing_confirm_forbidden',
30
+ });
31
+ throw new ForbiddenError('device approval is desktop-only');
32
+ }
33
+ requireUiTokenFromRequest(request, store.validateUiToken);
34
+ };
35
+ const isLoggedIn = (store) => (store.settings.getAppState(REMOTE_GATEWAY_URL_KEY)?.value ?? null) !== null &&
36
+ (store.settings.getAppState(REMOTE_DAEMON_TOKEN_KEY)?.value ?? null) !== null;
37
+ const remoteStatus = (store) => {
38
+ const status = store.getRemoteTunnelStatus();
39
+ return {
40
+ enabled: store.settings.getAppState(REMOTE_ENABLED_KEY)?.value === 'true',
41
+ loggedIn: isLoggedIn(store),
42
+ gatewayUrl: store.settings.getAppState(REMOTE_GATEWAY_URL_KEY)?.value ?? null,
43
+ connected: status === 'online',
44
+ // The FULL tunnel state, not just online/offline — the desktop dot was stuck looking "connected or
45
+ // not" and couldn't show connecting / reconnecting / revoked / logged-out. (connecting+reconnecting
46
+ // are the states a flaky link spends real time in; surfacing them is what makes the dot honest.)
47
+ connection: status,
48
+ };
49
+ };
50
+ // RemoteDeviceRecord is already camelCase + metadata-only (no key material). Identity-mapped so the
51
+ // response shape stays pinned to the store's invariant-7 projection.
52
+ const toDeviceView = (record) => ({
53
+ id: record.id,
54
+ name: record.name,
55
+ createdAt: record.createdAt,
56
+ lastActive: record.lastActive,
57
+ revokedAt: record.revokedAt,
58
+ });
59
+ export const remoteRoutes = [
60
+ route('GET', '/api/remote/status', (ctx) => {
61
+ gateUi(ctx);
62
+ sendJson(ctx.response, 200, remoteStatus(ctx.store));
63
+ }),
64
+ route('PUT', '/api/remote/enabled', async (ctx) => {
65
+ const { request, response, store } = ctx;
66
+ const body = await readJsonBody(request);
67
+ const enabled = body.enabled === true;
68
+ // D0.4 — conditional gate: a remote MAY turn the tunnel OFF (self-disconnect) but NEVER ON. So a
69
+ // tunnel-tagged enable:true is the only enabled-route action that is desktop-only.
70
+ if (enabled && store.authorizeRemoteTunnelRequest(request)) {
71
+ throw new ForbiddenError('remote cannot self-enable');
72
+ }
73
+ gateUi(ctx);
74
+ store.setRemoteEnabled(enabled);
75
+ sendJson(response, 200, remoteStatus(store));
76
+ }),
77
+ // ── trust-root pairing set (desktop-only) ──────────────────────────────────────────────────────
78
+ route('POST', '/api/remote/pairings', (ctx) => {
79
+ gateLocalDesktopOnly(ctx);
80
+ if (!isLoggedIn(ctx.store)) {
81
+ sendJson(ctx.response, 503, { error: 'remote not logged in' });
82
+ return;
83
+ }
84
+ try {
85
+ const ticket = ctx.store.getRemotePairing().beginPairing();
86
+ sendJson(ctx.response, 200, ticket);
87
+ }
88
+ catch {
89
+ // beginPairing throws if gateway/daemon id vanished between the status read and here.
90
+ sendJson(ctx.response, 503, { error: 'remote not logged in' });
91
+ }
92
+ }),
93
+ route('GET', '/api/remote/pairings/pending', (ctx) => {
94
+ // HARDEN minor: desktop-only. The confirm dialog backs this; a phone has no use for another
95
+ // device's in-flight SAS, and exposing it would needlessly widen the secret surface (invariant 7).
96
+ gateLocalDesktopOnly(ctx);
97
+ sendJson(ctx.response, 200, ctx.store.getRemotePairing().listPending());
98
+ }),
99
+ route('POST', '/api/remote/pairings/:pairingId/confirm', async (ctx) => {
100
+ const { params, request, response, store } = ctx;
101
+ const pairingId = getRequiredParam(response, params, 'pairingId', 'Pairing id is required');
102
+ if (!pairingId)
103
+ return;
104
+ gateLocalDesktopOnly(ctx);
105
+ const body = await readJsonBody(request).catch(() => ({}));
106
+ // THE trust-root action (D3): confirmRemotePairing drives the engine's local insert (the only
107
+ // caller of deviceStore.insert) AND the gateway device-row registration + the `confirmed` signal
108
+ // to the phone, in that order. A gateway-POST failure (or a missing boundJti) rejects here: the
109
+ // local row exists but the phone is NOT told OK, so it can re-scan. Surface 502 so the desktop
110
+ // operator sees the pairing didn't complete end-to-end rather than a false success.
111
+ let record;
112
+ try {
113
+ record = await store.confirmRemotePairing(pairingId, body.name === undefined ? undefined : body.name);
114
+ }
115
+ catch {
116
+ sendJson(response, 502, { error: 'gateway registration failed; rescan to retry' });
117
+ return;
118
+ }
119
+ if (!record) {
120
+ // Unknown / expired / wrong-state pairing. Nothing persisted.
121
+ sendJson(response, 404, { error: 'pairing not found or no longer confirmable' });
122
+ return;
123
+ }
124
+ // The newly-persisted device is live in the provider now. Reconcile the tunnel so a freshly
125
+ // enabled remote starts connecting (no-op if already online / disabled).
126
+ store.setRemoteEnabled(store.settings.getAppState(REMOTE_ENABLED_KEY)?.value === 'true');
127
+ sendJson(response, 200, { device: toDeviceView(record) });
128
+ }),
129
+ route('POST', '/api/remote/pairings/:pairingId/reject', (ctx) => {
130
+ const { params, response, store } = ctx;
131
+ const pairingId = getRequiredParam(response, params, 'pairingId', 'Pairing id is required');
132
+ if (!pairingId)
133
+ return;
134
+ gateLocalDesktopOnly(ctx);
135
+ store.getRemotePairing().rejectPairing(pairingId, 'user_rejected');
136
+ response.statusCode = 204;
137
+ response.end();
138
+ }),
139
+ // ── equal-authority device management ──────────────────────────────────────────────────────────
140
+ route('GET', '/api/remote/devices', (ctx) => {
141
+ gateUi(ctx);
142
+ const includeRevoked = new URL(ctx.request.url ?? '', 'http://x').searchParams.has('includeRevoked');
143
+ sendJson(ctx.response, 200, ctx.store.getRemoteDeviceStore().list(includeRevoked).map(toDeviceView));
144
+ }),
145
+ route('POST', '/api/remote/devices/:deviceId/revoke', (ctx) => {
146
+ const { params, response, store } = ctx;
147
+ const deviceId = getRequiredParam(response, params, 'deviceId', 'Device id is required');
148
+ if (!deviceId)
149
+ return;
150
+ gateUi(ctx);
151
+ // Revocation closed loop (§6 orchestrator in the runtime store): provider drop + tunnel close +
152
+ // audit. Equal-authority: a phone may revoke any device, including itself.
153
+ store.revokeRemoteDevice(deviceId);
154
+ response.statusCode = 204;
155
+ response.end();
156
+ }),
157
+ route('GET', '/api/remote/audit', (ctx) => {
158
+ gateUi(ctx);
159
+ const url = new URL(ctx.request.url ?? '', 'http://x');
160
+ const rawLimit = Number(url.searchParams.get('limit'));
161
+ const limit = Number.isFinite(rawLimit) && rawLimit > 0 ? Math.min(rawLimit, 500) : 100;
162
+ const deviceId = url.searchParams.get('deviceId');
163
+ const audit = ctx.store.getRemoteAuditStore();
164
+ sendJson(ctx.response, 200, deviceId ? audit.listForDevice(deviceId, limit) : audit.list(limit));
165
+ }),
166
+ ];
@@ -8,7 +8,7 @@ export const runtimeRoutes = [
8
8
  if (!workspaceId) {
9
9
  return;
10
10
  }
11
- requireUiTokenFromRequest(request, store.validateUiToken);
11
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
12
12
  sendJson(response, 200, store.listTerminalRuns(workspaceId));
13
13
  }),
14
14
  route('POST', '/api/workspaces/:workspaceId/shell/start', async ({ params, request, response, store }) => {
@@ -16,7 +16,7 @@ export const runtimeRoutes = [
16
16
  if (!workspaceId) {
17
17
  return;
18
18
  }
19
- requireUiTokenFromRequest(request, store.validateUiToken);
19
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
20
20
  const run = await store.startWorkspaceShell(workspaceId);
21
21
  const summary = store
22
22
  .listTerminalRuns(workspaceId)
@@ -35,7 +35,7 @@ export const runtimeRoutes = [
35
35
  if (!workspaceId || !runId) {
36
36
  return;
37
37
  }
38
- requireUiTokenFromRequest(request, store.validateUiToken);
38
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
39
39
  if (!store.closeWorkspaceShell(workspaceId, runId)) {
40
40
  sendJson(response, 404, { error: 'Shell run not found' });
41
41
  return;
@@ -49,7 +49,7 @@ export const runtimeRoutes = [
49
49
  if (!workspaceId || !agentId) {
50
50
  return;
51
51
  }
52
- requireUiTokenFromRequest(request, store.validateUiToken);
52
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
53
53
  const body = await readJsonBody(request);
54
54
  store.configureAgentLaunch(workspaceId, agentId, {
55
55
  command: body.command,
@@ -64,7 +64,7 @@ export const runtimeRoutes = [
64
64
  if (!runId) {
65
65
  return;
66
66
  }
67
- requireUiTokenFromRequest(request, store.validateUiToken);
67
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
68
68
  store.stopAgentRun(runId);
69
69
  sendJson(response, 202, { ok: true });
70
70
  }),
@@ -73,7 +73,7 @@ export const runtimeRoutes = [
73
73
  if (!runId) {
74
74
  return;
75
75
  }
76
- requireUiTokenFromRequest(request, store.validateUiToken);
76
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
77
77
  sendJson(response, 200, serializeLiveAgentRun(store.getLiveRun(runId)));
78
78
  }),
79
79
  ];
@@ -85,15 +85,15 @@ const readRoleTemplateBody = async (request) => {
85
85
  };
86
86
  export const settingsRoutes = [
87
87
  route('GET', '/api/settings/command-presets', ({ request, response, store }) => {
88
- requireUiTokenFromRequest(request, store.validateUiToken);
88
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
89
89
  sendJson(response, 200, store.settings.listCommandPresets().map(serializeCommandPreset));
90
90
  }),
91
91
  route('POST', '/api/settings/command-presets', async ({ request, response, store }) => {
92
- requireUiTokenFromRequest(request, store.validateUiToken);
92
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
93
93
  sendJson(response, 201, serializeCommandPreset(store.settings.createCommandPreset(await readCommandPresetBody(request))));
94
94
  }),
95
95
  route('PATCH', '/api/settings/command-presets/:presetId', async ({ params, request, response, store }) => {
96
- requireUiTokenFromRequest(request, store.validateUiToken);
96
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
97
97
  const presetId = getRequiredParam(response, params, 'presetId', 'Preset id is required');
98
98
  if (!presetId)
99
99
  return;
@@ -104,7 +104,7 @@ export const settingsRoutes = [
104
104
  sendJson(response, 200, serializeCommandPreset(store.settings.updateCommandPreset(presetId, next)));
105
105
  }),
106
106
  route('DELETE', '/api/settings/command-presets/:presetId', ({ params, request, response, store }) => {
107
- requireUiTokenFromRequest(request, store.validateUiToken);
107
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
108
108
  const presetId = getRequiredParam(response, params, 'presetId', 'Preset id is required');
109
109
  if (!presetId)
110
110
  return;
@@ -113,15 +113,15 @@ export const settingsRoutes = [
113
113
  response.end();
114
114
  }),
115
115
  route('GET', '/api/settings/role-templates', ({ request, response, store }) => {
116
- requireUiTokenFromRequest(request, store.validateUiToken);
116
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
117
117
  sendJson(response, 200, store.settings.listRoleTemplates().map(serializeRoleTemplate));
118
118
  }),
119
119
  route('POST', '/api/settings/role-templates', async ({ request, response, store }) => {
120
- requireUiTokenFromRequest(request, store.validateUiToken);
120
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
121
121
  sendJson(response, 201, serializeRoleTemplate(store.settings.createRoleTemplate(await readRoleTemplateBody(request))));
122
122
  }),
123
123
  route('PATCH', '/api/settings/role-templates/:templateId', async ({ params, request, response, store }) => {
124
- requireUiTokenFromRequest(request, store.validateUiToken);
124
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
125
125
  const templateId = getRequiredParam(response, params, 'templateId', 'Template id is required');
126
126
  if (!templateId)
127
127
  return;
@@ -134,7 +134,7 @@ export const settingsRoutes = [
134
134
  sendJson(response, 200, serializeRoleTemplate(store.settings.updateRoleTemplate(templateId, next)));
135
135
  }),
136
136
  route('DELETE', '/api/settings/role-templates/:templateId', ({ params, request, response, store }) => {
137
- requireUiTokenFromRequest(request, store.validateUiToken);
137
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
138
138
  const templateId = getRequiredParam(response, params, 'templateId', 'Template id is required');
139
139
  if (!templateId)
140
140
  return;
@@ -143,14 +143,14 @@ export const settingsRoutes = [
143
143
  response.end();
144
144
  }),
145
145
  route('GET', '/api/settings/app-state/:key', ({ params, request, response, store }) => {
146
- requireUiTokenFromRequest(request, store.validateUiToken);
146
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
147
147
  const key = getRequiredParam(response, params, 'key', 'App state key is required');
148
148
  if (!key)
149
149
  return;
150
150
  sendJson(response, 200, store.settings.getAppState(key) ?? { key, value: null });
151
151
  }),
152
152
  route('PUT', '/api/settings/app-state/:key', async ({ params, request, response, store }) => {
153
- requireUiTokenFromRequest(request, store.validateUiToken);
153
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
154
154
  const key = getRequiredParam(response, params, 'key', 'App state key is required');
155
155
  if (!key)
156
156
  return;
@@ -160,12 +160,12 @@ export const settingsRoutes = [
160
160
  response.end();
161
161
  }),
162
162
  route('GET', '/api/settings/workflow-cli-policy', ({ request, response, store }) => {
163
- requireUiTokenFromRequest(request, store.validateUiToken);
163
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
164
164
  const policy = readWorkflowCliPolicy(store.settings.getAppState(WORKFLOW_CLI_POLICY_KEY)?.value ?? null);
165
165
  sendJson(response, 200, { ...policy, supported: [...CANONICAL_WORKFLOW_CLIS] });
166
166
  }),
167
167
  route('PUT', '/api/settings/workflow-cli-policy', async ({ request, response, store }) => {
168
- requireUiTokenFromRequest(request, store.validateUiToken);
168
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
169
169
  const body = await readJsonBody(request);
170
170
  // Strict validation: a bad payload is rejected (400) rather than persisted.
171
171
  const clean = (() => {
@@ -181,12 +181,12 @@ export const settingsRoutes = [
181
181
  sendJson(response, 200, { ...clean, supported: [...CANONICAL_WORKFLOW_CLIS] });
182
182
  }),
183
183
  route('GET', '/api/settings/workflow-feature', ({ request, response, store }) => {
184
- requireUiTokenFromRequest(request, store.validateUiToken);
184
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
185
185
  const enabled = readWorkflowEnabled(store.settings.getAppState(WORKFLOW_ENABLED_KEY)?.value ?? null);
186
186
  sendJson(response, 200, { enabled });
187
187
  }),
188
188
  route('PUT', '/api/settings/workflow-feature', async ({ request, response, store }) => {
189
- requireUiTokenFromRequest(request, store.validateUiToken);
189
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
190
190
  const body = await readJsonBody(request);
191
191
  if (typeof body.enabled !== 'boolean') {
192
192
  throw new BadRequestError('workflow-feature requires { enabled: boolean }');
@@ -196,12 +196,12 @@ export const settingsRoutes = [
196
196
  sendJson(response, 200, { enabled: body.enabled });
197
197
  }),
198
198
  route('GET', '/api/settings/team-autostaff', ({ request, response, store }) => {
199
- requireUiTokenFromRequest(request, store.validateUiToken);
199
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
200
200
  const enabled = readAutostaffEnabled(store.settings.getAppState(AUTOSTAFF_ENABLED_KEY)?.value ?? null);
201
201
  sendJson(response, 200, { enabled });
202
202
  }),
203
203
  route('PUT', '/api/settings/team-autostaff', async ({ request, response, store }) => {
204
- requireUiTokenFromRequest(request, store.validateUiToken);
204
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
205
205
  const body = await readJsonBody(request);
206
206
  if (typeof body.enabled !== 'boolean') {
207
207
  throw new BadRequestError('team-autostaff requires { enabled: boolean }');
@@ -29,7 +29,7 @@ export const taskRoutes = [
29
29
  if (!workspaceId) {
30
30
  return;
31
31
  }
32
- requireUiTokenFromRequest(request, store.validateUiToken);
32
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
33
33
  const workspace = store.getWorkspaceSnapshot(workspaceId);
34
34
  sendJson(response, 200, { content: tasksFileService.readTasks(workspace.summary.path) });
35
35
  }),
@@ -38,7 +38,7 @@ export const taskRoutes = [
38
38
  if (!workspaceId) {
39
39
  return;
40
40
  }
41
- requireUiTokenFromRequest(request, store.validateUiToken);
41
+ requireUiTokenFromRequest(request, store.validateUiToken, store.authorizeRemoteTunnelRequest);
42
42
  const body = await readJsonBody(request);
43
43
  const workspace = store.getWorkspaceSnapshot(workspaceId);
44
44
  tasksFileService.writeTasks(workspace.summary.path, body.content);
@@ -0,0 +1,2 @@
1
+ import type { RouteDefinition } from './route-types.js';
2
+ export declare const teamMemoryRoutes: RouteDefinition[];
@@ -0,0 +1,154 @@
1
+ import { isMemoryKind, MEMORY_BODY_MAX_CHARS, MEMORY_KINDS, MEMORY_QUERY_MAX_CHARS, MEMORY_SEARCH_MAX_LIMIT, MEMORY_TAG_MAX_CHARS, MEMORY_TAG_MAX_COUNT, } from '../shared/team-memory.js';
2
+ import { BadRequestError } from './http-errors.js';
3
+ import { readJsonBody, route, sendJson } from './route-helpers.js';
4
+ import { authenticateCliAgent, requireCommandForRole } from './team-authz.js';
5
+ import { serializeMemoryEntry, serializeMemorySearchResult, } from './team-memory-http-serializers.js';
6
+ import { MemoryEntryStatusError } from './team-memory-store.js';
7
+ const requireNonEmptyString = (value, field) => {
8
+ if (typeof value !== 'string' || value.trim().length === 0) {
9
+ throw new BadRequestError(`Missing ${field}`);
10
+ }
11
+ return value.trim();
12
+ };
13
+ const requireMemoryBody = (value) => {
14
+ const body = requireNonEmptyString(value, 'body');
15
+ if ([...body].length > MEMORY_BODY_MAX_CHARS) {
16
+ throw new BadRequestError(`body must be ${MEMORY_BODY_MAX_CHARS} characters or fewer`);
17
+ }
18
+ return body;
19
+ };
20
+ const requireMemoryQuery = (value) => {
21
+ const query = requireNonEmptyString(value, 'query');
22
+ if ([...query].length > MEMORY_QUERY_MAX_CHARS) {
23
+ throw new BadRequestError(`query must be ${MEMORY_QUERY_MAX_CHARS} characters or fewer`);
24
+ }
25
+ return query;
26
+ };
27
+ const parseLimit = (value) => {
28
+ if (value === undefined)
29
+ return undefined;
30
+ if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) {
31
+ throw new BadRequestError('limit must be a non-negative integer');
32
+ }
33
+ return Math.min(value, MEMORY_SEARCH_MAX_LIMIT);
34
+ };
35
+ const parseKind = (value) => {
36
+ if (value === undefined)
37
+ return 'fact';
38
+ if (!isMemoryKind(value)) {
39
+ throw new BadRequestError(`kind must be one of: ${MEMORY_KINDS.join(', ')}`);
40
+ }
41
+ return value;
42
+ };
43
+ const parseTags = (value) => {
44
+ if (value === undefined)
45
+ return [];
46
+ if (!Array.isArray(value)) {
47
+ throw new BadRequestError('tags must be an array of strings');
48
+ }
49
+ if (value.length > MEMORY_TAG_MAX_COUNT) {
50
+ throw new BadRequestError(`tags must contain ${MEMORY_TAG_MAX_COUNT} items or fewer`);
51
+ }
52
+ const tags = [];
53
+ for (const item of value) {
54
+ if (typeof item !== 'string' || item.trim().length === 0) {
55
+ throw new BadRequestError('tags must be non-empty strings');
56
+ }
57
+ const tag = item.trim();
58
+ if ([...tag].length > MEMORY_TAG_MAX_CHARS) {
59
+ throw new BadRequestError(`tags must be ${MEMORY_TAG_MAX_CHARS} characters or fewer`);
60
+ }
61
+ if (!tags.includes(tag))
62
+ tags.push(tag);
63
+ }
64
+ return tags;
65
+ };
66
+ const authenticateMemoryRequest = (body, store) => {
67
+ const workspaceId = requireNonEmptyString(body.project_id, 'project_id');
68
+ const fromAgentId = requireNonEmptyString(body.from_agent_id, 'from_agent_id');
69
+ const token = typeof body.token === 'string' ? body.token : undefined;
70
+ const agent = authenticateCliAgent({
71
+ fromAgentId,
72
+ getAgent: store.getAgent,
73
+ token,
74
+ validateToken: store.validateAgentToken,
75
+ workspaceId,
76
+ });
77
+ return { agent, workspaceId };
78
+ };
79
+ export const teamMemoryRoutes = [
80
+ route('POST', '/api/team/memory/add', async ({ request, response, store }) => {
81
+ const body = await readJsonBody(request);
82
+ const { agent, workspaceId } = authenticateMemoryRequest(body, store);
83
+ requireCommandForRole(agent, 'memory_add');
84
+ const memory = store.addMemoryEntry({
85
+ actor: {
86
+ id: agent.id,
87
+ name: agent.name,
88
+ role: agent.role,
89
+ },
90
+ body: requireMemoryBody(body.body),
91
+ kind: parseKind(body.kind),
92
+ tags: parseTags(body.tags),
93
+ workspaceId,
94
+ });
95
+ sendJson(response, 200, {
96
+ memory: serializeMemoryEntry(memory),
97
+ ok: true,
98
+ });
99
+ }),
100
+ route('POST', '/api/team/memory/show', async ({ request, response, store }) => {
101
+ const body = await readJsonBody(request);
102
+ const { agent, workspaceId } = authenticateMemoryRequest(body, store);
103
+ requireCommandForRole(agent, 'memory_show');
104
+ const memoryId = requireNonEmptyString(body.memory_id, 'memory_id');
105
+ const memory = store.getMemoryEntry(workspaceId, memoryId);
106
+ if (!memory) {
107
+ sendJson(response, 404, { error: `Memory entry not found: ${memoryId}` });
108
+ return;
109
+ }
110
+ sendJson(response, 200, {
111
+ memory: serializeMemoryEntry(memory),
112
+ ok: true,
113
+ });
114
+ }),
115
+ route('POST', '/api/team/memory/search', async ({ request, response, store }) => {
116
+ const body = await readJsonBody(request);
117
+ const { agent, workspaceId } = authenticateMemoryRequest(body, store);
118
+ requireCommandForRole(agent, 'memory_search');
119
+ const limit = parseLimit(body.limit);
120
+ const results = store.searchMemoryEntries(workspaceId, requireMemoryQuery(body.query), {
121
+ ...(limit !== undefined ? { limit } : {}),
122
+ });
123
+ sendJson(response, 200, {
124
+ ok: true,
125
+ results: results.map(serializeMemorySearchResult),
126
+ });
127
+ }),
128
+ route('POST', '/api/team/memory/forget', async ({ request, response, store }) => {
129
+ const body = await readJsonBody(request);
130
+ const { agent, workspaceId } = authenticateMemoryRequest(body, store);
131
+ requireCommandForRole(agent, 'memory_forget');
132
+ const memoryId = requireNonEmptyString(body.memory_id, 'memory_id');
133
+ const memory = store.getMemoryEntry(workspaceId, memoryId);
134
+ if (!memory) {
135
+ sendJson(response, 404, { error: `Memory entry not found: ${memoryId}` });
136
+ return;
137
+ }
138
+ try {
139
+ sendJson(response, 200, {
140
+ memory: serializeMemoryEntry(store.archiveMemoryEntry(workspaceId, memoryId)),
141
+ ok: true,
142
+ });
143
+ }
144
+ catch (error) {
145
+ if (error instanceof MemoryEntryStatusError) {
146
+ sendJson(response, 409, {
147
+ error: `Memory entry has status ${error.actualStatus}; expected active`,
148
+ });
149
+ return;
150
+ }
151
+ throw error;
152
+ }
153
+ }),
154
+ ];
@@ -0,0 +1,2 @@
1
+ import type { RouteDefinition } from './route-types.js';
2
+ export declare const teamRecallRoutes: RouteDefinition[];