praxis-agent 0.11.0 → 0.12.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/README.md +5 -1
- package/dist/cli/interactive.d.ts +3 -1
- package/dist/cli/interactive.js +135 -3
- package/dist/cli/tui/sandbox-dashboard.d.ts +10 -0
- package/dist/cli/tui/sandbox-dashboard.js +78 -0
- package/dist/cli/tui/sandbox-settings.d.ts +32 -0
- package/dist/cli/tui/sandbox-settings.js +146 -0
- package/dist/cli/tui/slash-commands.js +5 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +27 -3
- package/dist/permissions/bash-path-safety.d.ts +2 -0
- package/dist/permissions/bash-path-safety.js +7 -0
- package/dist/permissions/claude-permission-resolver.d.ts +11 -0
- package/dist/permissions/claude-permission-resolver.js +15 -0
- package/dist/sandbox/claude-sandbox-runtime.d.ts +54 -0
- package/dist/sandbox/claude-sandbox-runtime.js +175 -0
- package/dist/sandbox/claude-sandbox-settings.d.ts +23 -0
- package/dist/sandbox/claude-sandbox-settings.js +286 -0
- package/dist/tools/local-tools.d.ts +18 -0
- package/dist/tools/local-tools.js +47 -8
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -87,7 +87,7 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
87
87
|
`/rewind`, runtime `/cd`, transcript-free
|
|
88
88
|
`/btw` side questions with background-Agent handoff, interactive
|
|
89
89
|
`/background` terminal handoff, unified `/status`/`/config`/`/usage` settings
|
|
90
|
-
tabs, `/mcp`, `/memory` shared
|
|
90
|
+
tabs, `/sandbox` mode/dependency/override/config controls, `/mcp`, `/memory` shared
|
|
91
91
|
instruction and auto-memory access, and live extension-reload controls,
|
|
92
92
|
cursor/history composer, per-session model/effort/permission controls,
|
|
93
93
|
context/status/skill/task dashboards, prompt stash and continuation shortcuts,
|
|
@@ -121,6 +121,10 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
|
|
|
121
121
|
constraints, internal auto-memory/session/task path handling, compound `cd` +
|
|
122
122
|
Git protection, mode-ordered `acceptEdits` handling, live raw/resolved-path
|
|
123
123
|
grants for external directories,
|
|
124
|
+
Claude-compatible opt-in Bash sandboxing with filesystem and network
|
|
125
|
+
isolation, explicit ask/deny precedence, sandbox-only auto-allow,
|
|
126
|
+
write-allowlist/deny-within-allow enforcement, per-command overrides and
|
|
127
|
+
exclusions, violation reporting, and bare-repository control-file cleanup,
|
|
124
128
|
safe-property Skill auto-allow, interactive
|
|
125
129
|
workspace-directory add/remove controls, path confinement, credential
|
|
126
130
|
redaction, and sanitized child processes.
|
|
@@ -18,6 +18,7 @@ import { loadTuiKeybindings, type TuiKeybindingsFile } from './tui/keybindings.j
|
|
|
18
18
|
import { completeTuiWorkspaceDirectory, resolveTuiWorkspaceDirectory } from './tui/workspace-directories.js';
|
|
19
19
|
import { type TuiThemeSettings } from './tui/theme.js';
|
|
20
20
|
import { type CustomThemeBase, type CustomThemeToken, type TuiCustomTheme } from './tui/custom-themes.js';
|
|
21
|
+
import { type TuiSandboxStore } from './tui/sandbox-settings.js';
|
|
21
22
|
import { type ConfigSettingsTarget } from './tui/config-settings.js';
|
|
22
23
|
import { type PraxisRuntimeSettings } from './tui/runtime-settings.js';
|
|
23
24
|
import { type TuiNotificationWriter } from './tui/terminal-notifications.js';
|
|
@@ -152,6 +153,7 @@ interface InteractiveAppProps {
|
|
|
152
153
|
}): Promise<void>;
|
|
153
154
|
remove?(rule: TuiPermissionRule): Promise<void>;
|
|
154
155
|
};
|
|
156
|
+
sandboxStore?: TuiSandboxStore;
|
|
155
157
|
recentlyDeniedStore?: RecentlyDeniedStore;
|
|
156
158
|
themeStore?: {
|
|
157
159
|
load(): Promise<TuiThemeSettings>;
|
|
@@ -174,7 +176,7 @@ interface InteractiveAppProps {
|
|
|
174
176
|
notificationWriter?: TuiNotificationWriter;
|
|
175
177
|
elicitationUrlOpener?: (url: string) => void | Promise<void>;
|
|
176
178
|
}
|
|
177
|
-
export declare function InteractiveApp({ factory, initialSessions, initialPrompt, initialHistory, signal, onCancel, onTurnChange, onCleanup, onRendererChange, onBackground, axScreenReader, allowNewSession, resume, display, terminalWidth, slashCommands, agents, allowDangerouslySkipPermissions, additionalDirectories, diffLoader, fileLoader, externalEditor, keybindingsConfigRoot, keybindingsFile, keybindingsLoader, keybindingsEditor, memoryFilesLoader, memoryEditor, memoryFolderOpener, suspendProcess, clipboardReader, clipboardWriter, sideQuestionClipboardWriter, exportWriter, permissionRuleStore, recentlyDeniedStore: suppliedRecentlyDeniedStore, terminalSetup: terminalSetupOverride, themeStore, initialThemeSettings, initialThemeLoadError, workspaceDirectoryResolver, workspaceDirectoryCompleter, runtimeSettings: suppliedRuntimeSettings, runtimeSettingsTarget, notificationWriter, elicitationUrlOpener, }: InteractiveAppProps): import("react").JSX.Element;
|
|
179
|
+
export declare function InteractiveApp({ factory, initialSessions, initialPrompt, initialHistory, signal, onCancel, onTurnChange, onCleanup, onRendererChange, onBackground, axScreenReader, allowNewSession, resume, display, terminalWidth, slashCommands, agents, allowDangerouslySkipPermissions, additionalDirectories, diffLoader, fileLoader, externalEditor, keybindingsConfigRoot, keybindingsFile, keybindingsLoader, keybindingsEditor, memoryFilesLoader, memoryEditor, memoryFolderOpener, suspendProcess, clipboardReader, clipboardWriter, sideQuestionClipboardWriter, exportWriter, permissionRuleStore, sandboxStore: suppliedSandboxStore, recentlyDeniedStore: suppliedRecentlyDeniedStore, terminalSetup: terminalSetupOverride, themeStore, initialThemeSettings, initialThemeLoadError, workspaceDirectoryResolver, workspaceDirectoryCompleter, runtimeSettings: suppliedRuntimeSettings, runtimeSettingsTarget, notificationWriter, elicitationUrlOpener, }: InteractiveAppProps): import("react").JSX.Element;
|
|
178
180
|
export declare function runInteractive(options: {
|
|
179
181
|
factory: InteractiveServiceFactory;
|
|
180
182
|
initialPrompt?: string;
|
package/dist/cli/interactive.js
CHANGED
|
@@ -33,6 +33,8 @@ import { commitElicitationText, createTuiElicitationForm, elicitationFormIsValid
|
|
|
33
33
|
import { openTuiUrl } from './tui/open-url.js';
|
|
34
34
|
import { projectTuiToolPermission, ToolPermissionDialog, } from './tui/tool-permission.js';
|
|
35
35
|
import { ConfigDashboard, projectConfigRows } from './tui/config-dashboard.js';
|
|
36
|
+
import { SandboxDashboard, tuiSandboxTabs } from './tui/sandbox-dashboard.js';
|
|
37
|
+
import { createTuiSandboxStore, } from './tui/sandbox-settings.js';
|
|
36
38
|
import { loadConfigSettings, saveConfigSetting, configSettingDefinition, resolveConfigSettingsLocation, } from './tui/config-settings.js';
|
|
37
39
|
import { projectRuntimeSettings, loadRuntimeSettings, } from './tui/runtime-settings.js';
|
|
38
40
|
import { autoUpdateTarget, copyCandidates, externalEditorInitialContent, formatTurnDuration, questionTimeoutMilliseconds, sessionRecap, spinnerTip, shouldShowCopyPicker, } from './tui/runtime-interactions.js';
|
|
@@ -232,7 +234,18 @@ function ordinal(value) {
|
|
|
232
234
|
return `${value}th`;
|
|
233
235
|
}
|
|
234
236
|
}
|
|
235
|
-
|
|
237
|
+
const HIDDEN_TUI_SLASH_COMMANDS = new Set([
|
|
238
|
+
'background',
|
|
239
|
+
'new',
|
|
240
|
+
'output-style',
|
|
241
|
+
'reload-skills',
|
|
242
|
+
'sessions',
|
|
243
|
+
'skill',
|
|
244
|
+
'terminal-setup',
|
|
245
|
+
'update',
|
|
246
|
+
'usage',
|
|
247
|
+
]);
|
|
248
|
+
export function InteractiveApp({ factory, initialSessions, initialPrompt, initialHistory = [], signal, onCancel, onTurnChange, onCleanup, onRendererChange, onBackground, axScreenReader = false, allowNewSession = true, resume, display = { version: 'dev', cwd: process.cwd() }, terminalWidth, slashCommands = EMPTY_SLASH_COMMANDS, agents = EMPTY_AGENTS, allowDangerouslySkipPermissions = false, additionalDirectories = [], diffLoader, fileLoader, externalEditor = editTuiPrompt, keybindingsConfigRoot, keybindingsFile = ensureTuiKeybindingsFile, keybindingsLoader = loadTuiKeybindings, keybindingsEditor = openTuiEditorFile, memoryFilesLoader = (configRoot, cwd) => loadTuiMemoryFiles({ configRoot, cwd }), memoryEditor = openTuiEditorFile, memoryFolderOpener = openTuiMemoryFolder, suspendProcess = suspendTuiProcess, clipboardReader = readTuiClipboard, clipboardWriter = writeTuiClipboard, sideQuestionClipboardWriter = writeTuiOsc52Clipboard, exportWriter = writeConversationExport, permissionRuleStore, sandboxStore: suppliedSandboxStore, recentlyDeniedStore: suppliedRecentlyDeniedStore, terminalSetup: terminalSetupOverride, themeStore, initialThemeSettings, initialThemeLoadError, workspaceDirectoryResolver = resolveTuiWorkspaceDirectory, workspaceDirectoryCompleter = completeTuiWorkspaceDirectory, runtimeSettings: suppliedRuntimeSettings, runtimeSettingsTarget, notificationWriter, elicitationUrlOpener = openTuiUrl, }) {
|
|
236
249
|
const { exit, suspendTerminal, waitUntilRenderFlush } = useApp();
|
|
237
250
|
const width = useTerminalWidth(terminalWidth);
|
|
238
251
|
const keybindingsRoot = useMemo(() => resolve(keybindingsConfigRoot ??
|
|
@@ -369,6 +382,18 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
369
382
|
permissionMode: permissionMode(display.permissionMode),
|
|
370
383
|
additionalDirectories: [...new Set(additionalDirectories)],
|
|
371
384
|
}));
|
|
385
|
+
const sandboxStore = useMemo(() => suppliedSandboxStore ??
|
|
386
|
+
createTuiSandboxStore({
|
|
387
|
+
configRoot: keybindingsRoot,
|
|
388
|
+
cwd: runtimeCwd,
|
|
389
|
+
homeDirectory: homedir(),
|
|
390
|
+
additionalDirectories: runtimePreferences.additionalDirectories,
|
|
391
|
+
}), [
|
|
392
|
+
keybindingsRoot,
|
|
393
|
+
runtimeCwd,
|
|
394
|
+
runtimePreferences.additionalDirectories,
|
|
395
|
+
suppliedSandboxStore,
|
|
396
|
+
]);
|
|
372
397
|
const runtimePreferencesRef = useRef(runtimePreferences);
|
|
373
398
|
const [permission, setPermission] = useState(null);
|
|
374
399
|
const permissionRef = useRef(null);
|
|
@@ -3157,6 +3182,62 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
3157
3182
|
}
|
|
3158
3183
|
return;
|
|
3159
3184
|
}
|
|
3185
|
+
if (earlyMenu?.kind === 'sandbox') {
|
|
3186
|
+
if (key.escape || value === '\u001B') {
|
|
3187
|
+
updateMenu(null);
|
|
3188
|
+
return;
|
|
3189
|
+
}
|
|
3190
|
+
const tabs = tuiSandboxTabs(earlyMenu.snapshot);
|
|
3191
|
+
if (key.leftArrow || key.rightArrow || key.tab) {
|
|
3192
|
+
const index = tabs.indexOf(earlyMenu.tab);
|
|
3193
|
+
const direction = key.leftArrow || (key.tab && key.shift) ? -1 : 1;
|
|
3194
|
+
const tab = tabs[Math.max(0, Math.min(tabs.length - 1, index + direction))] ??
|
|
3195
|
+
earlyMenu.tab;
|
|
3196
|
+
updateMenu({ ...earlyMenu, tab, selectedIndex: 0 });
|
|
3197
|
+
return;
|
|
3198
|
+
}
|
|
3199
|
+
const rowCount = earlyMenu.tab === 'mode'
|
|
3200
|
+
? 3
|
|
3201
|
+
: earlyMenu.tab === 'overrides' && earlyMenu.snapshot.settings.enabled
|
|
3202
|
+
? 2
|
|
3203
|
+
: 0;
|
|
3204
|
+
if (rowCount > 0 && (key.upArrow || key.downArrow)) {
|
|
3205
|
+
updateMenu({
|
|
3206
|
+
...earlyMenu,
|
|
3207
|
+
selectedIndex: Math.max(0, Math.min(rowCount - 1, earlyMenu.selectedIndex + (key.upArrow ? -1 : 1))),
|
|
3208
|
+
});
|
|
3209
|
+
return;
|
|
3210
|
+
}
|
|
3211
|
+
if (rowCount > 0 && key.return) {
|
|
3212
|
+
const saving = (async () => {
|
|
3213
|
+
setBusy(true);
|
|
3214
|
+
try {
|
|
3215
|
+
const snapshot = earlyMenu.tab === 'mode'
|
|
3216
|
+
? await sandboxStore.setMode(['auto-allow', 'regular', 'disabled'][earlyMenu.selectedIndex] ?? 'auto-allow')
|
|
3217
|
+
: await sandboxStore.setAllowUnsandboxedCommands(earlyMenu.selectedIndex === 0);
|
|
3218
|
+
await retireService();
|
|
3219
|
+
const tabs = tuiSandboxTabs(snapshot);
|
|
3220
|
+
updateMenu({
|
|
3221
|
+
kind: 'sandbox',
|
|
3222
|
+
tab: tabs.includes(earlyMenu.tab)
|
|
3223
|
+
? earlyMenu.tab
|
|
3224
|
+
: (tabs[0] ?? 'dependencies'),
|
|
3225
|
+
selectedIndex: earlyMenu.selectedIndex,
|
|
3226
|
+
snapshot,
|
|
3227
|
+
});
|
|
3228
|
+
}
|
|
3229
|
+
catch (error) {
|
|
3230
|
+
warn(error);
|
|
3231
|
+
}
|
|
3232
|
+
finally {
|
|
3233
|
+
setBusy(false);
|
|
3234
|
+
}
|
|
3235
|
+
})();
|
|
3236
|
+
onTurnChange?.(saving);
|
|
3237
|
+
void saving.finally(() => onTurnChange?.(null));
|
|
3238
|
+
}
|
|
3239
|
+
return;
|
|
3240
|
+
}
|
|
3160
3241
|
if (earlyMenu?.kind === 'config') {
|
|
3161
3242
|
if (key.escape || value === '\u001B') {
|
|
3162
3243
|
if (earlyMenu.searchFocused && earlyMenu.query) {
|
|
@@ -4673,9 +4754,13 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
4673
4754
|
commandNameQuery === selectedCommand.name.toLocaleLowerCase();
|
|
4674
4755
|
const selectedCommandMatchesName = selectedCommand !== undefined &&
|
|
4675
4756
|
selectedCommand.name.toLocaleLowerCase().startsWith(commandNameQuery);
|
|
4757
|
+
const exactHiddenCommand = HIDDEN_TUI_SLASH_COMMANDS.has(commandNameQuery);
|
|
4676
4758
|
if (selectedCommand &&
|
|
4677
4759
|
(key.tab ||
|
|
4678
|
-
(key.return &&
|
|
4760
|
+
(key.return &&
|
|
4761
|
+
!exactSelectedCommand &&
|
|
4762
|
+
!exactHiddenCommand &&
|
|
4763
|
+
selectedCommandMatchesName))) {
|
|
4679
4764
|
updateComposerInput(`/${selectedCommand.name} `);
|
|
4680
4765
|
setCommandPaletteOpen(false);
|
|
4681
4766
|
return;
|
|
@@ -4702,6 +4787,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
4702
4787
|
if (!prompt || prompt === '!')
|
|
4703
4788
|
return;
|
|
4704
4789
|
const copyCommand = /^\/copy(?:\s+(\d+))?$/u.exec(prompt);
|
|
4790
|
+
const sandboxCommand = /^\/sandbox(?:\s+([\s\S]+))?$/u.exec(prompt);
|
|
4705
4791
|
const tuiCommand = /^\/tui(?:\s+(default|fullscreen))?$/u.exec(prompt);
|
|
4706
4792
|
const renameCommand = /^\/rename(?:\s+(.+))?$/u.exec(prompt);
|
|
4707
4793
|
const cdCommand = /^\/cd(?:\s+(.+))?$/u.exec(prompt);
|
|
@@ -4883,6 +4969,52 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
4883
4969
|
append({ kind: 'user', text: prompt });
|
|
4884
4970
|
updateMenu({ kind: 'export', selectedIndex: 0 });
|
|
4885
4971
|
}
|
|
4972
|
+
else if (sandboxCommand) {
|
|
4973
|
+
const operation = (async () => {
|
|
4974
|
+
setBusy(true);
|
|
4975
|
+
try {
|
|
4976
|
+
const args = sandboxCommand[1]?.trim();
|
|
4977
|
+
if (args) {
|
|
4978
|
+
const exclude = /^exclude(?:\s+([\s\S]+))?$/u.exec(args);
|
|
4979
|
+
if (!exclude) {
|
|
4980
|
+
throw new Error(`Unknown subcommand "${args.split(/\s+/u)[0] ?? args}". Available subcommand: exclude`);
|
|
4981
|
+
}
|
|
4982
|
+
if (!exclude[1]?.trim()) {
|
|
4983
|
+
throw new Error('Please provide a command pattern to exclude (e.g. /sandbox exclude "npm run test:*")');
|
|
4984
|
+
}
|
|
4985
|
+
const result = await sandboxStore.exclude(exclude[1]);
|
|
4986
|
+
await retireService();
|
|
4987
|
+
append({
|
|
4988
|
+
kind: 'local-result',
|
|
4989
|
+
text: `Added "${result.pattern}" to excluded commands in ${result.settingsPath}`,
|
|
4990
|
+
});
|
|
4991
|
+
return;
|
|
4992
|
+
}
|
|
4993
|
+
const snapshot = await sandboxStore.load();
|
|
4994
|
+
if (!snapshot.supported) {
|
|
4995
|
+
throw new Error('Sandboxing is currently only supported on macOS, Linux, and WSL2.');
|
|
4996
|
+
}
|
|
4997
|
+
if (snapshot.unavailableReason?.includes('not in sandbox.enabledPlatforms')) {
|
|
4998
|
+
throw new Error(snapshot.unavailableReason);
|
|
4999
|
+
}
|
|
5000
|
+
const tabs = tuiSandboxTabs(snapshot);
|
|
5001
|
+
updateMenu({
|
|
5002
|
+
kind: 'sandbox',
|
|
5003
|
+
tab: tabs[0] ?? 'dependencies',
|
|
5004
|
+
selectedIndex: 0,
|
|
5005
|
+
snapshot,
|
|
5006
|
+
});
|
|
5007
|
+
}
|
|
5008
|
+
catch (error) {
|
|
5009
|
+
warn(error);
|
|
5010
|
+
}
|
|
5011
|
+
finally {
|
|
5012
|
+
setBusy(false);
|
|
5013
|
+
}
|
|
5014
|
+
})();
|
|
5015
|
+
onTurnChange?.(operation);
|
|
5016
|
+
void operation.finally(() => onTurnChange?.(null));
|
|
5017
|
+
}
|
|
4886
5018
|
else if (prompt === '/permissions') {
|
|
4887
5019
|
const loading = (async () => {
|
|
4888
5020
|
setBusy(true);
|
|
@@ -5197,7 +5329,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
5197
5329
|
? `restore ${menu.point.fileChanges.join(', ')}.`
|
|
5198
5330
|
: 'be unchanged.'] }), _jsx(Text, { children: " " }), rewindActions(menu.point).map((option, index) => (_jsxs(Text, { inverse: menu.selectedIndex === index, children: [menu.selectedIndex === index ? '❯ ' : ' ', index + 1, ". ", option.label] }, option.action))), _jsx(Text, { children: " " }), _jsx(Text, { color: activePalette.warning, children: "\u26A0 Rewinding does not affect files edited manually or via bash." })] })) : menu?.kind === 'rewind-context' ? (_jsxs(DialogFrame, { title: `Summarize ${menu.direction === 'from' ? 'from' : 'up to'} here`, screenReader: axScreenReader, children: [_jsx(Text, { children: menu.direction === 'from'
|
|
5199
5331
|
? 'Messages after this point will be summarized.'
|
|
5200
|
-
: 'Messages up to this point will be summarized.' }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["add context (optional): ", input] }), _jsx(Text, { dimColor: true, children: "Enter to summarize \u00B7 Esc to go back" })] })) : menu?.kind === 'permission-rule-input' ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: ["Add ", menu.behavior, " permission rule"] }), _jsx(Text, { children: "Permission rules are a tool name, optionally followed by a specifier in parentheses." }), _jsx(Text, { children: "e.g., WebFetch or Bash(ls *)" }), _jsx(Text, { children: " " }), _jsx(Box, { borderStyle: axScreenReader ? undefined : 'round', paddingX: axScreenReader ? 0 : 1, children: _jsx(Text, { ...(input ? {} : { dimColor: true }), children: input || 'Enter permission rule…' }) }), _jsx(Text, { dimColor: true, children: "Enter to submit \u00B7 Esc to cancel" })] })) : menu?.kind === 'workspace-directory-input' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Add directory to workspace" }), _jsx(Text, { children: "Praxis Code will be able to read files in this directory and make edits when auto-accept edits is on." }), _jsx(Text, { children: " " }), _jsx(Text, { children: "Enter the path to the directory:" }), _jsx(Box, { borderStyle: axScreenReader ? undefined : 'round', paddingX: axScreenReader ? 0 : 1, children: _jsx(Text, { ...(input ? {} : { dimColor: true }), children: input || 'Directory path…' }) }), _jsx(Text, { dimColor: true, children: "Tab to complete \u00B7 Enter to add \u00B7 Esc to cancel" })] })) : menu?.kind === 'permission-delete' ? (_jsxs(DialogFrame, { title: permissionDeleteTitle(menu.rule.behavior), screenReader: axScreenReader, children: [_jsx(Text, { children: menu.rule.rule }), permissionRuleDescription(menu.rule.rule) ? (_jsx(Text, { dimColor: true, children: permissionRuleDescription(menu.rule.rule) })) : null, _jsx(Text, { dimColor: true, children: permissionScopeLabel(menu.rule.scope) }), _jsx(Text, { children: " " }), _jsx(Text, { children: "Are you sure you want to delete this permission rule?" }), _jsx(Text, { children: " " }), _jsxs(Text, { inverse: !axScreenReader && menu.selectedIndex === 0, children: [selectionPrefix(menu.selectedIndex === 0, axScreenReader), "1. Yes"] }), _jsxs(Text, { inverse: !axScreenReader && menu.selectedIndex === 1, children: [selectionPrefix(menu.selectedIndex === 1, axScreenReader), "2. No"] }), _jsx(Text, { dimColor: true, children: "Esc to cancel" })] })) : menu?.kind === 'workspace-directory-delete' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Remove directory from workspace?" }), _jsxs(Text, { children: [" ", menu.path] }), _jsx(Text, { children: " " }), _jsx(Text, { children: "Praxis Code will no longer have access to files in this directory." }), _jsx(Text, { children: " " }), _jsxs(Text, { inverse: !axScreenReader && menu.selectedIndex === 0, children: [selectionPrefix(menu.selectedIndex === 0, axScreenReader), "1. Yes"] }), _jsxs(Text, { inverse: !axScreenReader && menu.selectedIndex === 1, children: [selectionPrefix(menu.selectedIndex === 1, axScreenReader), "2. No"] }), _jsx(Text, { dimColor: true, children: "Enter to confirm \u00B7 Esc to cancel" })] })) : menu ? (menu.kind === 'help' ? (_jsx(HelpMenu, { tabIndex: menu.tabIndex, selectedIndex: menu.selectedIndex, builtinCommands: builtinSlashCommands, customCommands: customSlashCommands, width: width, screenReader: axScreenReader })) : menu.kind === 'diff' ? (_jsx(DiffDashboard, { snapshots: menu.snapshots, sourceIndex: menu.sourceIndex, selectedIndex: menu.selectedIndex, viewingFile: menu.viewingFile, scrollOffset: menu.scrollOffset, width: width, screenReader: axScreenReader })) : menu.kind === 'permission-dashboard' ? (_jsx(PermissionDashboard, { tabIndex: menu.tabIndex, selectedIndex: menu.selectedIndex, query: menu.query, rules: menu.rules, recentDenied: recentDenied, retryingDeniedId: retryingDeniedId, workspaceDirectories: workspaceDirectories, width: width, screenReader: axScreenReader })) : menu.kind === 'permission-scope' ? (_jsx(SelectionMenu, { title: "Where should this rule be saved?", description: `${menu.rule}${permissionRuleDescription(menu.rule)
|
|
5332
|
+
: 'Messages up to this point will be summarized.' }), _jsx(Text, { children: " " }), _jsxs(Text, { children: ["add context (optional): ", input] }), _jsx(Text, { dimColor: true, children: "Enter to summarize \u00B7 Esc to go back" })] })) : menu?.kind === 'permission-rule-input' ? (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { bold: true, children: ["Add ", menu.behavior, " permission rule"] }), _jsx(Text, { children: "Permission rules are a tool name, optionally followed by a specifier in parentheses." }), _jsx(Text, { children: "e.g., WebFetch or Bash(ls *)" }), _jsx(Text, { children: " " }), _jsx(Box, { borderStyle: axScreenReader ? undefined : 'round', paddingX: axScreenReader ? 0 : 1, children: _jsx(Text, { ...(input ? {} : { dimColor: true }), children: input || 'Enter permission rule…' }) }), _jsx(Text, { dimColor: true, children: "Enter to submit \u00B7 Esc to cancel" })] })) : menu?.kind === 'workspace-directory-input' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Add directory to workspace" }), _jsx(Text, { children: "Praxis Code will be able to read files in this directory and make edits when auto-accept edits is on." }), _jsx(Text, { children: " " }), _jsx(Text, { children: "Enter the path to the directory:" }), _jsx(Box, { borderStyle: axScreenReader ? undefined : 'round', paddingX: axScreenReader ? 0 : 1, children: _jsx(Text, { ...(input ? {} : { dimColor: true }), children: input || 'Directory path…' }) }), _jsx(Text, { dimColor: true, children: "Tab to complete \u00B7 Enter to add \u00B7 Esc to cancel" })] })) : menu?.kind === 'permission-delete' ? (_jsxs(DialogFrame, { title: permissionDeleteTitle(menu.rule.behavior), screenReader: axScreenReader, children: [_jsx(Text, { children: menu.rule.rule }), permissionRuleDescription(menu.rule.rule) ? (_jsx(Text, { dimColor: true, children: permissionRuleDescription(menu.rule.rule) })) : null, _jsx(Text, { dimColor: true, children: permissionScopeLabel(menu.rule.scope) }), _jsx(Text, { children: " " }), _jsx(Text, { children: "Are you sure you want to delete this permission rule?" }), _jsx(Text, { children: " " }), _jsxs(Text, { inverse: !axScreenReader && menu.selectedIndex === 0, children: [selectionPrefix(menu.selectedIndex === 0, axScreenReader), "1. Yes"] }), _jsxs(Text, { inverse: !axScreenReader && menu.selectedIndex === 1, children: [selectionPrefix(menu.selectedIndex === 1, axScreenReader), "2. No"] }), _jsx(Text, { dimColor: true, children: "Esc to cancel" })] })) : menu?.kind === 'workspace-directory-delete' ? (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Remove directory from workspace?" }), _jsxs(Text, { children: [" ", menu.path] }), _jsx(Text, { children: " " }), _jsx(Text, { children: "Praxis Code will no longer have access to files in this directory." }), _jsx(Text, { children: " " }), _jsxs(Text, { inverse: !axScreenReader && menu.selectedIndex === 0, children: [selectionPrefix(menu.selectedIndex === 0, axScreenReader), "1. Yes"] }), _jsxs(Text, { inverse: !axScreenReader && menu.selectedIndex === 1, children: [selectionPrefix(menu.selectedIndex === 1, axScreenReader), "2. No"] }), _jsx(Text, { dimColor: true, children: "Enter to confirm \u00B7 Esc to cancel" })] })) : menu ? (menu.kind === 'help' ? (_jsx(HelpMenu, { tabIndex: menu.tabIndex, selectedIndex: menu.selectedIndex, builtinCommands: builtinSlashCommands, customCommands: customSlashCommands, width: width, screenReader: axScreenReader })) : menu.kind === 'diff' ? (_jsx(DiffDashboard, { snapshots: menu.snapshots, sourceIndex: menu.sourceIndex, selectedIndex: menu.selectedIndex, viewingFile: menu.viewingFile, scrollOffset: menu.scrollOffset, width: width, screenReader: axScreenReader })) : menu.kind === 'permission-dashboard' ? (_jsx(PermissionDashboard, { tabIndex: menu.tabIndex, selectedIndex: menu.selectedIndex, query: menu.query, rules: menu.rules, recentDenied: recentDenied, retryingDeniedId: retryingDeniedId, workspaceDirectories: workspaceDirectories, width: width, screenReader: axScreenReader })) : menu.kind === 'sandbox' ? (_jsx(SandboxDashboard, { snapshot: menu.snapshot, tab: menu.tab, selectedIndex: menu.selectedIndex, width: width, screenReader: axScreenReader })) : menu.kind === 'permission-scope' ? (_jsx(SelectionMenu, { title: "Where should this rule be saved?", description: `${menu.rule}${permissionRuleDescription(menu.rule)
|
|
5201
5333
|
? ` · ${permissionRuleDescription(menu.rule)}`
|
|
5202
5334
|
: ''}`, options: [
|
|
5203
5335
|
{
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { TuiSandboxSnapshot, TuiSandboxTab } from './sandbox-settings.js';
|
|
2
|
+
export declare function tuiSandboxTabs(snapshot: TuiSandboxSnapshot): readonly TuiSandboxTab[];
|
|
3
|
+
export declare function SandboxDashboard({ snapshot, tab, selectedIndex, width, screenReader, }: {
|
|
4
|
+
snapshot: TuiSandboxSnapshot;
|
|
5
|
+
tab: TuiSandboxTab;
|
|
6
|
+
selectedIndex: number;
|
|
7
|
+
width: number;
|
|
8
|
+
screenReader: boolean;
|
|
9
|
+
}): import("react").JSX.Element;
|
|
10
|
+
//# sourceMappingURL=sandbox-dashboard.d.ts.map
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
const TABS = {
|
|
4
|
+
mode: 'Mode',
|
|
5
|
+
dependencies: 'Dependencies',
|
|
6
|
+
overrides: 'Overrides',
|
|
7
|
+
config: 'Config',
|
|
8
|
+
};
|
|
9
|
+
export function tuiSandboxTabs(snapshot) {
|
|
10
|
+
if (snapshot.dependencies.errors.length > 0)
|
|
11
|
+
return ['dependencies'];
|
|
12
|
+
return [
|
|
13
|
+
'mode',
|
|
14
|
+
...(snapshot.dependencies.warnings.length > 0
|
|
15
|
+
? ['dependencies']
|
|
16
|
+
: []),
|
|
17
|
+
'overrides',
|
|
18
|
+
'config',
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
function prefix(selected, screenReader) {
|
|
22
|
+
if (screenReader)
|
|
23
|
+
return selected ? 'Selected: ' : '';
|
|
24
|
+
return selected ? '❯ ' : ' ';
|
|
25
|
+
}
|
|
26
|
+
export function SandboxDashboard({ snapshot, tab, selectedIndex, width, screenReader, }) {
|
|
27
|
+
const tabs = tuiSandboxTabs(snapshot);
|
|
28
|
+
const mode = !snapshot.settings.enabled
|
|
29
|
+
? 'disabled'
|
|
30
|
+
: snapshot.settings.autoAllowBashIfSandboxed
|
|
31
|
+
? 'auto-allow'
|
|
32
|
+
: 'regular';
|
|
33
|
+
const modeRows = [
|
|
34
|
+
{
|
|
35
|
+
value: 'auto-allow',
|
|
36
|
+
label: 'Sandbox BashTool, with auto-allow',
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
value: 'regular',
|
|
40
|
+
label: 'Sandbox BashTool, with regular permissions',
|
|
41
|
+
},
|
|
42
|
+
{ value: 'disabled', label: 'No Sandbox' },
|
|
43
|
+
];
|
|
44
|
+
const overrideRows = [
|
|
45
|
+
{
|
|
46
|
+
value: true,
|
|
47
|
+
label: 'Allow unsandboxed fallback',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
value: false,
|
|
51
|
+
label: 'Strict sandbox mode',
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
const filesystem = snapshot.settings.runtimeConfig.filesystem;
|
|
55
|
+
const network = snapshot.settings.runtimeConfig.network;
|
|
56
|
+
return (_jsxs(Box, { flexDirection: "column", width: Math.min(100, width), children: [!screenReader ? (_jsx(Text, { dimColor: true, children: '─'.repeat(Math.min(100, width)) })) : null, _jsx(Text, { bold: true, children: " Sandbox:" }), _jsxs(Text, { children: [' ', tabs.map((candidate) => (_jsxs(Text, { inverse: !screenReader && candidate === tab, children: [screenReader && candidate === tab ? 'Current tab: ' : ' ', TABS[candidate], ' '] }, candidate)))] }), _jsx(Text, { children: " " }), tab === 'mode' ? (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: " Configure Mode:" }), _jsx(Text, { children: " " }), modeRows.map((row, index) => (_jsxs(Text, { inverse: !screenReader && selectedIndex === index, children: [prefix(selectedIndex === index, screenReader), index + 1, ". ", row.label, row.value === mode ? ' (current)' : ''] }, row.value))), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Auto-allow mode runs Bash in the sandbox automatically. Explicit ask and deny rules are always respected." })] })) : tab === 'dependencies' ? (_jsxs(Box, { flexDirection: "column", children: [snapshot.platform === 'macos' ? (_jsxs(Text, { children: ["seatbelt: ", _jsx(Text, { color: "green", children: "built-in (macOS)" })] })) : (_jsxs(_Fragment, { children: [_jsxs(Text, { children: ["bubblewrap (bwrap):", ' ', _jsx(Text, { color: snapshot.dependencies.errors.some((error) => error.includes('bwrap'))
|
|
57
|
+
? 'red'
|
|
58
|
+
: 'green', children: snapshot.dependencies.errors.some((error) => error.includes('bwrap'))
|
|
59
|
+
? 'not installed'
|
|
60
|
+
: 'installed' })] }), _jsxs(Text, { children: ["socat:", ' ', _jsx(Text, { color: snapshot.dependencies.errors.some((error) => error.includes('socat'))
|
|
61
|
+
? 'red'
|
|
62
|
+
: 'green', children: snapshot.dependencies.errors.some((error) => error.includes('socat'))
|
|
63
|
+
? 'not installed'
|
|
64
|
+
: 'installed' })] }), _jsxs(Text, { children: ["seccomp filter:", ' ', _jsx(Text, { color: snapshot.dependencies.warnings.length > 0
|
|
65
|
+
? 'yellow'
|
|
66
|
+
: 'green', children: snapshot.dependencies.warnings.length > 0
|
|
67
|
+
? 'not installed'
|
|
68
|
+
: 'installed' })] })] })), _jsxs(Text, { children: ["ripgrep (rg):", ' ', _jsx(Text, { color: snapshot.dependencies.errors.some((error) => error.includes('ripgrep'))
|
|
69
|
+
? 'red'
|
|
70
|
+
: 'green', children: snapshot.dependencies.errors.some((error) => error.includes('ripgrep'))
|
|
71
|
+
? 'not found'
|
|
72
|
+
: 'found' })] }), snapshot.dependencies.errors.map((error) => (_jsx(Text, { color: "red", children: error }, error))), snapshot.dependencies.warnings.map((warning) => (_jsx(Text, { color: "yellow", children: warning }, warning))), snapshot.unavailableReason ? (_jsx(Text, { color: "red", children: snapshot.unavailableReason })) : null] })) : tab === 'overrides' ? (_jsxs(_Fragment, { children: [!snapshot.settings.enabled ? (_jsx(Text, { dimColor: true, children: "Sandbox is not enabled" })) : (overrideRows.map((row, index) => (_jsxs(Text, { inverse: !screenReader && selectedIndex === index, children: [prefix(selectedIndex === index, screenReader), index + 1, ". ", row.label, row.value === snapshot.settings.allowUnsandboxedCommands
|
|
73
|
+
? ' (current)'
|
|
74
|
+
: ''] }, String(row.value))))), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: "Closed mode ignores dangerouslyDisableSandbox and requires commands to remain sandboxed." })] })) : !snapshot.settings.enabled ? (_jsx(Text, { dimColor: true, children: "Sandbox is not enabled" })) : (_jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: "Excluded Commands:" }), _jsx(Text, { dimColor: true, children: snapshot.settings.excludedCommands.join(', ') || 'None' }), _jsx(Text, { bold: true, children: "Filesystem Read Restrictions:" }), _jsxs(Text, { dimColor: true, children: ["Denied: ", filesystem?.denyRead?.join(', ') || 'None'] }), _jsxs(Text, { dimColor: true, children: ["Allowed within denied: ", filesystem?.allowRead?.join(', ') || 'None'] }), _jsx(Text, { bold: true, children: "Filesystem Write Restrictions:" }), _jsxs(Text, { dimColor: true, children: ["Allowed: ", filesystem?.allowWrite?.join(', ') || 'None'] }), _jsxs(Text, { dimColor: true, children: ["Denied within allowed: ", filesystem?.denyWrite?.join(', ') || 'None'] }), _jsx(Text, { bold: true, children: "Network Restrictions:" }), _jsxs(Text, { dimColor: true, children: ["Allowed: ", network?.allowedDomains?.join(', ') || 'None'] }), _jsxs(Text, { dimColor: true, children: ["Denied: ", network?.deniedDomains?.join(', ') || 'None'] }), _jsx(Text, { bold: true, children: "Allowed Unix Sockets:" }), _jsx(Text, { dimColor: true, children: network?.allowUnixSockets?.join(', ') || 'None' }), (snapshot.globPatternWarnings?.length ?? 0) > 0 ? (_jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color: "yellow", children: "\u26A0 Warning: Glob patterns not fully supported on Linux" }), _jsx(Text, { dimColor: true, children: snapshot.globPatternWarnings?.slice(0, 3).join(', ') })] })) : null] })), _jsx(Text, { children: " " }), _jsx(Text, { dimColor: true, children: tab === 'mode' || (tab === 'overrides' && snapshot.settings.enabled)
|
|
75
|
+
? '↑/↓ navigate · Enter to select · ←/→ switch tabs · Esc to cancel'
|
|
76
|
+
: '←/→ switch tabs · Esc to cancel' })] }));
|
|
77
|
+
}
|
|
78
|
+
//# sourceMappingURL=sandbox-dashboard.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { SandboxDependencyCheck } from '@anthropic-ai/sandbox-runtime';
|
|
2
|
+
import { loadClaudeSettings } from '../../compatibility/claude/shared-resources.js';
|
|
3
|
+
import { type ClaudeSandboxSettings } from '../../sandbox/claude-sandbox-settings.js';
|
|
4
|
+
export type TuiSandboxMode = 'auto-allow' | 'regular' | 'disabled';
|
|
5
|
+
export type TuiSandboxTab = 'mode' | 'dependencies' | 'overrides' | 'config';
|
|
6
|
+
export interface TuiSandboxSnapshot {
|
|
7
|
+
settings: ClaudeSandboxSettings;
|
|
8
|
+
dependencies: SandboxDependencyCheck;
|
|
9
|
+
supported: boolean;
|
|
10
|
+
platform: 'macos' | 'linux' | 'windows' | 'wsl';
|
|
11
|
+
globPatternWarnings?: readonly string[];
|
|
12
|
+
unavailableReason?: string;
|
|
13
|
+
}
|
|
14
|
+
export interface TuiSandboxStore {
|
|
15
|
+
load(): Promise<TuiSandboxSnapshot>;
|
|
16
|
+
setMode(mode: TuiSandboxMode): Promise<TuiSandboxSnapshot>;
|
|
17
|
+
setAllowUnsandboxedCommands(allow: boolean): Promise<TuiSandboxSnapshot>;
|
|
18
|
+
exclude(pattern: string): Promise<{
|
|
19
|
+
pattern: string;
|
|
20
|
+
settingsPath: string;
|
|
21
|
+
snapshot: TuiSandboxSnapshot;
|
|
22
|
+
}>;
|
|
23
|
+
}
|
|
24
|
+
export declare function linuxGlobPatternWarnings(resources: Awaited<ReturnType<typeof loadClaudeSettings>>, platform: 'macos' | 'linux' | 'windows' | 'wsl'): string[];
|
|
25
|
+
export declare function createTuiSandboxStore({ configRoot, cwd, homeDirectory, additionalDirectories, environment, }: {
|
|
26
|
+
configRoot: string;
|
|
27
|
+
cwd: string;
|
|
28
|
+
homeDirectory: string;
|
|
29
|
+
additionalDirectories?: readonly string[];
|
|
30
|
+
environment?: NodeJS.ProcessEnv;
|
|
31
|
+
}): TuiSandboxStore;
|
|
32
|
+
//# sourceMappingURL=sandbox-settings.d.ts.map
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join, relative } from 'node:path';
|
|
3
|
+
import { loadClaudeSettings } from '../../compatibility/claude/shared-resources.js';
|
|
4
|
+
import { writeFileAtomically } from '../../platform/atomic-write.js';
|
|
5
|
+
import { claudeSandboxRuntime } from '../../sandbox/claude-sandbox-runtime.js';
|
|
6
|
+
import { claudeSandboxTempDirectory, loadClaudeSandboxSettings, } from '../../sandbox/claude-sandbox-settings.js';
|
|
7
|
+
function isRecord(value) {
|
|
8
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
9
|
+
}
|
|
10
|
+
async function readLocalSettings(path) {
|
|
11
|
+
try {
|
|
12
|
+
const value = JSON.parse(await readFile(path, 'utf8'));
|
|
13
|
+
if (!isRecord(value))
|
|
14
|
+
throw new Error(`Settings must be an object: ${path}`);
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error.code === 'ENOENT')
|
|
19
|
+
return {};
|
|
20
|
+
throw error;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
async function updateLocalSandbox(path, update) {
|
|
24
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
25
|
+
let source;
|
|
26
|
+
try {
|
|
27
|
+
source = await readFile(path, 'utf8');
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (error.code !== 'ENOENT')
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
const settings = await readLocalSettings(path);
|
|
34
|
+
const current = settings.sandbox;
|
|
35
|
+
if (current !== undefined && !isRecord(current)) {
|
|
36
|
+
throw new Error(`sandbox must be an object: ${path}`);
|
|
37
|
+
}
|
|
38
|
+
const committed = await writeFileAtomically(path, `${JSON.stringify({ ...settings, sandbox: update(current ?? {}) }, null, 2)}\n`, {
|
|
39
|
+
beforeCommit: async () => {
|
|
40
|
+
try {
|
|
41
|
+
return (await readFile(path, 'utf8')) === source;
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (error.code === 'ENOENT')
|
|
45
|
+
return source === undefined;
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
if (committed)
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
throw new Error(`Settings changed concurrently: ${path}`);
|
|
54
|
+
}
|
|
55
|
+
export function linuxGlobPatternWarnings(resources, platform) {
|
|
56
|
+
if (platform !== 'linux' && platform !== 'wsl')
|
|
57
|
+
return [];
|
|
58
|
+
const warnings = [];
|
|
59
|
+
for (const resource of resources) {
|
|
60
|
+
if (!isRecord(resource.value) || !isRecord(resource.value.permissions))
|
|
61
|
+
continue;
|
|
62
|
+
for (const behavior of ['allow', 'deny']) {
|
|
63
|
+
const rules = resource.value.permissions[behavior];
|
|
64
|
+
if (!Array.isArray(rules))
|
|
65
|
+
continue;
|
|
66
|
+
for (const rule of rules) {
|
|
67
|
+
if (typeof rule !== 'string')
|
|
68
|
+
continue;
|
|
69
|
+
const match = /^(?:Edit|Read)\((.*)\)$/u.exec(rule);
|
|
70
|
+
const pattern = match?.[1]?.replace(/\/\*\*$/u, '');
|
|
71
|
+
if (pattern && /[*?[\]]/u.test(pattern))
|
|
72
|
+
warnings.push(rule);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return [...new Set(warnings)];
|
|
77
|
+
}
|
|
78
|
+
export function createTuiSandboxStore({ configRoot, cwd, homeDirectory, additionalDirectories = [], environment = process.env, }) {
|
|
79
|
+
const localSettingsPath = join(cwd, '.claude', 'settings.local.json');
|
|
80
|
+
const load = async () => {
|
|
81
|
+
const resources = await loadClaudeSettings({ configRoot, cwd });
|
|
82
|
+
const settings = loadClaudeSandboxSettings({
|
|
83
|
+
resources,
|
|
84
|
+
cwd,
|
|
85
|
+
configRoot,
|
|
86
|
+
homeDirectory,
|
|
87
|
+
additionalDirectories,
|
|
88
|
+
tempDirectory: claudeSandboxTempDirectory(environment),
|
|
89
|
+
});
|
|
90
|
+
await claudeSandboxRuntime.initialize(settings);
|
|
91
|
+
const unavailableReason = claudeSandboxRuntime.unavailableReason(settings);
|
|
92
|
+
const platform = claudeSandboxRuntime.platformName();
|
|
93
|
+
return {
|
|
94
|
+
settings,
|
|
95
|
+
dependencies: claudeSandboxRuntime.dependencyCheck(settings),
|
|
96
|
+
supported: claudeSandboxRuntime.isSupportedPlatform(),
|
|
97
|
+
platform,
|
|
98
|
+
globPatternWarnings: linuxGlobPatternWarnings(resources, platform),
|
|
99
|
+
...(unavailableReason ? { unavailableReason } : {}),
|
|
100
|
+
};
|
|
101
|
+
};
|
|
102
|
+
return {
|
|
103
|
+
load,
|
|
104
|
+
async setMode(mode) {
|
|
105
|
+
await updateLocalSandbox(localSettingsPath, (sandbox) => ({
|
|
106
|
+
...sandbox,
|
|
107
|
+
enabled: mode !== 'disabled',
|
|
108
|
+
autoAllowBashIfSandboxed: mode === 'auto-allow',
|
|
109
|
+
}));
|
|
110
|
+
return load();
|
|
111
|
+
},
|
|
112
|
+
async setAllowUnsandboxedCommands(allow) {
|
|
113
|
+
await updateLocalSandbox(localSettingsPath, (sandbox) => ({
|
|
114
|
+
...sandbox,
|
|
115
|
+
allowUnsandboxedCommands: allow,
|
|
116
|
+
}));
|
|
117
|
+
return load();
|
|
118
|
+
},
|
|
119
|
+
async exclude(pattern) {
|
|
120
|
+
const cleanPattern = pattern.trim().replace(/^["']|["']$/gu, '');
|
|
121
|
+
if (!cleanPattern) {
|
|
122
|
+
throw new Error('Please provide a command pattern to exclude');
|
|
123
|
+
}
|
|
124
|
+
await updateLocalSandbox(localSettingsPath, (sandbox) => {
|
|
125
|
+
const excluded = sandbox.excludedCommands;
|
|
126
|
+
if (excluded !== undefined &&
|
|
127
|
+
(!Array.isArray(excluded) ||
|
|
128
|
+
excluded.some((value) => typeof value !== 'string'))) {
|
|
129
|
+
throw new Error(`sandbox.excludedCommands must be an array of strings: ${localSettingsPath}`);
|
|
130
|
+
}
|
|
131
|
+
return {
|
|
132
|
+
...sandbox,
|
|
133
|
+
excludedCommands: [
|
|
134
|
+
...new Set([...(excluded ?? []), cleanPattern]),
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
pattern: cleanPattern,
|
|
140
|
+
settingsPath: relative(cwd, localSettingsPath),
|
|
141
|
+
snapshot: await load(),
|
|
142
|
+
};
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=sandbox-settings.js.map
|
|
@@ -64,6 +64,11 @@ export const BUILTIN_TUI_SLASH_COMMANDS = [
|
|
|
64
64
|
description: 'Manage allow and deny tool permission rules',
|
|
65
65
|
source: 'builtin',
|
|
66
66
|
},
|
|
67
|
+
{
|
|
68
|
+
name: 'sandbox',
|
|
69
|
+
description: 'Configure sandbox mode and command isolation',
|
|
70
|
+
source: 'builtin',
|
|
71
|
+
},
|
|
67
72
|
{
|
|
68
73
|
name: 'resume',
|
|
69
74
|
description: 'Resume a previous conversation',
|
package/dist/cli.d.ts
CHANGED
|
@@ -114,6 +114,7 @@ export interface CliDependencies extends InteractiveServiceFactory {
|
|
|
114
114
|
exposeToolRegistry?: boolean;
|
|
115
115
|
emitToolUseSummaries?: boolean;
|
|
116
116
|
cwd?: string;
|
|
117
|
+
sandboxOriginalCwd?: string;
|
|
117
118
|
configRoot?: string;
|
|
118
119
|
environment?: Readonly<Record<string, string>>;
|
|
119
120
|
providerEnvironment?: NodeJS.ProcessEnv;
|
package/dist/cli.js
CHANGED
|
@@ -43,6 +43,8 @@ import { FilteredToolRegistry } from './tools/filtered-tool-registry.js';
|
|
|
43
43
|
import { WebToolRegistry } from './tools/web.js';
|
|
44
44
|
import { WorkspaceContext } from './application/session-worktree.js';
|
|
45
45
|
import { launchTmuxWorktree } from './platform/tmux-worktree.js';
|
|
46
|
+
import { claudeSandboxRuntime } from './sandbox/claude-sandbox-runtime.js';
|
|
47
|
+
import { claudeSandboxTempDirectory, loadClaudeSandboxSettings, } from './sandbox/claude-sandbox-settings.js';
|
|
46
48
|
import { createErrorResult, createSuccessResult, parseCliInvocation, readStreamJsonMessages, StreamJsonOutput, } from './cli/protocol.js';
|
|
47
49
|
import { describeClaudePlugin, initClaudePlugin, installClaudePlugin, loadClaudePlugins, readPluginRegistry, setClaudePluginEnabled, uninstallClaudePlugin, updateClaudePlugin, validateClaudePlugin, } from './plugins/claude-plugin-runtime.js';
|
|
48
50
|
import { addClaudeMarketplace, disableAllNativePlugins, installClaudeMarketplacePlugin, listClaudeMarketplaceAvailablePlugins, listNativePluginRecords, readClaudeKnownMarketplaces, removeClaudeMarketplace, setNativePluginEnabled, saveClaudePluginConfig, uninstallNativePlugin, updateClaudeMarketplace, updateNativePlugin, validateClaudeMarketplace, } from './plugins/claude-plugin-marketplace.js';
|
|
@@ -688,8 +690,9 @@ const consoleIO = {
|
|
|
688
690
|
isTTY: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
689
691
|
readStdinLines: () => process.stdin,
|
|
690
692
|
};
|
|
691
|
-
const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = false, approveRecovery, approveTool, agent, model: interactiveModel, effort: interactiveEffort, permissionMode: interactivePermissionMode, isSessionActionApproved, controls = DEFAULT_CLI_CONTROLS, interactive = false, sessionKind, signal, exposeToolRegistry = false, onElicitation, askUser, approvePlan, emitToolUseSummaries = false, cwd: requestedCwd, configRoot: requestedConfigRoot, environment, providerEnvironment: requestedProviderEnvironment, }) => {
|
|
693
|
+
const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = false, approveRecovery, approveTool, agent, model: interactiveModel, effort: interactiveEffort, permissionMode: interactivePermissionMode, isSessionActionApproved, controls = DEFAULT_CLI_CONTROLS, interactive = false, sessionKind, signal, exposeToolRegistry = false, onElicitation, askUser, approvePlan, emitToolUseSummaries = false, cwd: requestedCwd, sandboxOriginalCwd, configRoot: requestedConfigRoot, environment, providerEnvironment: requestedProviderEnvironment, }) => {
|
|
692
694
|
const runtimeEnvironment = requestedProviderEnvironment ?? process.env;
|
|
695
|
+
const sandboxEnvironment = { ...runtimeEnvironment, ...environment };
|
|
693
696
|
const claudeVersion = await detectInstalledClaudeVersion();
|
|
694
697
|
const cwd = requestedCwd ?? process.cwd();
|
|
695
698
|
const workspace = new WorkspaceContext(cwd);
|
|
@@ -948,9 +951,26 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
|
|
|
948
951
|
...cli.additionalDirectories,
|
|
949
952
|
...(exposePlanDirectory ? [resolve(configRoot, 'plans')] : []),
|
|
950
953
|
];
|
|
954
|
+
const sandboxSettings = loadClaudeSandboxSettings({
|
|
955
|
+
resources: settings.filter((resource) => resource.plugin !== true),
|
|
956
|
+
cwd: workspace.cwd(),
|
|
957
|
+
originalCwd: sandboxOriginalCwd ?? workspace.cwd(),
|
|
958
|
+
configRoot,
|
|
959
|
+
homeDirectory: sandboxEnvironment.HOME ?? sandboxEnvironment.USERPROFILE ?? homedir(),
|
|
960
|
+
additionalDirectories: initialAdditionalDirectories,
|
|
961
|
+
tempDirectory: claudeSandboxTempDirectory(sandboxEnvironment),
|
|
962
|
+
});
|
|
963
|
+
await claudeSandboxRuntime.initialize(sandboxSettings);
|
|
964
|
+
const sandboxUnavailableReason = claudeSandboxRuntime.unavailableReason();
|
|
965
|
+
if (sandboxUnavailableReason && !sandboxSettings.failIfUnavailable) {
|
|
966
|
+
runtimeEventSink({ type: 'warning', message: sandboxUnavailableReason });
|
|
967
|
+
}
|
|
951
968
|
const permissionAdditionalDirectories = [
|
|
952
|
-
...
|
|
953
|
-
|
|
969
|
+
...new Set([
|
|
970
|
+
...initialAdditionalDirectories,
|
|
971
|
+
...controls.addDirectories.map((directory) => resolve(cwd, directory)),
|
|
972
|
+
...(memoryDirectory ? [memoryDirectory] : []),
|
|
973
|
+
]),
|
|
954
974
|
];
|
|
955
975
|
const initialAdditionalReadDirectories = [claudeBackgroundTaskParent(cwd)];
|
|
956
976
|
const permissionResolverForMode = (permissionMode) => new ClaudeExtensionPermissionResolver(new ClaudePermissionResolver({
|
|
@@ -962,6 +982,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
|
|
|
962
982
|
disallowedTools: cli.disallowedTools,
|
|
963
983
|
additionalDirectories: permissionAdditionalDirectories,
|
|
964
984
|
additionalReadDirectories: initialAdditionalReadDirectories,
|
|
985
|
+
sandbox: claudeSandboxRuntime,
|
|
965
986
|
permissionMode,
|
|
966
987
|
...(isSessionActionApproved ? { isSessionActionApproved } : {}),
|
|
967
988
|
...(permissionMode === 'auto'
|
|
@@ -978,6 +999,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
|
|
|
978
999
|
...(memoryDirectory ? { sharedMemoryDirectory: memoryDirectory } : {}),
|
|
979
1000
|
additionalDirectories: initialAdditionalDirectories,
|
|
980
1001
|
additionalReadDirectories: initialAdditionalReadDirectories,
|
|
1002
|
+
sandbox: claudeSandboxRuntime,
|
|
981
1003
|
...(environment ? { environment } : {}),
|
|
982
1004
|
});
|
|
983
1005
|
const runtimeMcpResources = async (candidates) => {
|
|
@@ -1463,6 +1485,7 @@ const defaultPluginEvalRuntimeFactory = {
|
|
|
1463
1485
|
USERPROFILE: options.home,
|
|
1464
1486
|
},
|
|
1465
1487
|
providerEnvironment: process.env,
|
|
1488
|
+
isSessionActionApproved: (call) => options.allowedTools.includes(call.name),
|
|
1466
1489
|
controls: {
|
|
1467
1490
|
...DEFAULT_CLI_CONTROLS,
|
|
1468
1491
|
sessionPersistence: false,
|
|
@@ -1568,6 +1591,7 @@ const defaultDependencies = {
|
|
|
1568
1591
|
createService: ({ additionalDirectories, cwd, ...options }) => createDefaultService({
|
|
1569
1592
|
...options,
|
|
1570
1593
|
...(cwd === undefined ? {} : { cwd }),
|
|
1594
|
+
sandboxOriginalCwd: process.cwd(),
|
|
1571
1595
|
...(agent === undefined ? {} : { agent }),
|
|
1572
1596
|
controls: {
|
|
1573
1597
|
...interactiveControls,
|