@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.
- package/dist/assets/{index-BnaWRV1a.js → index-nefOyhzb.js} +168 -168
- 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 +2 -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-templates.js +242 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-templates.js.map +1 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-trace.js +53 -0
- package/dist-server/server/modules/orchestration/workflows/workflow-trace.js.map +1 -1
- package/dist-server/server/modules/orchestration/workflows/workflow.routes.js +137 -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/scripts/smoke/workflow-templates.mjs +43 -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 +28 -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-templates.ts +272 -0
- package/server/modules/orchestration/workflows/workflow-trace.ts +54 -0
- package/server/modules/orchestration/workflows/workflow.routes.ts +165 -0
- 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');
|
package/server/claude-sdk.js
CHANGED
|
@@ -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
|
+
}
|