@tt-a1i/hive 1.4.4 → 1.5.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.
- package/CHANGELOG.md +22 -0
- package/README.md +8 -0
- package/assets/qq-group.jpg +0 -0
- package/dist/bin/team.cmd +1 -0
- package/dist/src/cli/hive-update.d.ts +45 -17
- package/dist/src/cli/hive-update.js +63 -25
- package/dist/src/cli/hive.d.ts +25 -0
- package/dist/src/cli/hive.js +41 -3
- package/dist/src/cli/team.d.ts +1 -0
- package/dist/src/cli/team.js +199 -3
- package/dist/src/server/agent-command-resolver.js +3 -19
- package/dist/src/server/agent-manager-support.d.ts +2 -2
- package/dist/src/server/agent-manager-support.js +98 -24
- package/dist/src/server/agent-run-starter.d.ts +7 -1
- package/dist/src/server/agent-run-starter.js +9 -2
- package/dist/src/server/agent-run-store.d.ts +1 -1
- package/dist/src/server/agent-runtime-close.d.ts +1 -0
- package/dist/src/server/agent-runtime-close.js +25 -1
- package/dist/src/server/agent-runtime-contract.d.ts +2 -1
- package/dist/src/server/agent-runtime.d.ts +1 -1
- package/dist/src/server/agent-runtime.js +8 -2
- package/dist/src/server/agent-startup-instructions.d.ts +8 -1
- package/dist/src/server/agent-startup-instructions.js +15 -9
- package/dist/src/server/agent-stdin-dispatcher.d.ts +12 -5
- package/dist/src/server/agent-stdin-dispatcher.js +129 -40
- package/dist/src/server/cron-util.d.ts +7 -0
- package/dist/src/server/cron-util.js +19 -0
- package/dist/src/server/dispatch-ledger-store.d.ts +22 -0
- package/dist/src/server/dispatch-ledger-store.js +51 -3
- package/dist/src/server/env-sync-message.js +9 -9
- package/dist/src/server/fs-pick-folder.js +4 -0
- package/dist/src/server/fs-sandbox.js +36 -7
- package/dist/src/server/hive-team-guidance.d.ts +11 -6
- package/dist/src/server/hive-team-guidance.js +252 -71
- package/dist/src/server/live-run-registry.d.ts +1 -0
- package/dist/src/server/live-run-registry.js +1 -1
- package/dist/src/server/open-target-commands.js +5 -6
- package/dist/src/server/orchestrator-autostart.d.ts +12 -0
- package/dist/src/server/orchestrator-autostart.js +15 -13
- package/dist/src/server/path-canonicalization.d.ts +3 -0
- package/dist/src/server/path-canonicalization.js +29 -0
- package/dist/src/server/platform-path.d.ts +3 -0
- package/dist/src/server/platform-path.js +13 -0
- package/dist/src/server/post-start-input-writer.d.ts +1 -1
- package/dist/src/server/post-start-input-writer.js +110 -13
- package/dist/src/server/preset-launch-support.d.ts +1 -1
- package/dist/src/server/preset-launch-support.js +33 -2
- package/dist/src/server/recovery-summary.d.ts +6 -1
- package/dist/src/server/recovery-summary.js +17 -17
- package/dist/src/server/restart-policy-support.d.ts +6 -1
- package/dist/src/server/restart-policy-support.js +9 -1
- package/dist/src/server/restart-policy.d.ts +2 -2
- package/dist/src/server/restart-policy.js +3 -1
- package/dist/src/server/role-template-store.d.ts +1 -0
- package/dist/src/server/role-template-store.js +11 -1
- package/dist/src/server/route-types.d.ts +43 -0
- package/dist/src/server/routes-runtime.js +2 -1
- package/dist/src/server/routes-settings.js +76 -0
- package/dist/src/server/routes-team.js +211 -1
- package/dist/src/server/routes-workflow-schedules.d.ts +2 -0
- package/dist/src/server/routes-workflow-schedules.js +58 -0
- package/dist/src/server/routes-workflows.d.ts +2 -0
- package/dist/src/server/routes-workflows.js +83 -0
- package/dist/src/server/routes.js +4 -0
- package/dist/src/server/runtime-restart-policy.d.ts +3 -1
- package/dist/src/server/runtime-restart-policy.js +3 -1
- package/dist/src/server/runtime-store-contract.d.ts +122 -0
- package/dist/src/server/runtime-store-contract.js +1 -0
- package/dist/src/server/runtime-store-helpers.d.ts +9 -0
- package/dist/src/server/runtime-store-helpers.js +101 -2
- package/dist/src/server/runtime-store-workflows.d.ts +6 -0
- package/dist/src/server/runtime-store-workflows.js +100 -0
- package/dist/src/server/runtime-store.d.ts +3 -72
- package/dist/src/server/runtime-store.js +70 -4
- package/dist/src/server/session-capture-codex.d.ts +3 -3
- package/dist/src/server/session-capture-codex.js +9 -7
- package/dist/src/server/session-capture-gemini.d.ts +1 -1
- package/dist/src/server/session-capture-gemini.js +6 -3
- package/dist/src/server/settings-store.d.ts +3 -0
- package/dist/src/server/settings-store.js +1 -0
- package/dist/src/server/sqlite-schema-v19.d.ts +2 -0
- package/dist/src/server/sqlite-schema-v19.js +17 -0
- package/dist/src/server/sqlite-schema-v20.d.ts +2 -0
- package/dist/src/server/sqlite-schema-v20.js +20 -0
- package/dist/src/server/sqlite-schema-v21.d.ts +2 -0
- package/dist/src/server/sqlite-schema-v21.js +20 -0
- package/dist/src/server/sqlite-schema.d.ts +1 -1
- package/dist/src/server/sqlite-schema.js +97 -1
- package/dist/src/server/system-message.d.ts +7 -0
- package/dist/src/server/system-message.js +8 -1
- package/dist/src/server/tasks-file-watcher.d.ts +13 -1
- package/dist/src/server/tasks-file-watcher.js +127 -23
- package/dist/src/server/tasks-file.d.ts +2 -1
- package/dist/src/server/tasks-file.js +32 -9
- package/dist/src/server/tasks-websocket-server.js +13 -14
- package/dist/src/server/team-authz.d.ts +1 -1
- package/dist/src/server/team-authz.js +9 -1
- package/dist/src/server/team-autostaff.d.ts +16 -0
- package/dist/src/server/team-autostaff.js +16 -0
- package/dist/src/server/team-list-serializer.d.ts +1 -1
- package/dist/src/server/team-list-serializer.js +3 -1
- package/dist/src/server/team-operations.d.ts +15 -1
- package/dist/src/server/team-operations.js +116 -11
- package/dist/src/server/terminal-protocol.js +9 -3
- package/dist/src/server/terminal-stream-hub.js +16 -10
- package/dist/src/server/terminal-ws-server.js +10 -8
- package/dist/src/server/websocket-upgrade-safety.d.ts +10 -0
- package/dist/src/server/websocket-upgrade-safety.js +35 -0
- package/dist/src/server/windows-command-line.d.ts +3 -0
- package/dist/src/server/windows-command-line.js +9 -0
- package/dist/src/server/windows-filename.d.ts +2 -0
- package/dist/src/server/windows-filename.js +33 -0
- package/dist/src/server/workflow-cli-policy.d.ts +60 -0
- package/dist/src/server/workflow-cli-policy.js +110 -0
- package/dist/src/server/workflow-dispatch-awaiter.d.ts +12 -0
- package/dist/src/server/workflow-dispatch-awaiter.js +80 -0
- package/dist/src/server/workflow-feature.d.ts +15 -0
- package/dist/src/server/workflow-feature.js +15 -0
- package/dist/src/server/workflow-http-serializers.d.ts +64 -0
- package/dist/src/server/workflow-http-serializers.js +58 -0
- package/dist/src/server/workflow-run-log-store.d.ts +19 -0
- package/dist/src/server/workflow-run-log-store.js +45 -0
- package/dist/src/server/workflow-run-store.d.ts +50 -0
- package/dist/src/server/workflow-run-store.js +103 -0
- package/dist/src/server/workflow-runner.d.ts +147 -0
- package/dist/src/server/workflow-runner.js +401 -0
- package/dist/src/server/workflow-schedule-create.d.ts +14 -0
- package/dist/src/server/workflow-schedule-create.js +41 -0
- package/dist/src/server/workflow-schedule-store.d.ts +43 -0
- package/dist/src/server/workflow-schedule-store.js +112 -0
- package/dist/src/server/workflow-scheduler.d.ts +36 -0
- package/dist/src/server/workflow-scheduler.js +97 -0
- package/dist/src/server/workflow-script-loader.d.ts +34 -0
- package/dist/src/server/workflow-script-loader.js +106 -0
- package/dist/src/server/workspace-path-validation.js +16 -4
- package/dist/src/server/workspace-shell-runtime.d.ts +5 -0
- package/dist/src/server/workspace-shell-runtime.js +24 -2
- package/dist/src/server/workspace-store-contract.d.ts +4 -1
- package/dist/src/server/workspace-store-hydration.js +23 -7
- package/dist/src/server/workspace-store-mutations.js +2 -5
- package/dist/src/server/workspace-store-support.d.ts +4 -0
- package/dist/src/server/workspace-store-support.js +13 -1
- package/dist/src/server/workspace-store.js +38 -4
- package/dist/src/shared/types.d.ts +16 -1
- package/package.json +4 -2
- package/web/dist/assets/{AddWorkerDialog-DeZhTQLi.js → AddWorkerDialog-CcC-7kgG.js} +2 -2
- package/web/dist/assets/AddWorkspaceDialog-BDpOTfmt.js +1 -0
- package/web/dist/assets/{FirstRunWizard-B5wLcat5.js → FirstRunWizard-BYX_ocQn.js} +1 -1
- package/web/dist/assets/{MarketplaceDrawer-BC0eBOEW.js → MarketplaceDrawer-DUxSk7db.js} +1 -1
- package/web/dist/assets/WhatsNewDialog-B_RlCXcV.js +1 -0
- package/web/dist/assets/WorkerModal-D9-7YfZZ.js +1 -0
- package/web/dist/assets/WorkspaceTaskDrawer-BCKoF7qc.js +1 -0
- package/web/dist/assets/{WorkspaceTerminalPanels-CvibsPSd.js → WorkspaceTerminalPanels-Dq8y91t2.js} +1 -1
- package/web/dist/assets/index-BiOvKIVw.css +1 -0
- package/web/dist/assets/index-DMRUklT3.js +73 -0
- package/web/dist/assets/path-join-7MR1s7b1.js +1 -0
- package/web/dist/index.html +2 -2
- package/web/dist/sw.js +1 -1
- package/web/dist/assets/AddWorkspaceDialog-DDpXNEKf.js +0 -1
- package/web/dist/assets/WorkerModal-BwMHq-Bi.js +0 -1
- package/web/dist/assets/WorkspaceTaskDrawer-CxvT4nqs.js +0 -1
- package/web/dist/assets/index-BEsTmfrO.css +0 -1
- package/web/dist/assets/index-Ddb7bDN5.js +0 -75
- package/web/dist/assets/path-join-S7qkXQtP.js +0 -1
|
@@ -2,14 +2,19 @@ import type { TeamListItem } from '../shared/types.js';
|
|
|
2
2
|
import type { AgentRuntime } from './agent-runtime.js';
|
|
3
3
|
import type { DispatchRecord } from './dispatch-ledger-store.js';
|
|
4
4
|
import type { MessageLogHandle, MessageLogRecord } from './message-log-store.js';
|
|
5
|
+
import type { WorkflowDispatchAwaiter } from './workflow-dispatch-awaiter.js';
|
|
5
6
|
import type { WorkspaceStore } from './workspace-store.js';
|
|
6
7
|
export declare const formatUnknownWorkerError: (workerName: string, roster: readonly TeamListItem[]) => string;
|
|
7
8
|
export interface TeamOperationsInput {
|
|
8
9
|
agentRuntime: AgentRuntime;
|
|
9
10
|
createDispatch: (input: {
|
|
10
11
|
fromAgentId?: string;
|
|
12
|
+
label?: string;
|
|
13
|
+
phase?: string;
|
|
14
|
+
stepIndex?: number;
|
|
11
15
|
text: string;
|
|
12
16
|
toAgentId: string;
|
|
17
|
+
workflowRunId?: string;
|
|
13
18
|
workspaceId: string;
|
|
14
19
|
}) => DispatchRecord;
|
|
15
20
|
deleteDispatch: (dispatchId: string) => void;
|
|
@@ -30,11 +35,20 @@ export interface TeamOperationsInput {
|
|
|
30
35
|
workspaceId: string;
|
|
31
36
|
}) => DispatchRecord | undefined;
|
|
32
37
|
markDispatchSubmitted: (dispatchId: string) => void;
|
|
38
|
+
workflowDispatchAwaiter: WorkflowDispatchAwaiter;
|
|
33
39
|
workspaceStore: WorkspaceStore;
|
|
40
|
+
/** Auto-dismiss an ephemeral orchestrator-spawned worker after its
|
|
41
|
+
* dispatch report. Wired in runtime-store-helpers to remove the worker
|
|
42
|
+
* completely (stop run, drop launch config, drop the row). M11. */
|
|
43
|
+
dismissEphemeralWorker?: (workspaceId: string, workerId: string) => void;
|
|
34
44
|
}
|
|
35
45
|
export interface DispatchTaskInput {
|
|
36
46
|
fromAgentId?: string;
|
|
37
47
|
hivePort?: string;
|
|
48
|
+
workflowRunId?: string;
|
|
49
|
+
stepIndex?: number;
|
|
50
|
+
phase?: string;
|
|
51
|
+
label?: string;
|
|
38
52
|
}
|
|
39
53
|
export interface ReportTaskInput {
|
|
40
54
|
artifacts?: string[];
|
|
@@ -57,7 +71,7 @@ export interface ReportTaskResult {
|
|
|
57
71
|
forwardError: string | null;
|
|
58
72
|
forwarded: boolean;
|
|
59
73
|
}
|
|
60
|
-
export declare const createTeamOperations: ({ agentRuntime, createDispatch, deleteDispatch, deleteMessage, findOpenDispatch, findOpenDispatchById, insertMessage, markDispatchCancelled, markDispatchReportedByWorker, markDispatchSubmitted, workspaceStore, }: TeamOperationsInput) => {
|
|
74
|
+
export declare const createTeamOperations: ({ agentRuntime, createDispatch, deleteDispatch, deleteMessage, findOpenDispatch, findOpenDispatchById, insertMessage, markDispatchCancelled, markDispatchReportedByWorker, markDispatchSubmitted, workflowDispatchAwaiter, workspaceStore, dismissEphemeralWorker, }: TeamOperationsInput) => {
|
|
61
75
|
cancelTask(workspaceId: string, dispatchId: string, input: CancelTaskInput): {
|
|
62
76
|
dispatch: DispatchRecord;
|
|
63
77
|
forwardError: string | null;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ConflictError, PtyInactiveError } from './http-errors.js';
|
|
2
2
|
import { createReportMessage, createSendMessage, createStatusMessage, createUserInputMessage, } from './runtime-message-builders.js';
|
|
3
|
+
import { getWorkflowAgentId } from './workspace-store-support.js';
|
|
3
4
|
/* Roster snapshot embedded in the 409 the orchestrator sees when it
|
|
4
5
|
dispatches to a missing name. Format prioritizes the orchestrator's
|
|
5
6
|
parsing path: bullet-per-member with role + live status + pending
|
|
@@ -23,7 +24,7 @@ export const formatUnknownWorkerError = (workerName, roster) => {
|
|
|
23
24
|
].join('\n');
|
|
24
25
|
};
|
|
25
26
|
const reportForwardErrorMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
26
|
-
export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispatch, deleteMessage, findOpenDispatch, findOpenDispatchById, insertMessage, markDispatchCancelled, markDispatchReportedByWorker, markDispatchSubmitted, workspaceStore, }) => {
|
|
27
|
+
export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispatch, deleteMessage, findOpenDispatch, findOpenDispatchById, insertMessage, markDispatchCancelled, markDispatchReportedByWorker, markDispatchSubmitted, workflowDispatchAwaiter, workspaceStore, dismissEphemeralWorker, }) => {
|
|
27
28
|
const ensureWorkerRun = async (workspaceId, workerId, hivePort) => {
|
|
28
29
|
if (agentRuntime.getActiveRunByAgentId(workspaceId, workerId)) {
|
|
29
30
|
return;
|
|
@@ -45,10 +46,25 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
|
|
|
45
46
|
throw error;
|
|
46
47
|
}
|
|
47
48
|
};
|
|
49
|
+
const cancelUndeliveredDispatch = (workspaceId, workerId, dispatchId, reason, workflowRunId) => {
|
|
50
|
+
const cancelled = markDispatchCancelled({ dispatchId, reason, workspaceId });
|
|
51
|
+
if (!cancelled)
|
|
52
|
+
return;
|
|
53
|
+
try {
|
|
54
|
+
workspaceStore.markTaskCancelled(workspaceId, workerId);
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
console.error('[hive] swallowed:teamDispatch.markTaskCancelled', error);
|
|
58
|
+
}
|
|
59
|
+
if (workflowRunId !== undefined) {
|
|
60
|
+
workflowDispatchAwaiter.notifyCancel(dispatchId, reason);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
48
63
|
const dispatchTask = async (workspaceId, workerId, text, input = {}) => {
|
|
49
64
|
const message = createSendMessage(workspaceId, workerId, text, input.fromAgentId);
|
|
50
65
|
const messageHandle = insertMessage(message);
|
|
51
66
|
let dispatch;
|
|
67
|
+
let pendingMarked = false;
|
|
52
68
|
try {
|
|
53
69
|
const dispatchInput = {
|
|
54
70
|
text,
|
|
@@ -57,18 +73,69 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
|
|
|
57
73
|
};
|
|
58
74
|
if (input.fromAgentId)
|
|
59
75
|
dispatchInput.fromAgentId = input.fromAgentId;
|
|
76
|
+
if (input.workflowRunId !== undefined)
|
|
77
|
+
dispatchInput.workflowRunId = input.workflowRunId;
|
|
78
|
+
if (input.stepIndex !== undefined)
|
|
79
|
+
dispatchInput.stepIndex = input.stepIndex;
|
|
80
|
+
if (input.phase !== undefined)
|
|
81
|
+
dispatchInput.phase = input.phase;
|
|
82
|
+
if (input.label !== undefined)
|
|
83
|
+
dispatchInput.label = input.label;
|
|
60
84
|
dispatch = createDispatch(dispatchInput);
|
|
85
|
+
const dispatchId = dispatch.id;
|
|
61
86
|
if (input.fromAgentId) {
|
|
62
87
|
const sender = workspaceStore.getAgent(workspaceId, input.fromAgentId);
|
|
63
88
|
await ensureWorkerRun(workspaceId, workerId, input.hivePort ?? '');
|
|
64
89
|
const worker = workspaceStore.getWorker(workspaceId, workerId);
|
|
65
|
-
|
|
66
|
-
|
|
90
|
+
const isWorkflowDispatch = input.workflowRunId !== undefined || input.fromAgentId === getWorkflowAgentId(workspaceId);
|
|
91
|
+
markDispatchSubmitted(dispatchId);
|
|
92
|
+
workspaceStore.markTaskDispatched(workspaceId, workerId);
|
|
93
|
+
pendingMarked = true;
|
|
94
|
+
try {
|
|
95
|
+
const writePrompt = agentRuntime.writeSendPrompt(workspaceId, workerId, dispatchId, sender.name, worker.description, text);
|
|
96
|
+
void writePrompt.catch((error) => {
|
|
97
|
+
// `team send` is intentionally asynchronous (§3.3). A worker that
|
|
98
|
+
// exits during paste-submit did not receive actionable work, so
|
|
99
|
+
// close the open dispatch instead of leaving a fake pending task.
|
|
100
|
+
try {
|
|
101
|
+
cancelUndeliveredDispatch(workspaceId, workerId, dispatchId, reportForwardErrorMessage(error), input.workflowRunId);
|
|
102
|
+
}
|
|
103
|
+
catch (cancelError) {
|
|
104
|
+
if (!isWorkflowDispatch)
|
|
105
|
+
console.error('[hive] swallowed:teamDispatch.cancelUndelivered', cancelError);
|
|
106
|
+
}
|
|
107
|
+
if (!isWorkflowDispatch)
|
|
108
|
+
console.error('[hive] swallowed:teamDispatch.writePrompt', error);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
try {
|
|
113
|
+
cancelUndeliveredDispatch(workspaceId, workerId, dispatchId, reportForwardErrorMessage(error), input.workflowRunId);
|
|
114
|
+
}
|
|
115
|
+
catch (cancelError) {
|
|
116
|
+
if (!isWorkflowDispatch)
|
|
117
|
+
console.error('[hive] swallowed:teamDispatch.cancelUndelivered', cancelError);
|
|
118
|
+
}
|
|
119
|
+
if (!isWorkflowDispatch)
|
|
120
|
+
console.error('[hive] swallowed:teamDispatch.writePrompt', error);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
workspaceStore.markTaskDispatched(workspaceId, workerId);
|
|
125
|
+
pendingMarked = true;
|
|
67
126
|
}
|
|
68
|
-
workspaceStore.markTaskDispatched(workspaceId, workerId);
|
|
69
127
|
return dispatch;
|
|
70
128
|
}
|
|
71
129
|
catch (error) {
|
|
130
|
+
if (pendingMarked) {
|
|
131
|
+
try {
|
|
132
|
+
workspaceStore.markTaskCancelled(workspaceId, workerId);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// Best-effort compensation for the in-memory pending count; the
|
|
136
|
+
// durable send message is deleted below.
|
|
137
|
+
}
|
|
138
|
+
}
|
|
72
139
|
if (dispatch)
|
|
73
140
|
deleteDispatch(dispatch.id);
|
|
74
141
|
deleteMessage(messageHandle);
|
|
@@ -164,17 +231,19 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
|
|
|
164
231
|
const status = input.status;
|
|
165
232
|
const artifacts = input.artifacts ?? [];
|
|
166
233
|
const worker = workspaceStore.getWorker(workspaceId, workerId);
|
|
167
|
-
if (input.requireActiveRun === true &&
|
|
168
|
-
!agentRuntime.getActiveRunByAgentId(workspaceId, `${workspaceId}:orchestrator`)) {
|
|
169
|
-
throw new PtyInactiveError(`No active run for agent: ${workspaceId}:orchestrator`);
|
|
170
|
-
}
|
|
171
234
|
const openDispatch = findOpenDispatch(workspaceId, workerId, input.dispatchId);
|
|
172
|
-
if (!openDispatch && input.dispatchId) {
|
|
173
|
-
throw new ConflictError(`No open dispatch for worker: ${worker.name}`);
|
|
174
|
-
}
|
|
175
235
|
if (!openDispatch) {
|
|
176
236
|
throw new ConflictError(`No open dispatch for worker: ${worker.name}`);
|
|
177
237
|
}
|
|
238
|
+
const isWorkflowDispatch = openDispatch.fromAgentId === getWorkflowAgentId(workspaceId);
|
|
239
|
+
// Pre-check the orchestrator PTY only when the report is heading there.
|
|
240
|
+
// Workflow-sourced dispatches don't need an orchestrator: the runner
|
|
241
|
+
// is in-process and will resolve its awaiter directly.
|
|
242
|
+
if (input.requireActiveRun === true && !isWorkflowDispatch) {
|
|
243
|
+
if (!agentRuntime.getActiveRunByAgentId(workspaceId, `${workspaceId}:orchestrator`)) {
|
|
244
|
+
throw new PtyInactiveError(`No active run for agent: ${workspaceId}:orchestrator`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
178
247
|
const messageHandle = insertMessage(createReportMessage(workspaceId, workerId, text, status, artifacts));
|
|
179
248
|
try {
|
|
180
249
|
const dispatch = markDispatchReportedByWorker({
|
|
@@ -190,6 +259,24 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
|
|
|
190
259
|
workspaceStore.markTaskReported(workspaceId, workerId);
|
|
191
260
|
let forwardError = null;
|
|
192
261
|
let forwarded = false;
|
|
262
|
+
// Workflow-sourced dispatches: the source is the in-process runner, not a
|
|
263
|
+
// PTY. Resolve its awaiting Promise instead of injecting into orchestrator
|
|
264
|
+
// stdin (which would do nothing — `__workflow__` has no PTY).
|
|
265
|
+
if (dispatch.fromAgentId === getWorkflowAgentId(workspaceId)) {
|
|
266
|
+
try {
|
|
267
|
+
workflowDispatchAwaiter.notifyReport(dispatch.id, {
|
|
268
|
+
artifacts,
|
|
269
|
+
text,
|
|
270
|
+
...(status ? { status } : {}),
|
|
271
|
+
});
|
|
272
|
+
forwarded = true;
|
|
273
|
+
}
|
|
274
|
+
catch (error) {
|
|
275
|
+
forwardError = reportForwardErrorMessage(error);
|
|
276
|
+
console.error('[hive] swallowed:teamReport.workflowForward', error);
|
|
277
|
+
}
|
|
278
|
+
return { dispatch, forwardError, forwarded };
|
|
279
|
+
}
|
|
193
280
|
if (input.requireActiveRun === true) {
|
|
194
281
|
try {
|
|
195
282
|
agentRuntime.writeReportPrompt(workspaceId, worker.name, workerId, text, artifacts, {
|
|
@@ -202,6 +289,24 @@ export const createTeamOperations = ({ agentRuntime, createDispatch, deleteDispa
|
|
|
202
289
|
console.error('[hive] swallowed:teamReport.forward', error);
|
|
203
290
|
}
|
|
204
291
|
}
|
|
292
|
+
// M11: if this worker was spawned with `team spawn --ephemeral`, this
|
|
293
|
+
// first successful report is its trigger to auto-dismiss. Deferred via
|
|
294
|
+
// queueMicrotask so the orchestrator's forward write lands BEFORE the
|
|
295
|
+
// worker's PTY is torn down (otherwise the inject + dismiss race).
|
|
296
|
+
// Skipped for workflow dispatches — workflow workers are managed by
|
|
297
|
+
// the runner's own finally block.
|
|
298
|
+
if (worker.ephemeral === true &&
|
|
299
|
+
worker.spawnedBy === 'orchestrator' &&
|
|
300
|
+
dismissEphemeralWorker) {
|
|
301
|
+
queueMicrotask(() => {
|
|
302
|
+
try {
|
|
303
|
+
dismissEphemeralWorker(workspaceId, workerId);
|
|
304
|
+
}
|
|
305
|
+
catch (error) {
|
|
306
|
+
console.error('[hive] swallowed:teamReport.ephemeralDismiss', error);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
}
|
|
205
310
|
return { dispatch, forwardError, forwarded };
|
|
206
311
|
}
|
|
207
312
|
catch (error) {
|
|
@@ -14,7 +14,11 @@ export const parseTerminalControlMessage = (raw) => {
|
|
|
14
14
|
}
|
|
15
15
|
const resizeCols = asInteger(cols);
|
|
16
16
|
const resizeRows = asInteger(rows);
|
|
17
|
-
if (parsed.type === 'resize' &&
|
|
17
|
+
if (parsed.type === 'resize' &&
|
|
18
|
+
resizeCols !== undefined &&
|
|
19
|
+
resizeRows !== undefined &&
|
|
20
|
+
resizeCols > 0 &&
|
|
21
|
+
resizeRows > 0) {
|
|
18
22
|
const message = {
|
|
19
23
|
type: 'resize',
|
|
20
24
|
cols: resizeCols,
|
|
@@ -22,10 +26,12 @@ export const parseTerminalControlMessage = (raw) => {
|
|
|
22
26
|
};
|
|
23
27
|
const parsedPixelWidth = asInteger(pixelWidth);
|
|
24
28
|
const parsedPixelHeight = asInteger(pixelHeight);
|
|
25
|
-
if (parsedPixelWidth !== undefined)
|
|
29
|
+
if (parsedPixelWidth !== undefined && parsedPixelWidth >= 0) {
|
|
26
30
|
message.pixelWidth = parsedPixelWidth;
|
|
27
|
-
|
|
31
|
+
}
|
|
32
|
+
if (parsedPixelHeight !== undefined && parsedPixelHeight >= 0) {
|
|
28
33
|
message.pixelHeight = parsedPixelHeight;
|
|
34
|
+
}
|
|
29
35
|
return message;
|
|
30
36
|
}
|
|
31
37
|
throw new Error('Invalid terminal control message');
|
|
@@ -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
|
|
89
|
-
controlSocket
|
|
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
|
-
|
|
113
|
-
socket.send(serializeTerminalRestore(snapshot));
|
|
114
|
+
sendWebSocketMessage(socket, serializeTerminalRestore(snapshot), `terminal ${runId} restore`);
|
|
114
115
|
})
|
|
115
116
|
.catch(() => {
|
|
116
|
-
|
|
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
|
|
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
|
-
|
|
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?.
|
|
180
|
-
viewer.controlSocket?.
|
|
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
|
-
|
|
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
|
-
|
|
79
|
+
rejectWebSocketUpgrade(socket, '403 Forbidden');
|
|
79
80
|
return;
|
|
80
81
|
}
|
|
81
82
|
if (!validateUpgradeSession(request)) {
|
|
82
|
-
|
|
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
|
-
|
|
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,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,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,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;
|