@jean2/sdk 0.9.0
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/client.d.ts +34 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +102 -0
- package/dist/client.js.map +1 -0
- package/dist/emitter.d.ts +12 -0
- package/dist/emitter.d.ts.map +1 -0
- package/dist/emitter.js +59 -0
- package/dist/emitter.js.map +1 -0
- package/dist/errors.d.ts +25 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +49 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +36 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/namespaces/chat.d.ts +12 -0
- package/dist/namespaces/chat.d.ts.map +1 -0
- package/dist/namespaces/chat.js +18 -0
- package/dist/namespaces/chat.js.map +1 -0
- package/dist/namespaces/permissions.d.ts +26 -0
- package/dist/namespaces/permissions.d.ts.map +1 -0
- package/dist/namespaces/permissions.js +33 -0
- package/dist/namespaces/permissions.js.map +1 -0
- package/dist/namespaces/providers.d.ts +8 -0
- package/dist/namespaces/providers.d.ts.map +1 -0
- package/dist/namespaces/providers.js +13 -0
- package/dist/namespaces/providers.js.map +1 -0
- package/dist/namespaces/queue.d.ts +11 -0
- package/dist/namespaces/queue.d.ts.map +1 -0
- package/dist/namespaces/queue.js +19 -0
- package/dist/namespaces/queue.js.map +1 -0
- package/dist/namespaces/sessions.d.ts +28 -0
- package/dist/namespaces/sessions.d.ts.map +1 -0
- package/dist/namespaces/sessions.js +43 -0
- package/dist/namespaces/sessions.js.map +1 -0
- package/dist/namespaces/terminal.d.ts +100 -0
- package/dist/namespaces/terminal.d.ts.map +1 -0
- package/dist/namespaces/terminal.js +495 -0
- package/dist/namespaces/terminal.js.map +1 -0
- package/dist/rest/attachments.d.ts +40 -0
- package/dist/rest/attachments.d.ts.map +1 -0
- package/dist/rest/attachments.js +40 -0
- package/dist/rest/attachments.js.map +1 -0
- package/dist/rest/config.d.ts +88 -0
- package/dist/rest/config.d.ts.map +1 -0
- package/dist/rest/config.js +83 -0
- package/dist/rest/config.js.map +1 -0
- package/dist/rest/files.d.ts +37 -0
- package/dist/rest/files.d.ts.map +1 -0
- package/dist/rest/files.js +59 -0
- package/dist/rest/files.js.map +1 -0
- package/dist/rest/http-namespace.d.ts +54 -0
- package/dist/rest/http-namespace.d.ts.map +1 -0
- package/dist/rest/http-namespace.js +67 -0
- package/dist/rest/http-namespace.js.map +1 -0
- package/dist/rest/mcp.d.ts +43 -0
- package/dist/rest/mcp.d.ts.map +1 -0
- package/dist/rest/mcp.js +41 -0
- package/dist/rest/mcp.js.map +1 -0
- package/dist/rest/models.d.ts +12 -0
- package/dist/rest/models.d.ts.map +1 -0
- package/dist/rest/models.js +10 -0
- package/dist/rest/models.js.map +1 -0
- package/dist/rest/preconfigs.d.ts +51 -0
- package/dist/rest/preconfigs.d.ts.map +1 -0
- package/dist/rest/preconfigs.js +29 -0
- package/dist/rest/preconfigs.js.map +1 -0
- package/dist/rest/prompts.d.ts +12 -0
- package/dist/rest/prompts.d.ts.map +1 -0
- package/dist/rest/prompts.js +10 -0
- package/dist/rest/prompts.js.map +1 -0
- package/dist/rest/providers.d.ts +56 -0
- package/dist/rest/providers.d.ts.map +1 -0
- package/dist/rest/providers.js +59 -0
- package/dist/rest/providers.js.map +1 -0
- package/dist/rest/sessions.d.ts +54 -0
- package/dist/rest/sessions.d.ts.map +1 -0
- package/dist/rest/sessions.js +47 -0
- package/dist/rest/sessions.js.map +1 -0
- package/dist/rest/terminals.d.ts +28 -0
- package/dist/rest/terminals.d.ts.map +1 -0
- package/dist/rest/terminals.js +22 -0
- package/dist/rest/terminals.js.map +1 -0
- package/dist/rest/tools.d.ts +34 -0
- package/dist/rest/tools.d.ts.map +1 -0
- package/dist/rest/tools.js +37 -0
- package/dist/rest/tools.js.map +1 -0
- package/dist/rest/workspaces.d.ts +28 -0
- package/dist/rest/workspaces.d.ts.map +1 -0
- package/dist/rest/workspaces.js +34 -0
- package/dist/rest/workspaces.js.map +1 -0
- package/dist/shared-protocol/client.d.ts +148 -0
- package/dist/shared-protocol/client.d.ts.map +1 -0
- package/dist/shared-protocol/client.js +4 -0
- package/dist/shared-protocol/client.js.map +1 -0
- package/dist/shared-protocol/index.d.ts +4 -0
- package/dist/shared-protocol/index.d.ts.map +1 -0
- package/dist/shared-protocol/index.js +4 -0
- package/dist/shared-protocol/index.js.map +1 -0
- package/dist/shared-protocol/server.d.ts +233 -0
- package/dist/shared-protocol/server.d.ts.map +1 -0
- package/dist/shared-protocol/server.js +2 -0
- package/dist/shared-protocol/server.js.map +1 -0
- package/dist/shared-protocol/terminal.d.ts +55 -0
- package/dist/shared-protocol/terminal.d.ts.map +1 -0
- package/dist/shared-protocol/terminal.js +2 -0
- package/dist/shared-protocol/terminal.js.map +1 -0
- package/dist/shared-types/configuration.d.ts +100 -0
- package/dist/shared-types/configuration.d.ts.map +1 -0
- package/dist/shared-types/configuration.js +2 -0
- package/dist/shared-types/configuration.js.map +1 -0
- package/dist/shared-types/file.d.ts +47 -0
- package/dist/shared-types/file.d.ts.map +1 -0
- package/dist/shared-types/file.js +2 -0
- package/dist/shared-types/file.js.map +1 -0
- package/dist/shared-types/index.d.ts +20 -0
- package/dist/shared-types/index.d.ts.map +1 -0
- package/dist/shared-types/index.js +20 -0
- package/dist/shared-types/index.js.map +1 -0
- package/dist/shared-types/interrupt.d.ts +25 -0
- package/dist/shared-types/interrupt.d.ts.map +1 -0
- package/dist/shared-types/interrupt.js +2 -0
- package/dist/shared-types/interrupt.js.map +1 -0
- package/dist/shared-types/mcp.d.ts +49 -0
- package/dist/shared-types/mcp.d.ts.map +1 -0
- package/dist/shared-types/mcp.js +6 -0
- package/dist/shared-types/mcp.js.map +1 -0
- package/dist/shared-types/message.d.ts +166 -0
- package/dist/shared-types/message.d.ts.map +1 -0
- package/dist/shared-types/message.js +34 -0
- package/dist/shared-types/message.js.map +1 -0
- package/dist/shared-types/model.d.ts +60 -0
- package/dist/shared-types/model.d.ts.map +1 -0
- package/dist/shared-types/model.js +2 -0
- package/dist/shared-types/model.js.map +1 -0
- package/dist/shared-types/permission.d.ts +209 -0
- package/dist/shared-types/permission.d.ts.map +1 -0
- package/dist/shared-types/permission.js +787 -0
- package/dist/shared-types/permission.js.map +1 -0
- package/dist/shared-types/preconfig.d.ts +31 -0
- package/dist/shared-types/preconfig.d.ts.map +1 -0
- package/dist/shared-types/preconfig.js +2 -0
- package/dist/shared-types/preconfig.js.map +1 -0
- package/dist/shared-types/prompt.d.ts +6 -0
- package/dist/shared-types/prompt.d.ts.map +1 -0
- package/dist/shared-types/prompt.js +2 -0
- package/dist/shared-types/prompt.js.map +1 -0
- package/dist/shared-types/provider.d.ts +31 -0
- package/dist/shared-types/provider.d.ts.map +1 -0
- package/dist/shared-types/provider.js +2 -0
- package/dist/shared-types/provider.js.map +1 -0
- package/dist/shared-types/runtime.d.ts +27 -0
- package/dist/shared-types/runtime.d.ts.map +1 -0
- package/dist/shared-types/runtime.js +2 -0
- package/dist/shared-types/runtime.js.map +1 -0
- package/dist/shared-types/server.d.ts +16 -0
- package/dist/shared-types/server.d.ts.map +1 -0
- package/dist/shared-types/server.js +2 -0
- package/dist/shared-types/server.js.map +1 -0
- package/dist/shared-types/session.d.ts +24 -0
- package/dist/shared-types/session.d.ts.map +1 -0
- package/dist/shared-types/session.js +2 -0
- package/dist/shared-types/session.js.map +1 -0
- package/dist/shared-types/skill.d.ts +24 -0
- package/dist/shared-types/skill.d.ts.map +1 -0
- package/dist/shared-types/skill.js +2 -0
- package/dist/shared-types/skill.js.map +1 -0
- package/dist/shared-types/task.d.ts +45 -0
- package/dist/shared-types/task.d.ts.map +1 -0
- package/dist/shared-types/task.js +2 -0
- package/dist/shared-types/task.js.map +1 -0
- package/dist/shared-types/tool.d.ts +250 -0
- package/dist/shared-types/tool.d.ts.map +1 -0
- package/dist/shared-types/tool.js +3 -0
- package/dist/shared-types/tool.js.map +1 -0
- package/dist/shared-types/ui.d.ts +35 -0
- package/dist/shared-types/ui.d.ts.map +1 -0
- package/dist/shared-types/ui.js +52 -0
- package/dist/shared-types/ui.js.map +1 -0
- package/dist/shared-types/visualization.d.ts +208 -0
- package/dist/shared-types/visualization.d.ts.map +1 -0
- package/dist/shared-types/visualization.js +2 -0
- package/dist/shared-types/visualization.js.map +1 -0
- package/dist/shared-types/workspace.d.ts +9 -0
- package/dist/shared-types/workspace.d.ts.map +1 -0
- package/dist/shared-types/workspace.js +2 -0
- package/dist/shared-types/workspace.js.map +1 -0
- package/dist/shared-utils/index.d.ts +2 -0
- package/dist/shared-utils/index.d.ts.map +1 -0
- package/dist/shared-utils/index.js +2 -0
- package/dist/shared-utils/index.js.map +1 -0
- package/dist/shared-utils/model-context.d.ts +15 -0
- package/dist/shared-utils/model-context.d.ts.map +1 -0
- package/dist/shared-utils/model-context.js +22 -0
- package/dist/shared-utils/model-context.js.map +1 -0
- package/dist/shared.d.ts +4 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +4 -0
- package/dist/shared.js.map +1 -0
- package/dist/transport/http.d.ts +37 -0
- package/dist/transport/http.d.ts.map +1 -0
- package/dist/transport/http.js +75 -0
- package/dist/transport/http.js.map +1 -0
- package/dist/transport/websocket.d.ts +33 -0
- package/dist/transport/websocket.d.ts.map +1 -0
- package/dist/transport/websocket.js +145 -0
- package/dist/transport/websocket.js.map +1 -0
- package/dist/types/index.d.ts +22 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +3 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/rest-responses.d.ts +389 -0
- package/dist/types/rest-responses.d.ts.map +1 -0
- package/dist/types/rest-responses.js +2 -0
- package/dist/types/rest-responses.js.map +1 -0
- package/dist/types/sdk-types.d.ts +32 -0
- package/dist/types/sdk-types.d.ts.map +1 -0
- package/dist/types/sdk-types.js +2 -0
- package/dist/types/sdk-types.js.map +1 -0
- package/dist/types/server-messages.d.ts +132 -0
- package/dist/types/server-messages.d.ts.map +1 -0
- package/dist/types/server-messages.js +135 -0
- package/dist/types/server-messages.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/package.json +31 -0
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// Canonical Permission Contract
|
|
3
|
+
//
|
|
4
|
+
// Design principles:
|
|
5
|
+
// 1. Tool-authored, platform-rendered: Tools define structured PermissionAsk
|
|
6
|
+
// objects; client renders generically via the same UI
|
|
7
|
+
// 2. Shell is first-class: Explicit shell-command permission category with
|
|
8
|
+
// pattern matching (command names like rm, sudo, curl)
|
|
9
|
+
// 3. Explicit grant semantics: Specific grant scopes (once/session/workspace)
|
|
10
|
+
// with duration support. No 'always' scope — use 'workspace' instead.
|
|
11
|
+
// 4. ask.* is the only interactive channel: No side-channel grants
|
|
12
|
+
// 5. Persisted grants human-readable: Structured metadata for UI review
|
|
13
|
+
// =============================================================================
|
|
14
|
+
// =============================================================================
|
|
15
|
+
// Shell-Specific Permission Helpers
|
|
16
|
+
// =============================================================================
|
|
17
|
+
export const SHELL_DANGEROUS_COMMANDS = [
|
|
18
|
+
'rm', 'rmdir', 'del', 'erase',
|
|
19
|
+
'sudo', 'su', 'doas',
|
|
20
|
+
'chmod', 'chown',
|
|
21
|
+
'dd', 'mkfs', 'format',
|
|
22
|
+
'shutdown', 'reboot', 'halt',
|
|
23
|
+
'iptables', 'ufw', 'firewall-cmd',
|
|
24
|
+
'curl', 'wget', 'nc', 'netcat',
|
|
25
|
+
'eval', 'exec',
|
|
26
|
+
];
|
|
27
|
+
export const SHELL_FILESYSTEM_COMMANDS = [
|
|
28
|
+
'mv', 'cp', 'mkdir', 'touch', 'ln',
|
|
29
|
+
'git push', 'git reset --hard',
|
|
30
|
+
];
|
|
31
|
+
export const SHELL_SHELL_OPERATORS = ['&&', '||', '|', '>', '>>', '`', '$(', ';'];
|
|
32
|
+
export function splitShellCommandSegments(command) {
|
|
33
|
+
const normalized = command
|
|
34
|
+
.replace(/&&|\|\||;|\||>>|>|`|\$\(/g, '\n')
|
|
35
|
+
.split('\n')
|
|
36
|
+
.map(part => part.trim())
|
|
37
|
+
.filter(Boolean);
|
|
38
|
+
return normalized.map((segment) => {
|
|
39
|
+
const parts = segment.split(/\s+/);
|
|
40
|
+
const baseCommand = parts[0]?.replace(/.*\//, '') || '';
|
|
41
|
+
const args = parts.slice(1);
|
|
42
|
+
const flags = args.filter(arg => arg.startsWith('-'));
|
|
43
|
+
return {
|
|
44
|
+
raw: segment,
|
|
45
|
+
baseCommand,
|
|
46
|
+
args,
|
|
47
|
+
flags,
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function normalizeShellCommandIdentity(segment) {
|
|
52
|
+
const base = segment.baseCommand;
|
|
53
|
+
const firstNonFlagArg = segment.args.find(arg => !arg.startsWith('-'));
|
|
54
|
+
if (!base) {
|
|
55
|
+
return segment.raw;
|
|
56
|
+
}
|
|
57
|
+
if (base === 'git' && firstNonFlagArg) {
|
|
58
|
+
return `git ${firstNonFlagArg}`;
|
|
59
|
+
}
|
|
60
|
+
if (['npm', 'pnpm', 'yarn', 'bun', 'cargo'].includes(base) && firstNonFlagArg) {
|
|
61
|
+
return `${base} ${firstNonFlagArg}`;
|
|
62
|
+
}
|
|
63
|
+
return base;
|
|
64
|
+
}
|
|
65
|
+
function getShellCommandDangerPriority(identity) {
|
|
66
|
+
const lower = identity.toLowerCase();
|
|
67
|
+
if (lower === 'shutdown' || lower === 'reboot' || lower === 'halt')
|
|
68
|
+
return 5;
|
|
69
|
+
if (lower === 'sudo' || lower === 'su' || lower === 'doas' || lower === 'chmod' || lower === 'chown')
|
|
70
|
+
return 4;
|
|
71
|
+
if (lower === 'rm' || lower.startsWith('rm ') || lower === 'rmdir' || lower === 'del' || lower === 'erase' || lower === 'dd' || lower === 'mkfs' || lower === 'format')
|
|
72
|
+
return 3;
|
|
73
|
+
if (lower === 'mv' || lower === 'cp' || lower === 'mkdir' || lower === 'touch' || lower === 'ln' || lower === 'git push' || lower === 'git reset')
|
|
74
|
+
return 2;
|
|
75
|
+
if (lower === 'curl' || lower === 'wget' || lower === 'nc' || lower === 'netcat' || lower === 'iptables' || lower === 'ufw' || lower === 'firewall-cmd' || lower === 'eval' || lower === 'exec')
|
|
76
|
+
return 1;
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
export function getEffectiveShellCommandIdentity(command) {
|
|
80
|
+
const segments = splitShellCommandSegments(command);
|
|
81
|
+
if (segments.length === 0) {
|
|
82
|
+
return '';
|
|
83
|
+
}
|
|
84
|
+
let selectedIdentity = normalizeShellCommandIdentity(segments[0]);
|
|
85
|
+
let selectedPriority = getShellCommandDangerPriority(selectedIdentity);
|
|
86
|
+
for (const segment of segments.slice(1)) {
|
|
87
|
+
const identity = normalizeShellCommandIdentity(segment);
|
|
88
|
+
const priority = getShellCommandDangerPriority(identity);
|
|
89
|
+
if (priority > selectedPriority) {
|
|
90
|
+
selectedIdentity = identity;
|
|
91
|
+
selectedPriority = priority;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return selectedIdentity;
|
|
95
|
+
}
|
|
96
|
+
function isDangerousShellIdentity(identity) {
|
|
97
|
+
return getShellCommandDangerPriority(identity) > 0;
|
|
98
|
+
}
|
|
99
|
+
export const SENSITIVE_FILE_PATTERNS = [
|
|
100
|
+
'.env', '.pem', '.key', '.ssh/', 'id_rsa', 'id_ed25519',
|
|
101
|
+
'.gitconfig', '.npmrc', 'credentials', 'secrets', 'password',
|
|
102
|
+
'.htpasswd',
|
|
103
|
+
];
|
|
104
|
+
// =============================================================================
|
|
105
|
+
// Scope Policy: allowed scopes per intent type
|
|
106
|
+
// =============================================================================
|
|
107
|
+
const FILE_READ_ALLOWED_SCOPES = ['once', 'session', 'workspace'];
|
|
108
|
+
const FILE_WRITE_ALLOWED_SCOPES = ['once', 'session', 'workspace'];
|
|
109
|
+
const FILE_DELETE_ALLOWED_SCOPES = ['once', 'session'];
|
|
110
|
+
const NETWORK_REQUEST_ALLOWED_SCOPES = ['once', 'session', 'workspace'];
|
|
111
|
+
const SHELL_EXECUTE_ALLOWED_SCOPES = ['once', 'session'];
|
|
112
|
+
export function getAllowedScopesForIntent(intent) {
|
|
113
|
+
if (!intent.persistable)
|
|
114
|
+
return ['once'];
|
|
115
|
+
switch (intent.resource) {
|
|
116
|
+
case 'file':
|
|
117
|
+
switch (intent.action) {
|
|
118
|
+
case 'read': return FILE_READ_ALLOWED_SCOPES;
|
|
119
|
+
case 'write': return FILE_WRITE_ALLOWED_SCOPES;
|
|
120
|
+
case 'delete': return FILE_DELETE_ALLOWED_SCOPES;
|
|
121
|
+
default: return ['once'];
|
|
122
|
+
}
|
|
123
|
+
case 'network':
|
|
124
|
+
return NETWORK_REQUEST_ALLOWED_SCOPES;
|
|
125
|
+
case 'shell-command':
|
|
126
|
+
return SHELL_EXECUTE_ALLOWED_SCOPES;
|
|
127
|
+
default:
|
|
128
|
+
return ['once'];
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// =============================================================================
|
|
132
|
+
// Shell Effect Analyzer
|
|
133
|
+
// =============================================================================
|
|
134
|
+
const FILE_READ_COMMANDS = ['cat', 'head', 'tail', 'less', 'more', 'wc', 'file', 'stat', 'ls', 'find', 'grep', 'awk', 'sed', 'sort', 'uniq', 'diff', 'comm', 'cut', 'tr', 'tee'];
|
|
135
|
+
const FILE_WRITE_COMMANDS = ['touch', 'mkdir'];
|
|
136
|
+
const FILE_DELETE_COMMANDS = ['rm', 'rmdir', 'del', 'erase'];
|
|
137
|
+
const NETWORK_COMMANDS = ['curl', 'wget', 'nc', 'netcat'];
|
|
138
|
+
function isFileReadCommand(cmd) {
|
|
139
|
+
return FILE_READ_COMMANDS.includes(cmd);
|
|
140
|
+
}
|
|
141
|
+
function isFileWriteCommand(cmd) {
|
|
142
|
+
return FILE_WRITE_COMMANDS.includes(cmd);
|
|
143
|
+
}
|
|
144
|
+
function isFileDeleteCommand(cmd) {
|
|
145
|
+
return FILE_DELETE_COMMANDS.includes(cmd);
|
|
146
|
+
}
|
|
147
|
+
function isNetworkCommand(cmd) {
|
|
148
|
+
return NETWORK_COMMANDS.includes(cmd);
|
|
149
|
+
}
|
|
150
|
+
function _isOperatorCommand(command) {
|
|
151
|
+
return SHELL_SHELL_OPERATORS.some(op => command.includes(op));
|
|
152
|
+
}
|
|
153
|
+
function hasDynamicExpansion(command) {
|
|
154
|
+
return /\$\{?[^}]+\}?/.test(command) || command.includes('$(');
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Extract non-flag, non-command arguments from a shell command string.
|
|
158
|
+
* Returns the arguments that likely represent file paths or targets.
|
|
159
|
+
*/
|
|
160
|
+
function _extractTargetArguments(command) {
|
|
161
|
+
const parts = command.trim().split(/\s+/);
|
|
162
|
+
const args = [];
|
|
163
|
+
let i = 1; // skip base command
|
|
164
|
+
// Skip subcommand for git/npm/etc
|
|
165
|
+
const base = parts[0]?.replace(/.*\//, '') || '';
|
|
166
|
+
if (['git'].includes(base) && parts[1] && !parts[1].startsWith('-')) {
|
|
167
|
+
i = 2; // skip "git" and "push"/etc
|
|
168
|
+
}
|
|
169
|
+
if (['npm', 'pnpm', 'yarn', 'bun', 'cargo'].includes(base) && parts[1] && !parts[1].startsWith('-')) {
|
|
170
|
+
i = 2;
|
|
171
|
+
}
|
|
172
|
+
for (; i < parts.length; i++) {
|
|
173
|
+
const part = parts[i];
|
|
174
|
+
if (!part)
|
|
175
|
+
continue;
|
|
176
|
+
// Skip flags
|
|
177
|
+
if (part.startsWith('-'))
|
|
178
|
+
continue;
|
|
179
|
+
// Skip output redirection targets
|
|
180
|
+
if (part === '>' || part === '>>') {
|
|
181
|
+
i++;
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
args.push(part);
|
|
185
|
+
}
|
|
186
|
+
return args;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Analyze a shell command into structured permission intents.
|
|
190
|
+
*
|
|
191
|
+
* This is the core of the target-based permission system.
|
|
192
|
+
* Instead of "allow cat", it produces "allow file read .env".
|
|
193
|
+
*/
|
|
194
|
+
export function analyzeShellCommandEffects(params) {
|
|
195
|
+
const { command, baseCommand, resolvedPaths, hasOperators, riskCategory } = params;
|
|
196
|
+
const intents = [];
|
|
197
|
+
const hasDynamic = hasDynamicExpansion(command);
|
|
198
|
+
// If operators are present, the command is too complex for reliable target analysis
|
|
199
|
+
if (hasOperators) {
|
|
200
|
+
intents.push({
|
|
201
|
+
resource: 'shell-command',
|
|
202
|
+
action: 'execute',
|
|
203
|
+
targets: [{ target: baseCommand, matcher: 'exact' }],
|
|
204
|
+
persistable: false,
|
|
205
|
+
nonPersistableReason: 'command contains shell operators',
|
|
206
|
+
allowedScopes: ['once', 'session'],
|
|
207
|
+
});
|
|
208
|
+
return intents;
|
|
209
|
+
}
|
|
210
|
+
// Network commands
|
|
211
|
+
if (isNetworkCommand(baseCommand)) {
|
|
212
|
+
const targets = extractNetworkTargets(command, baseCommand);
|
|
213
|
+
if (targets.length > 0) {
|
|
214
|
+
const intent = {
|
|
215
|
+
resource: 'network',
|
|
216
|
+
action: 'request',
|
|
217
|
+
targets,
|
|
218
|
+
persistable: !hasDynamic,
|
|
219
|
+
nonPersistableReason: hasDynamic ? 'dynamic variable expansion' : undefined,
|
|
220
|
+
allowedScopes: [],
|
|
221
|
+
};
|
|
222
|
+
intent.allowedScopes = getAllowedScopesForIntent(intent);
|
|
223
|
+
intents.push(intent);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
// No extractable URL/host
|
|
227
|
+
intents.push({
|
|
228
|
+
resource: 'shell-command',
|
|
229
|
+
action: 'execute',
|
|
230
|
+
targets: [{ target: baseCommand, matcher: 'exact' }],
|
|
231
|
+
persistable: false,
|
|
232
|
+
nonPersistableReason: 'could not extract network target',
|
|
233
|
+
allowedScopes: ['once', 'session'],
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return intents;
|
|
237
|
+
}
|
|
238
|
+
// File delete commands
|
|
239
|
+
if (isFileDeleteCommand(baseCommand)) {
|
|
240
|
+
if (resolvedPaths.length > 0 && !hasDynamic) {
|
|
241
|
+
const isRecursive = command.includes('-r') || command.includes('-R') || command.includes('-rf');
|
|
242
|
+
const targets = resolvedPaths.map(p => ({
|
|
243
|
+
target: isRecursive ? ensureTrailingSlash(p) : p,
|
|
244
|
+
matcher: isRecursive ? 'prefix' : 'exact',
|
|
245
|
+
}));
|
|
246
|
+
const intent = {
|
|
247
|
+
resource: 'file',
|
|
248
|
+
action: 'delete',
|
|
249
|
+
targets,
|
|
250
|
+
persistable: true,
|
|
251
|
+
allowedScopes: [],
|
|
252
|
+
};
|
|
253
|
+
intent.allowedScopes = getAllowedScopesForIntent(intent);
|
|
254
|
+
intents.push(intent);
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
intents.push({
|
|
258
|
+
resource: 'shell-command',
|
|
259
|
+
action: 'execute',
|
|
260
|
+
targets: [{ target: baseCommand, matcher: 'exact' }],
|
|
261
|
+
persistable: false,
|
|
262
|
+
nonPersistableReason: hasDynamic ? 'dynamic variable expansion' : 'could not resolve target paths',
|
|
263
|
+
allowedScopes: ['once'],
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
return intents;
|
|
267
|
+
}
|
|
268
|
+
// File read commands
|
|
269
|
+
if (isFileReadCommand(baseCommand)) {
|
|
270
|
+
if (resolvedPaths.length > 0 && !hasDynamic) {
|
|
271
|
+
const targets = resolvedPaths.map(p => ({
|
|
272
|
+
target: p,
|
|
273
|
+
matcher: 'exact',
|
|
274
|
+
}));
|
|
275
|
+
const intent = {
|
|
276
|
+
resource: 'file',
|
|
277
|
+
action: 'read',
|
|
278
|
+
targets,
|
|
279
|
+
persistable: true,
|
|
280
|
+
allowedScopes: [],
|
|
281
|
+
};
|
|
282
|
+
intent.allowedScopes = getAllowedScopesForIntent(intent);
|
|
283
|
+
intents.push(intent);
|
|
284
|
+
}
|
|
285
|
+
else if (resolvedPaths.length === 0) {
|
|
286
|
+
// File read command with no resolved paths (e.g., "cat" alone or piping)
|
|
287
|
+
// This is a shell-execute fallback
|
|
288
|
+
intents.push({
|
|
289
|
+
resource: 'shell-command',
|
|
290
|
+
action: 'execute',
|
|
291
|
+
targets: [{ target: baseCommand, matcher: 'exact' }],
|
|
292
|
+
persistable: false,
|
|
293
|
+
nonPersistableReason: 'no resolvable file targets',
|
|
294
|
+
allowedScopes: ['once', 'session'],
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
// Has dynamic expansion
|
|
299
|
+
intents.push({
|
|
300
|
+
resource: 'shell-command',
|
|
301
|
+
action: 'execute',
|
|
302
|
+
targets: [{ target: baseCommand, matcher: 'exact' }],
|
|
303
|
+
persistable: false,
|
|
304
|
+
nonPersistableReason: 'dynamic variable expansion',
|
|
305
|
+
allowedScopes: ['once'],
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
return intents;
|
|
309
|
+
}
|
|
310
|
+
// File write / modification commands (mv, cp, mkdir, touch, ln)
|
|
311
|
+
if (isFileWriteCommand(baseCommand) || ['mv', 'cp', 'ln'].includes(baseCommand)) {
|
|
312
|
+
if (resolvedPaths.length > 0 && !hasDynamic) {
|
|
313
|
+
const targets = resolvedPaths.map(p => ({
|
|
314
|
+
target: p,
|
|
315
|
+
matcher: 'exact',
|
|
316
|
+
}));
|
|
317
|
+
const intent = {
|
|
318
|
+
resource: 'file',
|
|
319
|
+
action: 'write',
|
|
320
|
+
targets,
|
|
321
|
+
persistable: true,
|
|
322
|
+
allowedScopes: [],
|
|
323
|
+
};
|
|
324
|
+
intent.allowedScopes = getAllowedScopesForIntent(intent);
|
|
325
|
+
intents.push(intent);
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
intents.push({
|
|
329
|
+
resource: 'shell-command',
|
|
330
|
+
action: 'execute',
|
|
331
|
+
targets: [{ target: baseCommand, matcher: 'exact' }],
|
|
332
|
+
persistable: false,
|
|
333
|
+
nonPersistableReason: hasDynamic ? 'dynamic variable expansion' : 'could not resolve target paths',
|
|
334
|
+
allowedScopes: ['once'],
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
return intents;
|
|
338
|
+
}
|
|
339
|
+
// Sensitive files category — even if command isn't a known file reader,
|
|
340
|
+
// if it references sensitive files, produce a file-read intent
|
|
341
|
+
if (riskCategory === 'sensitive-files' && resolvedPaths.length > 0 && !hasDynamic) {
|
|
342
|
+
const targets = resolvedPaths.map(p => ({
|
|
343
|
+
target: p,
|
|
344
|
+
matcher: 'exact',
|
|
345
|
+
}));
|
|
346
|
+
const intent = {
|
|
347
|
+
resource: 'file',
|
|
348
|
+
action: 'read',
|
|
349
|
+
targets,
|
|
350
|
+
persistable: true,
|
|
351
|
+
allowedScopes: [],
|
|
352
|
+
};
|
|
353
|
+
intent.allowedScopes = getAllowedScopesForIntent(intent);
|
|
354
|
+
intents.push(intent);
|
|
355
|
+
return intents;
|
|
356
|
+
}
|
|
357
|
+
// All other dangerous/system commands that we can't classify
|
|
358
|
+
// (sudo, chmod, chown, shutdown, eval, etc.)
|
|
359
|
+
intents.push({
|
|
360
|
+
resource: 'shell-command',
|
|
361
|
+
action: 'execute',
|
|
362
|
+
targets: [{ target: baseCommand, matcher: 'exact' }],
|
|
363
|
+
persistable: false,
|
|
364
|
+
nonPersistableReason: 'unclassifiable system command',
|
|
365
|
+
allowedScopes: ['once', 'session'],
|
|
366
|
+
});
|
|
367
|
+
return intents;
|
|
368
|
+
}
|
|
369
|
+
function ensureTrailingSlash(path) {
|
|
370
|
+
if (path.endsWith('/'))
|
|
371
|
+
return path;
|
|
372
|
+
return path + '/';
|
|
373
|
+
}
|
|
374
|
+
function extractNetworkTargets(command, _baseCommand) {
|
|
375
|
+
const targets = [];
|
|
376
|
+
const urlRegex = /https?:\/\/([^\s/$.?#].[^\s]*)/gi;
|
|
377
|
+
let match;
|
|
378
|
+
while ((match = urlRegex.exec(command)) !== null) {
|
|
379
|
+
const fullUrl = match[0];
|
|
380
|
+
const host = match[1].split('/')[0];
|
|
381
|
+
if (host) {
|
|
382
|
+
targets.push({
|
|
383
|
+
target: host,
|
|
384
|
+
matcher: 'exact',
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
// Also store the full URL prefix as a secondary target
|
|
388
|
+
if (fullUrl.length > host.length + 8) { // more than just "http://host"
|
|
389
|
+
const pathPrefix = fullUrl.substring(0, fullUrl.indexOf('/', 8) + 1);
|
|
390
|
+
if (pathPrefix && pathPrefix.length > 8) {
|
|
391
|
+
targets.push({
|
|
392
|
+
target: pathPrefix,
|
|
393
|
+
matcher: 'prefix',
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
// Deduplicate
|
|
399
|
+
const seen = new Set();
|
|
400
|
+
return targets.filter(t => {
|
|
401
|
+
if (seen.has(t.target))
|
|
402
|
+
return false;
|
|
403
|
+
seen.add(t.target);
|
|
404
|
+
return true;
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Create a shell permission ask with structured intent analysis.
|
|
409
|
+
*
|
|
410
|
+
* This function now runs the shell effect analyzer to produce target-based
|
|
411
|
+
* intents instead of command-identity patterns. For example:
|
|
412
|
+
* - "cat .env" → resource='file', action='read', target='/workspace/.env'
|
|
413
|
+
* - "rm -rf build" → resource='file', action='delete', target='build/' (prefix)
|
|
414
|
+
* - "curl https://api.example.com" → resource='network', action='request', target='api.example.com'
|
|
415
|
+
*
|
|
416
|
+
* Falls back to resource='shell-command' when analysis can't extract targets.
|
|
417
|
+
*/
|
|
418
|
+
export function createShellPermissionAskStructured(params) {
|
|
419
|
+
const commandIdentity = getEffectiveShellCommandIdentity(params.command);
|
|
420
|
+
// Run the intent analysis
|
|
421
|
+
const intents = analyzeShellCommandEffects({
|
|
422
|
+
command: params.command,
|
|
423
|
+
baseCommand: params.baseCommand,
|
|
424
|
+
resolvedPaths: params.resolvedPaths ?? [],
|
|
425
|
+
hasOperators: params.hasOperators,
|
|
426
|
+
workspaceBound: params.workspaceBound,
|
|
427
|
+
riskCategory: params.riskCategory,
|
|
428
|
+
});
|
|
429
|
+
// Use the primary intent to determine resource, action, and scope
|
|
430
|
+
const primaryIntent = intents[0];
|
|
431
|
+
// Determine the effective resource and action from the primary intent
|
|
432
|
+
const effectiveResource = primaryIntent?.resource ?? 'shell-command';
|
|
433
|
+
const effectiveAction = primaryIntent?.action;
|
|
434
|
+
// Build patterns from intent targets (not command identity)
|
|
435
|
+
const patterns = [];
|
|
436
|
+
if (primaryIntent) {
|
|
437
|
+
for (const t of primaryIntent.targets) {
|
|
438
|
+
patterns.push(t.target);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
else if (commandIdentity) {
|
|
442
|
+
patterns.push(commandIdentity);
|
|
443
|
+
}
|
|
444
|
+
// Compute allowed scopes from policy
|
|
445
|
+
const allAllowedScopes = intents.flatMap(i => i.allowedScopes);
|
|
446
|
+
const allowedScopes = [...new Set(allAllowedScopes)];
|
|
447
|
+
// Determine max suggested duration based on policy
|
|
448
|
+
let duration = 'workspace';
|
|
449
|
+
if (params.riskCategory === 'destructive' || params.riskCategory === 'network' || isDangerousShellIdentity(commandIdentity)) {
|
|
450
|
+
duration = 'session';
|
|
451
|
+
}
|
|
452
|
+
// Enforce: if the intent is not persistable, cap at 'session'
|
|
453
|
+
if (primaryIntent && !primaryIntent.persistable) {
|
|
454
|
+
duration = allowedScopes.includes('session') ? 'session' : 'once';
|
|
455
|
+
}
|
|
456
|
+
// Build scope definition from primary intent
|
|
457
|
+
const scope = buildScopeFromIntent(primaryIntent, commandIdentity, params.flags);
|
|
458
|
+
// Build human-readable question with target info
|
|
459
|
+
const question = buildShellQuestion(params, primaryIntent);
|
|
460
|
+
return {
|
|
461
|
+
type: 'permission',
|
|
462
|
+
question,
|
|
463
|
+
risk: params.risk,
|
|
464
|
+
resource: effectiveResource,
|
|
465
|
+
action: effectiveAction,
|
|
466
|
+
scope,
|
|
467
|
+
patterns,
|
|
468
|
+
duration,
|
|
469
|
+
intents,
|
|
470
|
+
allowedScopes,
|
|
471
|
+
paths: params.resolvedPaths,
|
|
472
|
+
metadata: {
|
|
473
|
+
command: params.command,
|
|
474
|
+
baseCommand: commandIdentity,
|
|
475
|
+
flags: params.flags,
|
|
476
|
+
riskCategory: params.riskCategory,
|
|
477
|
+
reason: params.reason,
|
|
478
|
+
resolvedPaths: params.resolvedPaths,
|
|
479
|
+
workspaceBound: params.workspaceBound,
|
|
480
|
+
hasOperators: params.hasOperators,
|
|
481
|
+
effectiveResource,
|
|
482
|
+
effectiveAction,
|
|
483
|
+
description: buildDescription({
|
|
484
|
+
baseCommand: commandIdentity,
|
|
485
|
+
flags: params.flags,
|
|
486
|
+
riskCategory: params.riskCategory,
|
|
487
|
+
workspaceBound: params.workspaceBound,
|
|
488
|
+
hasOperators: params.hasOperators,
|
|
489
|
+
}),
|
|
490
|
+
},
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function buildScopeFromIntent(intent, commandIdentity, flags) {
|
|
494
|
+
if (!intent) {
|
|
495
|
+
return {
|
|
496
|
+
type: 'shell-command',
|
|
497
|
+
value: commandIdentity,
|
|
498
|
+
label: formatCommandLabel(commandIdentity, flags),
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
switch (intent.resource) {
|
|
502
|
+
case 'file':
|
|
503
|
+
return {
|
|
504
|
+
type: 'file',
|
|
505
|
+
value: intent.targets.map(t => t.target).join(', '),
|
|
506
|
+
label: `${intent.action} ${intent.targets.map(t => t.target.split('/').pop() || t.target).join(', ')}`,
|
|
507
|
+
};
|
|
508
|
+
case 'network':
|
|
509
|
+
return {
|
|
510
|
+
type: 'resource',
|
|
511
|
+
value: intent.targets.map(t => t.target).join(', '),
|
|
512
|
+
label: intent.targets.map(t => t.target).join(', '),
|
|
513
|
+
};
|
|
514
|
+
case 'shell-command':
|
|
515
|
+
return {
|
|
516
|
+
type: 'shell-command',
|
|
517
|
+
value: commandIdentity,
|
|
518
|
+
label: formatCommandLabel(commandIdentity, flags),
|
|
519
|
+
};
|
|
520
|
+
default:
|
|
521
|
+
return {
|
|
522
|
+
type: 'shell-command',
|
|
523
|
+
value: commandIdentity,
|
|
524
|
+
label: formatCommandLabel(commandIdentity, flags),
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
function buildShellQuestion(params, intent) {
|
|
529
|
+
// Build a more descriptive question when we have intent info
|
|
530
|
+
if (intent && intent.resource === 'file') {
|
|
531
|
+
const targetNames = intent.targets.map(t => t.target.split('/').pop() || t.target);
|
|
532
|
+
const actionLabel = intent.action === 'read' ? 'Read' : intent.action === 'write' ? 'Write to' : intent.action === 'delete' ? 'Delete' : 'Access';
|
|
533
|
+
let question = `${actionLabel} file "${targetNames.join('", "')}"`;
|
|
534
|
+
if (!params.workspaceBound) {
|
|
535
|
+
question += ' (outside workspace)';
|
|
536
|
+
}
|
|
537
|
+
question += `. Requires approval.`;
|
|
538
|
+
return question;
|
|
539
|
+
}
|
|
540
|
+
if (intent && intent.resource === 'network') {
|
|
541
|
+
const hosts = intent.targets.filter(t => t.matcher === 'exact').map(t => t.target);
|
|
542
|
+
if (hosts.length > 0) {
|
|
543
|
+
return `Network request to "${hosts.join('", "')}" via ${truncateCommand(params.command)}. Requires approval.`;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
// Fallback: original question format
|
|
547
|
+
let question = `Run command "${truncateCommand(params.command)}"`;
|
|
548
|
+
if (params.workspaceBound) {
|
|
549
|
+
question += ` (within workspace)`;
|
|
550
|
+
}
|
|
551
|
+
else {
|
|
552
|
+
question += ` (references paths outside workspace)`;
|
|
553
|
+
}
|
|
554
|
+
question += `: ${params.reason}. Requires approval.`;
|
|
555
|
+
return question;
|
|
556
|
+
}
|
|
557
|
+
export function createOutsideWorkspaceAsk(params) {
|
|
558
|
+
const commandIdentity = getEffectiveShellCommandIdentity(params.command);
|
|
559
|
+
const intents = analyzeShellCommandEffects({
|
|
560
|
+
command: params.command,
|
|
561
|
+
baseCommand: commandIdentity,
|
|
562
|
+
resolvedPaths: params.resolvedPaths,
|
|
563
|
+
hasOperators: params.hasOperators ?? false,
|
|
564
|
+
workspaceBound: false,
|
|
565
|
+
riskCategory: 'outside-workspace',
|
|
566
|
+
});
|
|
567
|
+
const primaryIntent = intents[0];
|
|
568
|
+
const effectiveResource = primaryIntent?.resource ?? 'shell-command';
|
|
569
|
+
const allAllowedScopes = intents.flatMap(i => i.allowedScopes);
|
|
570
|
+
const allowedScopes = [...new Set(allAllowedScopes)];
|
|
571
|
+
const patterns = [];
|
|
572
|
+
if (primaryIntent) {
|
|
573
|
+
for (const t of primaryIntent.targets) {
|
|
574
|
+
patterns.push(t.target);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
if (patterns.length === 0) {
|
|
578
|
+
patterns.push(commandIdentity || `cwd:${params.cwd}`);
|
|
579
|
+
}
|
|
580
|
+
const scope = {
|
|
581
|
+
type: 'path',
|
|
582
|
+
value: params.cwd,
|
|
583
|
+
label: params.cwd.split('/').pop() || params.cwd,
|
|
584
|
+
};
|
|
585
|
+
return {
|
|
586
|
+
type: 'permission',
|
|
587
|
+
question: `Command "${truncateCommand(params.command)}" runs in directory outside workspace (${params.cwd}). Requires approval.`,
|
|
588
|
+
risk: 'medium',
|
|
589
|
+
resource: effectiveResource,
|
|
590
|
+
action: primaryIntent?.action,
|
|
591
|
+
scope,
|
|
592
|
+
patterns,
|
|
593
|
+
duration: 'session',
|
|
594
|
+
intents,
|
|
595
|
+
allowedScopes,
|
|
596
|
+
metadata: {
|
|
597
|
+
command: params.command,
|
|
598
|
+
baseCommand: commandIdentity,
|
|
599
|
+
cwd: params.cwd,
|
|
600
|
+
resolvedPaths: params.resolvedPaths,
|
|
601
|
+
riskCategory: 'outside-workspace',
|
|
602
|
+
effectiveResource,
|
|
603
|
+
effectiveAction: primaryIntent?.action,
|
|
604
|
+
description: `External directory access: ${params.cwd}`,
|
|
605
|
+
hasOperators: params.hasOperators ?? false,
|
|
606
|
+
},
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
export function createWorkspaceModificationAsk(params) {
|
|
610
|
+
const commandIdentity = getEffectiveShellCommandIdentity(params.command);
|
|
611
|
+
const intents = analyzeShellCommandEffects({
|
|
612
|
+
command: params.command,
|
|
613
|
+
baseCommand: params.baseCommand,
|
|
614
|
+
resolvedPaths: params.resolvedPaths,
|
|
615
|
+
hasOperators: params.hasOperators ?? false,
|
|
616
|
+
workspaceBound: true,
|
|
617
|
+
riskCategory: 'workspace-modification',
|
|
618
|
+
});
|
|
619
|
+
const primaryIntent = intents[0];
|
|
620
|
+
const effectiveResource = primaryIntent?.resource ?? 'shell-command';
|
|
621
|
+
const effectiveAction = primaryIntent?.action;
|
|
622
|
+
const allAllowedScopes = intents.flatMap(i => i.allowedScopes);
|
|
623
|
+
const allowedScopes = [...new Set(allAllowedScopes)];
|
|
624
|
+
const patterns = [];
|
|
625
|
+
if (primaryIntent) {
|
|
626
|
+
for (const t of primaryIntent.targets) {
|
|
627
|
+
patterns.push(t.target);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
if (patterns.length === 0) {
|
|
631
|
+
patterns.push(commandIdentity || params.baseCommand);
|
|
632
|
+
}
|
|
633
|
+
// Build better question for file-targeted operations
|
|
634
|
+
let question;
|
|
635
|
+
if (primaryIntent && primaryIntent.resource === 'file') {
|
|
636
|
+
const targetNames = primaryIntent.targets.map(t => t.target.split('/').pop() || t.target);
|
|
637
|
+
const actionLabel = primaryIntent.action === 'write' ? 'Write to' : primaryIntent.action === 'read' ? 'Read' : 'Modify';
|
|
638
|
+
question = `${actionLabel} "${targetNames.join('", "')}" via ${truncateCommand(params.command)}. Requires approval.`;
|
|
639
|
+
}
|
|
640
|
+
else {
|
|
641
|
+
question = `Run filesystem command "${truncateCommand(params.command)}" within workspace. Requires approval.`;
|
|
642
|
+
}
|
|
643
|
+
const scope = buildScopeFromIntent(primaryIntent, commandIdentity, []);
|
|
644
|
+
return {
|
|
645
|
+
type: 'permission',
|
|
646
|
+
question,
|
|
647
|
+
risk: 'medium',
|
|
648
|
+
resource: effectiveResource,
|
|
649
|
+
action: effectiveAction,
|
|
650
|
+
scope,
|
|
651
|
+
patterns,
|
|
652
|
+
duration: 'session',
|
|
653
|
+
intents,
|
|
654
|
+
allowedScopes,
|
|
655
|
+
paths: params.resolvedPaths,
|
|
656
|
+
metadata: {
|
|
657
|
+
command: params.command,
|
|
658
|
+
baseCommand: commandIdentity || params.baseCommand,
|
|
659
|
+
resolvedPaths: params.resolvedPaths,
|
|
660
|
+
riskCategory: 'workspace-modification',
|
|
661
|
+
effectiveResource,
|
|
662
|
+
effectiveAction,
|
|
663
|
+
description: `Workspace filesystem: ${commandIdentity || params.baseCommand}`,
|
|
664
|
+
hasOperators: params.hasOperators ?? false,
|
|
665
|
+
},
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
function truncateCommand(cmd, maxLen = 80) {
|
|
669
|
+
return cmd.length > maxLen ? cmd.slice(0, maxLen) + '...' : cmd;
|
|
670
|
+
}
|
|
671
|
+
function formatCommandLabel(baseCommand, flags) {
|
|
672
|
+
if (!baseCommand) {
|
|
673
|
+
return '';
|
|
674
|
+
}
|
|
675
|
+
if (flags.length === 0) {
|
|
676
|
+
return baseCommand;
|
|
677
|
+
}
|
|
678
|
+
const commonFlags = flags.slice(0, 3).join(' ');
|
|
679
|
+
return commonFlags ? `${baseCommand} ${commonFlags}` : baseCommand;
|
|
680
|
+
}
|
|
681
|
+
function buildDescription(params) {
|
|
682
|
+
const parts = [];
|
|
683
|
+
switch (params.riskCategory) {
|
|
684
|
+
case 'destructive':
|
|
685
|
+
parts.push('Destructive operation');
|
|
686
|
+
break;
|
|
687
|
+
case 'network':
|
|
688
|
+
parts.push('Network access');
|
|
689
|
+
break;
|
|
690
|
+
case 'side-effect':
|
|
691
|
+
parts.push('Side-effect command');
|
|
692
|
+
break;
|
|
693
|
+
case 'workspace-modification':
|
|
694
|
+
parts.push('Workspace modification');
|
|
695
|
+
break;
|
|
696
|
+
case 'outside-workspace':
|
|
697
|
+
parts.push('Outside workspace');
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
if (!params.workspaceBound) {
|
|
701
|
+
parts.push('References external paths');
|
|
702
|
+
}
|
|
703
|
+
if (params.hasOperators) {
|
|
704
|
+
parts.push('Contains shell operators');
|
|
705
|
+
}
|
|
706
|
+
return parts.join(' • ');
|
|
707
|
+
}
|
|
708
|
+
export function createFilePermissionAsk(params) {
|
|
709
|
+
const operationLabel = params.operation === 'read' ? 'Reading' : params.operation === 'write' ? 'Writing' : 'Editing';
|
|
710
|
+
const fileName = params.path.split('/').pop() || params.path;
|
|
711
|
+
let question = `${operationLabel} file "${fileName}"`;
|
|
712
|
+
if (params.isSensitiveFile) {
|
|
713
|
+
question += ' (sensitive file)';
|
|
714
|
+
}
|
|
715
|
+
else if (params.isOutsideWorkspace) {
|
|
716
|
+
question += ' (outside workspace)';
|
|
717
|
+
}
|
|
718
|
+
question += ' requires approval.';
|
|
719
|
+
if (params.reason) {
|
|
720
|
+
question += ` ${params.reason}`;
|
|
721
|
+
}
|
|
722
|
+
const patterns = [];
|
|
723
|
+
patterns.push(`file:${params.path}`);
|
|
724
|
+
patterns.push(`file:${fileName}`);
|
|
725
|
+
if (params.isOutsideWorkspace) {
|
|
726
|
+
patterns.push('outside-workspace');
|
|
727
|
+
}
|
|
728
|
+
if (params.isSensitiveFile) {
|
|
729
|
+
patterns.push('sensitive-file');
|
|
730
|
+
}
|
|
731
|
+
let duration = 'workspace';
|
|
732
|
+
if (params.isSensitiveFile || params.risk === 'high' || params.risk === 'critical') {
|
|
733
|
+
duration = 'session';
|
|
734
|
+
}
|
|
735
|
+
return {
|
|
736
|
+
type: 'permission',
|
|
737
|
+
question,
|
|
738
|
+
risk: params.risk,
|
|
739
|
+
resource: 'file',
|
|
740
|
+
paths: [params.path],
|
|
741
|
+
patterns,
|
|
742
|
+
duration,
|
|
743
|
+
metadata: {
|
|
744
|
+
operation: params.operation,
|
|
745
|
+
path: params.path,
|
|
746
|
+
fileName,
|
|
747
|
+
isOutsideWorkspace: params.isOutsideWorkspace ?? false,
|
|
748
|
+
isSensitiveFile: params.isSensitiveFile ?? false,
|
|
749
|
+
reason: params.reason,
|
|
750
|
+
},
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
export function createWebfetchPermissionAsk(params) {
|
|
754
|
+
let question = `Fetch URL "${params.host}"`;
|
|
755
|
+
if (params.isHttp) {
|
|
756
|
+
question += ' (unencrypted HTTP)';
|
|
757
|
+
}
|
|
758
|
+
question += ' requires approval.';
|
|
759
|
+
if (params.reason) {
|
|
760
|
+
question += ` ${params.reason}`;
|
|
761
|
+
}
|
|
762
|
+
const patterns = [params.host, params.url];
|
|
763
|
+
let duration = 'workspace';
|
|
764
|
+
if (params.isHttp || params.risk === 'high' || params.risk === 'critical') {
|
|
765
|
+
duration = 'session';
|
|
766
|
+
}
|
|
767
|
+
return {
|
|
768
|
+
type: 'permission',
|
|
769
|
+
question,
|
|
770
|
+
risk: params.risk,
|
|
771
|
+
resource: 'network',
|
|
772
|
+
scope: {
|
|
773
|
+
type: 'resource',
|
|
774
|
+
value: params.url,
|
|
775
|
+
label: params.host,
|
|
776
|
+
},
|
|
777
|
+
patterns,
|
|
778
|
+
duration,
|
|
779
|
+
metadata: {
|
|
780
|
+
url: params.url,
|
|
781
|
+
host: params.host,
|
|
782
|
+
isHttp: params.isHttp ?? false,
|
|
783
|
+
reason: params.reason,
|
|
784
|
+
},
|
|
785
|
+
};
|
|
786
|
+
}
|
|
787
|
+
//# sourceMappingURL=permission.js.map
|