@pixelbyte-software/pixcode 1.42.3 → 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 (31) hide show
  1. package/dist/assets/{index-BnaWRV1a.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/workflows/workflow-runner.js +115 -1
  15. package/dist-server/server/modules/orchestration/workflows/workflow-runner.js.map +1 -1
  16. package/dist-server/server/modules/orchestration/workflows/workflow-trace.js +32 -0
  17. package/dist-server/server/modules/orchestration/workflows/workflow-trace.js.map +1 -1
  18. package/dist-server/server/modules/orchestration/workflows/workflow.routes.js +103 -0
  19. package/dist-server/server/modules/orchestration/workflows/workflow.routes.js.map +1 -1
  20. package/package.json +1 -1
  21. package/scripts/smoke/permission-policy.mjs +50 -0
  22. package/server/claude-sdk.js +24 -2
  23. package/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.ts +8 -0
  24. package/server/modules/orchestration/a2a/adapters/claude-code.adapter.ts +2 -0
  25. package/server/modules/orchestration/a2a/routes.ts +6 -0
  26. package/server/modules/orchestration/index.ts +18 -0
  27. package/server/modules/orchestration/security/permission-policy.ts +401 -0
  28. package/server/modules/orchestration/workflows/workflow-runner.ts +141 -1
  29. package/server/modules/orchestration/workflows/workflow-trace.ts +32 -0
  30. package/server/modules/orchestration/workflows/workflow.routes.ts +121 -0
  31. package/server/modules/orchestration/workflows/workflow.types.ts +9 -1
@@ -0,0 +1,401 @@
1
+ import os from 'node:os';
2
+ import path from 'node:path';
3
+
4
+ export const PIXCODE_PERMISSION_POLICY_PROTOCOL = 'pixcode.permission-policy.v1' as const;
5
+
6
+ export const PERMISSION_CAPABILITIES = [
7
+ 'shell',
8
+ 'file_write',
9
+ 'external_directory',
10
+ 'network',
11
+ 'secret',
12
+ ] as const;
13
+
14
+ export const PERMISSION_POLICY_MODES = ['allow', 'deny', 'prompt', 'audit'] as const;
15
+
16
+ export type PermissionCapability = typeof PERMISSION_CAPABILITIES[number];
17
+ export type PermissionPolicyMode = typeof PERMISSION_POLICY_MODES[number];
18
+ export type PermissionDecisionStatus = 'allowed' | 'denied' | 'needs_approval';
19
+
20
+ export interface PermissionPolicy {
21
+ protocol: typeof PIXCODE_PERMISSION_POLICY_PROTOCOL;
22
+ modes: Record<PermissionCapability, PermissionPolicyMode>;
23
+ allowedExternalDirectories: string[];
24
+ audit: boolean;
25
+ }
26
+
27
+ export interface PermissionRequest {
28
+ requestId?: string;
29
+ source: 'workflow_node' | 'provider_tool' | 'api' | 'shell' | 'file' | string;
30
+ capability?: PermissionCapability;
31
+ capabilities?: PermissionCapability[];
32
+ toolName?: string;
33
+ command?: string;
34
+ input?: unknown;
35
+ cwd?: string;
36
+ workspacePath?: string;
37
+ targetPaths?: string[];
38
+ summary?: string;
39
+ }
40
+
41
+ export interface PermissionPolicyContext {
42
+ runId?: string;
43
+ nodeId?: string;
44
+ workflowId?: string;
45
+ adapterId?: string;
46
+ agentLabel?: string;
47
+ userId?: string | number | null;
48
+ }
49
+
50
+ export interface PermissionApprovalRequest {
51
+ id: string;
52
+ protocol: typeof PIXCODE_PERMISSION_POLICY_PROTOCOL;
53
+ status: 'pending' | 'allowed' | 'denied' | 'canceled';
54
+ capabilities: PermissionCapability[];
55
+ source: string;
56
+ toolName?: string;
57
+ runId?: string;
58
+ nodeId?: string;
59
+ workflowId?: string;
60
+ adapterId?: string;
61
+ agentLabel?: string;
62
+ summary?: string;
63
+ message: string;
64
+ createdAt: number;
65
+ resolvedAt?: number;
66
+ resolvedBy?: string | number | null;
67
+ resolutionMessage?: string;
68
+ }
69
+
70
+ export interface PermissionPolicyEvent {
71
+ id: string;
72
+ protocol: typeof PIXCODE_PERMISSION_POLICY_PROTOCOL;
73
+ status: PermissionDecisionStatus;
74
+ behavior: 'allow' | 'deny' | 'prompt';
75
+ capabilities: PermissionCapability[];
76
+ source: string;
77
+ toolName?: string;
78
+ summary?: string;
79
+ message: string;
80
+ modeByCapability: Partial<Record<PermissionCapability, PermissionPolicyMode>>;
81
+ audit: boolean;
82
+ runId?: string;
83
+ nodeId?: string;
84
+ workflowId?: string;
85
+ adapterId?: string;
86
+ agentLabel?: string;
87
+ createdAt: number;
88
+ }
89
+
90
+ export interface PermissionDecision {
91
+ protocol: typeof PIXCODE_PERMISSION_POLICY_PROTOCOL;
92
+ requestId: string;
93
+ status: PermissionDecisionStatus;
94
+ behavior: 'allow' | 'deny' | 'prompt';
95
+ capabilities: PermissionCapability[];
96
+ modeByCapability: Partial<Record<PermissionCapability, PermissionPolicyMode>>;
97
+ message: string;
98
+ audit: boolean;
99
+ event: PermissionPolicyEvent;
100
+ approvalRequest?: PermissionApprovalRequest;
101
+ }
102
+
103
+ const DEFAULT_MODES: Record<PermissionCapability, PermissionPolicyMode> = {
104
+ shell: 'audit',
105
+ file_write: 'audit',
106
+ external_directory: 'audit',
107
+ network: 'audit',
108
+ secret: 'audit',
109
+ };
110
+
111
+ const NETWORK_COMMAND_PATTERN = /\b(curl|wget|fetch|ssh|scp|rsync|gh|git\s+(?:clone|fetch|pull|push)|npm\s+(?:install|publish|view|pack|audit)|pnpm\s+(?:install|publish)|yarn\s+(?:install|publish)|pip\s+install|cargo\s+(?:install|publish)|go\s+(?:get|install)|docker\s+(?:pull|push|run))\b|https?:\/\//iu;
112
+ const FILE_WRITE_COMMAND_PATTERN = /(^|\s)(apply_patch|touch|mkdir|rm|mv|cp|tee|sed\s+-i|perl\s+-pi|truncate|chmod|chown)\b|>>?|\b(write|edit|patch|delete|create)\s+(?:file|folder|directory)\b/iu;
113
+ const SECRET_COMMAND_PATTERN = /\b(printenv|env|set)\b|\b(cat|type|less|more)\s+(\S*\.env\b|\S*secret\S*|\S*token\S*|\S*key\S*)/iu;
114
+ const SECRET_VALUE_PATTERN = /\b(?:sk|ghp|github_pat|glpat|npm)_[A-Za-z0-9_=-]{12,}\b|(?:api[_-]?key|secret|token|password)\s*[:=]\s*['"]?[A-Za-z0-9_./+=-]{8,}/iu;
115
+
116
+ function readRecord(value: unknown): Record<string, unknown> | undefined {
117
+ return value && typeof value === 'object' ? value as Record<string, unknown> : undefined;
118
+ }
119
+
120
+ function readString(value: unknown): string | undefined {
121
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
122
+ }
123
+
124
+ function readMode(value: unknown): PermissionPolicyMode | undefined {
125
+ return typeof value === 'string' && (PERMISSION_POLICY_MODES as readonly string[]).includes(value)
126
+ ? value as PermissionPolicyMode
127
+ : undefined;
128
+ }
129
+
130
+ function uniqueCapabilities(values: Array<PermissionCapability | undefined>): PermissionCapability[] {
131
+ return [...new Set(values.filter((value): value is PermissionCapability => Boolean(value)))];
132
+ }
133
+
134
+ function requestId(value?: string): string {
135
+ return value?.trim() || `perm_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
136
+ }
137
+
138
+ function normalizePath(value: string | undefined): string | undefined {
139
+ if (!value?.trim()) return undefined;
140
+ try {
141
+ return path.resolve(value);
142
+ } catch {
143
+ return undefined;
144
+ }
145
+ }
146
+
147
+ function isInsidePath(basePath: string | undefined, candidatePath: string | undefined): boolean {
148
+ const base = normalizePath(basePath);
149
+ const candidate = normalizePath(candidatePath);
150
+ if (!base || !candidate) return true;
151
+ if (candidate === base) return true;
152
+ const relative = path.relative(base, candidate);
153
+ return Boolean(relative && !relative.startsWith('..') && !path.isAbsolute(relative));
154
+ }
155
+
156
+ function collectInputPaths(input: unknown): string[] {
157
+ const paths: string[] = [];
158
+ const visit = (value: unknown) => {
159
+ if (typeof value === 'string') {
160
+ if (/^(?:[A-Za-z]:[\\/]|\/|~\/|\.\.?[\\/])/u.test(value.trim())) {
161
+ paths.push(value.trim().replace(/^~(?=$|[\\/])/u, os.homedir()));
162
+ }
163
+ return;
164
+ }
165
+ if (Array.isArray(value)) {
166
+ value.forEach(visit);
167
+ return;
168
+ }
169
+ const record = readRecord(value);
170
+ if (!record) return;
171
+ for (const [key, nested] of Object.entries(record)) {
172
+ if (/path|file|dir|cwd|target|source|destination/iu.test(key)) {
173
+ visit(nested);
174
+ }
175
+ }
176
+ };
177
+ visit(input);
178
+ return paths;
179
+ }
180
+
181
+ function inferCapabilities(request: PermissionRequest): PermissionCapability[] {
182
+ const capabilities: PermissionCapability[] = [];
183
+ if (request.capability) capabilities.push(request.capability);
184
+ if (Array.isArray(request.capabilities)) capabilities.push(...request.capabilities);
185
+
186
+ const toolName = request.toolName?.toLocaleLowerCase('en') ?? '';
187
+ const command = [
188
+ request.command,
189
+ typeof request.input === 'string' ? request.input : undefined,
190
+ readString(readRecord(request.input)?.command),
191
+ readString(readRecord(request.input)?.query),
192
+ ].filter(Boolean).join('\n');
193
+ const inputText = [
194
+ command,
195
+ typeof request.summary === 'string' ? request.summary : undefined,
196
+ typeof request.input === 'object' ? JSON.stringify(request.input) : undefined,
197
+ ].filter(Boolean).join('\n');
198
+
199
+ if (toolName === 'bash' || toolName.includes('shell') || toolName.includes('terminal') || command.trim()) {
200
+ capabilities.push('shell');
201
+ }
202
+ if (/write|edit|multiedit|notebookedit|delete|patch/iu.test(toolName) || FILE_WRITE_COMMAND_PATTERN.test(command)) {
203
+ capabilities.push('file_write');
204
+ }
205
+ if (/webfetch|websearch|fetch|search/iu.test(toolName) || NETWORK_COMMAND_PATTERN.test(command)) {
206
+ capabilities.push('network');
207
+ }
208
+ if (/secret|credential|token|keychain|vault/iu.test(toolName) || SECRET_COMMAND_PATTERN.test(command) || SECRET_VALUE_PATTERN.test(inputText)) {
209
+ capabilities.push('secret');
210
+ }
211
+
212
+ const targetPaths = [
213
+ ...(Array.isArray(request.targetPaths) ? request.targetPaths : []),
214
+ ...collectInputPaths(request.input),
215
+ ];
216
+ if (targetPaths.some((targetPath) => !isInsidePath(request.workspacePath ?? request.cwd, targetPath))) {
217
+ capabilities.push('external_directory');
218
+ }
219
+
220
+ return uniqueCapabilities(capabilities);
221
+ }
222
+
223
+ export const DEFAULT_PERMISSION_POLICY: PermissionPolicy = {
224
+ protocol: PIXCODE_PERMISSION_POLICY_PROTOCOL,
225
+ modes: DEFAULT_MODES,
226
+ allowedExternalDirectories: [],
227
+ audit: true,
228
+ };
229
+
230
+ export function normalizePermissionPolicy(value?: unknown): PermissionPolicy {
231
+ const record = readRecord(value);
232
+ const modeRecord = readRecord(record?.modes) ?? readRecord(record?.rules) ?? record ?? {};
233
+ const modes = { ...DEFAULT_MODES };
234
+ for (const capability of PERMISSION_CAPABILITIES) {
235
+ modes[capability] = readMode(modeRecord[capability]) ?? modes[capability];
236
+ }
237
+
238
+ const allowedExternalDirectories = Array.isArray(record?.allowedExternalDirectories)
239
+ ? record.allowedExternalDirectories
240
+ .map((item) => readString(item))
241
+ .filter((item): item is string => Boolean(item))
242
+ : [];
243
+
244
+ return {
245
+ protocol: PIXCODE_PERMISSION_POLICY_PROTOCOL,
246
+ modes,
247
+ allowedExternalDirectories,
248
+ audit: typeof record?.audit === 'boolean' ? record.audit : true,
249
+ };
250
+ }
251
+
252
+ export function resolvePermissionPolicyFromMetadata(metadata?: Record<string, unknown>): PermissionPolicy {
253
+ const settings = readRecord(metadata?.settings);
254
+ return normalizePermissionPolicy(metadata?.permissionPolicy ?? settings?.permissionPolicy);
255
+ }
256
+
257
+ export function redactPermissionText(
258
+ value: string | undefined,
259
+ context?: { workspacePath?: string; cwd?: string; projectPath?: string },
260
+ ): string | undefined {
261
+ if (!value?.trim()) return undefined;
262
+ const candidates = [
263
+ os.homedir(),
264
+ context?.workspacePath,
265
+ context?.cwd,
266
+ context?.projectPath,
267
+ ].filter((item): item is string => Boolean(item && item.length > 2));
268
+
269
+ let text = value;
270
+ for (const candidate of candidates) {
271
+ text = text.split(candidate).join('[workspace]');
272
+ }
273
+
274
+ return text
275
+ .replace(SECRET_VALUE_PATTERN, '[redacted-secret]')
276
+ .replace(/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}\b/gu, '[redacted-email]')
277
+ .trim();
278
+ }
279
+
280
+ function modeAllowsExternalDirectory(
281
+ request: PermissionRequest,
282
+ policy: PermissionPolicy,
283
+ ): boolean {
284
+ const targetPaths = [
285
+ ...(Array.isArray(request.targetPaths) ? request.targetPaths : []),
286
+ ...collectInputPaths(request.input),
287
+ ];
288
+ if (!targetPaths.length || !policy.allowedExternalDirectories.length) return false;
289
+
290
+ return targetPaths.every((targetPath) =>
291
+ policy.allowedExternalDirectories.some((allowedPath) => isInsidePath(allowedPath, targetPath)),
292
+ );
293
+ }
294
+
295
+ export function createPermissionApprovalRequest(
296
+ decision: Omit<PermissionDecision, 'approvalRequest'>,
297
+ context?: PermissionPolicyContext,
298
+ ): PermissionApprovalRequest {
299
+ return {
300
+ id: decision.requestId,
301
+ protocol: PIXCODE_PERMISSION_POLICY_PROTOCOL,
302
+ status: 'pending',
303
+ capabilities: decision.capabilities,
304
+ source: decision.event.source,
305
+ toolName: decision.event.toolName,
306
+ runId: context?.runId,
307
+ nodeId: context?.nodeId,
308
+ workflowId: context?.workflowId,
309
+ adapterId: context?.adapterId,
310
+ agentLabel: context?.agentLabel,
311
+ summary: decision.event.summary,
312
+ message: decision.message,
313
+ createdAt: decision.event.createdAt,
314
+ };
315
+ }
316
+
317
+ export function evaluatePermissionRequest({
318
+ policy: policyInput,
319
+ request,
320
+ context,
321
+ }: {
322
+ policy?: unknown;
323
+ request: PermissionRequest;
324
+ context?: PermissionPolicyContext;
325
+ }): PermissionDecision {
326
+ const policy = normalizePermissionPolicy(policyInput);
327
+ const id = requestId(request.requestId);
328
+ const capabilities = inferCapabilities(request);
329
+ const modeByCapability: Partial<Record<PermissionCapability, PermissionPolicyMode>> = {};
330
+
331
+ for (const capability of capabilities) {
332
+ const mode = capability === 'external_directory' && modeAllowsExternalDirectory(request, policy)
333
+ ? 'allow'
334
+ : policy.modes[capability];
335
+ modeByCapability[capability] = mode;
336
+ }
337
+
338
+ const modes = Object.values(modeByCapability);
339
+ const behavior: PermissionDecision['behavior'] = modes.includes('deny')
340
+ ? 'deny'
341
+ : modes.includes('prompt')
342
+ ? 'prompt'
343
+ : 'allow';
344
+ const status: PermissionDecisionStatus = behavior === 'deny'
345
+ ? 'denied'
346
+ : behavior === 'prompt'
347
+ ? 'needs_approval'
348
+ : 'allowed';
349
+ const summary = redactPermissionText(
350
+ request.summary
351
+ ?? request.command
352
+ ?? (request.toolName ? `${request.toolName} tool request` : `${request.source} permission request`),
353
+ {
354
+ workspacePath: request.workspacePath,
355
+ cwd: request.cwd,
356
+ },
357
+ );
358
+ const capabilityText = capabilities.length ? capabilities.join(', ') : 'unclassified';
359
+ const message = behavior === 'deny'
360
+ ? `Permission policy denied ${capabilityText}.`
361
+ : behavior === 'prompt'
362
+ ? `Permission policy requires approval for ${capabilityText}.`
363
+ : `Permission policy allowed ${capabilityText}.`;
364
+ const event: PermissionPolicyEvent = {
365
+ id,
366
+ protocol: PIXCODE_PERMISSION_POLICY_PROTOCOL,
367
+ status,
368
+ behavior,
369
+ capabilities,
370
+ source: request.source,
371
+ toolName: request.toolName,
372
+ summary,
373
+ message,
374
+ modeByCapability,
375
+ audit: policy.audit || modes.includes('audit'),
376
+ runId: context?.runId,
377
+ nodeId: context?.nodeId,
378
+ workflowId: context?.workflowId,
379
+ adapterId: context?.adapterId,
380
+ agentLabel: context?.agentLabel,
381
+ createdAt: Date.now(),
382
+ };
383
+ const decisionBase: Omit<PermissionDecision, 'approvalRequest'> = {
384
+ protocol: PIXCODE_PERMISSION_POLICY_PROTOCOL,
385
+ requestId: id,
386
+ status,
387
+ behavior,
388
+ capabilities,
389
+ modeByCapability,
390
+ message,
391
+ audit: event.audit,
392
+ event,
393
+ };
394
+
395
+ return {
396
+ ...decisionBase,
397
+ approvalRequest: behavior === 'prompt'
398
+ ? createPermissionApprovalRequest(decisionBase, context)
399
+ : undefined,
400
+ };
401
+ }
@@ -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,
@@ -36,7 +43,12 @@ import {
36
43
  getStaticProviderModels,
37
44
  } from '@/services/model-registry.js';
38
45
  // @ts-ignore — plain-JS service
39
- import { notifyRunFailed, notifyRunStopped } from '@/services/notification-orchestrator.js';
46
+ import {
47
+ createNotificationEvent,
48
+ notifyRunFailed,
49
+ notifyRunStopped,
50
+ notifyUserIfEnabled,
51
+ } from '@/services/notification-orchestrator.js';
40
52
 
41
53
  const TERMINAL = new Set(['completed', 'failed', 'canceled']);
42
54
  const SKIPPED = 'skipped';
@@ -245,6 +257,49 @@ function notifyWorkflowRunFinished(run: WorkflowRun): void {
245
257
  }
246
258
  }
247
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
+
248
303
  function readBoolean(value: unknown): boolean | undefined {
249
304
  return typeof value === 'boolean' ? value : undefined;
250
305
  }
@@ -1216,8 +1271,10 @@ class WorkflowRunner {
1216
1271
  const runtimeWorkflow = expandWorkflowForRun(workflow, metadata);
1217
1272
  validateWorkflow(runtimeWorkflow);
1218
1273
  const workspaceTarget = resolveWorkflowWorkspace(metadata);
1274
+ const permissionPolicy = resolvePermissionPolicyFromMetadata(metadata);
1219
1275
  const runMetadata: Record<string, unknown> = {
1220
1276
  ...metadata,
1277
+ permissionPolicy,
1221
1278
  projectPath: workspaceTarget.projectPath,
1222
1279
  selectedProjectPath: workspaceTarget.selectedProjectPath,
1223
1280
  workspaceTarget: workspaceTargetMetadata(workspaceTarget),
@@ -1602,6 +1659,37 @@ class WorkflowRunner {
1602
1659
  }
1603
1660
  }
1604
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
+
1605
1693
  private async executeNode(
1606
1694
  node: WorkflowNode,
1607
1695
  workflow: Workflow,
@@ -1680,6 +1768,49 @@ class WorkflowRunner {
1680
1768
  };
1681
1769
  workflowStore.setRun(run);
1682
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
+ }
1683
1814
  let body: { id?: string; error?: { message?: string } };
1684
1815
  try {
1685
1816
  const submit = await fetch(`${localA2ABaseUrl()}/tasks`, {
@@ -1701,6 +1832,15 @@ class WorkflowRunner {
1701
1832
  assignment: node.assignment,
1702
1833
  model: effectiveModel,
1703
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
+ },
1704
1844
  toolsSettings: node.toolsSettings,
1705
1845
  projectPath,
1706
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);