@pixelbyte-software/pixcode 1.42.2 → 1.42.4
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/dist/assets/{index-CMeiCqQf.js → index-cTGs3Dvx.js} +72 -72
- package/dist/index.html +1 -1
- package/dist-server/server/claude-sdk.js +23 -2
- package/dist-server/server/claude-sdk.js.map +1 -1
- package/dist-server/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.js.map +1 -1
- package/dist-server/server/modules/orchestration/a2a/adapters/claude-code.adapter.js +2 -0
- package/dist-server/server/modules/orchestration/a2a/adapters/claude-code.adapter.js.map +1 -1
- package/dist-server/server/modules/orchestration/a2a/routes.js +2 -0
- package/dist-server/server/modules/orchestration/a2a/routes.js.map +1 -1
- package/dist-server/server/modules/orchestration/index.js +1 -0
- package/dist-server/server/modules/orchestration/index.js.map +1 -1
- package/dist-server/server/modules/orchestration/security/permission-policy.js +269 -0
- package/dist-server/server/modules/orchestration/security/permission-policy.js.map +1 -0
- package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js +86 -0
- package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js.map +1 -1
- package/dist-server/server/modules/orchestration/tasks/task-run-graph.js +158 -0
- package/dist-server/server/modules/orchestration/tasks/task-run-graph.js.map +1 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +121 -1
- package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -1
- package/dist-server/server/modules/orchestration/workflows/workflow-trace.js +32 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-trace.js.map +1 -1
- package/dist-server/server/modules/orchestration/workflows/workflow.routes.js +103 -0
- package/dist-server/server/modules/orchestration/workflows/workflow.routes.js.map +1 -1
- package/dist-server/server/routes/taskmaster.js +93 -25
- package/dist-server/server/routes/taskmaster.js.map +1 -1
- package/package.json +1 -1
- package/scripts/smoke/permission-policy.mjs +50 -0
- package/scripts/smoke/taskmaster-run-graph.mjs +55 -0
- package/server/claude-sdk.js +24 -2
- package/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.ts +8 -0
- package/server/modules/orchestration/a2a/adapters/claude-code.adapter.ts +2 -0
- package/server/modules/orchestration/a2a/routes.ts +6 -0
- package/server/modules/orchestration/index.ts +18 -0
- package/server/modules/orchestration/security/permission-policy.ts +401 -0
- package/server/modules/orchestration/tasks/orchestration-task.service.ts +94 -0
- package/server/modules/orchestration/tasks/orchestration-task.types.ts +10 -0
- package/server/modules/orchestration/tasks/task-run-graph.ts +219 -0
- package/server/modules/orchestration/workflows/workflow-runner.ts +148 -2
- package/server/modules/orchestration/workflows/workflow-trace.ts +32 -0
- package/server/modules/orchestration/workflows/workflow.routes.ts +121 -0
- package/server/modules/orchestration/workflows/workflow.types.ts +9 -1
- package/server/routes/taskmaster.js +90 -23
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import type { WorkflowNodeRun, WorkflowRun } from '@/modules/orchestration/workflows/workflow.types.js';
|
|
2
|
+
import { workflowStore } from '@/modules/orchestration/workflows/workflow-store.js';
|
|
3
|
+
import { orchestrationTaskService } from '@/modules/orchestration/tasks/orchestration-task.service.js';
|
|
4
|
+
import type { OrchestrationTask } from '@/modules/orchestration/tasks/orchestration-task.types.js';
|
|
5
|
+
|
|
6
|
+
export const PIXCODE_TASK_RUN_GRAPH_PROTOCOL = 'pixcode.task-run-graph.v1';
|
|
7
|
+
|
|
8
|
+
export type TaskRunGraphCriterionStatus = 'pending' | 'passed' | 'failed';
|
|
9
|
+
|
|
10
|
+
export interface TaskRunGraphCriterion {
|
|
11
|
+
id: string;
|
|
12
|
+
label: string;
|
|
13
|
+
status: TaskRunGraphCriterionStatus;
|
|
14
|
+
source: 'taskmaster' | 'workflow';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface TaskRunGraphRunSummary {
|
|
18
|
+
id: string;
|
|
19
|
+
workflowId: string;
|
|
20
|
+
status: WorkflowRun['status'];
|
|
21
|
+
startedAt: number;
|
|
22
|
+
finishedAt?: number;
|
|
23
|
+
taskmasterId?: string;
|
|
24
|
+
orchestrationTaskId?: string;
|
|
25
|
+
changedFiles: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TaskRunGraph {
|
|
29
|
+
protocol: typeof PIXCODE_TASK_RUN_GRAPH_PROTOCOL;
|
|
30
|
+
projectId: string;
|
|
31
|
+
taskmasterId?: string;
|
|
32
|
+
orchestrationTaskId?: string;
|
|
33
|
+
workflowRuns: TaskRunGraphRunSummary[];
|
|
34
|
+
changedFiles: string[];
|
|
35
|
+
acceptanceCriteria: TaskRunGraphCriterion[];
|
|
36
|
+
status: {
|
|
37
|
+
totalRuns: number;
|
|
38
|
+
completedRuns: number;
|
|
39
|
+
failedRuns: number;
|
|
40
|
+
passedCriteria: number;
|
|
41
|
+
failedCriteria: number;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readString(value: unknown): string | undefined {
|
|
46
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readRecord(value: unknown): Record<string, unknown> | undefined {
|
|
50
|
+
return value && typeof value === 'object' ? value as Record<string, unknown> : undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function uniqueStrings(values: Array<string | undefined>): string[] {
|
|
54
|
+
return [...new Set(values.filter((value): value is string => Boolean(value?.trim())))]
|
|
55
|
+
.sort((a, b) => a.localeCompare(b));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function taskStatusPassed(status: unknown): boolean {
|
|
59
|
+
const normalized = String(status ?? '').toLocaleLowerCase('en');
|
|
60
|
+
return normalized === 'done' || normalized === 'completed';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function taskStatusFailed(status: unknown): boolean {
|
|
64
|
+
const normalized = String(status ?? '').toLocaleLowerCase('en');
|
|
65
|
+
return normalized === 'failed' || normalized === 'blocked' || normalized === 'cancelled' || normalized === 'canceled';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function criterionStatus(status: unknown): TaskRunGraphCriterionStatus {
|
|
69
|
+
if (taskStatusPassed(status)) return 'passed';
|
|
70
|
+
if (taskStatusFailed(status)) return 'failed';
|
|
71
|
+
return 'pending';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function taskmasterAcceptanceCriteria(taskmasterTask: Record<string, unknown> | undefined): TaskRunGraphCriterion[] {
|
|
75
|
+
if (!taskmasterTask) return [];
|
|
76
|
+
|
|
77
|
+
const criteria: TaskRunGraphCriterion[] = [];
|
|
78
|
+
const testStrategy = readString(taskmasterTask.testStrategy) ?? readString(taskmasterTask.test_strategy);
|
|
79
|
+
if (testStrategy) {
|
|
80
|
+
criteria.push({
|
|
81
|
+
id: 'test-strategy',
|
|
82
|
+
label: testStrategy,
|
|
83
|
+
status: criterionStatus(taskmasterTask.status),
|
|
84
|
+
source: 'taskmaster',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const subtasks = Array.isArray(taskmasterTask.subtasks) ? taskmasterTask.subtasks : [];
|
|
89
|
+
subtasks.forEach((subtask, index) => {
|
|
90
|
+
const record = readRecord(subtask);
|
|
91
|
+
if (!record) return;
|
|
92
|
+
const title = readString(record.title);
|
|
93
|
+
if (!title) return;
|
|
94
|
+
criteria.push({
|
|
95
|
+
id: `subtask-${readString(record.id) ?? index + 1}`,
|
|
96
|
+
label: title,
|
|
97
|
+
status: criterionStatus(record.status),
|
|
98
|
+
source: 'taskmaster',
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return criteria;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function metadataTaskmasterId(run: WorkflowRun): string | undefined {
|
|
106
|
+
return readString(run.metadata?.taskmasterId)
|
|
107
|
+
?? readString(readRecord(run.metadata?.taskGraph)?.taskmasterId)
|
|
108
|
+
?? readString(readRecord(run.metadata?.replay)?.taskmasterId);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function metadataOrchestrationTaskId(run: WorkflowRun): string | undefined {
|
|
112
|
+
return readString(run.metadata?.orchestrationTaskId)
|
|
113
|
+
?? readString(readRecord(run.metadata?.taskGraph)?.orchestrationTaskId);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function artifactChangedFiles(node: WorkflowNodeRun): string[] {
|
|
117
|
+
const files: string[] = [];
|
|
118
|
+
|
|
119
|
+
if (node.handoffArtifact?.changedFiles) {
|
|
120
|
+
files.push(...node.handoffArtifact.changedFiles);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
for (const artifact of node.artifacts ?? []) {
|
|
124
|
+
const data = readRecord(artifact.data);
|
|
125
|
+
const metadata = readRecord(artifact.metadata);
|
|
126
|
+
const candidates = [
|
|
127
|
+
readString(metadata?.path),
|
|
128
|
+
readString(metadata?.file),
|
|
129
|
+
readString(data?.path),
|
|
130
|
+
readString(data?.file),
|
|
131
|
+
];
|
|
132
|
+
files.push(...candidates.filter((value): value is string => Boolean(value)));
|
|
133
|
+
|
|
134
|
+
for (const key of ['files', 'changedFiles']) {
|
|
135
|
+
const value = data?.[key] ?? metadata?.[key];
|
|
136
|
+
if (Array.isArray(value)) {
|
|
137
|
+
files.push(...value.map((entry) => readString(entry)).filter((entry): entry is string => Boolean(entry)));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return uniqueStrings(files);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function changedFilesFromWorkflowRun(run: WorkflowRun): string[] {
|
|
146
|
+
return uniqueStrings(run.nodeRuns.flatMap((node) => artifactChangedFiles(node)));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function workflowRunMatchesTask(run: WorkflowRun, task: OrchestrationTask | undefined, taskmasterId: string | undefined): boolean {
|
|
150
|
+
const runTaskmasterId = metadataTaskmasterId(run);
|
|
151
|
+
const runOrchestrationTaskId = metadataOrchestrationTaskId(run);
|
|
152
|
+
if (taskmasterId && runTaskmasterId === taskmasterId) return true;
|
|
153
|
+
if (task?.id && runOrchestrationTaskId === task.id) return true;
|
|
154
|
+
if (task?.workflowRunIds?.includes(run.id)) return true;
|
|
155
|
+
return false;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function runSummary(run: WorkflowRun): TaskRunGraphRunSummary {
|
|
159
|
+
return {
|
|
160
|
+
id: run.id,
|
|
161
|
+
workflowId: run.workflowId,
|
|
162
|
+
status: run.status,
|
|
163
|
+
startedAt: run.startedAt,
|
|
164
|
+
finishedAt: run.finishedAt,
|
|
165
|
+
taskmasterId: metadataTaskmasterId(run),
|
|
166
|
+
orchestrationTaskId: metadataOrchestrationTaskId(run),
|
|
167
|
+
changedFiles: changedFilesFromWorkflowRun(run),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function buildTaskRunGraph({
|
|
172
|
+
projectId,
|
|
173
|
+
taskmasterId,
|
|
174
|
+
taskmasterTask,
|
|
175
|
+
}: {
|
|
176
|
+
projectId: string;
|
|
177
|
+
taskmasterId?: string;
|
|
178
|
+
taskmasterTask?: Record<string, unknown>;
|
|
179
|
+
}): TaskRunGraph {
|
|
180
|
+
const orchestrationTask = taskmasterId
|
|
181
|
+
? orchestrationTaskService.list(projectId).find((task) => task.taskmasterId === taskmasterId)
|
|
182
|
+
: undefined;
|
|
183
|
+
const workflowRuns = workflowStore
|
|
184
|
+
.listRuns()
|
|
185
|
+
.filter((run) => workflowRunMatchesTask(run, orchestrationTask, taskmasterId))
|
|
186
|
+
.map(runSummary);
|
|
187
|
+
const workflowCriteria: TaskRunGraphCriterion[] = workflowRuns.map((run) => ({
|
|
188
|
+
id: `run-${run.id}`,
|
|
189
|
+
label: `Workflow ${run.workflowId} ${run.status}`,
|
|
190
|
+
status: run.status === 'completed' ? 'passed' : run.status === 'failed' || run.status === 'canceled' ? 'failed' : 'pending',
|
|
191
|
+
source: 'workflow',
|
|
192
|
+
}));
|
|
193
|
+
const acceptanceCriteria = [
|
|
194
|
+
...taskmasterAcceptanceCriteria(taskmasterTask),
|
|
195
|
+
...(orchestrationTask?.acceptanceCriteria ?? []),
|
|
196
|
+
...workflowCriteria,
|
|
197
|
+
];
|
|
198
|
+
const changedFiles = uniqueStrings([
|
|
199
|
+
...(orchestrationTask?.changedFiles ?? []),
|
|
200
|
+
...workflowRuns.flatMap((run) => run.changedFiles),
|
|
201
|
+
]);
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
protocol: PIXCODE_TASK_RUN_GRAPH_PROTOCOL,
|
|
205
|
+
projectId,
|
|
206
|
+
taskmasterId,
|
|
207
|
+
orchestrationTaskId: orchestrationTask?.id,
|
|
208
|
+
workflowRuns,
|
|
209
|
+
changedFiles,
|
|
210
|
+
acceptanceCriteria,
|
|
211
|
+
status: {
|
|
212
|
+
totalRuns: workflowRuns.length,
|
|
213
|
+
completedRuns: workflowRuns.filter((run) => run.status === 'completed').length,
|
|
214
|
+
failedRuns: workflowRuns.filter((run) => run.status === 'failed' || run.status === 'canceled').length,
|
|
215
|
+
passedCriteria: acceptanceCriteria.filter((criterion) => criterion.status === 'passed').length,
|
|
216
|
+
failedCriteria: acceptanceCriteria.filter((criterion) => criterion.status === 'failed').length,
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
}
|
|
@@ -21,6 +21,13 @@ import {
|
|
|
21
21
|
classifyWorkflowFailure,
|
|
22
22
|
resolveWorkflowFallbackDecision,
|
|
23
23
|
} from '@/modules/orchestration/workflows/workflow-fallback-policy.js';
|
|
24
|
+
import {
|
|
25
|
+
evaluatePermissionRequest,
|
|
26
|
+
resolvePermissionPolicyFromMetadata,
|
|
27
|
+
type PermissionDecision,
|
|
28
|
+
type PermissionPolicy,
|
|
29
|
+
type PermissionPolicyEvent,
|
|
30
|
+
} from '@/modules/orchestration/security/permission-policy.js';
|
|
24
31
|
import {
|
|
25
32
|
type ResolvedWorkspaceTarget,
|
|
26
33
|
resolveWorkflowWorkspace,
|
|
@@ -28,6 +35,7 @@ import {
|
|
|
28
35
|
workspaceTargetMetadata,
|
|
29
36
|
} from '@/modules/orchestration/workflows/workspace-target.js';
|
|
30
37
|
import { workflowStore } from '@/modules/orchestration/workflows/workflow-store.js';
|
|
38
|
+
import { orchestrationTaskService } from '@/modules/orchestration/tasks/orchestration-task.service.js';
|
|
31
39
|
// @ts-ignore — plain-JS service
|
|
32
40
|
import {
|
|
33
41
|
getDefaultProviderModel,
|
|
@@ -35,7 +43,12 @@ import {
|
|
|
35
43
|
getStaticProviderModels,
|
|
36
44
|
} from '@/services/model-registry.js';
|
|
37
45
|
// @ts-ignore — plain-JS service
|
|
38
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
createNotificationEvent,
|
|
48
|
+
notifyRunFailed,
|
|
49
|
+
notifyRunStopped,
|
|
50
|
+
notifyUserIfEnabled,
|
|
51
|
+
} from '@/services/notification-orchestrator.js';
|
|
39
52
|
|
|
40
53
|
const TERMINAL = new Set(['completed', 'failed', 'canceled']);
|
|
41
54
|
const SKIPPED = 'skipped';
|
|
@@ -244,6 +257,49 @@ function notifyWorkflowRunFinished(run: WorkflowRun): void {
|
|
|
244
257
|
}
|
|
245
258
|
}
|
|
246
259
|
|
|
260
|
+
function permissionPolicyFromRun(run: WorkflowRun): PermissionPolicy {
|
|
261
|
+
return resolvePermissionPolicyFromMetadata(run.metadata);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function permissionPolicyEvents(run: WorkflowRun): PermissionPolicyEvent[] {
|
|
265
|
+
return Array.isArray(run.metadata?.permissionPolicyEvents)
|
|
266
|
+
? run.metadata.permissionPolicyEvents.filter((event): event is PermissionPolicyEvent =>
|
|
267
|
+
Boolean(event && typeof event === 'object'),
|
|
268
|
+
)
|
|
269
|
+
: [];
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function permissionApprovalRequests(run: WorkflowRun): Array<Record<string, unknown>> {
|
|
273
|
+
return Array.isArray(run.metadata?.pendingPermissionApprovals)
|
|
274
|
+
? run.metadata.pendingPermissionApprovals.filter((event): event is Record<string, unknown> =>
|
|
275
|
+
Boolean(event && typeof event === 'object'),
|
|
276
|
+
)
|
|
277
|
+
: [];
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function notifyPermissionApprovalRequested(run: WorkflowRun, decision: PermissionDecision): void {
|
|
281
|
+
const userId = readNotificationUserId(run.metadata);
|
|
282
|
+
if (!userId || !decision.approvalRequest) return;
|
|
283
|
+
|
|
284
|
+
const event = (createNotificationEvent as unknown as (payload: Record<string, unknown>) => unknown)({
|
|
285
|
+
provider: 'system',
|
|
286
|
+
sessionId: run.id,
|
|
287
|
+
kind: 'action_required',
|
|
288
|
+
code: 'permission.required',
|
|
289
|
+
meta: {
|
|
290
|
+
toolName: decision.capabilities.join(', '),
|
|
291
|
+
sessionName: workflowNotificationTitle(run),
|
|
292
|
+
},
|
|
293
|
+
severity: 'warning',
|
|
294
|
+
requiresUserAction: true,
|
|
295
|
+
dedupeKey: `workflow:permission:${run.id}:${decision.requestId}`,
|
|
296
|
+
});
|
|
297
|
+
(notifyUserIfEnabled as (payload: { userId: string | number; event: unknown }) => void)({
|
|
298
|
+
userId,
|
|
299
|
+
event,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
247
303
|
function readBoolean(value: unknown): boolean | undefined {
|
|
248
304
|
return typeof value === 'boolean' ? value : undefined;
|
|
249
305
|
}
|
|
@@ -1215,8 +1271,10 @@ class WorkflowRunner {
|
|
|
1215
1271
|
const runtimeWorkflow = expandWorkflowForRun(workflow, metadata);
|
|
1216
1272
|
validateWorkflow(runtimeWorkflow);
|
|
1217
1273
|
const workspaceTarget = resolveWorkflowWorkspace(metadata);
|
|
1218
|
-
const
|
|
1274
|
+
const permissionPolicy = resolvePermissionPolicyFromMetadata(metadata);
|
|
1275
|
+
const runMetadata: Record<string, unknown> = {
|
|
1219
1276
|
...metadata,
|
|
1277
|
+
permissionPolicy,
|
|
1220
1278
|
projectPath: workspaceTarget.projectPath,
|
|
1221
1279
|
selectedProjectPath: workspaceTarget.selectedProjectPath,
|
|
1222
1280
|
workspaceTarget: workspaceTargetMetadata(workspaceTarget),
|
|
@@ -1232,6 +1290,10 @@ class WorkflowRunner {
|
|
|
1232
1290
|
metadata: runMetadata,
|
|
1233
1291
|
};
|
|
1234
1292
|
workflowStore.setRun(run);
|
|
1293
|
+
const orchestrationTaskId = readString(runMetadata.orchestrationTaskId);
|
|
1294
|
+
if (orchestrationTaskId) {
|
|
1295
|
+
orchestrationTaskService.linkWorkflowRun(orchestrationTaskId, run);
|
|
1296
|
+
}
|
|
1235
1297
|
void this.execute(runtimeWorkflow, run);
|
|
1236
1298
|
return run;
|
|
1237
1299
|
}
|
|
@@ -1591,11 +1653,43 @@ class WorkflowRunner {
|
|
|
1591
1653
|
} finally {
|
|
1592
1654
|
run.finishedAt = run.finishedAt ?? Date.now();
|
|
1593
1655
|
workflowStore.setRun(run);
|
|
1656
|
+
orchestrationTaskService.updateFromWorkflowRun(run);
|
|
1594
1657
|
notifyWorkflowRunFinished(run);
|
|
1595
1658
|
this.cancelingRuns.delete(run.id);
|
|
1596
1659
|
}
|
|
1597
1660
|
}
|
|
1598
1661
|
|
|
1662
|
+
private recordPermissionDecision(
|
|
1663
|
+
run: WorkflowRun,
|
|
1664
|
+
nodeRun: WorkflowNodeRun,
|
|
1665
|
+
decision: PermissionDecision,
|
|
1666
|
+
): void {
|
|
1667
|
+
nodeRun.permissionDecisions = [
|
|
1668
|
+
...(nodeRun.permissionDecisions ?? []),
|
|
1669
|
+
decision,
|
|
1670
|
+
];
|
|
1671
|
+
|
|
1672
|
+
const existingApprovals = permissionApprovalRequests(run)
|
|
1673
|
+
.filter((approval) => approval.id !== decision.approvalRequest?.id);
|
|
1674
|
+
run.metadata = {
|
|
1675
|
+
...run.metadata,
|
|
1676
|
+
permissionPolicyEvents: [
|
|
1677
|
+
...permissionPolicyEvents(run),
|
|
1678
|
+
decision.event,
|
|
1679
|
+
],
|
|
1680
|
+
pendingPermissionApprovals: decision.approvalRequest
|
|
1681
|
+
? [
|
|
1682
|
+
...existingApprovals,
|
|
1683
|
+
decision.approvalRequest,
|
|
1684
|
+
]
|
|
1685
|
+
: existingApprovals,
|
|
1686
|
+
};
|
|
1687
|
+
|
|
1688
|
+
if (decision.approvalRequest) {
|
|
1689
|
+
notifyPermissionApprovalRequested(run, decision);
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1599
1693
|
private async executeNode(
|
|
1600
1694
|
node: WorkflowNode,
|
|
1601
1695
|
workflow: Workflow,
|
|
@@ -1674,6 +1768,49 @@ class WorkflowRunner {
|
|
|
1674
1768
|
};
|
|
1675
1769
|
workflowStore.setRun(run);
|
|
1676
1770
|
}
|
|
1771
|
+
const permissionPolicy = permissionPolicyFromRun(run);
|
|
1772
|
+
nodeRun.permissionPolicy = permissionPolicy;
|
|
1773
|
+
const permissionDecision = evaluatePermissionRequest({
|
|
1774
|
+
policy: permissionPolicy,
|
|
1775
|
+
request: {
|
|
1776
|
+
source: 'workflow_node',
|
|
1777
|
+
toolName: node.adapterId,
|
|
1778
|
+
input: {
|
|
1779
|
+
assignment: node.assignment,
|
|
1780
|
+
stage: node.stage,
|
|
1781
|
+
toolsSettings: node.toolsSettings,
|
|
1782
|
+
},
|
|
1783
|
+
cwd: projectPath,
|
|
1784
|
+
workspacePath: workspaceTarget.appRoot,
|
|
1785
|
+
targetPaths: [projectPath],
|
|
1786
|
+
summary: [
|
|
1787
|
+
node.agentLabel || node.id,
|
|
1788
|
+
node.stage ? `stage=${node.stage}` : undefined,
|
|
1789
|
+
node.assignment,
|
|
1790
|
+
].filter(Boolean).join(' / '),
|
|
1791
|
+
},
|
|
1792
|
+
context: {
|
|
1793
|
+
runId: run.id,
|
|
1794
|
+
nodeId: node.id,
|
|
1795
|
+
workflowId: run.workflowId,
|
|
1796
|
+
adapterId: node.adapterId,
|
|
1797
|
+
agentLabel: node.agentLabel,
|
|
1798
|
+
userId: readNotificationUserId(run.metadata),
|
|
1799
|
+
},
|
|
1800
|
+
});
|
|
1801
|
+
this.recordPermissionDecision(run, nodeRun, permissionDecision);
|
|
1802
|
+
workflowStore.setRun(run);
|
|
1803
|
+
if (permissionDecision.behavior === 'deny') {
|
|
1804
|
+
nodeRun.finishedAt = Date.now();
|
|
1805
|
+
nodeRun.status = 'failed';
|
|
1806
|
+
nodeRun.error = permissionDecision.message;
|
|
1807
|
+
workflowStore.setRun(run);
|
|
1808
|
+
if (node.onFail === 'continue') {
|
|
1809
|
+
completed.add(node.id);
|
|
1810
|
+
return;
|
|
1811
|
+
}
|
|
1812
|
+
throw new Error(permissionDecision.message);
|
|
1813
|
+
}
|
|
1677
1814
|
let body: { id?: string; error?: { message?: string } };
|
|
1678
1815
|
try {
|
|
1679
1816
|
const submit = await fetch(`${localA2ABaseUrl()}/tasks`, {
|
|
@@ -1695,6 +1832,15 @@ class WorkflowRunner {
|
|
|
1695
1832
|
assignment: node.assignment,
|
|
1696
1833
|
model: effectiveModel,
|
|
1697
1834
|
permissionMode: effectivePermissionMode,
|
|
1835
|
+
permissionPolicy,
|
|
1836
|
+
permissionPolicyContext: {
|
|
1837
|
+
runId: run.id,
|
|
1838
|
+
nodeId: node.id,
|
|
1839
|
+
workflowId: run.workflowId,
|
|
1840
|
+
adapterId: node.adapterId,
|
|
1841
|
+
agentLabel: node.agentLabel,
|
|
1842
|
+
userId: readNotificationUserId(run.metadata),
|
|
1843
|
+
},
|
|
1698
1844
|
toolsSettings: node.toolsSettings,
|
|
1699
1845
|
projectPath,
|
|
1700
1846
|
workspaceTarget: workspaceTargetMetadata(workspaceTarget),
|
|
@@ -216,6 +216,38 @@ export function buildWorkflowTrace(run: WorkflowRun): WorkflowTraceEvent[] {
|
|
|
216
216
|
});
|
|
217
217
|
});
|
|
218
218
|
|
|
219
|
+
const permissionPolicyEvents = Array.isArray(run.metadata?.permissionPolicyEvents)
|
|
220
|
+
? run.metadata.permissionPolicyEvents
|
|
221
|
+
: [];
|
|
222
|
+
permissionPolicyEvents.forEach((event, index) => {
|
|
223
|
+
const record = readRecord(event);
|
|
224
|
+
if (!record) return;
|
|
225
|
+
const behavior = readString(record.behavior);
|
|
226
|
+
const capabilities = Array.isArray(record.capabilities)
|
|
227
|
+
? record.capabilities.filter((item): item is string => typeof item === 'string')
|
|
228
|
+
: [];
|
|
229
|
+
pushEvent(events, {
|
|
230
|
+
id: traceId([run.id, 'permission-policy', readString(record.id) ?? index]),
|
|
231
|
+
type: 'permission_policy',
|
|
232
|
+
severity: behavior === 'deny' ? 'error' : behavior === 'prompt' ? 'warning' : 'info',
|
|
233
|
+
status: behavior === 'deny' ? 'failed' : behavior === 'prompt' ? 'submitted' : 'completed',
|
|
234
|
+
timestamp: typeof record.createdAt === 'number' ? record.createdAt : run.startedAt + 0.85 + index,
|
|
235
|
+
actor: 'Pixcode',
|
|
236
|
+
nodeId: readString(record.nodeId),
|
|
237
|
+
adapterId: readString(record.adapterId),
|
|
238
|
+
agentLabel: readString(record.agentLabel),
|
|
239
|
+
title: 'Permission policy decision',
|
|
240
|
+
titleKey: 'workflow.trace.permissionPolicy',
|
|
241
|
+
summary: redactTraceText([
|
|
242
|
+
`Decision: ${behavior ?? readString(record.status) ?? 'unknown'}`,
|
|
243
|
+
capabilities.length > 0 ? `Capabilities: ${capabilities.join(', ')}` : undefined,
|
|
244
|
+
readString(record.summary),
|
|
245
|
+
readString(record.message),
|
|
246
|
+
].filter(Boolean).join('\n'), run),
|
|
247
|
+
metadata: record,
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
219
251
|
run.nodeRuns.forEach((node, index) => {
|
|
220
252
|
const base = eventBase(node);
|
|
221
253
|
const timestamp = nodeTimestamp(run, node, index);
|
|
@@ -9,6 +9,14 @@ import {
|
|
|
9
9
|
import { workflowStore } from '@/modules/orchestration/workflows/workflow-store.js';
|
|
10
10
|
import { buildWorkflowTrace } from '@/modules/orchestration/workflows/workflow-trace.js';
|
|
11
11
|
import { findPixcodeAppRoot } from '@/modules/orchestration/workflows/workspace-target.js';
|
|
12
|
+
import {
|
|
13
|
+
DEFAULT_PERMISSION_POLICY,
|
|
14
|
+
PERMISSION_CAPABILITIES,
|
|
15
|
+
PERMISSION_POLICY_MODES,
|
|
16
|
+
PIXCODE_PERMISSION_POLICY_PROTOCOL,
|
|
17
|
+
evaluatePermissionRequest,
|
|
18
|
+
normalizePermissionPolicy,
|
|
19
|
+
} from '@/modules/orchestration/security/permission-policy.js';
|
|
12
20
|
|
|
13
21
|
const TERMINAL_RUN_STATES = new Set(['completed', 'failed', 'canceled']);
|
|
14
22
|
|
|
@@ -73,6 +81,36 @@ function replayOptions(req: express.Request): {
|
|
|
73
81
|
};
|
|
74
82
|
}
|
|
75
83
|
|
|
84
|
+
function readRunArray(run: { metadata?: Record<string, unknown> }, key: string): Array<Record<string, unknown>> {
|
|
85
|
+
const value = run.metadata?.[key];
|
|
86
|
+
return Array.isArray(value)
|
|
87
|
+
? value.filter((item): item is Record<string, unknown> => Boolean(item && typeof item === 'object'))
|
|
88
|
+
: [];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function updateApproval(
|
|
92
|
+
run: { metadata?: Record<string, unknown> },
|
|
93
|
+
requestId: string,
|
|
94
|
+
patch: Record<string, unknown>,
|
|
95
|
+
): boolean {
|
|
96
|
+
const approvals = readRunArray(run, 'pendingPermissionApprovals');
|
|
97
|
+
let changed = false;
|
|
98
|
+
const nextApprovals = approvals.map((approval) => {
|
|
99
|
+
if (approval.id !== requestId) return approval;
|
|
100
|
+
changed = true;
|
|
101
|
+
return {
|
|
102
|
+
...approval,
|
|
103
|
+
...patch,
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
if (!changed) return false;
|
|
107
|
+
run.metadata = {
|
|
108
|
+
...run.metadata,
|
|
109
|
+
pendingPermissionApprovals: nextApprovals,
|
|
110
|
+
};
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
|
|
76
114
|
function sendRunSnapshot(res: express.Response, runId: string): boolean {
|
|
77
115
|
const run = workflowStore.getRun(runId);
|
|
78
116
|
if (!run) {
|
|
@@ -162,6 +200,89 @@ export function createWorkflowRouter(): Router {
|
|
|
162
200
|
});
|
|
163
201
|
});
|
|
164
202
|
|
|
203
|
+
router.get('/workflows/permission-policy', (_req, res) => {
|
|
204
|
+
res.json({
|
|
205
|
+
protocol: PIXCODE_PERMISSION_POLICY_PROTOCOL,
|
|
206
|
+
capabilities: PERMISSION_CAPABILITIES,
|
|
207
|
+
modes: PERMISSION_POLICY_MODES,
|
|
208
|
+
defaultPolicy: DEFAULT_PERMISSION_POLICY,
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
router.post('/workflows/permission-policy/evaluate', (req, res) => {
|
|
213
|
+
try {
|
|
214
|
+
res.json({
|
|
215
|
+
decision: evaluatePermissionRequest({
|
|
216
|
+
policy: normalizePermissionPolicy(req.body?.policy),
|
|
217
|
+
request: req.body?.request ?? { source: 'api' },
|
|
218
|
+
}),
|
|
219
|
+
});
|
|
220
|
+
} catch (error) {
|
|
221
|
+
res.status(400).json({
|
|
222
|
+
error: {
|
|
223
|
+
code: 'PERMISSION_POLICY_INVALID',
|
|
224
|
+
message: error instanceof Error ? error.message : String(error),
|
|
225
|
+
},
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
router.get('/workflows/runs/:runId/permission-approvals', (req, res) => {
|
|
231
|
+
const run = workflowStore.getRun(req.params.runId);
|
|
232
|
+
if (!run) {
|
|
233
|
+
res.status(404).json({ error: { code: 'RUN_NOT_FOUND', message: req.params.runId } });
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
res.json({
|
|
238
|
+
runId: run.id,
|
|
239
|
+
pendingApprovals: readRunArray(run, 'pendingPermissionApprovals')
|
|
240
|
+
.filter((approval) => approval.status === 'pending'),
|
|
241
|
+
approvalHistory: readRunArray(run, 'pendingPermissionApprovals')
|
|
242
|
+
.filter((approval) => approval.status !== 'pending'),
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
router.post('/workflows/runs/:runId/permission-approvals/:requestId', (req, res) => {
|
|
247
|
+
const run = workflowStore.getRun(req.params.runId);
|
|
248
|
+
if (!run) {
|
|
249
|
+
res.status(404).json({ error: { code: 'RUN_NOT_FOUND', message: req.params.runId } });
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const allow = req.body?.allow === true;
|
|
254
|
+
const deny = req.body?.allow === false;
|
|
255
|
+
if (!allow && !deny) {
|
|
256
|
+
res.status(400).json({
|
|
257
|
+
error: {
|
|
258
|
+
code: 'PERMISSION_DECISION_REQUIRED',
|
|
259
|
+
message: 'Permission approval requires allow=true or allow=false.',
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
const updated = updateApproval(run, req.params.requestId, {
|
|
266
|
+
status: allow ? 'allowed' : 'denied',
|
|
267
|
+
resolvedAt: Date.now(),
|
|
268
|
+
resolvedBy: readRequestUserId(req),
|
|
269
|
+
resolutionMessage: readOptionalString(req.body?.message),
|
|
270
|
+
});
|
|
271
|
+
if (!updated) {
|
|
272
|
+
res.status(404).json({ error: { code: 'APPROVAL_NOT_FOUND', message: req.params.requestId } });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
workflowStore.setRun(run);
|
|
277
|
+
res.json({
|
|
278
|
+
runId: run.id,
|
|
279
|
+
pendingApprovals: readRunArray(run, 'pendingPermissionApprovals')
|
|
280
|
+
.filter((approval) => approval.status === 'pending'),
|
|
281
|
+
approvalHistory: readRunArray(run, 'pendingPermissionApprovals')
|
|
282
|
+
.filter((approval) => approval.status !== 'pending'),
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
165
286
|
router.get('/workflows/runs/:runId/replay-plan', (req, res) => {
|
|
166
287
|
const run = workflowStore.getRun(req.params.runId);
|
|
167
288
|
if (!run) {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { WorkflowContextPacket } from '@/modules/orchestration/workflows/context-packet.js';
|
|
2
2
|
import type { WorkflowFallbackTrigger } from '@/modules/orchestration/workflows/workflow-fallback-policy.js';
|
|
3
3
|
import type { WorkflowHandoffArtifact } from '@/modules/orchestration/workflows/handoff-artifact.js';
|
|
4
|
+
import type {
|
|
5
|
+
PermissionDecision,
|
|
6
|
+
PermissionPolicy,
|
|
7
|
+
} from '@/modules/orchestration/security/permission-policy.js';
|
|
4
8
|
|
|
5
9
|
export type WorkflowRunStatus = 'queued' | 'running' | 'completed' | 'failed' | 'canceled';
|
|
6
10
|
export type WorkflowNodeStatus = WorkflowRunStatus | 'skipped';
|
|
@@ -18,6 +22,8 @@ export interface WorkflowNode {
|
|
|
18
22
|
assignment?: string;
|
|
19
23
|
model?: string;
|
|
20
24
|
permissionMode?: string;
|
|
25
|
+
permissionPolicy?: PermissionPolicy;
|
|
26
|
+
permissionDecisions?: PermissionDecision[];
|
|
21
27
|
toolsSettings?: Record<string, unknown>;
|
|
22
28
|
isolation?: 'host' | 'worktree' | 'docker';
|
|
23
29
|
timeoutMs?: number;
|
|
@@ -44,6 +50,8 @@ export interface WorkflowNodeRun {
|
|
|
44
50
|
promptPreview?: string;
|
|
45
51
|
model?: string;
|
|
46
52
|
permissionMode?: string;
|
|
53
|
+
permissionPolicy?: PermissionPolicy;
|
|
54
|
+
permissionDecisions?: PermissionDecision[];
|
|
47
55
|
timeoutMs?: number;
|
|
48
56
|
stage?: string;
|
|
49
57
|
internal?: boolean;
|
|
@@ -84,7 +92,7 @@ export interface WorkflowRun {
|
|
|
84
92
|
|
|
85
93
|
export interface WorkflowTraceEvent {
|
|
86
94
|
id: string;
|
|
87
|
-
type: 'run' | 'node' | 'provider' | 'message' | 'artifact' | 'file' | 'error';
|
|
95
|
+
type: 'run' | 'node' | 'provider' | 'message' | 'artifact' | 'file' | 'error' | 'permission_policy';
|
|
88
96
|
severity: 'info' | 'warning' | 'error';
|
|
89
97
|
status: WorkflowRunStatus | WorkflowNodeStatus | 'submitted';
|
|
90
98
|
timestamp: number;
|