agent-relay-server 0.108.0 → 0.110.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/docs/openapi.json +143 -1
- package/package.json +3 -3
- package/public/assets/MessageBubble-CmqJMGYF.js +4 -0
- package/public/assets/{MessageBubble-DYJezSjU.js.map → MessageBubble-CmqJMGYF.js.map} +1 -1
- package/public/assets/PlanGraphPanel-K6aFJmdd.js +2 -0
- package/public/assets/PlanGraphPanel-K6aFJmdd.js.map +1 -0
- package/public/assets/activity-DyqKrJe_.js +2 -0
- package/public/assets/activity-DyqKrJe_.js.map +1 -0
- package/public/assets/{agent-profiles-Di2w5U1o.js → agent-profiles-DRQ5vbPo.js} +2 -2
- package/public/assets/{agent-profiles-Di2w5U1o.js.map → agent-profiles-DRQ5vbPo.js.map} +1 -1
- package/public/assets/agents-BJ9sm23O.js +2 -0
- package/public/assets/{agents-GSJXvmU1.js.map → agents-BJ9sm23O.js.map} +1 -1
- package/public/assets/{analytics-B9USQLl5.js → analytics-Q6Dl_CE9.js} +3 -3
- package/public/assets/{analytics-B9USQLl5.js.map → analytics-Q6Dl_CE9.js.map} +1 -1
- package/public/assets/{automation-XRePs1dT.js → automation-CJ2-uiYt.js} +2 -2
- package/public/assets/{automation-XRePs1dT.js.map → automation-CJ2-uiYt.js.map} +1 -1
- package/public/assets/{badge-D8fABzMs.js → badge-kdQDWO6e.js} +2 -2
- package/public/assets/{badge-D8fABzMs.js.map → badge-kdQDWO6e.js.map} +1 -1
- package/public/assets/{branch-state-badge-C_VNyER3.js → branch-state-badge-BRjUWyMT.js} +2 -2
- package/public/assets/{branch-state-badge-C_VNyER3.js.map → branch-state-badge-BRjUWyMT.js.map} +1 -1
- package/public/assets/{button-KASZcscA.js → button-BKFvNXa3.js} +2 -2
- package/public/assets/{button-KASZcscA.js.map → button-BKFvNXa3.js.map} +1 -1
- package/public/assets/card-B0y1i_2D.js +2 -0
- package/public/assets/{card-Cds0MWME.js.map → card-B0y1i_2D.js.map} +1 -1
- package/public/assets/{channels-DACvNoC2.js → channels-DOYB1iiA.js} +2 -2
- package/public/assets/{channels-DACvNoC2.js.map → channels-DOYB1iiA.js.map} +1 -1
- package/public/assets/chat-D0VtHShz.js +2 -0
- package/public/assets/{chat-D_0uHgB6.js.map → chat-D0VtHShz.js.map} +1 -1
- package/public/assets/{connectors-C0B38LUK.js → connectors-C5CeK-2M.js} +2 -2
- package/public/assets/{connectors-C0B38LUK.js.map → connectors-C5CeK-2M.js.map} +1 -1
- package/public/assets/{copy-button-d9DFNX9v.js → copy-button-aLaiAkv2.js} +2 -2
- package/public/assets/{copy-button-d9DFNX9v.js.map → copy-button-aLaiAkv2.js.map} +1 -1
- package/public/assets/{display-1J__Ti1K.js → display-_5kFrHJh.js} +3 -3
- package/public/assets/{display-1J__Ti1K.js.map → display-_5kFrHJh.js.map} +1 -1
- package/public/assets/{dist-DOAjFOS4.js → dist-BDYU37NZ.js} +2 -2
- package/public/assets/{dist-DOAjFOS4.js.map → dist-BDYU37NZ.js.map} +1 -1
- package/public/assets/dist-BnfqT2cl.js +2 -0
- package/public/assets/{dist-DC-TqTz2.js.map → dist-BnfqT2cl.js.map} +1 -1
- package/public/assets/dist-CF28Y5wI.js +2 -0
- package/public/assets/{dist-CBVZk9U8.js.map → dist-CF28Y5wI.js.map} +1 -1
- package/public/assets/{dist-DTXKbk2K.js → dist-DDh8a_SK.js} +2 -2
- package/public/assets/{dist-DTXKbk2K.js.map → dist-DDh8a_SK.js.map} +1 -1
- package/public/assets/{dist-BbUJ1Vht.js → dist-DKoRwEZq.js} +2 -2
- package/public/assets/{dist-BbUJ1Vht.js.map → dist-DKoRwEZq.js.map} +1 -1
- package/public/assets/{dist-UhEAFdvZ.js → dist-DQPz52Zx.js} +2 -2
- package/public/assets/{dist-UhEAFdvZ.js.map → dist-DQPz52Zx.js.map} +1 -1
- package/public/assets/{dist-CgqfdHSt.js → dist-D_qcpfbI.js} +2 -2
- package/public/assets/{dist-CgqfdHSt.js.map → dist-D_qcpfbI.js.map} +1 -1
- package/public/assets/{dist-DybScJmm.js → dist-DnyQI0qt.js} +2 -2
- package/public/assets/{dist-DybScJmm.js.map → dist-DnyQI0qt.js.map} +1 -1
- package/public/assets/{dist-BwwlmyBq.js → dist-EO8jbi2F.js} +2 -2
- package/public/assets/{dist-BwwlmyBq.js.map → dist-EO8jbi2F.js.map} +1 -1
- package/public/assets/{dist-BlZvUZZB.js → dist-avJPT3qF.js} +2 -2
- package/public/assets/{dist-BlZvUZZB.js.map → dist-avJPT3qF.js.map} +1 -1
- package/public/assets/dist-cyag2tir.js +2 -0
- package/public/assets/{dist-DreO5AcZ.js.map → dist-cyag2tir.js.map} +1 -1
- package/public/assets/{es2015-DUjFyjDp.js → es2015-C9FSEGd7.js} +2 -2
- package/public/assets/{es2015-DUjFyjDp.js.map → es2015-C9FSEGd7.js.map} +1 -1
- package/public/assets/{formatted-body-impl-BKWCAjBd.js → formatted-body-impl-ChCxpVtI.js} +2 -2
- package/public/assets/{formatted-body-impl-BKWCAjBd.js.map → formatted-body-impl-ChCxpVtI.js.map} +1 -1
- package/public/assets/index-BIRtwnUI.js +23 -0
- package/public/assets/index-BIRtwnUI.js.map +1 -0
- package/public/assets/index-D-Ntiy3M.css +2 -0
- package/public/assets/{input-CtHfLn1i.js → input-DfN6sUbX.js} +2 -2
- package/public/assets/{input-CtHfLn1i.js.map → input-DfN6sUbX.js.map} +1 -1
- package/public/assets/{insights-BzNoYvDe.js → insights-BGuB8DWZ.js} +2 -2
- package/public/assets/{insights-BzNoYvDe.js.map → insights-BGuB8DWZ.js.map} +1 -1
- package/public/assets/{integrations-DLzJpt9G.js → integrations-CztQfrI7.js} +2 -2
- package/public/assets/{integrations-DLzJpt9G.js.map → integrations-CztQfrI7.js.map} +1 -1
- package/public/assets/{lib-l-JkMVR9.js → lib-DRRHEqRZ.js} +2 -2
- package/public/assets/{lib-l-JkMVR9.js.map → lib-DRRHEqRZ.js.map} +1 -1
- package/public/assets/{lucide-react-C4eXYCwx.js → lucide-react-DmbfpRvJ.js} +2 -2
- package/public/assets/{lucide-react-C4eXYCwx.js.map → lucide-react-DmbfpRvJ.js.map} +1 -1
- package/public/assets/{maintenance-B6_0-ECm.js → maintenance-BFyuZuv5.js} +2 -2
- package/public/assets/{maintenance-B6_0-ECm.js.map → maintenance-BFyuZuv5.js.map} +1 -1
- package/public/assets/managed-agents-BIXZUkrA.js +4 -0
- package/public/assets/{managed-agents-CpzKXDtV.js.map → managed-agents-BIXZUkrA.js.map} +1 -1
- package/public/assets/markdown-preview-impl-DTI--VmH.js +2 -0
- package/public/assets/{markdown-preview-impl-DxBnlJ2k.js.map → markdown-preview-impl-DTI--VmH.js.map} +1 -1
- package/public/assets/memory-OoM2E30h.js +2 -0
- package/public/assets/{memory-Bnm7ILWL.js.map → memory-OoM2E30h.js.map} +1 -1
- package/public/assets/{messages-D9LkocTc.js → messages-BHvC3SNR.js} +2 -2
- package/public/assets/{messages-D9LkocTc.js.map → messages-BHvC3SNR.js.map} +1 -1
- package/public/assets/{orchestrators-C__JfrNr.js → orchestrators-9pzQqf6-.js} +2 -2
- package/public/assets/{orchestrators-C__JfrNr.js.map → orchestrators-9pzQqf6-.js.map} +1 -1
- package/public/assets/overview-L8iYOYmR.js +2 -0
- package/public/assets/{overview-BQ0b0V7Q.js.map → overview-L8iYOYmR.js.map} +1 -1
- package/public/assets/{pairs-ChyeBs0Z.js → pairs-BCejxa6_.js} +2 -2
- package/public/assets/{pairs-ChyeBs0Z.js.map → pairs-BCejxa6_.js.map} +1 -1
- package/public/assets/projects-cOeuiMjl.js +2 -0
- package/public/assets/{projects-v5Kp1Oaq.js.map → projects-cOeuiMjl.js.map} +1 -1
- package/public/assets/prompt-settings-D9y9hhSy.js +2 -0
- package/public/assets/{prompt-settings-YhAOlYc6.js.map → prompt-settings-D9y9hhSy.js.map} +1 -1
- package/public/assets/{react-dom-BMF4hNzG.js → react-dom-jQ4Vs380.js} +2 -2
- package/public/assets/{react-dom-BMF4hNzG.js.map → react-dom-jQ4Vs380.js.map} +1 -1
- package/public/assets/registry-BwBg64Vj.js +2 -0
- package/public/assets/{registry-DZoAfrzx.js.map → registry-BwBg64Vj.js.map} +1 -1
- package/public/assets/{security-DRCj00eA.js → security-upd2zf8l.js} +2 -2
- package/public/assets/{security-DRCj00eA.js.map → security-upd2zf8l.js.map} +1 -1
- package/public/assets/{select-ZOpLw2Fd.js → select-Cu55Vi-R.js} +2 -2
- package/public/assets/{select-ZOpLw2Fd.js.map → select-Cu55Vi-R.js.map} +1 -1
- package/public/assets/settings-Dk5iNqCZ.js +6 -0
- package/public/assets/{settings-DGgwr9WH.js.map → settings-Dk5iNqCZ.js.map} +1 -1
- package/public/assets/store-DDWg67Pg.js +9 -0
- package/public/assets/store-DDWg67Pg.js.map +1 -0
- package/public/assets/tasks-BoDuvHnZ.js +2 -0
- package/public/assets/tasks-BoDuvHnZ.js.map +1 -0
- package/public/assets/teams-C6Pn-gpw.js +2 -0
- package/public/assets/{teams-BJpaoG2m.js.map → teams-C6Pn-gpw.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-CQhh6oXa.js → terminal-viewer-impl-afW96xEF.js} +3 -3
- package/public/assets/{terminal-viewer-impl-CQhh6oXa.js.map → terminal-viewer-impl-afW96xEF.js.map} +1 -1
- package/public/assets/{types-BiJFuTiL.js → types-DSwhOFcJ.js} +2 -2
- package/public/assets/types-DSwhOFcJ.js.map +1 -0
- package/public/assets/{use-keyboard-viewport-RYMALvu1.js → use-keyboard-viewport-Dw2CHZ50.js} +2 -2
- package/public/assets/{use-keyboard-viewport-RYMALvu1.js.map → use-keyboard-viewport-Dw2CHZ50.js.map} +1 -1
- package/public/assets/user-avatar-tSPnqEk5.js +2 -0
- package/public/assets/{user-avatar-BYGrUx1U.js.map → user-avatar-tSPnqEk5.js.map} +1 -1
- package/public/assets/{work-queue-BH2-mARc.js → work-queue-DP3UdDwC.js} +2 -2
- package/public/assets/{work-queue-BH2-mARc.js.map → work-queue-DP3UdDwC.js.map} +1 -1
- package/public/assets/workspaces-Dt2xndG4.js +3 -0
- package/public/assets/workspaces-Dt2xndG4.js.map +1 -0
- package/public/index.html +23 -23
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/src/config.ts +5 -0
- package/src/bus.ts +3 -1
- package/src/db/merge-lease.ts +5 -1
- package/src/db/migrations.ts +21 -1
- package/src/db/schema.ts +7 -1
- package/src/db/task-detail-projection.ts +66 -0
- package/src/db/task-entity.ts +51 -0
- package/src/mcp/tools-spawn.ts +1 -1
- package/src/mcp-dispatch-tools.ts +49 -17
- package/src/mcp-task-memory-tools.ts +4 -21
- package/src/routes/agents-spawn.ts +3 -2
- package/src/routes/agents.ts +2 -0
- package/src/routes/index.ts +5 -1
- package/src/routes/messages.ts +6 -6
- package/src/routes/tasks.ts +76 -2
- package/src/runtime-tokens.ts +4 -0
- package/src/security.ts +8 -0
- package/src/services/register-agent.ts +46 -2
- package/src/services/spawn-agent.ts +114 -30
- package/src/services/task-entity.ts +8 -0
- package/src/spawn-command.ts +3 -0
- package/src/task-authz.ts +52 -0
- package/public/assets/MessageBubble-DYJezSjU.js +0 -4
- package/public/assets/PlanGraphPanel-MWcVw38W.js +0 -2
- package/public/assets/PlanGraphPanel-MWcVw38W.js.map +0 -1
- package/public/assets/activity-Db3X3dg8.js +0 -2
- package/public/assets/activity-Db3X3dg8.js.map +0 -1
- package/public/assets/agents-GSJXvmU1.js +0 -2
- package/public/assets/card-Cds0MWME.js +0 -2
- package/public/assets/chat-D_0uHgB6.js +0 -2
- package/public/assets/dist-CBVZk9U8.js +0 -2
- package/public/assets/dist-DC-TqTz2.js +0 -2
- package/public/assets/dist-DreO5AcZ.js +0 -2
- package/public/assets/index-BjjzI2LT.css +0 -2
- package/public/assets/index-DlBSuzd2.js +0 -23
- package/public/assets/index-DlBSuzd2.js.map +0 -1
- package/public/assets/managed-agents-CpzKXDtV.js +0 -4
- package/public/assets/markdown-preview-impl-DxBnlJ2k.js +0 -2
- package/public/assets/memory-Bnm7ILWL.js +0 -2
- package/public/assets/overview-BQ0b0V7Q.js +0 -2
- package/public/assets/projects-v5Kp1Oaq.js +0 -2
- package/public/assets/prompt-settings-YhAOlYc6.js +0 -2
- package/public/assets/registry-DZoAfrzx.js +0 -2
- package/public/assets/settings-DGgwr9WH.js +0 -6
- package/public/assets/store-Bnag1kY-.js +0 -9
- package/public/assets/store-Bnag1kY-.js.map +0 -1
- package/public/assets/tasks-DUTu8ZhA.js +0 -2
- package/public/assets/tasks-DUTu8ZhA.js.map +0 -1
- package/public/assets/teams-BJpaoG2m.js +0 -2
- package/public/assets/types-BiJFuTiL.js.map +0 -1
- package/public/assets/user-avatar-BYGrUx1U.js +0 -2
- package/public/assets/workspaces-DldYyXZR.js +0 -3
- package/public/assets/workspaces-DldYyXZR.js.map +0 -1
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { McpNotFoundError } from "./mcp-errors";
|
|
10
10
|
import { ValidationError } from "./db";
|
|
11
11
|
import { authContextFromMcp } from "./services/auth-context";
|
|
12
|
-
import { addTaskEntityDependency, createTaskEntity, getTaskEntity } from "./services/task-entity";
|
|
12
|
+
import { addTaskEntityDependency, createTaskEntity, getTaskEntity, recordTaskEntityDispatchDecision } from "./services/task-entity";
|
|
13
13
|
import { dispatchTask, type DispatchPin } from "./task-dispatcher";
|
|
14
14
|
import { cleanString } from "./validation";
|
|
15
15
|
import {
|
|
@@ -75,7 +75,7 @@ function taskIdArg(value: unknown): number {
|
|
|
75
75
|
return n;
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
function pinArg(value: unknown): DispatchPin {
|
|
78
|
+
export function pinArg(value: unknown): DispatchPin {
|
|
79
79
|
if (value === undefined || value === null) return {};
|
|
80
80
|
if (!isRecord(value)) throw new ValidationError("pin must be an object");
|
|
81
81
|
const pin: DispatchPin = {};
|
|
@@ -93,7 +93,7 @@ function pinArg(value: unknown): DispatchPin {
|
|
|
93
93
|
return pin;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
function dispatchShapeArg(value: unknown): TaskDispatch | undefined {
|
|
96
|
+
export function dispatchShapeArg(value: unknown): TaskDispatch | undefined {
|
|
97
97
|
if (value === undefined || value === null) return undefined;
|
|
98
98
|
if (!isRecord(value)) throw new ValidationError("dispatch must be an object");
|
|
99
99
|
return value as TaskDispatch;
|
|
@@ -108,37 +108,49 @@ function findExistingReviewDependent(dependentIds: number[]): number | undefined
|
|
|
108
108
|
return undefined;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
export
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
export interface RunTaskDispatchInput {
|
|
112
|
+
taskId?: number;
|
|
113
|
+
dispatch?: TaskDispatch;
|
|
114
|
+
pin?: DispatchPin;
|
|
115
|
+
createReview?: boolean;
|
|
116
|
+
actor: string;
|
|
117
|
+
}
|
|
115
118
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
+
export interface RunTaskDispatchResult {
|
|
120
|
+
taskId?: number;
|
|
121
|
+
decision: ReturnType<typeof dispatchTask>;
|
|
122
|
+
reviewTaskId?: number;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Core dispatch logic shared between the MCP tool (relay_task_dispatch) and the HTTP route
|
|
127
|
+
* (POST /api/tasks/:id/dispatch). Both paths call this function — do NOT fork it (#845).
|
|
128
|
+
*/
|
|
129
|
+
export function runTaskDispatch(input: RunTaskDispatchInput): RunTaskDispatchResult {
|
|
130
|
+
const { taskId, dispatch: inlineShape, pin = {}, createReview = true, actor } = input;
|
|
119
131
|
|
|
120
132
|
let parentTitle: string | undefined;
|
|
121
133
|
let parentTeamId: string | undefined;
|
|
122
134
|
let parentDependents: number[] = [];
|
|
123
135
|
let shape = inlineShape;
|
|
136
|
+
|
|
124
137
|
if (taskId !== undefined) {
|
|
125
138
|
const task = getTaskEntity(taskId);
|
|
126
139
|
if (!task) throw new McpNotFoundError("task not found");
|
|
127
140
|
parentTitle = task.title;
|
|
128
141
|
parentTeamId = task.teamId;
|
|
129
142
|
parentDependents = task.dependents;
|
|
130
|
-
// Inline dispatch (if supplied) overrides the stored work-shape for this call.
|
|
131
143
|
shape = inlineShape ?? task.dispatch;
|
|
132
144
|
}
|
|
133
145
|
if (!shape && !inlineShape) shape = {};
|
|
134
146
|
|
|
135
|
-
const decision = dispatchTask(shape,
|
|
147
|
+
const decision = dispatchTask(shape, pin);
|
|
148
|
+
if (taskId !== undefined) {
|
|
149
|
+
recordTaskEntityDispatchDecision(taskId, { decision, source: "dispatch", actor });
|
|
150
|
+
}
|
|
136
151
|
|
|
137
152
|
let reviewTaskId: number | undefined;
|
|
138
|
-
const createReview = args.createReview !== false; // default true
|
|
139
153
|
if (decision.review && createReview && taskId !== undefined) {
|
|
140
|
-
// Idempotent: if this build Task already has a review dependent, reuse it rather than
|
|
141
|
-
// spawning a duplicate on every re-dispatch.
|
|
142
154
|
const existing = findExistingReviewDependent(parentDependents);
|
|
143
155
|
if (existing !== undefined) {
|
|
144
156
|
reviewTaskId = existing;
|
|
@@ -150,7 +162,6 @@ export function relayDispatchTool(auth: McpAuthLike, name: string, args: Record<
|
|
|
150
162
|
createdBy: actor,
|
|
151
163
|
...(parentTeamId ? { teamId: parentTeamId } : {}),
|
|
152
164
|
});
|
|
153
|
-
// The review Task depends on the build Task (build must complete first).
|
|
154
165
|
addTaskEntityDependency(review.id, taskId, actor);
|
|
155
166
|
reviewTaskId = review.id;
|
|
156
167
|
}
|
|
@@ -158,7 +169,28 @@ export function relayDispatchTool(auth: McpAuthLike, name: string, args: Record<
|
|
|
158
169
|
|
|
159
170
|
return {
|
|
160
171
|
...(taskId !== undefined ? { taskId } : {}),
|
|
161
|
-
decision
|
|
172
|
+
decision,
|
|
162
173
|
...(reviewTaskId !== undefined ? { reviewTaskId } : {}),
|
|
163
174
|
};
|
|
164
175
|
}
|
|
176
|
+
|
|
177
|
+
export function relayDispatchTool(auth: McpAuthLike, name: string, args: Record<string, unknown>): Record<string, unknown> {
|
|
178
|
+
const actor = authContextFromMcp(auth).actor.id;
|
|
179
|
+
if (!isRecord(args)) throw new ValidationError("arguments must be an object");
|
|
180
|
+
if (name !== "relay_task_dispatch") throw new ValidationError(`unknown tool: ${name}`);
|
|
181
|
+
|
|
182
|
+
const hasTaskId = args.taskId !== undefined && args.taskId !== null;
|
|
183
|
+
const result = runTaskDispatch({
|
|
184
|
+
taskId: hasTaskId ? taskIdArg(args.taskId) : undefined,
|
|
185
|
+
dispatch: dispatchShapeArg(args.dispatch),
|
|
186
|
+
pin: pinArg(args.pin),
|
|
187
|
+
createReview: args.createReview !== false,
|
|
188
|
+
actor,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return {
|
|
192
|
+
...(result.taskId !== undefined ? { taskId: result.taskId } : {}),
|
|
193
|
+
decision: result.decision as unknown as Record<string, unknown>,
|
|
194
|
+
...(result.reviewTaskId !== undefined ? { reviewTaskId: result.reviewTaskId } : {}),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
@@ -1,10 +1,6 @@
|
|
|
1
|
-
import { stringValue } from "agent-relay-sdk";
|
|
2
1
|
import {
|
|
3
2
|
appendTaskMemoryDecision,
|
|
4
3
|
appendTaskMemoryRuledOut,
|
|
5
|
-
deliverableTaskIdsByOwnerAgent,
|
|
6
|
-
deliverableTaskIdsBySpawnRequestId,
|
|
7
|
-
getAgent,
|
|
8
4
|
readTaskMemoryEnvelope,
|
|
9
5
|
replaceTaskMemoryWorkingState,
|
|
10
6
|
ValidationError,
|
|
@@ -13,6 +9,7 @@ import { McpAuthError } from "./mcp-errors";
|
|
|
13
9
|
import { callerAgentId } from "./mcp/identity";
|
|
14
10
|
import { optionalPositiveInt, stringField } from "./mcp/validation";
|
|
15
11
|
import { renderContinuation } from "./self-resume";
|
|
12
|
+
import { agentBoundTaskIds, hasTaskReadAdminScope } from "./task-authz";
|
|
16
13
|
import type { McpAuthContext, ToolDefinition } from "./mcp/types";
|
|
17
14
|
|
|
18
15
|
const TASK_ID_PROP = { type: "integer", minimum: 1, description: "Task id. Optional when the caller is bound to exactly one Task." } as const;
|
|
@@ -96,11 +93,12 @@ export function relayTaskMemoryTool(auth: McpAuthContext, name: string, args: Re
|
|
|
96
93
|
|
|
97
94
|
function resolveTaskId(auth: McpAuthContext, args: Record<string, unknown>): number {
|
|
98
95
|
const explicit = optionalPositiveInt(args.taskId, "taskId");
|
|
99
|
-
const
|
|
96
|
+
const agentId = callerAgentId(auth);
|
|
97
|
+
const ids = agentId ? agentBoundTaskIds(agentId) : new Set<number>();
|
|
100
98
|
if (explicit !== undefined) {
|
|
101
99
|
// Deliberate privileged escape hatch: admin/server tooling can inspect or repair any
|
|
102
100
|
// task memory row, but ordinary provider tokens must be bound to the explicit task id.
|
|
103
|
-
if (
|
|
101
|
+
if (hasTaskReadAdminScope(auth.scopes) || ids.has(explicit)) return explicit;
|
|
104
102
|
throw new McpAuthError("taskId is not bound to the caller");
|
|
105
103
|
}
|
|
106
104
|
if (ids.size === 1) return [...ids][0]!;
|
|
@@ -108,21 +106,6 @@ function resolveTaskId(auth: McpAuthContext, args: Record<string, unknown>): num
|
|
|
108
106
|
throw new McpAuthError("caller is bound to multiple Tasks; pass taskId explicitly");
|
|
109
107
|
}
|
|
110
108
|
|
|
111
|
-
function boundTaskIds(auth: McpAuthContext): Set<number> {
|
|
112
|
-
const agentId = callerAgentId(auth);
|
|
113
|
-
if (!agentId) return new Set();
|
|
114
|
-
const ids = new Set<number>(deliverableTaskIdsByOwnerAgent(agentId));
|
|
115
|
-
const spawnRequestId = stringValue(getAgent(agentId)?.meta?.spawnRequestId);
|
|
116
|
-
if (spawnRequestId) {
|
|
117
|
-
for (const id of deliverableTaskIdsBySpawnRequestId(spawnRequestId)) ids.add(id);
|
|
118
|
-
}
|
|
119
|
-
return ids;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function hasTaskMemoryAdminScope(auth: McpAuthContext): boolean {
|
|
123
|
-
return auth.scopes.includes("*") || auth.scopes.includes("admin:*") || auth.scopes.includes("system:admin") || auth.scopes.includes("task:admin");
|
|
124
|
-
}
|
|
125
|
-
|
|
126
109
|
function response(envelope: ReturnType<typeof readTaskMemoryEnvelope>): Record<string, unknown> {
|
|
127
110
|
return {
|
|
128
111
|
taskId: envelope.taskId,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Auto-split from routes.ts (#299). Domain: agents-spawn.
|
|
2
|
-
import { APPROVAL_MODES, AUTO_MERGE_POLICIES, SPAWN_PROVIDERS, VALID_WORKSPACE_MODES, isRecord } from "agent-relay-sdk";
|
|
2
|
+
import { APPROVAL_MODES, AUTO_MERGE_POLICIES, SPAWN_PROVIDERS, VALID_AGENT_LIFECYCLES, VALID_WORKSPACE_MODES, isRecord } from "agent-relay-sdk";
|
|
3
3
|
import { McpAuthError, McpNotFoundError } from "../mcp-errors";
|
|
4
4
|
import { VALID_AGENT_STATUSES, error, json, parseBody, type Handler } from "./_shared";
|
|
5
5
|
import { ValidationError, listAgents } from "../db";
|
|
@@ -15,7 +15,7 @@ import { authContextFromRequest } from "../services/auth-context";
|
|
|
15
15
|
import { listHostDirectories } from "../agent-spawn";
|
|
16
16
|
import { rankRouteCandidates, type RouteAdvisorInput } from "../context-router";
|
|
17
17
|
import { quotaStateFromUnknown } from "../quota";
|
|
18
|
-
import { type AgentKind, type RegisterAgentInput, type SpawnApprovalMode, type SpawnProvider, type WorkspaceAutoMergePolicy, type WorkspaceLandingPolicy, type WorkspaceMode } from "../types";
|
|
18
|
+
import { type AgentKind, type AgentLifecycle, type RegisterAgentInput, type SpawnApprovalMode, type SpawnProvider, type WorkspaceAutoMergePolicy, type WorkspaceLandingPolicy, type WorkspaceMode } from "../types";
|
|
19
19
|
import { type ProviderEffort } from "agent-relay-sdk/provider-catalog";
|
|
20
20
|
|
|
21
21
|
const VALID_AGENT_KINDS = ["provider", "channel", "orchestrator", "system", "user"] as const;
|
|
@@ -128,6 +128,7 @@ export const postAgentSpawn: Handler = async (req) => {
|
|
|
128
128
|
effort: cleanString(parsed.body.effort, "effort", { max: 120 }) as ProviderEffort | undefined,
|
|
129
129
|
approvalMode: optionalEnum(parsed.body.approvalMode, "approvalMode", APPROVAL_MODES) as SpawnApprovalMode | undefined,
|
|
130
130
|
workspaceMode: optionalEnum(parsed.body.workspaceMode, "workspaceMode", VALID_WORKSPACE_MODES) as WorkspaceMode | undefined,
|
|
131
|
+
lifecycle: optionalEnum(parsed.body.lifecycle, "lifecycle", VALID_AGENT_LIFECYCLES) as AgentLifecycle | undefined,
|
|
131
132
|
label: cleanString(parsed.body.label, "label", { max: 120 }),
|
|
132
133
|
policyName: cleanString(parsed.body.policyName, "policyName", { max: 120 }),
|
|
133
134
|
profile: cleanString(parsed.body.profile, "profile", { max: 120 }),
|
package/src/routes/agents.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { cleanStringArray } from "../validation";
|
|
|
7
7
|
import { clearActiveMemories, memoryBroker } from "../memory-service";
|
|
8
8
|
import { emitAgentRemoved, emitAgentStatus } from "../sse";
|
|
9
9
|
import { notifyAgentReady } from "../agent-lifecycle-events";
|
|
10
|
+
import { bindSpawnedTaskToReadyAgent } from "../services/register-agent";
|
|
10
11
|
import { flushQueuedDirectMessages } from "../services/managed-running";
|
|
11
12
|
import { isAgentUnavailable } from "../db/agent-predicates";
|
|
12
13
|
import { getCompactionWatch } from "../compaction-watch";
|
|
@@ -299,6 +300,7 @@ export const patchAgentReady: Handler = async (req, params) => {
|
|
|
299
300
|
// agents and dedups repeat readies.
|
|
300
301
|
if (body.ready && !before?.ready) {
|
|
301
302
|
notifyAgentReady(params.id!);
|
|
303
|
+
bindSpawnedTaskToReadyAgent(params.id!, "agent-ready");
|
|
302
304
|
// #416 — also flush queued direct messages: a not-ready agent that becomes ready
|
|
303
305
|
// may have queued messages waiting (e.g. starting up, clearing context).
|
|
304
306
|
flushQueuedDirectMessages(params.id!);
|
package/src/routes/index.ts
CHANGED
|
@@ -45,7 +45,7 @@ import { getRecipeByName, getRecipeInstanceArtifacts, getRecipeInstanceById, get
|
|
|
45
45
|
import { getSettingRoute, getSettingsNamespaceRoute, getSettingsRoute, putSettingRoute, putSettingsDefaultRoute } from "./settings";
|
|
46
46
|
import { postNotificationPreviewRoute } from "./notifications";
|
|
47
47
|
import { getStewardConfigRoute, getWorkspaceConfigRoute, putStewardConfigRoute, putWorkspaceConfigRoute } from "./steward";
|
|
48
|
-
import { getTaskById, getTaskEvents, getTasks, patchTaskStatus, postClaimTask, postRenewTaskClaim, postTaskCreate, getTaskDetailRoute, patchTaskEntity, postTaskEntityStatus, postTaskEntityStage, postTaskEntityGate, postTaskDependency, deleteTaskDependency, postTaskRef, deleteTaskRef, postTaskAssign, postTaskComment, getTaskCloseBridge } from "./tasks";
|
|
48
|
+
import { getTaskById, getTaskEntities, getTaskEvents, getTasks, patchTaskStatus, postClaimTask, postRenewTaskClaim, postTaskCreate, getTaskDetailRoute, getTaskMemoryRoute, getTaskHistoryRoute, patchTaskEntity, postTaskEntityStatus, postTaskEntityStage, postTaskEntityGate, postTaskDependency, deleteTaskDependency, postTaskRef, deleteTaskRef, postTaskAssign, postTaskComment, getTaskCloseBridge, postTaskDispatch } from "./tasks";
|
|
49
49
|
import { getMyTeamRoute, getTeamOutcomesRoute, getTeamsRoute, postMyTeamDissolveRoute, putMyTeamBriefRoute } from "./teams";
|
|
50
50
|
import { postTeamDissolveByIdRoute } from "./teams";
|
|
51
51
|
import { deleteTeamTemplateRoute, getTeamTemplateRoute, getTeamTemplatesRoute, postTeamTemplateRoute, putTeamTemplateRoute } from "./team-templates";
|
|
@@ -350,10 +350,13 @@ const routes: Route[] = [
|
|
|
350
350
|
route("PATCH", "/api/automations/:id", patchAutomation),
|
|
351
351
|
route("DELETE", "/api/automations/:id", deleteAutomationById),
|
|
352
352
|
route("POST", "/api/automations/:id/run", postAutomationRun),
|
|
353
|
+
route("GET", "/api/task-entities", getTaskEntities),
|
|
353
354
|
route("GET", "/api/tasks", getTasks),
|
|
354
355
|
route("POST", "/api/tasks", postTaskCreate),
|
|
355
356
|
route("GET", "/api/tasks/:id", getTaskById),
|
|
356
357
|
route("GET", "/api/tasks/:id/detail", getTaskDetailRoute),
|
|
358
|
+
route("GET", "/api/tasks/:id/memory", getTaskMemoryRoute),
|
|
359
|
+
route("GET", "/api/tasks/:id/history", getTaskHistoryRoute),
|
|
357
360
|
route("GET", "/api/tasks/:id/artifacts", getTaskArtifactsRoute),
|
|
358
361
|
route("GET", "/api/tasks/:id/events", getTaskEvents),
|
|
359
362
|
route("POST", "/api/tasks/:id/claim", postClaimTask),
|
|
@@ -370,6 +373,7 @@ const routes: Route[] = [
|
|
|
370
373
|
route("POST", "/api/tasks/:id/assign", postTaskAssign),
|
|
371
374
|
route("POST", "/api/tasks/:id/comments", postTaskComment),
|
|
372
375
|
route("GET", "/api/tasks/:id/close-bridge", getTaskCloseBridge),
|
|
376
|
+
route("POST", "/api/tasks/:id/dispatch", postTaskDispatch),
|
|
373
377
|
|
|
374
378
|
route("POST", "/api/pairs", postPair),
|
|
375
379
|
route("GET", "/api/pairs", getPairs),
|
package/src/routes/messages.ts
CHANGED
|
@@ -162,12 +162,12 @@ export const postMessage: Handler = async (req) => {
|
|
|
162
162
|
// injection needs request-derived context; the "message sent" audit is the HTTP domain row.
|
|
163
163
|
const autoMemoryTarget = automaticMemoryTarget(message);
|
|
164
164
|
if (autoMemoryTarget) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
165
|
+
// Fire-and-forget: capture context synchronously then inject off the response path so
|
|
166
|
+
// POST latency is not gated on memory assembly (A1 fix, #847).
|
|
167
|
+
const ctx = memoryContext(req);
|
|
168
|
+
void injectMemoryForMessageDelivery(message, autoMemoryTarget, ctx)
|
|
169
|
+
.then((memoryInjection) => { if (memoryInjection) emitCommand(memoryInjection.command); })
|
|
170
|
+
.catch((e) => { console.warn(`[memory] automatic message context assembly failed: ${errMessage(e)}`); });
|
|
171
171
|
}
|
|
172
172
|
const isWork = message.kind === "task";
|
|
173
173
|
auditEvent({
|
package/src/routes/tasks.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
// Auto-split from routes.ts (#299). Domain: tasks.
|
|
2
2
|
import { MAX_BODY_BYTES } from "../config";
|
|
3
3
|
import { VALID_TASK_STATUSES, agentSessionStatus, auditEvent, dispatchTaskCallbacks, emitCommand, error, json, normalizeAgentSessionGuard, parseBody, parseId, parseQueryInt, resolveActorLabel, type Handler } from "./_shared";
|
|
4
|
+
import { getComponentAuth, isFullReachRequest } from "../security";
|
|
5
|
+
import { authContextFromRequest } from "../services/auth-context";
|
|
6
|
+
import { agentIdFromComponent, isTaskDispatchAuthorized, isTaskReadAuthorized } from "../task-authz";
|
|
7
|
+
import { dispatchShapeArg, pinArg, runTaskDispatch } from "../mcp-dispatch-tools";
|
|
4
8
|
import { ValidationError, claimTask, getMessage, getTask, listTaskEvents, listTasks, recordTaskEvent, renewTaskClaim, updateTaskStatus } from "../db";
|
|
9
|
+
import { readTaskMemoryEnvelope } from "../db/task-memory";
|
|
5
10
|
import {
|
|
6
11
|
addTaskEntityDependency, addTaskEntityRef, assignTaskEntityAgent, createTaskEntity, getTaskEntity,
|
|
7
|
-
listTaskEntities, recordTaskEntityGate, removeTaskEntityDependency, removeTaskEntityRef, setTaskEntityStage, setTaskEntityStatus, updateTaskEntity,
|
|
12
|
+
listTaskEntities, recordTaskEntityGate, removeTaskEntityDependency, removeTaskEntityRef, setTaskEntityStage, setTaskEntityStatus, taskEntityHistory, updateTaskEntity,
|
|
8
13
|
} from "../services/task-entity";
|
|
9
14
|
import { CommentThroughError, commentOnTaskThrough, taskCloseBridge } from "../services/task-issue-weave";
|
|
10
15
|
import { captureTaskResultMemory, injectMemoryForTaskClaim } from "../memory-service";
|
|
@@ -14,7 +19,9 @@ import { errMessage, isRecord, TASK_DELIVERABLE_STAGES, type TaskBlocker, type T
|
|
|
14
19
|
import { type AgentSessionGuard, type TaskStatus, type TaskStatusInput } from "../types";
|
|
15
20
|
|
|
16
21
|
function actorOf(req: Request, fallback?: unknown): string {
|
|
17
|
-
|
|
22
|
+
if (typeof fallback === "string" && fallback.trim()) return fallback.trim();
|
|
23
|
+
const ctx = authContextFromRequest(req);
|
|
24
|
+
return ctx.callerAgentId ?? resolveActorLabel(req) ?? (ctx.actor.id !== "system" ? ctx.actor.id : "dashboard");
|
|
18
25
|
}
|
|
19
26
|
|
|
20
27
|
function taskDetailOr404(id: number) {
|
|
@@ -61,6 +68,18 @@ export const getTasks: Handler = (req) => {
|
|
|
61
68
|
}));
|
|
62
69
|
};
|
|
63
70
|
|
|
71
|
+
export const getTaskEntities: Handler = (req) => {
|
|
72
|
+
const url = new URL(req.url);
|
|
73
|
+
const limitRaw = parseQueryInt(url.searchParams.get("limit"), { min: 1, max: 500 });
|
|
74
|
+
if (Number.isNaN(limitRaw)) return error("limit must be an integer between 1 and 500");
|
|
75
|
+
return json(listTaskEntities({
|
|
76
|
+
status: url.searchParams.get("status") ?? undefined,
|
|
77
|
+
deliverableStage: url.searchParams.get("deliverableStage") ?? undefined,
|
|
78
|
+
teamId: url.searchParams.get("teamId") ?? undefined,
|
|
79
|
+
limit: limitRaw ?? 100,
|
|
80
|
+
}));
|
|
81
|
+
};
|
|
82
|
+
|
|
64
83
|
// --- #834 durable Task entity (kind='deliverable') HTTP surface ---
|
|
65
84
|
|
|
66
85
|
export const postTaskCreate: Handler = async (req) => {
|
|
@@ -101,6 +120,28 @@ export const getTaskDetailRoute: Handler = (_req, params) => {
|
|
|
101
120
|
return taskDetailOr404(id);
|
|
102
121
|
};
|
|
103
122
|
|
|
123
|
+
export const getTaskMemoryRoute: Handler = (req, params) => {
|
|
124
|
+
const id = parseId(params.id);
|
|
125
|
+
if (id === null) return error("invalid task id");
|
|
126
|
+
if (!getTaskEntity(id)) return error("deliverable task not found", 404);
|
|
127
|
+
const component = getComponentAuth(req);
|
|
128
|
+
const agentId = component ? agentIdFromComponent(component) : undefined;
|
|
129
|
+
const scopes = component?.scope ?? [];
|
|
130
|
+
if (!isTaskReadAuthorized(id, agentId, scopes)) return error("forbidden", 403);
|
|
131
|
+
return json(readTaskMemoryEnvelope(id));
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export const getTaskHistoryRoute: Handler = (req, params) => {
|
|
135
|
+
const id = parseId(params.id);
|
|
136
|
+
if (id === null) return error("invalid task id");
|
|
137
|
+
if (!getTaskEntity(id)) return error("deliverable task not found", 404);
|
|
138
|
+
const component = getComponentAuth(req);
|
|
139
|
+
const agentId = component ? agentIdFromComponent(component) : undefined;
|
|
140
|
+
const scopes = component?.scope ?? [];
|
|
141
|
+
if (!isTaskReadAuthorized(id, agentId, scopes)) return error("forbidden", 403);
|
|
142
|
+
return json(taskEntityHistory(id));
|
|
143
|
+
};
|
|
144
|
+
|
|
104
145
|
export const patchTaskEntity: Handler = async (req, params) => {
|
|
105
146
|
const id = parseId(params.id);
|
|
106
147
|
if (id === null) return error("invalid task id");
|
|
@@ -291,6 +332,39 @@ export const postTaskComment: Handler = async (req, params) => {
|
|
|
291
332
|
}
|
|
292
333
|
};
|
|
293
334
|
|
|
335
|
+
// #845 — HTTP dispatch decision endpoint. Invokes the SAME path as the MCP relay_task_dispatch
|
|
336
|
+
// tool (via runTaskDispatch). Accepts an optional inline dispatch override + pin; always returns
|
|
337
|
+
// the decision + rationale so the UI can show the #839 guard before any spawn. Never spawns.
|
|
338
|
+
// Auth: admin scope OR a bound agent (#848: no scope-only gate).
|
|
339
|
+
export const postTaskDispatch: Handler = async (req, params) => {
|
|
340
|
+
const id = parseId(params.id);
|
|
341
|
+
if (id === null) return error("invalid task id");
|
|
342
|
+
if (!getTaskEntity(id)) return error("deliverable task not found", 404);
|
|
343
|
+
const component = getComponentAuth(req);
|
|
344
|
+
const agentId = component ? agentIdFromComponent(component) : undefined;
|
|
345
|
+
const scopes = component?.scope ?? [];
|
|
346
|
+
if (!isFullReachRequest(req) && !isTaskDispatchAuthorized(id, agentId, scopes)) return error("forbidden", 403);
|
|
347
|
+
const parsed = await parseBody<Record<string, unknown>>(req);
|
|
348
|
+
if (!parsed.ok) return error(parsed.error, parsed.status);
|
|
349
|
+
const body = parsed.body ?? {};
|
|
350
|
+
try {
|
|
351
|
+
const result = runTaskDispatch({
|
|
352
|
+
taskId: id,
|
|
353
|
+
dispatch: dispatchShapeArg(body.dispatch),
|
|
354
|
+
pin: pinArg(body.pin),
|
|
355
|
+
createReview: body.createReview !== false,
|
|
356
|
+
actor: actorOf(req),
|
|
357
|
+
});
|
|
358
|
+
return json({
|
|
359
|
+
taskId: result.taskId,
|
|
360
|
+
decision: result.decision,
|
|
361
|
+
...(result.reviewTaskId !== undefined ? { reviewTaskId: result.reviewTaskId } : {}),
|
|
362
|
+
});
|
|
363
|
+
} catch (e) {
|
|
364
|
+
return entityError(e);
|
|
365
|
+
}
|
|
366
|
+
};
|
|
367
|
+
|
|
294
368
|
// #833 — read the #482/#474 close-bridge projection for a Task's linked issues.
|
|
295
369
|
export const getTaskCloseBridge: Handler = (_req, params) => {
|
|
296
370
|
const id = parseId(params.id);
|
package/src/runtime-tokens.ts
CHANGED
|
@@ -94,6 +94,7 @@ export function issueRunnerRuntimeToken(input: {
|
|
|
94
94
|
label?: string;
|
|
95
95
|
policyName?: string;
|
|
96
96
|
spawnRequestId?: string;
|
|
97
|
+
taskId?: number;
|
|
97
98
|
createdBy?: string;
|
|
98
99
|
/**
|
|
99
100
|
* Agent profile name. The spawn grant (the `command:spawn`/`command:shutdown` scope + the
|
|
@@ -128,6 +129,7 @@ export function issueRunnerRuntimeToken(input: {
|
|
|
128
129
|
cwdPrefixes: [input.cwd],
|
|
129
130
|
...(input.policyName ? { policies: [input.policyName] } : {}),
|
|
130
131
|
...(input.spawnRequestId ? { spawnRequestIds: [input.spawnRequestId] } : {}),
|
|
132
|
+
...(input.taskId !== undefined ? { taskIds: [String(input.taskId)] } : {}),
|
|
131
133
|
...(grant.canSpawn && grant.maxSpawnedAgents ? { maxSpawnedAgents: grant.maxSpawnedAgents } : {}),
|
|
132
134
|
...(input.spawnedBy ? { spawnedBy: input.spawnedBy } : {}),
|
|
133
135
|
...(input.teamId ? { teamId: input.teamId } : {}),
|
|
@@ -250,6 +252,7 @@ export function runnerRuntimeTokenEnv(input: {
|
|
|
250
252
|
label?: string;
|
|
251
253
|
policyName?: string;
|
|
252
254
|
spawnRequestId?: string;
|
|
255
|
+
taskId?: number;
|
|
253
256
|
createdBy?: string;
|
|
254
257
|
/** Agent profile name — spawn grant is resolved from it in the mint (see issueRunnerRuntimeToken). */
|
|
255
258
|
profile?: string;
|
|
@@ -266,6 +269,7 @@ export function runnerRuntimeTokenEnv(input: {
|
|
|
266
269
|
AGENT_RELAY_TOKEN_JTI: issued.record.jti,
|
|
267
270
|
AGENT_RELAY_TOKEN_PROFILE: issued.record.profileId ?? "provider-agent",
|
|
268
271
|
...(issued.record.expiresAt ? { AGENT_RELAY_TOKEN_EXPIRES_AT: String(issued.record.expiresAt) } : {}),
|
|
272
|
+
...(input.taskId !== undefined ? { AGENT_RELAY_TASK_ID: String(input.taskId) } : {}),
|
|
269
273
|
};
|
|
270
274
|
}
|
|
271
275
|
|
package/src/security.ts
CHANGED
|
@@ -87,6 +87,10 @@ export function isAuthorized(req: Request): boolean {
|
|
|
87
87
|
return Boolean(presented && verifyComponentToken(presented));
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
export function isFullReachRequest(req: Request): boolean {
|
|
91
|
+
return Boolean(extractToken(req)) && isAuthorized(req) && !getComponentAuth(req) && !getIntegrationAuth(req);
|
|
92
|
+
}
|
|
93
|
+
|
|
90
94
|
type IntegrationAuth = IntegrationTokenConfig;
|
|
91
95
|
|
|
92
96
|
type AuthorizationResource = {
|
|
@@ -195,6 +199,8 @@ export function requiredScopeFor(method: string, pathname: string): string | nul
|
|
|
195
199
|
if (pathname === "/api/tokens/me") return null;
|
|
196
200
|
if (pathname.startsWith("/api/tokens") || pathname.startsWith("/api/token-profiles")) return method === "GET" ? "token:read" : "token:write";
|
|
197
201
|
if (pathname.startsWith("/api/maintenance")) return "system:admin";
|
|
202
|
+
// Dispatch is spawn-class — requires command:spawn, not generic task:write (#845/#848).
|
|
203
|
+
if (pathname.match(/^\/api\/tasks\/[^/]+\/dispatch$/)) return "command:spawn";
|
|
198
204
|
if (pathname.startsWith("/api/tasks")) return method === "GET" ? "task:read" : "task:write";
|
|
199
205
|
if (pathname.startsWith("/api/pairs")) return method === "GET" ? "pairs:read" : "pairs:write";
|
|
200
206
|
if (pathname === "/api/provider-quota-leases/acquire" || pathname === "/api/provider-quota-leases/release") return "quota:write";
|
|
@@ -304,6 +310,8 @@ export function requiredComponentScopeFor(method: string, pathname: string): str
|
|
|
304
310
|
if (pathname.startsWith("/api/messages") || pathname.startsWith("/api/inbox")) return method === "GET" ? "message:read" : "message:send";
|
|
305
311
|
if (pathname.startsWith("/api/agent-profiles")) return method === "GET" ? "agent:read" : "agent:write";
|
|
306
312
|
if (pathname.startsWith("/api/provisioning")) return method === "GET" ? "provisioning:read" : "provisioning:write";
|
|
313
|
+
// Dispatch is spawn-class — requires command:spawn, not generic task:write (#845/#848).
|
|
314
|
+
if (pathname.match(/^\/api\/tasks\/[^/]+\/dispatch$/)) return "command:spawn";
|
|
307
315
|
if (pathname.startsWith("/api/tasks")) return method === "GET" ? "task:read" : "task:write";
|
|
308
316
|
if (pathname.startsWith("/api/orchestrators")) return method === "GET" ? "agent:read" : "command:write";
|
|
309
317
|
if (pathname.startsWith("/api/plans")) return method === "GET" ? "plans:read" : "plans:write";
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
// managed came-up-running reconcile (delegated to managed-running.ts), the single
|
|
10
10
|
// status broadcast, the single timeline note, the spawned-child parent-wake, and
|
|
11
11
|
// the first-registration audit row.
|
|
12
|
-
import { isReservedAgentId } from "agent-relay-sdk";
|
|
13
|
-
import { ValidationError, createActivityEvent, getAgent, seedSpawnPromptMirror, seedSpawnReplyObligation, upsertAgent } from "../db";
|
|
12
|
+
import { errMessage, isReservedAgentId } from "agent-relay-sdk";
|
|
13
|
+
import { ValidationError, createActivityEvent, deliverableTaskIdsBySpawnRequestId, getAgent, getTaskDetail, seedSpawnPromptMirror, seedSpawnReplyObligation, upsertAgent } from "../db";
|
|
14
14
|
import { emitAgentStatus, emitNewMessage } from "../sse";
|
|
15
15
|
import { noteAgentTimelineEvent } from "../compaction-watch";
|
|
16
16
|
import { agentProviderTimelineEvent } from "../provider-timeline-event";
|
|
@@ -19,6 +19,7 @@ import { resolveSpawnLineage, resolveTeamLineage } from "../security";
|
|
|
19
19
|
import { flushQueuedDirectMessages, markManagedAgentRunning } from "./managed-running";
|
|
20
20
|
import { isAgentUnavailable } from "../db/agent-predicates";
|
|
21
21
|
import { recordSpawnPromptIssueActivity } from "./spawn-issue-activity";
|
|
22
|
+
import { assignTaskEntityAgent, setTaskEntityStatus } from "./task-entity";
|
|
22
23
|
import type { AuthContext } from "./auth-context";
|
|
23
24
|
import type { AgentCard, RegisterAgentInput } from "../types";
|
|
24
25
|
|
|
@@ -27,6 +28,48 @@ function metaStr(meta: Record<string, unknown> | undefined, key: string): string
|
|
|
27
28
|
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
function metaTaskId(meta: Record<string, unknown> | undefined): number | undefined {
|
|
32
|
+
const value = meta?.taskId;
|
|
33
|
+
const parsed = typeof value === "string" ? Number(value) : value;
|
|
34
|
+
return typeof parsed === "number" && Number.isSafeInteger(parsed) && parsed > 0 ? parsed : undefined;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function taskIdsForReadyAgent(agent: AgentCard): number[] {
|
|
38
|
+
const ids = new Set<number>();
|
|
39
|
+
const directTaskId = metaTaskId(agent.meta);
|
|
40
|
+
if (directTaskId !== undefined) ids.add(directTaskId);
|
|
41
|
+
const spawnRequestId = metaStr(agent.meta, "spawnRequestId");
|
|
42
|
+
if (spawnRequestId) {
|
|
43
|
+
for (const id of deliverableTaskIdsBySpawnRequestId(spawnRequestId)) ids.add(id);
|
|
44
|
+
}
|
|
45
|
+
return [...ids].sort((a, b) => a - b);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function bindSpawnedTaskToReadyAgent(agentId: string, actor = "agent-relay"): void {
|
|
49
|
+
const agent = getAgent(agentId);
|
|
50
|
+
if (!agent?.ready) return;
|
|
51
|
+
const taskIds = taskIdsForReadyAgent(agent);
|
|
52
|
+
for (const taskId of taskIds) {
|
|
53
|
+
try {
|
|
54
|
+
const before = getTaskDetail(taskId);
|
|
55
|
+
if (!before) continue;
|
|
56
|
+
if (before.ownerAgentId && before.ownerAgentId !== agent.id) continue;
|
|
57
|
+
const assigned = before.ownerAgentId === agent.id
|
|
58
|
+
? before
|
|
59
|
+
: assignTaskEntityAgent(taskId, agent.id, "worker", agent.spawnedBy ?? actor);
|
|
60
|
+
if (assigned.status === "open" || assigned.status === "claimed") {
|
|
61
|
+
setTaskEntityStatus(taskId, {
|
|
62
|
+
status: "in_progress",
|
|
63
|
+
note: `worker ${agent.id} ready`,
|
|
64
|
+
actor: agent.spawnedBy ?? actor,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.warn(`task ${taskId} ready binding skipped for ${agent.id}: ${errMessage(error)}`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
30
73
|
export function registerAgent(input: RegisterAgentInput, ctx: AuthContext): AgentCard {
|
|
31
74
|
// Reserved built-in identities — the `system` lifecycle sender and the whole `user:` human
|
|
32
75
|
// namespace (legacy bare `'user'`, `user:default`, every `user:<id>`) — are mechanical sinks,
|
|
@@ -73,6 +116,7 @@ export function registerAgent(input: RegisterAgentInput, ctx: AuthContext): Agen
|
|
|
73
116
|
// case) wakes its block-polling parent. Fire on the first-ever ready, regardless of
|
|
74
117
|
// transport. notifyAgentReady no-ops for non-spawned agents and dedups repeats.
|
|
75
118
|
if (agent.ready && !existing?.ready) notifyAgentReady(agent.id);
|
|
119
|
+
if (agent.ready && !existing?.ready) bindSpawnedTaskToReadyAgent(agent.id, ctx.actor.id);
|
|
76
120
|
|
|
77
121
|
// #416 — flush queued direct messages when a re-registering agent was previously unavailable
|
|
78
122
|
// (stale, compacting, blocked) or a new agent becomes ready. No-op for brand-new agents
|