@tt-a1i/hive 1.4.4 → 1.6.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 (180) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/README.en.md +21 -0
  3. package/README.md +16 -0
  4. package/assets/qq-group.jpg +0 -0
  5. package/dist/bin/team.cmd +1 -0
  6. package/dist/src/cli/hive-update.d.ts +45 -17
  7. package/dist/src/cli/hive-update.js +63 -25
  8. package/dist/src/cli/hive.d.ts +25 -0
  9. package/dist/src/cli/hive.js +41 -3
  10. package/dist/src/cli/team.d.ts +1 -0
  11. package/dist/src/cli/team.js +216 -3
  12. package/dist/src/server/agent-command-resolver.js +3 -19
  13. package/dist/src/server/agent-manager-support.d.ts +2 -2
  14. package/dist/src/server/agent-manager-support.js +98 -24
  15. package/dist/src/server/agent-run-starter.d.ts +6 -1
  16. package/dist/src/server/agent-run-starter.js +9 -2
  17. package/dist/src/server/agent-run-store.d.ts +1 -1
  18. package/dist/src/server/agent-runtime-close.d.ts +1 -0
  19. package/dist/src/server/agent-runtime-close.js +25 -1
  20. package/dist/src/server/agent-runtime-contract.d.ts +12 -1
  21. package/dist/src/server/agent-runtime-stop-run.d.ts +1 -1
  22. package/dist/src/server/agent-runtime-stop-run.js +4 -1
  23. package/dist/src/server/agent-runtime.d.ts +2 -1
  24. package/dist/src/server/agent-runtime.js +14 -3
  25. package/dist/src/server/agent-startup-instructions.d.ts +7 -1
  26. package/dist/src/server/agent-startup-instructions.js +17 -9
  27. package/dist/src/server/agent-stdin-dispatcher.d.ts +25 -5
  28. package/dist/src/server/agent-stdin-dispatcher.js +141 -40
  29. package/dist/src/server/cron-util.d.ts +7 -0
  30. package/dist/src/server/cron-util.js +19 -0
  31. package/dist/src/server/dispatch-ledger-store.d.ts +22 -0
  32. package/dist/src/server/dispatch-ledger-store.js +51 -3
  33. package/dist/src/server/env-sync-message.js +9 -9
  34. package/dist/src/server/feature-flags.d.ts +42 -0
  35. package/dist/src/server/feature-flags.js +24 -0
  36. package/dist/src/server/fs-pick-folder.js +4 -0
  37. package/dist/src/server/fs-sandbox.js +36 -7
  38. package/dist/src/server/hive-team-guidance.d.ts +12 -6
  39. package/dist/src/server/hive-team-guidance.js +253 -71
  40. package/dist/src/server/live-run-registry.d.ts +1 -0
  41. package/dist/src/server/live-run-registry.js +1 -1
  42. package/dist/src/server/open-target-commands.js +5 -6
  43. package/dist/src/server/orchestrator-autostart.d.ts +12 -0
  44. package/dist/src/server/orchestrator-autostart.js +15 -13
  45. package/dist/src/server/path-canonicalization.d.ts +3 -0
  46. package/dist/src/server/path-canonicalization.js +29 -0
  47. package/dist/src/server/platform-path.d.ts +3 -0
  48. package/dist/src/server/platform-path.js +13 -0
  49. package/dist/src/server/post-start-input-writer.d.ts +1 -1
  50. package/dist/src/server/post-start-input-writer.js +110 -13
  51. package/dist/src/server/preset-launch-support.d.ts +1 -1
  52. package/dist/src/server/preset-launch-support.js +33 -2
  53. package/dist/src/server/recovery-summary.d.ts +5 -1
  54. package/dist/src/server/recovery-summary.js +18 -17
  55. package/dist/src/server/report-outbox-store.d.ts +36 -0
  56. package/dist/src/server/report-outbox-store.js +33 -0
  57. package/dist/src/server/restart-policy-support.d.ts +5 -1
  58. package/dist/src/server/restart-policy-support.js +9 -1
  59. package/dist/src/server/restart-policy.d.ts +6 -2
  60. package/dist/src/server/restart-policy.js +51 -31
  61. package/dist/src/server/role-template-store.d.ts +1 -0
  62. package/dist/src/server/role-template-store.js +11 -1
  63. package/dist/src/server/route-types.d.ts +43 -0
  64. package/dist/src/server/routes-runtime.js +2 -1
  65. package/dist/src/server/routes-settings.js +76 -0
  66. package/dist/src/server/routes-tasks.js +23 -0
  67. package/dist/src/server/routes-team.js +211 -1
  68. package/dist/src/server/routes-workflow-schedules.d.ts +2 -0
  69. package/dist/src/server/routes-workflow-schedules.js +58 -0
  70. package/dist/src/server/routes-workflows.d.ts +2 -0
  71. package/dist/src/server/routes-workflows.js +83 -0
  72. package/dist/src/server/routes-workspaces.js +5 -0
  73. package/dist/src/server/routes.js +4 -0
  74. package/dist/src/server/runtime-restart-policy.d.ts +3 -1
  75. package/dist/src/server/runtime-restart-policy.js +2 -1
  76. package/dist/src/server/runtime-store-contract.d.ts +125 -0
  77. package/dist/src/server/runtime-store-contract.js +1 -0
  78. package/dist/src/server/runtime-store-helpers.d.ts +11 -0
  79. package/dist/src/server/runtime-store-helpers.js +106 -2
  80. package/dist/src/server/runtime-store-workflows.d.ts +6 -0
  81. package/dist/src/server/runtime-store-workflows.js +108 -0
  82. package/dist/src/server/runtime-store.d.ts +3 -72
  83. package/dist/src/server/runtime-store.js +71 -4
  84. package/dist/src/server/session-capture-codex.d.ts +3 -3
  85. package/dist/src/server/session-capture-codex.js +9 -7
  86. package/dist/src/server/session-capture-gemini.d.ts +1 -1
  87. package/dist/src/server/session-capture-gemini.js +6 -3
  88. package/dist/src/server/settings-store.d.ts +3 -0
  89. package/dist/src/server/settings-store.js +1 -0
  90. package/dist/src/server/sqlite-schema-v19.d.ts +2 -0
  91. package/dist/src/server/sqlite-schema-v19.js +17 -0
  92. package/dist/src/server/sqlite-schema-v20.d.ts +2 -0
  93. package/dist/src/server/sqlite-schema-v20.js +20 -0
  94. package/dist/src/server/sqlite-schema-v21.d.ts +2 -0
  95. package/dist/src/server/sqlite-schema-v21.js +20 -0
  96. package/dist/src/server/sqlite-schema.d.ts +1 -1
  97. package/dist/src/server/sqlite-schema.js +110 -1
  98. package/dist/src/server/system-message.d.ts +7 -0
  99. package/dist/src/server/system-message.js +8 -1
  100. package/dist/src/server/task-deps.d.ts +32 -0
  101. package/dist/src/server/task-deps.js +40 -0
  102. package/dist/src/server/tasks-file-watcher.d.ts +12 -1
  103. package/dist/src/server/tasks-file-watcher.js +128 -23
  104. package/dist/src/server/tasks-file.d.ts +3 -1
  105. package/dist/src/server/tasks-file.js +33 -9
  106. package/dist/src/server/tasks-websocket-server.js +13 -14
  107. package/dist/src/server/team-authz.d.ts +1 -1
  108. package/dist/src/server/team-authz.js +10 -1
  109. package/dist/src/server/team-autostaff.d.ts +16 -0
  110. package/dist/src/server/team-autostaff.js +16 -0
  111. package/dist/src/server/team-list-serializer.d.ts +1 -1
  112. package/dist/src/server/team-list-serializer.js +3 -1
  113. package/dist/src/server/team-operations.d.ts +21 -1
  114. package/dist/src/server/team-operations.js +183 -16
  115. package/dist/src/server/terminal-protocol.js +9 -3
  116. package/dist/src/server/terminal-stream-hub.js +16 -10
  117. package/dist/src/server/terminal-ws-server.js +10 -8
  118. package/dist/src/server/webhook-notifier.d.ts +34 -0
  119. package/dist/src/server/webhook-notifier.js +47 -0
  120. package/dist/src/server/websocket-upgrade-safety.d.ts +10 -0
  121. package/dist/src/server/websocket-upgrade-safety.js +35 -0
  122. package/dist/src/server/windows-command-line.d.ts +3 -0
  123. package/dist/src/server/windows-command-line.js +9 -0
  124. package/dist/src/server/windows-filename.d.ts +2 -0
  125. package/dist/src/server/windows-filename.js +33 -0
  126. package/dist/src/server/workflow-cli-policy.d.ts +60 -0
  127. package/dist/src/server/workflow-cli-policy.js +110 -0
  128. package/dist/src/server/workflow-dispatch-awaiter.d.ts +12 -0
  129. package/dist/src/server/workflow-dispatch-awaiter.js +80 -0
  130. package/dist/src/server/workflow-feature.d.ts +15 -0
  131. package/dist/src/server/workflow-feature.js +15 -0
  132. package/dist/src/server/workflow-http-serializers.d.ts +64 -0
  133. package/dist/src/server/workflow-http-serializers.js +58 -0
  134. package/dist/src/server/workflow-output-schema.d.ts +18 -0
  135. package/dist/src/server/workflow-output-schema.js +41 -0
  136. package/dist/src/server/workflow-run-log-store.d.ts +19 -0
  137. package/dist/src/server/workflow-run-log-store.js +45 -0
  138. package/dist/src/server/workflow-run-store.d.ts +50 -0
  139. package/dist/src/server/workflow-run-store.js +103 -0
  140. package/dist/src/server/workflow-runner.d.ts +147 -0
  141. package/dist/src/server/workflow-runner.js +411 -0
  142. package/dist/src/server/workflow-schedule-create.d.ts +14 -0
  143. package/dist/src/server/workflow-schedule-create.js +41 -0
  144. package/dist/src/server/workflow-schedule-store.d.ts +43 -0
  145. package/dist/src/server/workflow-schedule-store.js +112 -0
  146. package/dist/src/server/workflow-scheduler.d.ts +36 -0
  147. package/dist/src/server/workflow-scheduler.js +97 -0
  148. package/dist/src/server/workflow-script-loader.d.ts +34 -0
  149. package/dist/src/server/workflow-script-loader.js +106 -0
  150. package/dist/src/server/workspace-path-validation.js +16 -4
  151. package/dist/src/server/workspace-shell-runtime.d.ts +5 -0
  152. package/dist/src/server/workspace-shell-runtime.js +24 -2
  153. package/dist/src/server/workspace-store-contract.d.ts +4 -1
  154. package/dist/src/server/workspace-store-hydration.js +23 -7
  155. package/dist/src/server/workspace-store-mutations.js +2 -5
  156. package/dist/src/server/workspace-store-support.d.ts +4 -0
  157. package/dist/src/server/workspace-store-support.js +13 -1
  158. package/dist/src/server/workspace-store.js +38 -4
  159. package/dist/src/shared/types.d.ts +16 -1
  160. package/package.json +4 -2
  161. package/web/dist/assets/{AddWorkerDialog-DeZhTQLi.js → AddWorkerDialog-CGbaxu0T.js} +2 -2
  162. package/web/dist/assets/AddWorkspaceDialog-CNgExu6b.js +1 -0
  163. package/web/dist/assets/{FirstRunWizard-B5wLcat5.js → FirstRunWizard-DxGApUNc.js} +1 -1
  164. package/web/dist/assets/{MarketplaceDrawer-BC0eBOEW.js → MarketplaceDrawer-Bk6cpukn.js} +1 -1
  165. package/web/dist/assets/WhatsNewDialog-CSGzk-2U.js +1 -0
  166. package/web/dist/assets/WorkerModal-i2F3n3nZ.js +1 -0
  167. package/web/dist/assets/WorkspaceTaskDrawer-C_Ta_K13.js +1 -0
  168. package/web/dist/assets/WorkspaceTerminalPanels-VdDxtrQF.js +1 -0
  169. package/web/dist/assets/index-5zh61jMg.css +1 -0
  170. package/web/dist/assets/index-CAgGM6nb.js +75 -0
  171. package/web/dist/assets/path-join-7MR1s7b1.js +1 -0
  172. package/web/dist/index.html +2 -2
  173. package/web/dist/sw.js +1 -1
  174. package/web/dist/assets/AddWorkspaceDialog-DDpXNEKf.js +0 -1
  175. package/web/dist/assets/WorkerModal-BwMHq-Bi.js +0 -1
  176. package/web/dist/assets/WorkspaceTaskDrawer-CxvT4nqs.js +0 -1
  177. package/web/dist/assets/WorkspaceTerminalPanels-CvibsPSd.js +0 -1
  178. package/web/dist/assets/index-BEsTmfrO.css +0 -1
  179. package/web/dist/assets/index-Ddb7bDN5.js +0 -75
  180. package/web/dist/assets/path-join-S7qkXQtP.js +0 -1
@@ -1,6 +1,7 @@
1
1
  import { createTerminalOutputFlow } from './terminal-flow-control.js';
2
2
  import { parseTerminalControlMessage, serializeTerminalError, serializeTerminalExit, serializeTerminalRestore, } from './terminal-protocol.js';
3
3
  import { TerminalStateMirror } from './terminal-state-mirror.js';
4
+ import { attachWebSocketErrorHandler, sendWebSocketMessage } from './websocket-upgrade-safety.js';
4
5
  const normalizeTerminalInput = (raw, isBinary) => {
5
6
  const bytes = Buffer.isBuffer(raw)
6
7
  ? raw
@@ -85,8 +86,8 @@ export const createTerminalStreamHub = (store) => {
85
86
  const payload = serializeTerminalExit(run.exitCode);
86
87
  for (const viewer of state.viewers.values()) {
87
88
  const controlSocket = viewer.controlSocket;
88
- if (controlSocket && controlSocket.readyState === controlSocket.OPEN)
89
- controlSocket.send(payload);
89
+ if (controlSocket)
90
+ sendWebSocketMessage(controlSocket, payload, `terminal ${runId} exit`);
90
91
  }
91
92
  if (state.exitInterval)
92
93
  clearInterval(state.exitInterval);
@@ -103,18 +104,17 @@ export const createTerminalStreamHub = (store) => {
103
104
  return {
104
105
  attachControl(runId, clientId, socket, initialSize) {
105
106
  const state = getOrCreateState(runId, initialSize);
107
+ attachWebSocketErrorHandler(socket, `terminal ${runId} control`);
106
108
  const viewer = getOrCreateViewer(state, clientId);
107
109
  viewer.controlSocket = socket;
108
110
  startExitWatcher(runId, state);
109
111
  void state.mirror
110
112
  .getSnapshot()
111
113
  .then((snapshot) => {
112
- if (socket.readyState === socket.OPEN)
113
- socket.send(serializeTerminalRestore(snapshot));
114
+ sendWebSocketMessage(socket, serializeTerminalRestore(snapshot), `terminal ${runId} restore`);
114
115
  })
115
116
  .catch(() => {
116
- if (socket.readyState === socket.OPEN)
117
- socket.send(serializeTerminalRestore(''));
117
+ sendWebSocketMessage(socket, serializeTerminalRestore(''), `terminal ${runId} restore`);
118
118
  });
119
119
  socket.on('message', (raw) => {
120
120
  try {
@@ -131,7 +131,7 @@ export const createTerminalStreamHub = (store) => {
131
131
  return;
132
132
  }
133
133
  catch (error) {
134
- socket.send(serializeTerminalError(error instanceof Error ? error.message : 'Invalid control message'));
134
+ sendWebSocketMessage(socket, serializeTerminalError(error instanceof Error ? error.message : 'Invalid control message'), `terminal ${runId} control error`);
135
135
  }
136
136
  });
137
137
  socket.on('close', () => {
@@ -142,6 +142,7 @@ export const createTerminalStreamHub = (store) => {
142
142
  },
143
143
  attachIo(runId, clientId, socket, initialSize) {
144
144
  const state = getOrCreateState(runId, initialSize);
145
+ attachWebSocketErrorHandler(socket, `terminal ${runId} io`);
145
146
  const viewer = getOrCreateViewer(state, clientId);
146
147
  viewer.ioSocket = socket;
147
148
  viewer.flowState?.close();
@@ -158,7 +159,12 @@ export const createTerminalStreamHub = (store) => {
158
159
  },
159
160
  });
160
161
  socket.on('message', (raw, isBinary) => {
161
- store.writeRunInput(runId, normalizeTerminalInput(raw, isBinary));
162
+ try {
163
+ store.writeRunInput(runId, normalizeTerminalInput(raw, isBinary));
164
+ }
165
+ catch (error) {
166
+ sendWebSocketMessage(socket, serializeTerminalError(error instanceof Error ? error.message : 'Failed to write terminal input'), `terminal ${runId} input error`);
167
+ }
162
168
  });
163
169
  socket.on('close', () => {
164
170
  if (viewer.ioSocket === socket)
@@ -176,8 +182,8 @@ export const createTerminalStreamHub = (store) => {
176
182
  state.mirror.dispose();
177
183
  for (const viewer of state.viewers.values()) {
178
184
  viewer.flowState?.close();
179
- viewer.ioSocket?.close();
180
- viewer.controlSocket?.close();
185
+ viewer.ioSocket?.terminate();
186
+ viewer.controlSocket?.terminate();
181
187
  }
182
188
  runStates.delete(runId);
183
189
  }
@@ -3,6 +3,7 @@ import { getLocalRequestRejection } from './local-request-guard.js';
3
3
  import { createTasksWebSocketServer } from './tasks-websocket-server.js';
4
4
  import { createTerminalStreamHub } from './terminal-stream-hub.js';
5
5
  import { readCookie } from './ui-auth-helpers.js';
6
+ import { attachRawSocketErrorHandler, attachWebSocketServerErrorHandler, rejectWebSocketUpgrade, } from './websocket-upgrade-safety.js';
6
7
  const matchTerminalPath = (pathname) => {
7
8
  const match = /^\/ws\/terminal\/(?<runId>[^/]+)\/(?<channel>io|control)$/.exec(pathname);
8
9
  const groups = match?.groups;
@@ -24,13 +25,11 @@ const getInitialSize = (url) => {
24
25
  }
25
26
  return { cols, rows };
26
27
  };
27
- const rejectUpgrade = (socket, status) => {
28
- socket.write(`HTTP/1.1 ${status}\r\n\r\n`);
29
- socket.destroy();
30
- };
31
28
  export const createTerminalWebSocketServer = (server, store, tasksFileService) => {
32
29
  const ioWss = new WebSocketServer({ noServer: true });
33
30
  const controlWss = new WebSocketServer({ noServer: true });
31
+ attachWebSocketServerErrorHandler(ioWss, 'terminal io');
32
+ attachWebSocketServerErrorHandler(controlWss, 'terminal control');
34
33
  const tasksWss = createTasksWebSocketServer(server, store, tasksFileService);
35
34
  const hub = createTerminalStreamHub(store);
36
35
  const disposeTasksListener = store.registerTasksListener((workspaceId, content) => {
@@ -71,26 +70,29 @@ export const createTerminalWebSocketServer = (server, store, tasksFileService) =
71
70
  if (/^\/ws\/tasks\/.+/.test(pathname)) {
72
71
  return;
73
72
  }
74
- rejectUpgrade(socket, '404 Not Found');
73
+ attachRawSocketErrorHandler(socket, 'terminal upgrade');
74
+ rejectWebSocketUpgrade(socket, '404 Not Found');
75
75
  return;
76
76
  }
77
+ const detachRawSocketErrorHandler = attachRawSocketErrorHandler(socket, 'terminal upgrade');
77
78
  if (getLocalRequestRejection(request)) {
78
- rejectUpgrade(socket, '403 Forbidden');
79
+ rejectWebSocketUpgrade(socket, '403 Forbidden');
79
80
  return;
80
81
  }
81
82
  if (!validateUpgradeSession(request)) {
82
- rejectUpgrade(socket, '401 Unauthorized');
83
+ rejectWebSocketUpgrade(socket, '401 Unauthorized');
83
84
  return;
84
85
  }
85
86
  try {
86
87
  store.getLiveRun(match.runId);
87
88
  }
88
89
  catch {
89
- rejectUpgrade(socket, '404 Not Found');
90
+ rejectWebSocketUpgrade(socket, '404 Not Found');
90
91
  return;
91
92
  }
92
93
  const wss = match.channel === 'io' ? ioWss : controlWss;
93
94
  wss.handleUpgrade(request, socket, head, (ws) => {
95
+ detachRawSocketErrorHandler();
94
96
  const clientId = getClientId(url);
95
97
  if (match.channel === 'io')
96
98
  hub.attachIo(match.runId, clientId, ws, getInitialSize(url));
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Outbound completion/attention webhook. The user supplies a single URL (stored
3
+ * in app_state); the runtime POSTs a small JSON payload on server-side lifecycle
4
+ * events so they can wire their own Slack / Discord / Feishu / ntfy / Telegram
5
+ * without Hive taking on a relay, mobile app, or account system.
6
+ *
7
+ * Best-effort by design: fire-and-forget with a timeout, all errors swallowed —
8
+ * a flaky webhook must never block or fail a report/exit. This is a personal,
9
+ * local-trust setting: the URL is whatever the user typed, and a 127.0.0.1-bound
10
+ * runtime can reach localhost/intranet, so we only enforce an http(s) scheme and
11
+ * leave the rest to the operator (documented in the Settings UI).
12
+ */
13
+ export declare const WEBHOOK_URL_KEY = "notifications.webhook-url";
14
+ export type WebhookEventType = 'report_received' | 'agent_stopped' | 'workflow_finished';
15
+ export interface WebhookEvent {
16
+ type: WebhookEventType;
17
+ workspaceId: string;
18
+ agentId?: string;
19
+ agentName?: string;
20
+ summary?: string;
21
+ at: number;
22
+ }
23
+ export declare const readWebhookUrl: (raw: string | null | undefined) => string | null;
24
+ interface WebhookNotifierOptions {
25
+ getUrl: () => string | null;
26
+ /** Injectable for tests; defaults to global fetch. */
27
+ fetchImpl?: typeof fetch;
28
+ timeoutMs?: number;
29
+ }
30
+ export declare const createWebhookNotifier: ({ getUrl, fetchImpl, timeoutMs, }: WebhookNotifierOptions) => {
31
+ notify: (event: WebhookEvent) => void;
32
+ };
33
+ export type WebhookNotifier = ReturnType<typeof createWebhookNotifier>;
34
+ export {};
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Outbound completion/attention webhook. The user supplies a single URL (stored
3
+ * in app_state); the runtime POSTs a small JSON payload on server-side lifecycle
4
+ * events so they can wire their own Slack / Discord / Feishu / ntfy / Telegram
5
+ * without Hive taking on a relay, mobile app, or account system.
6
+ *
7
+ * Best-effort by design: fire-and-forget with a timeout, all errors swallowed —
8
+ * a flaky webhook must never block or fail a report/exit. This is a personal,
9
+ * local-trust setting: the URL is whatever the user typed, and a 127.0.0.1-bound
10
+ * runtime can reach localhost/intranet, so we only enforce an http(s) scheme and
11
+ * leave the rest to the operator (documented in the Settings UI).
12
+ */
13
+ export const WEBHOOK_URL_KEY = 'notifications.webhook-url';
14
+ export const readWebhookUrl = (raw) => {
15
+ const trimmed = raw?.trim();
16
+ return trimmed ? trimmed : null;
17
+ };
18
+ const isHttpUrl = (raw) => {
19
+ try {
20
+ const url = new URL(raw);
21
+ return url.protocol === 'http:' || url.protocol === 'https:';
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ };
27
+ export const createWebhookNotifier = ({ getUrl, fetchImpl = fetch, timeoutMs = 5000, }) => {
28
+ const notify = (event) => {
29
+ const url = readWebhookUrl(getUrl());
30
+ if (!url || !isHttpUrl(url))
31
+ return;
32
+ const controller = new AbortController();
33
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
34
+ void fetchImpl(url, {
35
+ method: 'POST',
36
+ headers: { 'content-type': 'application/json' },
37
+ body: JSON.stringify(event),
38
+ signal: controller.signal,
39
+ })
40
+ .catch(() => {
41
+ // Personal-trust, best-effort: a dead or slow webhook must not affect
42
+ // the report/exit path that triggered it.
43
+ })
44
+ .finally(() => clearTimeout(timer));
45
+ };
46
+ return { notify };
47
+ };
@@ -0,0 +1,10 @@
1
+ import type { Duplex } from 'node:stream';
2
+ import type WebSocket from 'ws';
3
+ import type { WebSocketServer } from 'ws';
4
+ type UpgradeSocket = Duplex;
5
+ export declare const attachRawSocketErrorHandler: (socket: UpgradeSocket, context: string) => () => Duplex;
6
+ export declare const attachWebSocketServerErrorHandler: (wss: WebSocketServer, context: string) => void;
7
+ export declare const attachWebSocketErrorHandler: (socket: WebSocket, context: string) => void;
8
+ export declare const rejectWebSocketUpgrade: (socket: UpgradeSocket, status: string) => void;
9
+ export declare const sendWebSocketMessage: (socket: WebSocket, payload: string, context: string) => boolean;
10
+ export {};
@@ -0,0 +1,35 @@
1
+ const logSocketError = (context, error) => {
2
+ console.error(`[hive] ${context}`, error);
3
+ };
4
+ export const attachRawSocketErrorHandler = (socket, context) => {
5
+ const handler = (error) => logSocketError(`${context} socket error`, error);
6
+ socket.on('error', handler);
7
+ return () => socket.off('error', handler);
8
+ };
9
+ export const attachWebSocketServerErrorHandler = (wss, context) => {
10
+ wss.on('error', (error) => logSocketError(`${context} websocket server error`, error));
11
+ };
12
+ export const attachWebSocketErrorHandler = (socket, context) => {
13
+ socket.on('error', (error) => logSocketError(`${context} websocket error`, error));
14
+ };
15
+ export const rejectWebSocketUpgrade = (socket, status) => {
16
+ try {
17
+ socket.write(`HTTP/1.1 ${status}\r\nConnection: close\r\n\r\n`);
18
+ }
19
+ catch (error) {
20
+ logSocketError(`failed to reject websocket upgrade with ${status}`, error);
21
+ }
22
+ socket.destroy();
23
+ };
24
+ export const sendWebSocketMessage = (socket, payload, context) => {
25
+ if (socket.readyState !== socket.OPEN)
26
+ return false;
27
+ try {
28
+ socket.send(payload);
29
+ return true;
30
+ }
31
+ catch (error) {
32
+ logSocketError(`${context} send failed`, error);
33
+ return false;
34
+ }
35
+ };
@@ -0,0 +1,3 @@
1
+ export declare const escapeCmdToken: (value: string) => string;
2
+ export declare const buildCmdCommand: (command: string, args?: readonly string[]) => string;
3
+ export declare const buildCmdCallCommand: (command: string, args?: readonly string[]) => string;
@@ -0,0 +1,9 @@
1
+ const CMD_META_CHARS = /[\s"&<>|^()%]/u;
2
+ export const escapeCmdToken = (value) => {
3
+ if (value.length === 0)
4
+ return '""';
5
+ const escaped = value.replace(/%/g, '%%').replace(/"/g, '""');
6
+ return CMD_META_CHARS.test(value) ? `"${escaped}"` : escaped;
7
+ };
8
+ export const buildCmdCommand = (command, args = []) => [command, ...args].map(escapeCmdToken).join(' ');
9
+ export const buildCmdCallCommand = (command, args = []) => `call ${buildCmdCommand(command, args)}`;
@@ -0,0 +1,2 @@
1
+ export declare const getWindowsFilenameError: (filename: string) => string | undefined;
2
+ export declare const assertWindowsSafeFilename: (filename: string) => void;
@@ -0,0 +1,33 @@
1
+ const WINDOWS_DEVICE_NAMES = /^(con|prn|aux|nul|com[1-9\u00b9\u00b2\u00b3]|lpt[1-9\u00b9\u00b2\u00b3])$/iu;
2
+ const WINDOWS_INVALID_FILENAME_CHARS = new Set(['<', '>', ':', '"', '/', '\\', '|', '?', '*']);
3
+ const hasWindowsInvalidFilenameChar = (filename) => {
4
+ for (const char of filename) {
5
+ if (WINDOWS_INVALID_FILENAME_CHARS.has(char))
6
+ return true;
7
+ const code = char.codePointAt(0) ?? 0;
8
+ if (code >= 0 && code <= 31)
9
+ return true;
10
+ }
11
+ return false;
12
+ };
13
+ export const getWindowsFilenameError = (filename) => {
14
+ if (!filename.trim())
15
+ return 'filename must not be empty';
16
+ if (filename === '.' || filename === '..')
17
+ return 'filename must not be a relative segment';
18
+ if (hasWindowsInvalidFilenameChar(filename)) {
19
+ return 'filename contains characters Windows cannot create';
20
+ }
21
+ if (/[. ]$/u.test(filename))
22
+ return 'filename must not end with a space or period';
23
+ const stem = filename.split('.')[0] ?? filename;
24
+ if (WINDOWS_DEVICE_NAMES.test(stem)) {
25
+ return `filename uses reserved Windows device name: ${stem}`;
26
+ }
27
+ return undefined;
28
+ };
29
+ export const assertWindowsSafeFilename = (filename) => {
30
+ const error = getWindowsFilenameError(filename);
31
+ if (error)
32
+ throw new Error(error);
33
+ };
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Workflow CLI policy — controls which CLI a workflow's `agent()` spawns.
3
+ *
4
+ * Before this, the runner hard-coded `opts.cli ?? 'claude'`: a user who only
5
+ * had Codex set up would have every workflow agent that omitted `cli` spawn a
6
+ * `claude` it can't run. The policy makes the default configurable and lets
7
+ * the user constrain which CLIs workflow agents may use.
8
+ *
9
+ * Stored GLOBALLY in `app_state` (CLI availability is a machine-level fact, not
10
+ * per-workspace) under WORKFLOW_CLI_POLICY_KEY as a JSON `{default, allowed}`.
11
+ * An absent/malformed value reads back as DEFAULT_WORKFLOW_CLI_POLICY, which is
12
+ * unrestricted and defaults to `claude` — i.e. exactly the old behavior, so
13
+ * upgrading without configuring anything changes nothing.
14
+ */
15
+ /** Canonical CLI set — mirrors the built-in command preset ids
16
+ * (see command-preset-defaults.ts). The allowlist is a subset of these. */
17
+ export declare const CANONICAL_WORKFLOW_CLIS: readonly ["claude", "codex", "opencode", "gemini"];
18
+ export type WorkflowCli = (typeof CANONICAL_WORKFLOW_CLIS)[number];
19
+ export declare const WORKFLOW_CLI_POLICY_KEY = "workflow.cli-policy";
20
+ export interface WorkflowCliPolicy {
21
+ /** CLI used when an `agent()` call omits `cli` and isn't a custom template. */
22
+ default: string;
23
+ /** CLIs an explicit `opts.cli` (and the default fallback) may use. */
24
+ allowed: string[];
25
+ }
26
+ export declare const DEFAULT_WORKFLOW_CLI_POLICY: WorkflowCliPolicy;
27
+ /**
28
+ * Lenient coercion used by the runtime reader: turn arbitrary stored data into
29
+ * a usable policy, never throwing. Junk in `allowed` is dropped; an empty
30
+ * result falls back to the full canonical default; a `default` outside the
31
+ * sanitized `allowed` is pulled back to the first allowed entry.
32
+ */
33
+ export declare const normalizeWorkflowCliPolicy: (input: unknown) => WorkflowCliPolicy;
34
+ /** Parse the raw `app_state` string. Absent / malformed → canonical default. */
35
+ export declare const readWorkflowCliPolicy: (raw: string | null | undefined) => WorkflowCliPolicy;
36
+ /**
37
+ * Strict validation for the settings API: reject bad input so a malformed
38
+ * policy can never be persisted (the reader tolerates junk, but we'd rather
39
+ * fail the write than silently store something the user didn't intend).
40
+ */
41
+ export declare const assertValidWorkflowCliPolicy: (input: unknown) => WorkflowCliPolicy;
42
+ export interface ResolveWorkflowCliInput {
43
+ /** `opts.cli` from the `agent()` call, if any. */
44
+ requestedCli?: string;
45
+ /** True when `agentType` resolved to a workspace custom role template. */
46
+ isCustomTemplate: boolean;
47
+ /** The custom template's `defaultCommand` (only consulted when custom). */
48
+ templateDefaultCommand?: string;
49
+ policy: WorkflowCliPolicy;
50
+ }
51
+ /**
52
+ * Resolve the CLI command a workflow agent should launch with.
53
+ *
54
+ * - An explicit `requestedCli` is ALWAYS validated against `allowed` (throws
55
+ * if disallowed, so the orchestrator gets a clear, fixable error).
56
+ * - When omitted: a custom template keeps its own `defaultCommand` (the user
57
+ * curated that role deliberately — exempt from the allowlist); a built-in
58
+ * role falls back to `policy.default`.
59
+ */
60
+ export declare const resolveWorkflowCli: ({ requestedCli, isCustomTemplate, templateDefaultCommand, policy, }: ResolveWorkflowCliInput) => string;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Workflow CLI policy — controls which CLI a workflow's `agent()` spawns.
3
+ *
4
+ * Before this, the runner hard-coded `opts.cli ?? 'claude'`: a user who only
5
+ * had Codex set up would have every workflow agent that omitted `cli` spawn a
6
+ * `claude` it can't run. The policy makes the default configurable and lets
7
+ * the user constrain which CLIs workflow agents may use.
8
+ *
9
+ * Stored GLOBALLY in `app_state` (CLI availability is a machine-level fact, not
10
+ * per-workspace) under WORKFLOW_CLI_POLICY_KEY as a JSON `{default, allowed}`.
11
+ * An absent/malformed value reads back as DEFAULT_WORKFLOW_CLI_POLICY, which is
12
+ * unrestricted and defaults to `claude` — i.e. exactly the old behavior, so
13
+ * upgrading without configuring anything changes nothing.
14
+ */
15
+ /** Canonical CLI set — mirrors the built-in command preset ids
16
+ * (see command-preset-defaults.ts). The allowlist is a subset of these. */
17
+ export const CANONICAL_WORKFLOW_CLIS = ['claude', 'codex', 'opencode', 'gemini'];
18
+ export const WORKFLOW_CLI_POLICY_KEY = 'workflow.cli-policy';
19
+ export const DEFAULT_WORKFLOW_CLI_POLICY = {
20
+ default: 'claude',
21
+ allowed: [...CANONICAL_WORKFLOW_CLIS],
22
+ };
23
+ const isCanonical = (value) => typeof value === 'string' && CANONICAL_WORKFLOW_CLIS.includes(value);
24
+ /** Canonical entries from `input`, deduped and in canonical order. */
25
+ const sanitizeAllowed = (input) => {
26
+ if (!Array.isArray(input))
27
+ return [];
28
+ const set = new Set(input.filter(isCanonical));
29
+ return CANONICAL_WORKFLOW_CLIS.filter((cli) => set.has(cli));
30
+ };
31
+ /**
32
+ * Lenient coercion used by the runtime reader: turn arbitrary stored data into
33
+ * a usable policy, never throwing. Junk in `allowed` is dropped; an empty
34
+ * result falls back to the full canonical default; a `default` outside the
35
+ * sanitized `allowed` is pulled back to the first allowed entry.
36
+ */
37
+ export const normalizeWorkflowCliPolicy = (input) => {
38
+ if (typeof input !== 'object' || input === null)
39
+ return DEFAULT_WORKFLOW_CLI_POLICY;
40
+ const record = input;
41
+ const allowed = sanitizeAllowed(record.allowed);
42
+ if (allowed.length === 0)
43
+ return DEFAULT_WORKFLOW_CLI_POLICY;
44
+ const requestedDefault = record.default;
45
+ const fallback = allowed[0];
46
+ const resolvedDefault = typeof requestedDefault === 'string' && allowed.includes(requestedDefault)
47
+ ? requestedDefault
48
+ : fallback;
49
+ return { default: resolvedDefault, allowed };
50
+ };
51
+ /** Parse the raw `app_state` string. Absent / malformed → canonical default. */
52
+ export const readWorkflowCliPolicy = (raw) => {
53
+ if (raw === null || raw === undefined)
54
+ return DEFAULT_WORKFLOW_CLI_POLICY;
55
+ try {
56
+ return normalizeWorkflowCliPolicy(JSON.parse(raw));
57
+ }
58
+ catch {
59
+ return DEFAULT_WORKFLOW_CLI_POLICY;
60
+ }
61
+ };
62
+ /**
63
+ * Strict validation for the settings API: reject bad input so a malformed
64
+ * policy can never be persisted (the reader tolerates junk, but we'd rather
65
+ * fail the write than silently store something the user didn't intend).
66
+ */
67
+ export const assertValidWorkflowCliPolicy = (input) => {
68
+ if (typeof input !== 'object' || input === null) {
69
+ throw new Error('workflow cli policy must be an object { default, allowed }');
70
+ }
71
+ const record = input;
72
+ if (!Array.isArray(record.allowed)) {
73
+ throw new Error('workflow cli policy `allowed` must be an array');
74
+ }
75
+ if (record.allowed.length === 0) {
76
+ throw new Error('workflow cli policy `allowed` must list at least one CLI');
77
+ }
78
+ const bad = record.allowed.find((entry) => !isCanonical(entry));
79
+ if (bad !== undefined) {
80
+ throw new Error(`workflow cli policy "allowed" has an unsupported CLI: ${JSON.stringify(bad)}. Supported: ${CANONICAL_WORKFLOW_CLIS.join(', ')}`);
81
+ }
82
+ if (typeof record.default !== 'string' || !record.allowed.includes(record.default)) {
83
+ throw new Error(`workflow cli policy "default" (${JSON.stringify(record.default)}) must be one of allowed: ${record.allowed.join(', ')}`);
84
+ }
85
+ // Dedupe + canonical order while preserving the (now-validated) selection.
86
+ return { default: record.default, allowed: sanitizeAllowed(record.allowed) };
87
+ };
88
+ /**
89
+ * Resolve the CLI command a workflow agent should launch with.
90
+ *
91
+ * - An explicit `requestedCli` is ALWAYS validated against `allowed` (throws
92
+ * if disallowed, so the orchestrator gets a clear, fixable error).
93
+ * - When omitted: a custom template keeps its own `defaultCommand` (the user
94
+ * curated that role deliberately — exempt from the allowlist); a built-in
95
+ * role falls back to `policy.default`.
96
+ */
97
+ export const resolveWorkflowCli = ({ requestedCli, isCustomTemplate, templateDefaultCommand, policy, }) => {
98
+ const explicit = requestedCli?.trim();
99
+ if (explicit) {
100
+ if (!policy.allowed.includes(explicit)) {
101
+ throw new Error(`Workflow agent cli '${explicit}' is not allowed in this workspace. ` +
102
+ `Allowed: ${policy.allowed.join(', ')}. ` +
103
+ `Pick an allowed cli, or change the workflow CLI policy in Settings.`);
104
+ }
105
+ return explicit;
106
+ }
107
+ if (isCustomTemplate && templateDefaultCommand)
108
+ return templateDefaultCommand;
109
+ return policy.default;
110
+ };
@@ -0,0 +1,12 @@
1
+ export interface ReportPayload {
2
+ text: string;
3
+ artifacts: string[];
4
+ status?: string;
5
+ }
6
+ export interface WorkflowDispatchAwaiter {
7
+ awaitReport(dispatchId: string, timeoutMs?: number): Promise<ReportPayload>;
8
+ notifyReport(dispatchId: string, payload: ReportPayload): void;
9
+ notifyCancel(dispatchId: string, reason: string): void;
10
+ cancelAll(reason: string): void;
11
+ }
12
+ export declare const createWorkflowDispatchAwaiter: () => WorkflowDispatchAwaiter;
@@ -0,0 +1,80 @@
1
+ // Default 10 minutes — workflows often run long. Callers may shorten via opts.
2
+ const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000;
3
+ export const createWorkflowDispatchAwaiter = () => {
4
+ const pending = new Map();
5
+ // TIER 2 #10 — deferred-cancel state. The runner creates a dispatch
6
+ // and THEN calls awaitReport, with at least one microtask gap between
7
+ // the two. If notifyCancel(id) fires in that gap, it currently hits
8
+ // an empty `pending` map and no-ops; the subsequent awaitReport(id)
9
+ // then waits the full DEFAULT_TIMEOUT_MS (10 min) on a dispatch that
10
+ // is already cancelled. Remembering the cancel intent here lets
11
+ // awaitReport reject synchronously the instant it gets registered.
12
+ // We also remember any pre-arrived report payload for the same
13
+ // reason — defensive against a future ordering change.
14
+ const cancelledIds = new Map();
15
+ const reportedIds = new Map();
16
+ const take = (dispatchId) => {
17
+ const entry = pending.get(dispatchId);
18
+ if (entry) {
19
+ clearTimeout(entry.timer);
20
+ pending.delete(dispatchId);
21
+ }
22
+ return entry;
23
+ };
24
+ return {
25
+ awaitReport(dispatchId, timeoutMs = DEFAULT_TIMEOUT_MS) {
26
+ return new Promise((resolve, reject) => {
27
+ // Pre-arrived events: drain immediately, no timer / no entry.
28
+ const earlyReport = reportedIds.get(dispatchId);
29
+ if (earlyReport) {
30
+ reportedIds.delete(dispatchId);
31
+ resolve(earlyReport);
32
+ return;
33
+ }
34
+ const earlyCancel = cancelledIds.get(dispatchId);
35
+ if (earlyCancel) {
36
+ cancelledIds.delete(dispatchId);
37
+ reject(new Error(`workflow dispatch ${dispatchId} cancelled: ${earlyCancel}`));
38
+ return;
39
+ }
40
+ const timer = setTimeout(() => {
41
+ pending.delete(dispatchId);
42
+ reject(new Error(`workflow dispatch ${dispatchId} timeout after ${timeoutMs}ms`));
43
+ }, timeoutMs);
44
+ pending.set(dispatchId, { resolve, reject, timer });
45
+ });
46
+ },
47
+ notifyReport(dispatchId, payload) {
48
+ const entry = take(dispatchId);
49
+ if (entry) {
50
+ entry.resolve(payload);
51
+ }
52
+ else {
53
+ // Pre-arrival: stash for the eventual awaitReport call. Cap
54
+ // stash size implicitly by overwriting on duplicate id.
55
+ reportedIds.set(dispatchId, payload);
56
+ }
57
+ },
58
+ notifyCancel(dispatchId, reason) {
59
+ const entry = take(dispatchId);
60
+ if (entry) {
61
+ entry.reject(new Error(`workflow dispatch ${dispatchId} cancelled: ${reason}`));
62
+ }
63
+ else {
64
+ cancelledIds.set(dispatchId, reason);
65
+ }
66
+ },
67
+ cancelAll(reason) {
68
+ for (const [id, entry] of pending) {
69
+ clearTimeout(entry.timer);
70
+ entry.reject(new Error(`workflow dispatch ${id} cancelled: ${reason}`));
71
+ }
72
+ pending.clear();
73
+ // cancelAll happens at runtime close — anything waiting at that
74
+ // moment should fail loudly, but the deferred state can be
75
+ // safely dropped since no future awaitReport will run.
76
+ cancelledIds.clear();
77
+ reportedIds.clear();
78
+ },
79
+ };
80
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Workflow experimental feature gate.
3
+ *
4
+ * Workflows (`team workflow run`, the scheduler, the UI drawer, and the chunk
5
+ * of orchestrator guidance that teaches them) are a power feature with sharp
6
+ * edges — runaway fan-outs, authoring footguns, no run-resume yet. They ship
7
+ * OFF by default; a user opts in from Settings. While off, the orchestrator is
8
+ * not even taught about workflows, which also keeps its always-on prompt lean.
9
+ *
10
+ * Stored GLOBALLY in `app_state` under WORKFLOW_ENABLED_KEY. Absent / anything
11
+ * other than the exact string "true" reads back as DISABLED.
12
+ */
13
+ export declare const WORKFLOW_ENABLED_KEY = "workflow.enabled";
14
+ export declare const readWorkflowEnabled: (raw: string | null | undefined) => boolean;
15
+ export declare const serializeWorkflowEnabled: (enabled: boolean) => string;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Workflow experimental feature gate.
3
+ *
4
+ * Workflows (`team workflow run`, the scheduler, the UI drawer, and the chunk
5
+ * of orchestrator guidance that teaches them) are a power feature with sharp
6
+ * edges — runaway fan-outs, authoring footguns, no run-resume yet. They ship
7
+ * OFF by default; a user opts in from Settings. While off, the orchestrator is
8
+ * not even taught about workflows, which also keeps its always-on prompt lean.
9
+ *
10
+ * Stored GLOBALLY in `app_state` under WORKFLOW_ENABLED_KEY. Absent / anything
11
+ * other than the exact string "true" reads back as DISABLED.
12
+ */
13
+ export const WORKFLOW_ENABLED_KEY = 'workflow.enabled';
14
+ export const readWorkflowEnabled = (raw) => raw === 'true';
15
+ export const serializeWorkflowEnabled = (enabled) => (enabled ? 'true' : 'false');