@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.
Files changed (42) hide show
  1. package/dist/assets/{index-CMeiCqQf.js → index-cTGs3Dvx.js} +72 -72
  2. package/dist/index.html +1 -1
  3. package/dist-server/server/claude-sdk.js +23 -2
  4. package/dist-server/server/claude-sdk.js.map +1 -1
  5. package/dist-server/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.js.map +1 -1
  6. package/dist-server/server/modules/orchestration/a2a/adapters/claude-code.adapter.js +2 -0
  7. package/dist-server/server/modules/orchestration/a2a/adapters/claude-code.adapter.js.map +1 -1
  8. package/dist-server/server/modules/orchestration/a2a/routes.js +2 -0
  9. package/dist-server/server/modules/orchestration/a2a/routes.js.map +1 -1
  10. package/dist-server/server/modules/orchestration/index.js +1 -0
  11. package/dist-server/server/modules/orchestration/index.js.map +1 -1
  12. package/dist-server/server/modules/orchestration/security/permission-policy.js +269 -0
  13. package/dist-server/server/modules/orchestration/security/permission-policy.js.map +1 -0
  14. package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js +86 -0
  15. package/dist-server/server/modules/orchestration/tasks/orchestration-task.service.js.map +1 -1
  16. package/dist-server/server/modules/orchestration/tasks/task-run-graph.js +158 -0
  17. package/dist-server/server/modules/orchestration/tasks/task-run-graph.js.map +1 -0
  18. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js +121 -1
  19. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -1
  20. package/dist-server/server/modules/orchestration/workflows/workflow-trace.js +32 -0
  21. package/dist-server/server/modules/orchestration/workflows/workflow-trace.js.map +1 -1
  22. package/dist-server/server/modules/orchestration/workflows/workflow.routes.js +103 -0
  23. package/dist-server/server/modules/orchestration/workflows/workflow.routes.js.map +1 -1
  24. package/dist-server/server/routes/taskmaster.js +93 -25
  25. package/dist-server/server/routes/taskmaster.js.map +1 -1
  26. package/package.json +1 -1
  27. package/scripts/smoke/permission-policy.mjs +50 -0
  28. package/scripts/smoke/taskmaster-run-graph.mjs +55 -0
  29. package/server/claude-sdk.js +24 -2
  30. package/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.ts +8 -0
  31. package/server/modules/orchestration/a2a/adapters/claude-code.adapter.ts +2 -0
  32. package/server/modules/orchestration/a2a/routes.ts +6 -0
  33. package/server/modules/orchestration/index.ts +18 -0
  34. package/server/modules/orchestration/security/permission-policy.ts +401 -0
  35. package/server/modules/orchestration/tasks/orchestration-task.service.ts +94 -0
  36. package/server/modules/orchestration/tasks/orchestration-task.types.ts +10 -0
  37. package/server/modules/orchestration/tasks/task-run-graph.ts +219 -0
  38. package/server/modules/orchestration/workflows/workflow-runner.ts +148 -2
  39. package/server/modules/orchestration/workflows/workflow-trace.ts +32 -0
  40. package/server/modules/orchestration/workflows/workflow.routes.ts +121 -0
  41. package/server/modules/orchestration/workflows/workflow.types.ts +9 -1
  42. 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 { notifyRunFailed, notifyRunStopped } from '@/services/notification-orchestrator.js';
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 runMetadata = {
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;