@causyne/agent 0.0.0-stage → 1.10.1
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/LICENSE +102 -0
- package/README.md +31 -2
- package/SECURITY.md +24 -0
- package/bin/causyne.js +13 -0
- package/docs/ARCHITECTURE.md +63 -0
- package/docs/PROVENANCE.md +29 -0
- package/docs/SECURITY_REVIEW.md +89 -0
- package/docs/THIRD_PARTY_NOTICES.md +49 -0
- package/native-skills/ai-engineering/context-window-management/SKILL.md +43 -0
- package/native-skills/ai-engineering/llm-evaluation/SKILL.md +46 -0
- package/native-skills/ai-engineering/model-cost-and-latency/SKILL.md +46 -0
- package/native-skills/ai-engineering/prompt-construction/SKILL.md +42 -0
- package/native-skills/ai-engineering/retrieval-augmented-generation/SKILL.md +42 -0
- package/native-skills/ai-engineering/structured-output-and-schemas/SKILL.md +45 -0
- package/native-skills/ai-engineering/tool-calling-agents/SKILL.md +43 -0
- package/native-skills/android/app-performance-profiling/SKILL.md +42 -0
- package/native-skills/android/background-work-scheduling/SKILL.md +47 -0
- package/native-skills/android/compose-state-and-recomposition/SKILL.md +43 -0
- package/native-skills/android/gradle-build-configuration/SKILL.md +41 -0
- package/native-skills/android/lifecycle-and-leaks/SKILL.md +46 -0
- package/native-skills/android/play-store-release/SKILL.md +43 -0
- package/native-skills/android/room-and-persistence/SKILL.md +40 -0
- package/native-skills/backend/authentication-and-sessions/SKILL.md +45 -0
- package/native-skills/backend/background-jobs-and-queues/SKILL.md +42 -0
- package/native-skills/backend/caching-strategy/SKILL.md +44 -0
- package/native-skills/backend/graceful-shutdown/SKILL.md +42 -0
- package/native-skills/backend/rate-limiting-and-quotas/SKILL.md +40 -0
- package/native-skills/backend/rest-api-design/SKILL.md +40 -0
- package/native-skills/backend/webhook-delivery-and-receipt/SKILL.md +43 -0
- package/native-skills/cloud/cost-optimisation/SKILL.md +46 -0
- package/native-skills/cloud/iam-least-privilege/SKILL.md +43 -0
- package/native-skills/cloud/infrastructure-as-code/SKILL.md +43 -0
- package/native-skills/cloud/multi-region-and-failover/SKILL.md +43 -0
- package/native-skills/cloud/serverless-patterns/SKILL.md +47 -0
- package/native-skills/data/data-quality-monitoring/SKILL.md +51 -0
- package/native-skills/data/etl-pipeline-reliability/SKILL.md +43 -0
- package/native-skills/data/exploratory-analysis/SKILL.md +43 -0
- package/native-skills/data/metric-definition/SKILL.md +42 -0
- package/native-skills/data/notebook-to-production/SKILL.md +43 -0
- package/native-skills/database/backup-and-recovery/SKILL.md +43 -0
- package/native-skills/database/connection-pooling/SKILL.md +42 -0
- package/native-skills/database/data-integrity-and-constraints/SKILL.md +42 -0
- package/native-skills/database/query-optimisation/SKILL.md +38 -0
- package/native-skills/database/safe-migrations/SKILL.md +47 -0
- package/native-skills/database/schema-design/SKILL.md +38 -0
- package/native-skills/database/transactions-and-isolation/SKILL.md +41 -0
- package/native-skills/design/colour-system-construction/SKILL.md +38 -0
- package/native-skills/design/design-direction-brief/SKILL.md +41 -0
- package/native-skills/design/motion-and-transitions/SKILL.md +38 -0
- package/native-skills/design/responsive-strategy/SKILL.md +39 -0
- package/native-skills/design/spacing-and-rhythm/SKILL.md +38 -0
- package/native-skills/design/type-scale-and-hierarchy/SKILL.md +39 -0
- package/native-skills/design/visual-qa-loop/SKILL.md +42 -0
- package/native-skills/devops/docker-multi-stage-builds/SKILL.md +60 -0
- package/native-skills/devops/github-actions-ci/SKILL.md +60 -0
- package/native-skills/devops/incident-triage/SKILL.md +38 -0
- package/native-skills/devops/kubernetes-deployment-hardening/SKILL.md +64 -0
- package/native-skills/devops/observability-baseline/SKILL.md +38 -0
- package/native-skills/devops/secret-management/SKILL.md +40 -0
- package/native-skills/devops/terraform-state-and-modules/SKILL.md +36 -0
- package/native-skills/devops/zero-downtime-deploys/SKILL.md +40 -0
- package/native-skills/git/bisect-regression-hunt/SKILL.md +43 -0
- package/native-skills/git/branch-strategy/SKILL.md +39 -0
- package/native-skills/git/commit-hygiene/SKILL.md +41 -0
- package/native-skills/git/history-recovery/SKILL.md +44 -0
- package/native-skills/git/large-repository-surgery/SKILL.md +46 -0
- package/native-skills/git/merge-conflict-resolution/SKILL.md +42 -0
- package/native-skills/media/cinematic-3d-shot/SKILL.md +104 -0
- package/native-skills/mobile/app-startup-optimisation/SKILL.md +41 -0
- package/native-skills/mobile/cross-platform-tradeoffs/SKILL.md +42 -0
- package/native-skills/mobile/deep-linking/SKILL.md +39 -0
- package/native-skills/mobile/mobile-storage-security/SKILL.md +45 -0
- package/native-skills/mobile/offline-first-sync/SKILL.md +42 -0
- package/native-skills/mobile/push-notifications/SKILL.md +38 -0
- package/native-skills/performance/algorithmic-complexity/SKILL.md +43 -0
- package/native-skills/performance/concurrency-and-parallelism/SKILL.md +42 -0
- package/native-skills/performance/load-testing/SKILL.md +42 -0
- package/native-skills/performance/memory-and-leak-hunting/SKILL.md +41 -0
- package/native-skills/performance/network-and-payload-efficiency/SKILL.md +42 -0
- package/native-skills/performance/profiling-methodology/SKILL.md +46 -0
- package/native-skills/python/async-correctness/SKILL.md +40 -0
- package/native-skills/python/cli-tool-construction/SKILL.md +44 -0
- package/native-skills/python/data-processing-pipelines/SKILL.md +38 -0
- package/native-skills/python/environment-isolation/SKILL.md +41 -0
- package/native-skills/python/packaging-and-distribution/SKILL.md +56 -0
- package/native-skills/python/pytest-suite-design/SKILL.md +39 -0
- package/native-skills/python/type-hints-and-static-checking/SKILL.md +42 -0
- package/native-skills/security/authorization-model/SKILL.md +42 -0
- package/native-skills/security/crypto-usage/SKILL.md +48 -0
- package/native-skills/security/dependency-supply-chain/SKILL.md +42 -0
- package/native-skills/security/input-validation-and-injection/SKILL.md +44 -0
- package/native-skills/security/secure-file-uploads/SKILL.md +46 -0
- package/native-skills/security/security-headers-and-csp/SKILL.md +44 -0
- package/native-skills/security/threat-modelling/SKILL.md +46 -0
- package/native-skills/testing/end-to-end-test-stability/SKILL.md +42 -0
- package/native-skills/testing/property-based-testing/SKILL.md +46 -0
- package/native-skills/testing/regression-test-from-a-bug/SKILL.md +41 -0
- package/native-skills/testing/snapshot-and-visual-regression/SKILL.md +42 -0
- package/native-skills/testing/test-doubles-and-mocking/SKILL.md +43 -0
- package/native-skills/testing/test-strategy-and-pyramid/SKILL.md +39 -0
- package/native-skills/testing/writing-good-assertions/SKILL.md +42 -0
- package/native-skills/web/accessible-components/SKILL.md +45 -0
- package/native-skills/web/api-client-resilience/SKILL.md +39 -0
- package/native-skills/web/bundle-size-reduction/SKILL.md +38 -0
- package/native-skills/web/core-web-vitals/SKILL.md +40 -0
- package/native-skills/web/css-layout-systems/SKILL.md +41 -0
- package/native-skills/web/form-validation-ux/SKILL.md +39 -0
- package/native-skills/web/react-state-architecture/SKILL.md +41 -0
- package/native-skills/web/ssr-hydration-correctness/SKILL.md +45 -0
- package/package.json +68 -4
- package/src/agentLoop.js +2458 -0
- package/src/appChannel.js +109 -0
- package/src/approvalGrants.js +66 -0
- package/src/auditLog.js +52 -0
- package/src/autoUpdate.js +12 -0
- package/src/capabilities.js +36 -0
- package/src/checkpoints.js +1199 -0
- package/src/cliMode.js +638 -0
- package/src/commands.js +805 -0
- package/src/compiledSkills.js +540 -0
- package/src/config.js +130 -0
- package/src/contextManager.js +498 -0
- package/src/cortex/autoCondenser.js +192 -0
- package/src/cortex/provisioner.js +27 -0
- package/src/cortex/vectorDb.js +447 -0
- package/src/designPalettes.js +99 -0
- package/src/devMode.js +6 -0
- package/src/embodimentSession.js +180 -0
- package/src/entrypoint.js +194 -0
- package/src/execSafe.js +21 -0
- package/src/gitSnapshot.js +378 -0
- package/src/mcp/jsonRpc.js +165 -0
- package/src/mcp/mcpClient.js +136 -0
- package/src/mcp/mcpRegistry.js +207 -0
- package/src/memory.js +62 -0
- package/src/memoryManager.js +124 -0
- package/src/modelCatalogue.js +132 -0
- package/src/modelIds.js +42 -0
- package/src/modelRouter.js +145 -0
- package/src/outputStore.js +246 -0
- package/src/perception.js +124 -0
- package/src/planCache.js +437 -0
- package/src/procSignal.js +32 -0
- package/src/providers/anthropic.js +408 -0
- package/src/providers/base.js +328 -0
- package/src/providers/dashscope-byok.js +86 -0
- package/src/providers/deepseek.js +80 -0
- package/src/providers/factory.js +37 -0
- package/src/providers/gemini.js +346 -0
- package/src/providers/moonshot.js +73 -0
- package/src/providers/muse.js +169 -0
- package/src/providers/ollama.js +104 -0
- package/src/providers/openai-compatible.js +84 -0
- package/src/providers/openai.js +20 -0
- package/src/providers/qwen-byok.js +76 -0
- package/src/providers/xai.js +73 -0
- package/src/provisioning/capabilityConsent.js +24 -0
- package/src/provisioning/capabilityProvisioner.js +271 -0
- package/src/provisioning/capabilityTools.js +29 -0
- package/src/reporting.js +373 -0
- package/src/sandbox.js +529 -0
- package/src/sessionStore.js +279 -0
- package/src/skillsManager.js +288 -0
- package/src/slopDetector.js +307 -0
- package/src/taskBuffer.js +45 -0
- package/src/taskShape.js +241 -0
- package/src/textDiff.js +150 -0
- package/src/tokenTracker.js +560 -0
- package/src/tool-scripts/ax_snapshot.applescript +39 -0
- package/src/tool-scripts/ax_snapshot.ps1 +10 -0
- package/src/tool-scripts/desktop_act.applescript +58 -0
- package/src/tool-scripts/resolve_automator.py +119 -0
- package/src/toolPacks.js +524 -0
- package/src/tools/archive.js +63 -0
- package/src/tools/astryxOracle.js +78 -0
- package/src/tools/axSnapshot.js +165 -0
- package/src/tools/blenderAutomator.js +349 -0
- package/src/tools/browserAutomator.js +252 -0
- package/src/tools/checkSystemSpecs.js +64 -0
- package/src/tools/clipboard.js +36 -0
- package/src/tools/codeSearchAst.js +101 -0
- package/src/tools/codeSearcher.js +230 -0
- package/src/tools/codeTools.js +262 -0
- package/src/tools/codebaseSearch.js +75 -0
- package/src/tools/createSkill.js +129 -0
- package/src/tools/deferredMcpSearch.js +246 -0
- package/src/tools/desktopAct.js +208 -0
- package/src/tools/desktopOpen.js +34 -0
- package/src/tools/dynamicLoader.js +44 -0
- package/src/tools/executeProceduralMedia.js +93 -0
- package/src/tools/fsDelete.js +51 -0
- package/src/tools/fsEdit.js +105 -0
- package/src/tools/fsFind.js +128 -0
- package/src/tools/fsList.js +77 -0
- package/src/tools/fsMove.js +57 -0
- package/src/tools/fsRead.js +87 -0
- package/src/tools/fsSearch.js +198 -0
- package/src/tools/fsWrite.js +41 -0
- package/src/tools/gameAiBehaviorTree.js +208 -0
- package/src/tools/gameAnimationRig.js +194 -0
- package/src/tools/gameAssetFetcher.js +372 -0
- package/src/tools/gameAssetHarvester.js +210 -0
- package/src/tools/gameAudioSynth.js +267 -0
- package/src/tools/gameBuilder.js +886 -0
- package/src/tools/gameCanvasAudit.js +101 -0
- package/src/tools/gameCinematicSequencer.js +151 -0
- package/src/tools/gameDataForge.js +124 -0
- package/src/tools/gameDialogueTreeGen.js +166 -0
- package/src/tools/gameEconomyBalancer.js +161 -0
- package/src/tools/gameEntitySpawner.js +207 -0
- package/src/tools/gameLoreForge.js +153 -0
- package/src/tools/gameQuestArcBuilder.js +152 -0
- package/src/tools/gameSceneBuilder.js +158 -0
- package/src/tools/gameShaderForge.js +181 -0
- package/src/tools/gameTilemapGenerator.js +164 -0
- package/src/tools/gameUiHudBuilder.js +292 -0
- package/src/tools/gameVfxEmitterGen.js +108 -0
- package/src/tools/gameWebRuntimeInspector.js +91 -0
- package/src/tools/generateMediaViaApi.js +358 -0
- package/src/tools/gitExec.js +54 -0
- package/src/tools/godotAutomator.js +437 -0
- package/src/tools/manageLocalAiDependencies.js +220 -0
- package/src/tools/memoryWrite.js +99 -0
- package/src/tools/multiEdit.js +107 -0
- package/src/tools/palmierAutomator.js +104 -0
- package/src/tools/pdfReader.js +112 -0
- package/src/tools/persistentShell.js +148 -0
- package/src/tools/printer.js +316 -0
- package/src/tools/procTerm.js +91 -0
- package/src/tools/processList.js +52 -0
- package/src/tools/readNativeSkill.js +39 -0
- package/src/tools/registry.js +277 -0
- package/src/tools/resolveAutomator.js +102 -0
- package/src/tools/screenshot.js +75 -0
- package/src/tools/subagentOrchestrator.js +305 -0
- package/src/tools/sysRun.js +125 -0
- package/src/tools/todoWrite.js +75 -0
- package/src/tools/toolDispatch.js +127 -0
- package/src/tools/toolForge.js +124 -0
- package/src/tools/updatePlan.js +88 -0
- package/src/tools/viewImage.js +82 -0
- package/src/tools/visionInspector.js +154 -0
- package/src/tools/webFetch.js +74 -0
- package/src/tools/webSearch.js +82 -0
- package/src/tui/ask.js +164 -0
- package/src/tui/banner.js +158 -0
- package/src/tui/block.js +152 -0
- package/src/tui/chatRenderer.js +953 -0
- package/src/tui/completion.js +424 -0
- package/src/tui/dialogs.js +606 -0
- package/src/tui/diff.js +296 -0
- package/src/tui/history.js +135 -0
- package/src/tui/home.js +217 -0
- package/src/tui/invocation.js +105 -0
- package/src/tui/keys.js +443 -0
- package/src/tui/lineEditor.js +256 -0
- package/src/tui/overlay.js +256 -0
- package/src/tui/prompt.js +1258 -0
- package/src/tui/selectList.js +335 -0
- package/src/tui/stage.js +330 -0
- package/src/tui/stageSurface.js +609 -0
- package/src/tui/statusLine.js +254 -0
- package/src/tui/text.js +345 -0
- package/src/tui/theme.js +393 -0
- package/src/tui/toolDisplay.js +176 -0
- package/src/tui/transcript.js +235 -0
- package/src/tui/viewport.js +337 -0
- package/src/uncertainty.js +271 -0
- package/src/userSkills.js +265 -0
- package/src/utils/cli_ui.js +0 -0
- package/src/utils/inquirer.js +12 -0
- package/src/utils/paths.js +37 -0
- package/src/version.js +38 -0
- package/src/vfeTracker.js +220 -0
package/src/sandbox.js
ADDED
|
@@ -0,0 +1,529 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import { loadConfig } from './config.js';
|
|
4
|
+
import { isDevEmulator } from './devMode.js';
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
function maskQuotedSpans(text) {
|
|
8
|
+
return text
|
|
9
|
+
.replace(/"(?:[^"\\]|\\.)*"/g, '""')
|
|
10
|
+
.replace(/'(?:[^'\\]|\\.)*'/g, "''")
|
|
11
|
+
.replace(/`(?:[^`\\]|\\.)*`/g, '``');
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Split a command line into the individual commands the shell will run.
|
|
16
|
+
*
|
|
17
|
+
* Prefix matching only ever inspected the FIRST word of the whole line, so any
|
|
18
|
+
* leading token disguised everything after it: `cd ~/Documents && rm -f x` was
|
|
19
|
+
* classified from `cd` and auto-executed with no approval. Each segment must be
|
|
20
|
+
* judged on its own.
|
|
21
|
+
*/
|
|
22
|
+
function splitShellSegments(text) {
|
|
23
|
+
return text
|
|
24
|
+
.split(/&&|\|\||[;\n|&]/)
|
|
25
|
+
.map((segment) => segment.trim())
|
|
26
|
+
.filter((segment) => segment.length > 0);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Read-only invocations of tools whose NAME is dangerous but whose subcommand
|
|
31
|
+
* is not. Keyed by the leading word; the pattern must match the whole segment.
|
|
32
|
+
*
|
|
33
|
+
* WHY THIS EXISTS. `brew` is in DANGEROUS_PREFIXES because `brew install` runs
|
|
34
|
+
* arbitrary install scripts. The prefix rule cannot tell that from
|
|
35
|
+
* `brew --version`, so asking Blender's version escalated a chain to
|
|
36
|
+
* DANGEROUS and put a red approval modal in front of the user. That is not a
|
|
37
|
+
* cosmetic problem: a gate that cries wolf on `--version` is a gate people
|
|
38
|
+
* learn to approve without reading, and then it is not a gate at all.
|
|
39
|
+
*
|
|
40
|
+
* THE RULE FOR ADDING TO THIS TABLE: the subcommand must be incapable of
|
|
41
|
+
* changing anything — no install, no remove, no start/stop, no write. When in
|
|
42
|
+
* doubt, leave it out; a false alarm is cheap and a missed one is not.
|
|
43
|
+
*
|
|
44
|
+
* `sudo` and `su` are deliberately absent and must stay absent: `sudo brew
|
|
45
|
+
* --version` is a privilege escalation whatever follows it.
|
|
46
|
+
*/
|
|
47
|
+
const READ_ONLY_INVOCATIONS = {
|
|
48
|
+
brew: /^brew\s+(--version|-v|list|info|search|config|outdated|deps|home|--prefix|--cellar)\b/,
|
|
49
|
+
apt: /^apt\s+(--version|list|show|search|policy)\b/,
|
|
50
|
+
'apt-get': /^apt-get\s+(--version|-v)\b/,
|
|
51
|
+
yum: /^yum\s+(--version|list|info|search)\b/,
|
|
52
|
+
dnf: /^dnf\s+(--version|list|info|search)\b/,
|
|
53
|
+
systemctl: /^systemctl\s+(status|show|cat|is-active|is-enabled|is-failed|list-units|list-unit-files|--version)\b/,
|
|
54
|
+
service: /^service\s+[\w.@-]+\s+status\b/,
|
|
55
|
+
crontab: /^crontab\s+-l\b/,
|
|
56
|
+
// `kill -l` lists signal names; `kill -0 PID` only probes whether a process
|
|
57
|
+
// exists. Neither delivers a terminating signal.
|
|
58
|
+
kill: /^kill\s+(-l|-0)\b/,
|
|
59
|
+
git: /^git\s+reset\s+(--\s*)?$/
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* True when a segment is a read-only use of an otherwise dangerous command.
|
|
64
|
+
*
|
|
65
|
+
* @param {string} segment One shell segment, already quote-masked.
|
|
66
|
+
* @returns {boolean}
|
|
67
|
+
*/
|
|
68
|
+
function isReadOnlyInvocation(segment) {
|
|
69
|
+
const text = String(segment || '').trim();
|
|
70
|
+
const leadingWord = text.split(/\s+/)[0];
|
|
71
|
+
const pattern = READ_ONLY_INVOCATIONS[leadingWord];
|
|
72
|
+
return Boolean(pattern && pattern.test(text));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Does this command line hand a string to something that will execute it?
|
|
77
|
+
*
|
|
78
|
+
* Quote masking is right for `echo "rm -rf /"` and wrong for
|
|
79
|
+
* `bash -c "rm -rf ~"` — in the second the quoted text IS the payload. When an
|
|
80
|
+
* interpreter is present we judge the raw line instead, so masking can never
|
|
81
|
+
* downgrade a real destructive command.
|
|
82
|
+
*/
|
|
83
|
+
// Includes the scripting interpreters (perl/ruby/awk/python/node/…) so that a
|
|
84
|
+
// `python3 -c "import os; os.system('rm -rf ~')"` or `perl -e '...'` is judged on
|
|
85
|
+
// the RAW line: the destructive payload lives in the quoted argument, and without
|
|
86
|
+
// these an attacker could hide any command inside an interpreter string and have
|
|
87
|
+
// it read as merely 'moderate' (auto-run in Mode C). Matching only forces the raw
|
|
88
|
+
// line to be scanned — a benign `python3 script.py` still classifies as before.
|
|
89
|
+
const INTERPRETER_INVOCATION = /(^|\s)(sh|bash|zsh|ksh|dash|fish|eval|xargs|ssh|sudo|env|nohup|command|perl|ruby|awk|gawk|mawk|tclsh|php|lua|luajit|python3|python|pwsh|powershell|node|deno|bun|rscript)(\s|$)/i;
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Files where a single appended line changes what the machine does next, or who
|
|
93
|
+
* can log into it. Written out plainly and unencoded — this is Vision section 10
|
|
94
|
+
* defence code and must stay auditable.
|
|
95
|
+
*/
|
|
96
|
+
/**
|
|
97
|
+
* Credential stores and key material.
|
|
98
|
+
*
|
|
99
|
+
* L4 (local-path audit): the DEFAULT workspace scope ends with the home
|
|
100
|
+
* directory itself as a fallback, which silently puts ~/.ssh, ~/.aws and
|
|
101
|
+
* ~/.causyne (the encrypted API key AND the .vault_key that decrypts it) inside
|
|
102
|
+
* the agent's reach with no prompt. Vision section 4 asks for freedom WITH a
|
|
103
|
+
* human in the loop rather than a wall, so these are not blocked — touching one
|
|
104
|
+
* is classified 'extreme', which always requires explicit approval in every
|
|
105
|
+
* autonomy mode, including Mode C.
|
|
106
|
+
*/
|
|
107
|
+
const CREDENTIAL_PATHS = [
|
|
108
|
+
/(^|\/)\.ssh(\/|$)/,
|
|
109
|
+
/(^|\/)\.aws(\/|$)/,
|
|
110
|
+
/(^|\/)\.gnupg(\/|$)/,
|
|
111
|
+
/(^|\/)\.docker\/config\.json$/,
|
|
112
|
+
/(^|\/)\.netrc$/,
|
|
113
|
+
/(^|\/)\.npmrc$/,
|
|
114
|
+
/(^|\/)\.kube(\/|$)/,
|
|
115
|
+
/(^|\/)Library\/Keychains(\/|$)/i,
|
|
116
|
+
// Causyne's own vault: config.json holds the encrypted provider key and
|
|
117
|
+
// .vault_key is the key that decrypts it.
|
|
118
|
+
/(^|\/)\.causyne(\/|$)/
|
|
119
|
+
];
|
|
120
|
+
|
|
121
|
+
export function isCredentialPath(targetPath) {
|
|
122
|
+
if (typeof targetPath !== 'string' || targetPath.length === 0) return false;
|
|
123
|
+
const normalized = targetPath.replace(/\\/g, '/');
|
|
124
|
+
return CREDENTIAL_PATHS.some(pattern => pattern.test(normalized));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const SENSITIVE_WRITE_TARGETS = [
|
|
128
|
+
// Shell startup files: an appended line runs on the user's next shell.
|
|
129
|
+
/(^|\/)\.(bashrc|bash_profile|bash_login|profile|zshrc|zprofile|zshenv|zlogin)$/,
|
|
130
|
+
/(^|\/)\.config\/fish\/config\.fish$/,
|
|
131
|
+
// SSH: authorized_keys grants login; config can silently reroute connections.
|
|
132
|
+
/(^|\/)\.ssh\//,
|
|
133
|
+
// Scheduled execution.
|
|
134
|
+
/(^|\/)(crontab|cron\.d)(\/|$)/,
|
|
135
|
+
/\/etc\//,
|
|
136
|
+
// Causyne's own state: config.json holds the encrypted API key and .vault_key
|
|
137
|
+
// is the key that decrypts it.
|
|
138
|
+
/(^|\/)\.causyne\//
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Extract the file targets a command line redirects into.
|
|
143
|
+
*
|
|
144
|
+
* Context-aware per rule ZM-09: `2>&1` and `>&2` are descriptor duplications,
|
|
145
|
+
* not files, and must not be reported as write targets — a guard that fires on
|
|
146
|
+
* those would be disabled within a day.
|
|
147
|
+
*/
|
|
148
|
+
function redirectionTargets(maskedCommand) {
|
|
149
|
+
const targets = [];
|
|
150
|
+
const pattern = /(?:^|[^0-9&>])>>?\s*([^\s;|&<>]+)/g;
|
|
151
|
+
let match;
|
|
152
|
+
while ((match = pattern.exec(maskedCommand)) !== null) {
|
|
153
|
+
const target = match[1];
|
|
154
|
+
if (!target || target.startsWith('&')) continue;
|
|
155
|
+
targets.push(target);
|
|
156
|
+
}
|
|
157
|
+
return targets;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function redirectsIntoSensitiveTarget(maskedCommand) {
|
|
161
|
+
for (const target of redirectionTargets(maskedCommand)) {
|
|
162
|
+
const normalizedTarget = target.replace(/^~/, '/home/user');
|
|
163
|
+
for (const pattern of SENSITIVE_WRITE_TARGETS) {
|
|
164
|
+
if (pattern.test(normalizedTarget)) return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export class Sandbox {
|
|
171
|
+
constructor() {
|
|
172
|
+
this.config = loadConfig();
|
|
173
|
+
|
|
174
|
+
// V5 Upgrade: Expanded default workspace paths
|
|
175
|
+
// Instead of only os.homedir() (which was too restrictive and caused path errors),
|
|
176
|
+
// we now default to common user directories while still blocking system dirs.
|
|
177
|
+
const home = os.homedir();
|
|
178
|
+
this.workspacePaths = this.config.workspacePaths || [
|
|
179
|
+
path.join(home, 'Desktop'),
|
|
180
|
+
path.join(home, 'Documents'),
|
|
181
|
+
path.join(home, 'Downloads'),
|
|
182
|
+
path.join(home, 'Projects'),
|
|
183
|
+
path.join(home, 'CausyneWorkspace'),
|
|
184
|
+
path.join(home, 'Causyne'),
|
|
185
|
+
home // Fallback: allow home dir itself (covers edge cases like ~/scripts, ~/.config)
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
// Developer emulator mode (hardened, secret-gated — see devMode.js) grants
|
|
189
|
+
// full local access for agent development on a Fast-tier emulator. It cannot
|
|
190
|
+
// be triggered by a bare env flag in the published package.
|
|
191
|
+
const devMode = isDevEmulator(this.config);
|
|
192
|
+
this.grantAllAccess = devMode ? true : (this.config.grantAllAccess || false);
|
|
193
|
+
this.allowedTools = devMode ? ['*'] : (this.config.allowedTools || ['*']);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
updateWorkspacePaths(newPaths) {
|
|
198
|
+
if (Array.isArray(newPaths) && newPaths.length > 0) {
|
|
199
|
+
// Validate: reject any system-critical paths
|
|
200
|
+
const BLOCKED_ROOTS = ['/etc', '/usr', '/bin', '/sbin', '/System', '/var', '/private', '/Library'];
|
|
201
|
+
const safePaths = newPaths.filter(p => {
|
|
202
|
+
const resolved = path.resolve(p);
|
|
203
|
+
return !BLOCKED_ROOTS.some(blocked => resolved.startsWith(blocked));
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
if (safePaths.length > 0) {
|
|
207
|
+
this.workspacePaths = safePaths;
|
|
208
|
+
return { success: true, paths: this.workspacePaths };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return { success: false, reason: 'Invalid or blocked paths provided' };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// V5: Add a single workspace path
|
|
215
|
+
addWorkspacePath(newPath) {
|
|
216
|
+
const resolved = path.resolve(newPath);
|
|
217
|
+
if (!this.workspacePaths.includes(resolved)) {
|
|
218
|
+
this.workspacePaths.push(resolved);
|
|
219
|
+
}
|
|
220
|
+
return this.workspacePaths;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
validate(toolName, args) {
|
|
224
|
+
// 1. Tool availability check
|
|
225
|
+
if (!this.allowedTools.includes('*') && !this.allowedTools.includes(toolName)) {
|
|
226
|
+
return { allowed: false, reason: `Tool ${toolName} not allowed on current tier`, risk: 'forbidden' };
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 2. Path scoping.
|
|
230
|
+
// Tools do not agree on what to call their path argument: MultiEdit uses
|
|
231
|
+
// file_path, the Blender/Godot automators use filepath and directory, the
|
|
232
|
+
// media tools use outputPath. Checking only `path` let all of those write
|
|
233
|
+
// outside the workspace unchallenged.
|
|
234
|
+
const PATH_ARGUMENTS = ['path', 'cwd', 'source', 'destination', 'file_path', 'filepath', 'directory', 'targetFile', 'outputFile', 'outputPath'];
|
|
235
|
+
const pathsToCheck = [];
|
|
236
|
+
for (const field of PATH_ARGUMENTS) {
|
|
237
|
+
if (args[field]) pathsToCheck.push(args[field]);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
for (const p of pathsToCheck) {
|
|
241
|
+
if (typeof p === 'string' && !this.isPathAllowed(p)) {
|
|
242
|
+
return { allowed: false, reason: `Path outside allowed workspace: ${p}`, risk: 'dangerous' };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// L4: a credential store is in reach by default (the home fallback), so it
|
|
247
|
+
// gets a human in the loop rather than a wall. 'extreme' is the one risk
|
|
248
|
+
// level Mode C cannot auto-approve.
|
|
249
|
+
for (const p of pathsToCheck) {
|
|
250
|
+
if (typeof p === 'string' && isCredentialPath(p)) {
|
|
251
|
+
return { allowed: true, risk: 'extreme', reason: `Touches credential material: ${p}` };
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 3. Command classification (for terminal tools).
|
|
256
|
+
// terminal_persistent_shell runs arbitrary commands exactly like sysRun does
|
|
257
|
+
// and must go through the same classifier — it previously bypassed it.
|
|
258
|
+
if (toolName === 'sysRun' || toolName === 'sysRunBg' || toolName === 'terminal_persistent_shell') {
|
|
259
|
+
const command = args.command || args.input;
|
|
260
|
+
if (command) {
|
|
261
|
+
const risk = this.classifyCommandRisk(command);
|
|
262
|
+
if (risk === 'forbidden') {
|
|
263
|
+
return { allowed: false, reason: 'Command is permanently blocked for safety', risk: 'forbidden' };
|
|
264
|
+
}
|
|
265
|
+
return { allowed: true, risk };
|
|
266
|
+
}
|
|
267
|
+
return { allowed: true, risk: 'moderate' };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// 4. SSRF protection (for web fetch)
|
|
271
|
+
if (toolName === 'web_fetch' && args.url) {
|
|
272
|
+
if (this.isInternalUrl(args.url)) {
|
|
273
|
+
return { allowed: false, reason: 'Cannot access internal/local network URLs (SSRF Protection)', risk: 'forbidden' };
|
|
274
|
+
}
|
|
275
|
+
if (args.outputFile) {
|
|
276
|
+
return { allowed: true, risk: 'extreme', reason: 'Downloads network content to disk' };
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (toolName === 'game_asset_fetch' && args.action === 'download_asset') {
|
|
281
|
+
if (!args.url || this.isInternalUrl(args.url)) {
|
|
282
|
+
return { allowed: false, reason: 'Cannot download from an internal or invalid URL', risk: 'forbidden' };
|
|
283
|
+
}
|
|
284
|
+
return { allowed: true, risk: 'extreme', reason: 'Downloads a third-party asset to disk' };
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Heavy AI downloads ALWAYS require consent (even in Mode C)
|
|
288
|
+
if (toolName === 'manage_local_dependencies') {
|
|
289
|
+
// grant_capability installs third-party packages into the user's home
|
|
290
|
+
// directory, so it belongs in exactly the same consent class as the other
|
|
291
|
+
// download actions. Routing it here is what keeps ZM-01 true: nothing is
|
|
292
|
+
// ever installed without an explicit human approval.
|
|
293
|
+
const heavyActions = ['pipInst', 'grant_capability', 'grant_mcp_server'];
|
|
294
|
+
if (heavyActions.includes(args.action)) {
|
|
295
|
+
return { allowed: true, risk: 'extreme' }; // Upgraded from dangerous to extreme for safe C Mode handling
|
|
296
|
+
}
|
|
297
|
+
return { allowed: true, risk: 'moderate' };
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Tools that overwrite or delete are inherently dangerous.
|
|
301
|
+
// These comparisons used the dotted schema names (`fs.delete`, `proc.term`,
|
|
302
|
+
// `fs.write`) while validate() is always called with the canonical wire name
|
|
303
|
+
// (`fs_delete`, ...), so none of them ever matched: the Human-in-the-Loop
|
|
304
|
+
// approval gate for destructive operations had never once fired.
|
|
305
|
+
if (toolName === 'fs_delete') {
|
|
306
|
+
// If wiping an entire directory tree, flag as extreme
|
|
307
|
+
if (args.path && !args.path.includes('.')) {
|
|
308
|
+
return { allowed: true, risk: 'extreme' };
|
|
309
|
+
}
|
|
310
|
+
return { allowed: true, risk: 'dangerous' };
|
|
311
|
+
}
|
|
312
|
+
if (toolName === 'proc_term') {
|
|
313
|
+
return { allowed: true, risk: 'dangerous' };
|
|
314
|
+
}
|
|
315
|
+
// Driving the mouse and keyboard reaches every application the user is
|
|
316
|
+
// signed into — mail, banking, anything with a session open. There is no
|
|
317
|
+
// safe subset and no autonomy mode in which it should run unannounced, so
|
|
318
|
+
|
|
319
|
+
// Mode C. Section 4 says gated, not walled — the capability is real, the
|
|
320
|
+
// human just says yes first.
|
|
321
|
+
if (toolName === 'desktop_act') {
|
|
322
|
+
return { allowed: true, risk: 'extreme' };
|
|
323
|
+
}
|
|
324
|
+
if (toolName === 'fs_write' || toolName === 'fs_move' || toolName === 'fs_edit' || toolName === 'MultiEdit') {
|
|
325
|
+
return { allowed: true, risk: 'moderate' };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return { allowed: true, risk: 'safe' };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
isPathAllowed(targetPath) {
|
|
332
|
+
// Expand ~
|
|
333
|
+
let resolved = targetPath;
|
|
334
|
+
if (resolved.startsWith('~/')) {
|
|
335
|
+
resolved = path.join(os.homedir(), resolved.slice(2));
|
|
336
|
+
}
|
|
337
|
+
resolved = path.resolve(resolved);
|
|
338
|
+
|
|
339
|
+
// V5: Block system-critical paths BEFORE checking allowed paths (OS Protection)
|
|
340
|
+
const isWindows = os.platform() === 'win32';
|
|
341
|
+
let isBlocked = false;
|
|
342
|
+
|
|
343
|
+
if (isWindows) {
|
|
344
|
+
const WIN_BLOCKED = ['C:\\Windows', 'C:\\Program Files', 'C:\\ProgramData'];
|
|
345
|
+
isBlocked = WIN_BLOCKED.some(blocked => resolved.toLowerCase().startsWith(blocked.toLowerCase()));
|
|
346
|
+
} else {
|
|
347
|
+
const UNIX_BLOCKED = ['/etc', '/usr/bin', '/usr/sbin', '/sbin', '/bin', '/System', '/private/var'];
|
|
348
|
+
isBlocked = UNIX_BLOCKED.some(blocked => resolved.startsWith(blocked));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (isBlocked) {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// V7 Ultimate Self-Preservation: Block access to the agent's own source code
|
|
356
|
+
// The AGI cannot read, modify, or delete causyne-agent itself.
|
|
357
|
+
const agentBinOrScript = process.argv[1] || '';
|
|
358
|
+
let agentRoot = '';
|
|
359
|
+
if (agentBinOrScript && agentBinOrScript.includes('causyne-agent')) {
|
|
360
|
+
const idx = agentBinOrScript.lastIndexOf('causyne-agent');
|
|
361
|
+
agentRoot = path.resolve(agentBinOrScript.slice(0, idx + 'causyne-agent'.length));
|
|
362
|
+
} else if (typeof __dirname !== 'undefined') {
|
|
363
|
+
agentRoot = path.resolve(__dirname, '..');
|
|
364
|
+
}
|
|
365
|
+
if (agentRoot && resolved.startsWith(agentRoot) && resolved !== path.resolve(os.homedir())) {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// V6: Override workspace limits if grantAllAccess is active
|
|
370
|
+
if (this.grantAllAccess) {
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// Check if resolved path is inside any allowed workspace path.
|
|
375
|
+
//
|
|
376
|
+
// L2 (local-path audit): this was a bare `resolved.startsWith(allowedBase)`,
|
|
377
|
+
// which is a STRING prefix test, not a PATH containment test. With the scope
|
|
378
|
+
// set to /Users/x/Projects it also accepted /Users/x/Projects-secret and
|
|
379
|
+
// /Users/x/ProjectsBACKUP — any sibling whose name merely begins with the
|
|
380
|
+
// workspace name. The separator check is what makes it a directory boundary.
|
|
381
|
+
return this.workspacePaths.some(workspace => {
|
|
382
|
+
let allowedBase = workspace;
|
|
383
|
+
if (allowedBase.startsWith('~/')) {
|
|
384
|
+
allowedBase = path.join(os.homedir(), allowedBase.slice(2));
|
|
385
|
+
}
|
|
386
|
+
allowedBase = path.resolve(allowedBase);
|
|
387
|
+
return resolved === allowedBase || resolved.startsWith(allowedBase + path.sep);
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
isInternalUrl(urlString) {
|
|
392
|
+
try {
|
|
393
|
+
const url = new URL(urlString);
|
|
394
|
+
const host = url.hostname;
|
|
395
|
+
|
|
396
|
+
// Block localhost, private IPs, cloud metadata
|
|
397
|
+
const blockedRegex = /^(localhost|127\.\d+\.\d+\.\d+|10\.\d+\.\d+\.\d+|172\.(1[6-9]|2\d|3[01])\.\d+\.\d+|192\.168\.\d+\.\d+|169\.254\.\d+\.\d+|0\.0\.0\.0)$/;
|
|
398
|
+
if (blockedRegex.test(host)) return true;
|
|
399
|
+
|
|
400
|
+
return false;
|
|
401
|
+
} catch(e) {
|
|
402
|
+
return true; // If URL is unparseable, block it to be safe
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
classifyCommandRisk(command) {
|
|
407
|
+
if (!command) return 'safe';
|
|
408
|
+
const normalized = command.trim().toLowerCase();
|
|
409
|
+
|
|
410
|
+
// FORBIDDEN — absolute blocklist (Technical + Ethical Safety Net Layer 3)
|
|
411
|
+
const FORBIDDEN_PATTERNS = [
|
|
412
|
+
/:\(\)\s*\{\s*:\|:&\s*\};/,
|
|
413
|
+
/curl\s+.*\|\s*(sh|bash)/,
|
|
414
|
+
/wget\s+.*\|\s*(sh|bash)/,
|
|
415
|
+
// Process substitution — `bash <(curl …)` / `sh <(wget …)` fetch-and-run,
|
|
416
|
+
// the pipe-to-shell trick's twin that the `| sh` patterns above miss.
|
|
417
|
+
/<\(\s*(curl|wget)\b/,
|
|
418
|
+
/>\s*\/dev\/sda/,
|
|
419
|
+
// Ethical Safety Gate: Offensive security attack tools & ransomware patterns
|
|
420
|
+
/nmap\s+-s[STUVFN]/i,
|
|
421
|
+
/sqlmap/i,
|
|
422
|
+
/metasploit|msfconsole|msfvenom/i,
|
|
423
|
+
/hydra\s+/i,
|
|
424
|
+
/john\s+/i,
|
|
425
|
+
/hashcat/i,
|
|
426
|
+
/aircrack|airmon|aireplay/i,
|
|
427
|
+
/ettercap|arpspoof|bettercap/i,
|
|
428
|
+
/nikto/i,
|
|
429
|
+
/xmrig|cpuminer|minerd/i,
|
|
430
|
+
/ransomware|encrypt.*ransom/i
|
|
431
|
+
];
|
|
432
|
+
|
|
433
|
+
// FORBIDDEN is matched against the WHOLE line, never against segments:
|
|
434
|
+
// several of these patterns deliberately span shell separators (the fork
|
|
435
|
+
// bomb contains ; and |, the pipe-to-shell patterns contain |), so
|
|
436
|
+
// splitting first would stop them matching at all.
|
|
437
|
+
for (const pattern of FORBIDDEN_PATTERNS) {
|
|
438
|
+
if (pattern.test(normalized)) return 'forbidden';
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Everything below judges what the shell will actually execute.
|
|
442
|
+
const masked = maskQuotedSpans(normalized);
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
// L3 (local-path audit): classify the REDIRECTION TARGET, not just the verb.
|
|
446
|
+
//
|
|
447
|
+
// sandbox.validate() path-scopes tool ARGUMENTS, but a shell redirection
|
|
448
|
+
// target is invisible to it, and the classifier only ever looked at the
|
|
449
|
+
// command word. So `echo evil >> ~/.bashrc` and
|
|
450
|
+
// `cat payload >> ~/.ssh/authorized_keys` both classified as SAFE and
|
|
451
|
+
// auto-executed in every autonomy mode — persistence and remote access with
|
|
452
|
+
// no approval prompt. Writing to these files is escalated regardless of
|
|
453
|
+
// which harmless-looking command does the writing.
|
|
454
|
+
if (redirectsIntoSensitiveTarget(masked)) return 'extreme';
|
|
455
|
+
const executesQuotedText = INTERPRETER_INVOCATION.test(masked);
|
|
456
|
+
const destructiveScanTarget = executesQuotedText ? normalized : masked;
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
const EXTREME_PATTERNS = [
|
|
460
|
+
/rm\s+(-rf|-fr)\s+[\/~]/,
|
|
461
|
+
/rm\s+(-rf|-fr)\s+\*/,
|
|
462
|
+
/mkfs/,
|
|
463
|
+
/dd\s+if=/,
|
|
464
|
+
/shutdown|reboot|init\s+[06]/,
|
|
465
|
+
/passwd|useradd|userdel/,
|
|
466
|
+
/iptables|ufw\s+delete/,
|
|
467
|
+
/format\s+[a-z]:/i,
|
|
468
|
+
/diskpart/i
|
|
469
|
+
];
|
|
470
|
+
|
|
471
|
+
for (const pattern of EXTREME_PATTERNS) {
|
|
472
|
+
if (pattern.test(destructiveScanTarget)) return 'extreme';
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// DANGEROUS — requires approval in Mode B, auto-executes in Mode C
|
|
476
|
+
const DANGEROUS_PREFIXES = [
|
|
477
|
+
'rm ', 'sudo ', 'su ', 'chmod ', 'chown ',
|
|
478
|
+
'apt ', 'apt-get ', 'brew ', 'yum ', 'dnf ',
|
|
479
|
+
'git reset', 'git push --force', 'git push -f', 'git rebase',
|
|
480
|
+
'kill ', 'pkill ', 'killall ',
|
|
481
|
+
'ssh ', 'scp ', 'rsync ',
|
|
482
|
+
'mv ', 'crontab ', 'systemctl ', 'service '
|
|
483
|
+
];
|
|
484
|
+
|
|
485
|
+
// Prefix rules are per-segment: `cd x && rm -f y` must be judged on `rm`,
|
|
486
|
+
// not on `cd`. Highest risk across all segments wins.
|
|
487
|
+
const segments = splitShellSegments(masked);
|
|
488
|
+
|
|
489
|
+
for (const segment of segments) {
|
|
490
|
+
for (const prefix of DANGEROUS_PREFIXES) {
|
|
491
|
+
if (segment.startsWith(prefix) && !isReadOnlyInvocation(segment)) return 'dangerous';
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// DANGEROUS — pattern form, for commands whose risk is in a flag rather than
|
|
496
|
+
// the leading word. `find … -exec/-delete` runs an arbitrary command against
|
|
497
|
+
// (or removes) every match, so it is at least as risky as a bare `rm`.
|
|
498
|
+
const DANGEROUS_PATTERNS = [
|
|
499
|
+
/\bfind\b[\s\S]*\s-(exec|execdir|delete)\b/,
|
|
500
|
+
];
|
|
501
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
502
|
+
if (pattern.test(destructiveScanTarget)) return 'dangerous';
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// MODERATE — auto-execute + notify in Mode B
|
|
506
|
+
const MODERATE_PREFIXES = [
|
|
507
|
+
'npm i', 'npm run', 'npx ', 'yarn ', 'pnpm ',
|
|
508
|
+
'pip install', 'pip uninstall', 'pip3 ',
|
|
509
|
+
'python', 'python3', 'node ', 'deno ', 'bun ',
|
|
510
|
+
'perl ', 'ruby ', 'awk ', 'gawk ', 'php ', 'lua ', 'tclsh ', 'rscript ',
|
|
511
|
+
'pwsh ', 'powershell ',
|
|
512
|
+
'git add', 'git commit', 'git push', 'git pull', 'git checkout',
|
|
513
|
+
'git merge', 'git branch', 'git tag',
|
|
514
|
+
'mkdir', 'touch', 'cp ',
|
|
515
|
+
'cargo ', 'go ', 'make', 'cmake', 'gcc', 'g++',
|
|
516
|
+
'docker ', 'docker-compose ', 'podman ',
|
|
517
|
+
'code ', 'open ', 'xdg-open ',
|
|
518
|
+
'curl ', 'wget '
|
|
519
|
+
];
|
|
520
|
+
|
|
521
|
+
for (const segment of segments) {
|
|
522
|
+
for (const prefix of MODERATE_PREFIXES) {
|
|
523
|
+
if (segment.startsWith(prefix)) return 'moderate';
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
return 'safe';
|
|
528
|
+
}
|
|
529
|
+
}
|