@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.
- package/dist/assets/{index-BnaWRV1a.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/workflows/workflow-runner.js +115 -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/package.json +1 -1
- package/scripts/smoke/permission-policy.mjs +50 -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/workflows/workflow-runner.ts +141 -1
- 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
|
@@ -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 {
|
|
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);
|