@pixelbyte-software/pixcode 1.42.3 → 1.42.5

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 (35) hide show
  1. package/dist/assets/{index-BnaWRV1a.js → index-nefOyhzb.js} +168 -168
  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 +2 -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-templates.js +242 -0
  17. package/dist-server/server/modules/orchestration/workflows/workflow-templates.js.map +1 -0
  18. package/dist-server/server/modules/orchestration/workflows/workflow-trace.js +53 -0
  19. package/dist-server/server/modules/orchestration/workflows/workflow-trace.js.map +1 -1
  20. package/dist-server/server/modules/orchestration/workflows/workflow.routes.js +137 -0
  21. package/dist-server/server/modules/orchestration/workflows/workflow.routes.js.map +1 -1
  22. package/package.json +1 -1
  23. package/scripts/smoke/permission-policy.mjs +50 -0
  24. package/scripts/smoke/workflow-templates.mjs +43 -0
  25. package/server/claude-sdk.js +24 -2
  26. package/server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.ts +8 -0
  27. package/server/modules/orchestration/a2a/adapters/claude-code.adapter.ts +2 -0
  28. package/server/modules/orchestration/a2a/routes.ts +6 -0
  29. package/server/modules/orchestration/index.ts +28 -0
  30. package/server/modules/orchestration/security/permission-policy.ts +401 -0
  31. package/server/modules/orchestration/workflows/workflow-runner.ts +141 -1
  32. package/server/modules/orchestration/workflows/workflow-templates.ts +272 -0
  33. package/server/modules/orchestration/workflows/workflow-trace.ts +54 -0
  34. package/server/modules/orchestration/workflows/workflow.routes.ts +165 -0
  35. package/server/modules/orchestration/workflows/workflow.types.ts +9 -1
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from 'node:assert/strict';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+
7
+ const root = process.cwd();
8
+
9
+ function read(relativePath) {
10
+ return fs.readFileSync(path.join(root, relativePath), 'utf8');
11
+ }
12
+
13
+ const policy = read('server/modules/orchestration/security/permission-policy.ts');
14
+ assert.match(policy, /PIXCODE_PERMISSION_POLICY_PROTOCOL/, 'Permission policy should declare a stable protocol id.');
15
+ assert.match(policy, /pixcode\.permission-policy\.v1/, 'Permission policy should use the v1 protocol id.');
16
+ assert.match(policy, /shell/, 'Permission policy should classify shell access.');
17
+ assert.match(policy, /file_write/, 'Permission policy should classify file-write access.');
18
+ assert.match(policy, /external_directory/, 'Permission policy should classify external directory access.');
19
+ assert.match(policy, /network/, 'Permission policy should classify network access.');
20
+ assert.match(policy, /secret/, 'Permission policy should classify secret access.');
21
+ assert.match(policy, /evaluatePermissionRequest/, 'Permission policy should expose a shared evaluator.');
22
+ assert.match(policy, /createPermissionApprovalRequest/, 'Permission policy should create pending approval artifacts.');
23
+ assert.match(policy, /redactPermissionText/, 'Permission policy should redact local paths and secrets.');
24
+
25
+ const runner = read('server/modules/orchestration/workflows/workflow-runner.ts');
26
+ assert.match(runner, /evaluatePermissionRequest/, 'Workflow runner should route node preflight through the shared policy evaluator.');
27
+ assert.match(runner, /permissionPolicyEvents/, 'Workflow runner should store permission policy audit events.');
28
+ assert.match(runner, /pendingPermissionApprovals/, 'Workflow runner should preserve pending approval context on the run.');
29
+
30
+ const trace = read('server/modules/orchestration/workflows/workflow-trace.ts');
31
+ assert.match(trace, /workflow\.trace\.permissionPolicy/, 'Trace timeline should surface permission policy decisions.');
32
+ assert.match(trace, /permission_policy/, 'Trace events should include permission policy entries.');
33
+
34
+ const routes = read('server/modules/orchestration/workflows/workflow.routes.ts');
35
+ assert.match(routes, /permission-policy/, 'Workflow routes should expose the policy contract.');
36
+ assert.match(routes, /permission-approvals/, 'Workflow routes should expose pending approval context.');
37
+
38
+ const claude = read('server/claude-sdk.js');
39
+ assert.match(claude, /evaluatePermissionRequest/, 'Claude tool approvals should use the shared policy evaluator.');
40
+ assert.match(claude, /permissionPolicy/, 'Claude runtime should accept policy metadata.');
41
+
42
+ const a2aContext = read('server/modules/orchestration/a2a/adapters/abstract-a2a.adapter.ts');
43
+ assert.match(a2aContext, /permissionPolicy/, 'A2A adapter context should carry the shared permission policy.');
44
+
45
+ const en = read('src/i18n/locales/en/common.json');
46
+ const tr = read('src/i18n/locales/tr/common.json');
47
+ assert.match(en, /"permission_policy"/, 'English trace type for permission policy is missing.');
48
+ assert.match(tr, /"permission_policy"/, 'Turkish trace type for permission policy is missing.');
49
+
50
+ console.log('permission-policy smoke passed');
@@ -0,0 +1,43 @@
1
+ #!/usr/bin/env node
2
+
3
+ import assert from 'node:assert/strict';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+
7
+ const root = process.cwd();
8
+
9
+ function read(relativePath) {
10
+ return fs.readFileSync(path.join(root, relativePath), 'utf8');
11
+ }
12
+
13
+ const templates = read('server/modules/orchestration/workflows/workflow-templates.ts');
14
+ assert.match(templates, /PIXCODE_WORKFLOW_TEMPLATE_PROTOCOL/, 'Workflow templates should declare a stable protocol id.');
15
+ assert.match(templates, /pixcode\.workflow-template\.v1/, 'Workflow templates should use the v1 protocol id.');
16
+ assert.match(templates, /bug_fix_team/, 'Bug fix team template is missing.');
17
+ assert.match(templates, /pr_review_team/, 'PR review team template is missing.');
18
+ assert.match(templates, /frontend_polish/, 'Frontend polish template is missing.');
19
+ assert.match(templates, /release_manager/, 'Release manager template is missing.');
20
+ assert.match(templates, /dependency_audit/, 'Dependency audit template is missing.');
21
+ assert.match(templates, /agentSlots/, 'Templates should use provider-independent agent slots.');
22
+ assert.match(templates, /acceptanceCriteria/, 'Templates should include acceptance criteria.');
23
+ assert.match(templates, /applyWorkflowTemplateToMetadata/, 'Templates should be applicable to run metadata.');
24
+
25
+ const routes = read('server/modules/orchestration/workflows/workflow.routes.ts');
26
+ assert.match(routes, /workflows\/templates/, 'Workflow routes should expose templates.');
27
+ assert.match(routes, /WORKFLOW_TEMPLATE_NOT_FOUND/, 'Workflow template start route should validate template ids.');
28
+ assert.match(routes, /applyWorkflowTemplateToMetadata/, 'Workflow routes should apply templates before starting runs.');
29
+
30
+ const trace = read('server/modules/orchestration/workflows/workflow-trace.ts');
31
+ assert.match(trace, /workflow\.trace\.template/, 'Trace timeline should surface template metadata.');
32
+
33
+ const page = read('src/components/orchestration/OrchestrationPage.tsx');
34
+ assert.match(page, /WorkflowTemplate/, 'Orchestration page should type workflow templates.');
35
+ assert.match(page, /applyTemplate/, 'Orchestration page should let users apply a template before launch.');
36
+ assert.match(page, /workflowTemplate/, 'Template run metadata should be sent with orchestration runs.');
37
+
38
+ const en = read('src/i18n/locales/en/common.json');
39
+ const tr = read('src/i18n/locales/tr/common.json');
40
+ assert.match(en, /"templates"/, 'English template UI label is missing.');
41
+ assert.match(tr, /"templates"/, 'Turkish template UI label is missing.');
42
+
43
+ console.log('workflow templates smoke passed');
@@ -29,6 +29,7 @@ import { sessionsService } from './modules/providers/services/sessions.service.j
29
29
  import { providerAuthService } from './modules/providers/services/provider-auth.service.js';
30
30
  import { resolveClaudeExecutable, resolveGitBashPath } from './services/install-jobs.js';
31
31
  import { createNormalizedMessage } from './shared/utils.js';
32
+ import { evaluatePermissionRequest } from './modules/orchestration/security/permission-policy.js';
32
33
 
33
34
  const activeSessions = new Map();
34
35
  const pendingToolApprovals = new Map();
@@ -184,7 +185,7 @@ function loadClaudeSettingsEnv(cwd) {
184
185
  * @returns {Object} SDK-compatible options
185
186
  */
186
187
  function mapCliOptionsToSDK(options = {}) {
187
- const { sessionId, cwd, toolsSettings, permissionMode } = options;
188
+ const { sessionId, cwd, toolsSettings, permissionMode, permissionPolicy, permissionPolicyContext } = options;
188
189
 
189
190
  const sdkOptions = {};
190
191
 
@@ -231,6 +232,8 @@ function mapCliOptionsToSDK(options = {}) {
231
232
  if (permissionMode && permissionMode !== 'default') {
232
233
  sdkOptions.permissionMode = permissionMode;
233
234
  }
235
+ sdkOptions.permissionPolicy = permissionPolicy;
236
+ sdkOptions.permissionPolicyContext = permissionPolicyContext;
234
237
 
235
238
  // Map tool settings
236
239
  const settings = toolsSettings || {
@@ -595,8 +598,27 @@ async function queryClaudeSDK(command, options = {}, ws) {
595
598
 
596
599
  sdkOptions.canUseTool = async (toolName, input, context) => {
597
600
  const requiresInteraction = TOOLS_REQUIRING_INTERACTION.has(toolName);
601
+ const policyDecision = evaluatePermissionRequest({
602
+ policy: sdkOptions.permissionPolicy,
603
+ request: {
604
+ source: 'provider_tool',
605
+ toolName,
606
+ input,
607
+ command: input && typeof input === 'object' && typeof input.command === 'string'
608
+ ? input.command
609
+ : undefined,
610
+ cwd: options.cwd,
611
+ workspacePath: options.cwd,
612
+ summary: `${toolName} tool request`,
613
+ },
614
+ context: sdkOptions.permissionPolicyContext,
615
+ });
616
+
617
+ if (policyDecision.behavior === 'deny') {
618
+ return { behavior: 'deny', message: policyDecision.message };
619
+ }
598
620
 
599
- if (!requiresInteraction) {
621
+ if (!requiresInteraction && policyDecision.behavior !== 'prompt') {
600
622
  if (sdkOptions.permissionMode === 'bypassPermissions') {
601
623
  return { behavior: 'allow', updatedInput: input };
602
624
  }
@@ -13,6 +13,10 @@ import type {
13
13
  TaskState,
14
14
  } from '@/modules/orchestration/a2a/types.js';
15
15
  import type { WorkspaceHandle } from '@/modules/orchestration/workspace/types.js';
16
+ import type {
17
+ PermissionPolicy,
18
+ PermissionPolicyContext,
19
+ } from '@/modules/orchestration/security/permission-policy.js';
16
20
 
17
21
  export interface AdapterContext {
18
22
  /** Isolated execution workspace for the task. */
@@ -21,6 +25,10 @@ export interface AdapterContext {
21
25
  cwd: string;
22
26
  /** pixcode permission mode passed through to the underlying CLI. */
23
27
  permissionMode?: string;
28
+ /** Provider-independent permission/sandbox policy evaluated before runtime tool use. */
29
+ permissionPolicy?: PermissionPolicy;
30
+ /** Run context preserved when the policy needs a human approval. */
31
+ permissionPolicyContext?: PermissionPolicyContext;
24
32
  /** Provider model selected by the user in Pixcode. */
25
33
  model?: string;
26
34
  /** Provider-specific tool / permission settings from Pixcode Settings. */
@@ -99,6 +99,8 @@ export class ClaudeCodeA2AAdapter extends AbstractA2AAdapter {
99
99
  {
100
100
  cwd: ctx.cwd,
101
101
  permissionMode: ctx.permissionMode ?? 'default',
102
+ permissionPolicy: ctx.permissionPolicy,
103
+ permissionPolicyContext: ctx.permissionPolicyContext,
102
104
  toolsSettings: ctx.toolsSettings,
103
105
  },
104
106
  fakeWS,
@@ -31,6 +31,10 @@ import type {
31
31
  WorkspaceMetadata,
32
32
  } from '@/modules/orchestration/workspace/types.js';
33
33
  import { WorkspaceError } from '@/modules/orchestration/workspace/types.js';
34
+ import type {
35
+ PermissionPolicy,
36
+ PermissionPolicyContext,
37
+ } from '@/modules/orchestration/security/permission-policy.js';
34
38
 
35
39
  type RoutingHints = {
36
40
  preferredAdapterId?: string;
@@ -475,6 +479,8 @@ export function createA2ARouter(): Router {
475
479
  workspace,
476
480
  model: readString(metadata?.model),
477
481
  permissionMode: readString(metadata?.permissionMode),
482
+ permissionPolicy: readObject(metadata?.permissionPolicy) as PermissionPolicy | undefined,
483
+ permissionPolicyContext: readObject(metadata?.permissionPolicyContext) as PermissionPolicyContext | undefined,
478
484
  toolsSettings: readObject(metadata?.toolsSettings),
479
485
  });
480
486
  } catch (err) {
@@ -19,9 +19,24 @@ export { AbstractA2AAdapter } from './a2a/adapters/abstract-a2a.adapter.js';
19
19
  export { a2aBus } from './a2a/bus.js';
20
20
  export { portWatcher } from './preview/port-watcher.js';
21
21
  export { createPreviewProxyRouter } from './preview/preview-proxy.js';
22
+ export {
23
+ DEFAULT_PERMISSION_POLICY,
24
+ PERMISSION_CAPABILITIES,
25
+ PERMISSION_POLICY_MODES,
26
+ PIXCODE_PERMISSION_POLICY_PROTOCOL,
27
+ evaluatePermissionRequest,
28
+ normalizePermissionPolicy,
29
+ resolvePermissionPolicyFromMetadata,
30
+ } from './security/permission-policy.js';
22
31
  export { createOrchestrationTaskRouter } from './tasks/orchestration-task.routes.js';
23
32
  export { orchestrationTaskService } from './tasks/orchestration-task.service.js';
24
33
  export { createWorkflowRouter } from './workflows/workflow.routes.js';
34
+ export {
35
+ PIXCODE_WORKFLOW_TEMPLATE_PROTOCOL,
36
+ applyWorkflowTemplateToMetadata,
37
+ builtInWorkflowTemplates,
38
+ getWorkflowTemplate,
39
+ } from './workflows/workflow-templates.js';
25
40
  export { workflowRunner } from './workflows/workflow-runner.js';
26
41
  export { workflowStore } from './workflows/workflow-store.js';
27
42
  export { workspaceManager } from './workspace/workspace-manager.js';
@@ -42,12 +57,25 @@ export type {
42
57
  PortEvent,
43
58
  PreviewArtifactData,
44
59
  } from './preview/types.js';
60
+ export type {
61
+ PermissionApprovalRequest,
62
+ PermissionCapability,
63
+ PermissionDecision,
64
+ PermissionPolicy,
65
+ PermissionPolicyContext,
66
+ PermissionPolicyEvent,
67
+ PermissionPolicyMode,
68
+ } from './security/permission-policy.js';
45
69
  export type {
46
70
  CreateOrchestrationTaskInput,
47
71
  DispatchOrchestrationTaskInput,
48
72
  OrchestrationTask,
49
73
  OrchestrationTaskState,
50
74
  } from './tasks/orchestration-task.types.js';
75
+ export type {
76
+ WorkflowTemplate,
77
+ WorkflowTemplateAgentSlot,
78
+ } from './workflows/workflow-templates.js';
51
79
  export type {
52
80
  Workflow,
53
81
  WorkflowNode,
@@ -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
+ }