aiden-runtime 4.1.5 → 4.5.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.
Files changed (163) hide show
  1. package/README.md +250 -847
  2. package/dist/api/server.js +32 -5
  3. package/dist/cli/v4/aidenCLI.js +351 -53
  4. package/dist/cli/v4/callbacks.js +170 -0
  5. package/dist/cli/v4/chatSession.js +138 -3
  6. package/dist/cli/v4/commands/_runtimeToggleHelpers.js +92 -0
  7. package/dist/cli/v4/commands/browserDepth.js +45 -0
  8. package/dist/cli/v4/commands/cron.js +264 -0
  9. package/dist/cli/v4/commands/daemon.js +541 -0
  10. package/dist/cli/v4/commands/daemonStatus.js +253 -0
  11. package/dist/cli/v4/commands/help.js +7 -0
  12. package/dist/cli/v4/commands/index.js +20 -1
  13. package/dist/cli/v4/commands/runs.js +203 -0
  14. package/dist/cli/v4/commands/sandbox.js +48 -0
  15. package/dist/cli/v4/commands/suggestions.js +68 -0
  16. package/dist/cli/v4/commands/tce.js +41 -0
  17. package/dist/cli/v4/commands/trigger.js +378 -0
  18. package/dist/cli/v4/commands/update.js +95 -3
  19. package/dist/cli/v4/daemonAgentBuilder.js +142 -0
  20. package/dist/cli/v4/defaultSoul.js +1 -1
  21. package/dist/cli/v4/display/capabilityCard.js +26 -0
  22. package/dist/cli/v4/display.js +18 -8
  23. package/dist/cli/v4/replyRenderer.js +31 -23
  24. package/dist/cli/v4/updateBootPrompt.js +170 -0
  25. package/dist/core/playwrightBridge.js +129 -0
  26. package/dist/core/v4/aidenAgent.js +308 -4
  27. package/dist/core/v4/browserState.js +436 -0
  28. package/dist/core/v4/checkpoint.js +79 -0
  29. package/dist/core/v4/daemon/bootstrap.js +604 -0
  30. package/dist/core/v4/daemon/cleanShutdown.js +154 -0
  31. package/dist/core/v4/daemon/cron/cronBridge.js +126 -0
  32. package/dist/core/v4/daemon/cron/cronEmitter.js +173 -0
  33. package/dist/core/v4/daemon/cron/migration.js +199 -0
  34. package/dist/core/v4/daemon/cron/misfirePolicy.js +115 -0
  35. package/dist/core/v4/daemon/daemonConfig.js +90 -0
  36. package/dist/core/v4/daemon/db/connection.js +106 -0
  37. package/dist/core/v4/daemon/db/migrations.js +296 -0
  38. package/dist/core/v4/daemon/db/schema/v1.spec.js +18 -0
  39. package/dist/core/v4/daemon/dispatcher/agentRunner.js +98 -0
  40. package/dist/core/v4/daemon/dispatcher/budgetGate.js +127 -0
  41. package/dist/core/v4/daemon/dispatcher/daemonApproval.js +113 -0
  42. package/dist/core/v4/daemon/dispatcher/dailyBudgetTracker.js +120 -0
  43. package/dist/core/v4/daemon/dispatcher/dispatcher.js +389 -0
  44. package/dist/core/v4/daemon/dispatcher/fireRateLimiter.js +113 -0
  45. package/dist/core/v4/daemon/dispatcher/index.js +53 -0
  46. package/dist/core/v4/daemon/dispatcher/promptTemplate.js +95 -0
  47. package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +356 -0
  48. package/dist/core/v4/daemon/dispatcher/resolveModel.js +93 -0
  49. package/dist/core/v4/daemon/dispatcher/sessionId.js +93 -0
  50. package/dist/core/v4/daemon/drain.js +156 -0
  51. package/dist/core/v4/daemon/eventLoopLag.js +73 -0
  52. package/dist/core/v4/daemon/health.js +159 -0
  53. package/dist/core/v4/daemon/idempotencyStore.js +204 -0
  54. package/dist/core/v4/daemon/index.js +179 -0
  55. package/dist/core/v4/daemon/instanceTracker.js +99 -0
  56. package/dist/core/v4/daemon/resourceRegistry.js +150 -0
  57. package/dist/core/v4/daemon/restartCode.js +32 -0
  58. package/dist/core/v4/daemon/restartFailureCounter.js +77 -0
  59. package/dist/core/v4/daemon/runStore.js +114 -0
  60. package/dist/core/v4/daemon/runtimeLock.js +167 -0
  61. package/dist/core/v4/daemon/signals.js +50 -0
  62. package/dist/core/v4/daemon/supervisor.js +272 -0
  63. package/dist/core/v4/daemon/triggerBus.js +279 -0
  64. package/dist/core/v4/daemon/triggers/email/allowlist.js +70 -0
  65. package/dist/core/v4/daemon/triggers/email/automatedSender.js +78 -0
  66. package/dist/core/v4/daemon/triggers/email/bodyExtractor.js +0 -0
  67. package/dist/core/v4/daemon/triggers/email/emailSeenStore.js +99 -0
  68. package/dist/core/v4/daemon/triggers/email/emailSpec.js +107 -0
  69. package/dist/core/v4/daemon/triggers/email/imapConnection.js +211 -0
  70. package/dist/core/v4/daemon/triggers/email/index.js +332 -0
  71. package/dist/core/v4/daemon/triggers/email/seenUids.js +60 -0
  72. package/dist/core/v4/daemon/triggers/fileObservationsStore.js +93 -0
  73. package/dist/core/v4/daemon/triggers/fileWatcher.js +253 -0
  74. package/dist/core/v4/daemon/triggers/fileWatcherSpec.js +88 -0
  75. package/dist/core/v4/daemon/triggers/fsIdentity.js +42 -0
  76. package/dist/core/v4/daemon/triggers/globMatcher.js +100 -0
  77. package/dist/core/v4/daemon/triggers/reconcile.js +206 -0
  78. package/dist/core/v4/daemon/triggers/settleStat.js +81 -0
  79. package/dist/core/v4/daemon/triggers/webhook.js +376 -0
  80. package/dist/core/v4/daemon/triggers/webhookDeliveriesStore.js +109 -0
  81. package/dist/core/v4/daemon/triggers/webhookIdempotency.js +72 -0
  82. package/dist/core/v4/daemon/triggers/webhookRateLimit.js +56 -0
  83. package/dist/core/v4/daemon/triggers/webhookSpec.js +76 -0
  84. package/dist/core/v4/daemon/triggers/webhookVerifier.js +128 -0
  85. package/dist/core/v4/daemon/types.js +15 -0
  86. package/dist/core/v4/dockerSession.js +461 -0
  87. package/dist/core/v4/dryRun.js +117 -0
  88. package/dist/core/v4/failureClassifier.js +779 -0
  89. package/dist/core/v4/recoveryReport.js +449 -0
  90. package/dist/core/v4/runtimeToggles.js +187 -0
  91. package/dist/core/v4/sandboxConfig.js +285 -0
  92. package/dist/core/v4/sandboxFs.js +316 -0
  93. package/dist/core/v4/suggestionCatalog.js +41 -0
  94. package/dist/core/v4/suggestionEngine.js +210 -0
  95. package/dist/core/v4/toolRegistry.js +18 -0
  96. package/dist/core/v4/turnState.js +587 -0
  97. package/dist/core/v4/update/checkUpdate.js +63 -3
  98. package/dist/core/v4/update/installMethodDetect.js +115 -0
  99. package/dist/core/v4/update/registryClient.js +121 -0
  100. package/dist/core/v4/update/skipState.js +75 -0
  101. package/dist/core/v4/verifier.js +448 -0
  102. package/dist/core/version.js +1 -1
  103. package/dist/tools/v4/browser/_observer.js +224 -0
  104. package/dist/tools/v4/browser/browserBlocker.js +396 -0
  105. package/dist/tools/v4/browser/browserClick.js +18 -1
  106. package/dist/tools/v4/browser/browserClose.js +18 -1
  107. package/dist/tools/v4/browser/browserExtract.js +5 -1
  108. package/dist/tools/v4/browser/browserFill.js +17 -1
  109. package/dist/tools/v4/browser/browserGetUrl.js +5 -1
  110. package/dist/tools/v4/browser/browserNavigate.js +16 -1
  111. package/dist/tools/v4/browser/browserScreenshot.js +5 -1
  112. package/dist/tools/v4/browser/browserScroll.js +18 -1
  113. package/dist/tools/v4/browser/browserType.js +17 -1
  114. package/dist/tools/v4/browser/captchaCheck.js +5 -1
  115. package/dist/tools/v4/executeCode.js +1 -0
  116. package/dist/tools/v4/files/fileCopy.js +56 -2
  117. package/dist/tools/v4/files/fileDelete.js +38 -1
  118. package/dist/tools/v4/files/fileList.js +12 -1
  119. package/dist/tools/v4/files/fileMove.js +59 -2
  120. package/dist/tools/v4/files/filePatch.js +43 -1
  121. package/dist/tools/v4/files/fileRead.js +12 -1
  122. package/dist/tools/v4/files/fileWrite.js +41 -1
  123. package/dist/tools/v4/index.js +71 -58
  124. package/dist/tools/v4/memory/memoryAdd.js +14 -0
  125. package/dist/tools/v4/memory/memoryRemove.js +14 -0
  126. package/dist/tools/v4/memory/memoryReplace.js +15 -0
  127. package/dist/tools/v4/memory/sessionSummary.js +12 -0
  128. package/dist/tools/v4/process/processKill.js +19 -0
  129. package/dist/tools/v4/process/processList.js +1 -0
  130. package/dist/tools/v4/process/processLogRead.js +1 -0
  131. package/dist/tools/v4/process/processSpawn.js +13 -0
  132. package/dist/tools/v4/process/processWait.js +1 -0
  133. package/dist/tools/v4/sessions/recallSession.js +1 -0
  134. package/dist/tools/v4/sessions/sessionList.js +1 -0
  135. package/dist/tools/v4/sessions/sessionSearch.js +1 -0
  136. package/dist/tools/v4/skills/lookupToolSchema.js +2 -0
  137. package/dist/tools/v4/skills/skillManage.js +13 -0
  138. package/dist/tools/v4/skills/skillView.js +1 -0
  139. package/dist/tools/v4/skills/skillsList.js +1 -0
  140. package/dist/tools/v4/subagent/subagentFanout.js +1 -0
  141. package/dist/tools/v4/system/aidenSelfUpdate.js +16 -0
  142. package/dist/tools/v4/system/appClose.js +13 -0
  143. package/dist/tools/v4/system/appInput.js +13 -0
  144. package/dist/tools/v4/system/appLaunch.js +13 -0
  145. package/dist/tools/v4/system/clipboardRead.js +1 -0
  146. package/dist/tools/v4/system/clipboardWrite.js +14 -0
  147. package/dist/tools/v4/system/mediaKey.js +12 -0
  148. package/dist/tools/v4/system/mediaSessions.js +1 -0
  149. package/dist/tools/v4/system/mediaTransport.js +13 -0
  150. package/dist/tools/v4/system/naturalEvents.js +1 -0
  151. package/dist/tools/v4/system/nowPlaying.js +1 -0
  152. package/dist/tools/v4/system/osProcessList.js +1 -0
  153. package/dist/tools/v4/system/screenshot.js +1 -0
  154. package/dist/tools/v4/system/systemInfo.js +1 -0
  155. package/dist/tools/v4/system/volumeSet.js +17 -0
  156. package/dist/tools/v4/terminal/shellExec.js +81 -9
  157. package/dist/tools/v4/web/deepResearch.js +1 -0
  158. package/dist/tools/v4/web/openUrl.js +1 -0
  159. package/dist/tools/v4/web/webFetch.js +1 -0
  160. package/dist/tools/v4/web/webPage.js +1 -0
  161. package/dist/tools/v4/web/webSearch.js +1 -0
  162. package/dist/tools/v4/web/youtubeSearch.js +1 -0
  163. package/package.json +7 -1
@@ -0,0 +1,285 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveRealPath = resolveRealPath;
7
+ exports._clearRealPathCacheForTests = _clearRealPathCacheForTests;
8
+ exports.inferDefaultRiskTier = inferDefaultRiskTier;
9
+ exports.readSandboxConfig = readSandboxConfig;
10
+ exports.getSandboxConfig = getSandboxConfig;
11
+ exports._resetSandboxConfigForTests = _resetSandboxConfigForTests;
12
+ /**
13
+ * Copyright (c) 2026 Shiva Deore (Taracod).
14
+ * Licensed under AGPL-3.0. See LICENSE for details.
15
+ *
16
+ * Aiden — local-first agent.
17
+ */
18
+ const runtimeToggles_1 = require("./runtimeToggles");
19
+ /**
20
+ * core/v4/sandboxConfig.ts — v4.4 Phase 1: Execution sandbox configuration.
21
+ *
22
+ * Single source of truth for sandbox enablement + resource policies +
23
+ * filesystem allow/deny lists + Docker hardening flags. Read from
24
+ * environment variables at construction time (matches v4.2's TurnState
25
+ * + v4.3's BrowserState env-driven pattern).
26
+ *
27
+ * Phase 1 ships the config types + reader + default-tier inference;
28
+ * downstream phases consume:
29
+ * - Phase 2 — fsAllowList / fsDenyList used by file_* tools
30
+ * - Phase 3 — defaultBackend / resourceLimits / networkMode /
31
+ * persistent / idleReaperMs used by shell_exec + Docker
32
+ * backend (long-lived container reuse, hardening flags)
33
+ * - Phase 4 — dryRun used by all `mutates: true` tools
34
+ * - Phase 5 — riskTier annotations consumed by FailureClassifier +
35
+ * ApprovalEngine (as a FLOOR; patterns can escalate)
36
+ *
37
+ * v4.4 Phase 6 — default-on transition. AIDEN_SANDBOX is now
38
+ * enabled by default; users opt out with `AIDEN_SANDBOX=0`. The
39
+ * strict `!== '0'` flip mirrors v4.2 Phase 6 (TCE) + v4.3 Phase 6
40
+ * (browser depth) semantics exactly.
41
+ *
42
+ * AIDEN_DRYRUN is orthogonal — independent flag, independent semantics.
43
+ * Phase 6 does NOT flip dry-run; it stays opt-in by design (dry-run
44
+ * is a deliberate "preview-only" mode users opt into, not a default).
45
+ *
46
+ * Pure module — no I/O, no side effects, no Playwright/Docker
47
+ * dependencies. Just env-var reads + path normalization helpers.
48
+ * Easy to unit test by passing a stubbed `env` argument.
49
+ */
50
+ const node_os_1 = __importDefault(require("node:os"));
51
+ const node_path_1 = __importDefault(require("node:path"));
52
+ const node_fs_1 = __importDefault(require("node:fs"));
53
+ // ── Defaults ────────────────────────────────────────────────────────────────
54
+ const DEFAULT_RESOURCE_LIMITS = {
55
+ memory: '1g',
56
+ cpus: '2',
57
+ pidsLimit: 256,
58
+ };
59
+ const DEFAULT_IDLE_REAPER_MS = 5 * 60 * 1000; // 5 minutes
60
+ const DEFAULT_IMAGE = 'node:22-alpine';
61
+ /**
62
+ * v4.4 Phase 2 — default write-permitted paths. Real-resolved at
63
+ * config-build time to handle symlinked HOME / cwd. Caller can
64
+ * extend via `AIDEN_SANDBOX_ALLOW=p1:p2:...`.
65
+ */
66
+ function buildDefaultAllowList() {
67
+ const home = node_os_1.default.homedir();
68
+ const tmp = node_os_1.default.tmpdir();
69
+ const cwd = process.cwd();
70
+ const paths = [
71
+ cwd,
72
+ node_path_1.default.join(home, 'Documents'),
73
+ node_path_1.default.join(home, 'Downloads'),
74
+ node_path_1.default.join(home, 'Desktop'),
75
+ tmp,
76
+ ];
77
+ return resolveRealPaths(paths);
78
+ }
79
+ /**
80
+ * v4.4 Phase 2 — default write-denied paths. Always wins over the
81
+ * allow list. Mirrors the consult-shaped deny-list pattern (sensitive
82
+ * configs, system dirs).
83
+ */
84
+ function buildDefaultDenyList() {
85
+ const home = node_os_1.default.homedir();
86
+ const paths = [
87
+ node_path_1.default.join(home, '.ssh'),
88
+ node_path_1.default.join(home, '.aws'),
89
+ node_path_1.default.join(home, '.gnupg'),
90
+ node_path_1.default.join(home, '.env'),
91
+ node_path_1.default.join(home, '.netrc'),
92
+ node_path_1.default.join(home, '.pgpass'),
93
+ node_path_1.default.join(home, '.npmrc'),
94
+ node_path_1.default.join(home, '.pypirc'),
95
+ node_path_1.default.join(home, '.bashrc'),
96
+ node_path_1.default.join(home, '.zshrc'),
97
+ node_path_1.default.join(home, '.profile'),
98
+ '/etc',
99
+ '/var',
100
+ '/usr',
101
+ '/boot',
102
+ '/sys',
103
+ '/proc',
104
+ ];
105
+ return resolveRealPaths(paths);
106
+ }
107
+ // ── Path normalization ──────────────────────────────────────────────────────
108
+ const _realPathCache = new Map();
109
+ /**
110
+ * Resolve a path to its canonical absolute form. `path.resolve` first
111
+ * (handles relative + `..`); then `fs.realpathSync` to follow symlinks.
112
+ * Symlink resolution defeats the bypass attack where an allowlisted
113
+ * directory contains a symlink to a denied path.
114
+ *
115
+ * Results cached for the lifetime of the module (paths rarely change
116
+ * during a process; the cache hit rate is high on the file-tool path
117
+ * where every call resolves the same handful of allowlist entries).
118
+ *
119
+ * Falls back gracefully when the path doesn't exist (returns the
120
+ * resolved-but-unrealpath form) — caller may be checking a path
121
+ * about to be created.
122
+ */
123
+ function resolveRealPath(input) {
124
+ if (_realPathCache.has(input))
125
+ return _realPathCache.get(input);
126
+ const resolved = node_path_1.default.resolve(input);
127
+ let real = resolved;
128
+ try {
129
+ real = node_fs_1.default.realpathSync.native ? node_fs_1.default.realpathSync.native(resolved) : node_fs_1.default.realpathSync(resolved);
130
+ }
131
+ catch {
132
+ // Path may not exist yet (e.g. file_write target). Use the
133
+ // resolved form; symlink-bypass on a non-existent path isn't
134
+ // a real attack vector.
135
+ }
136
+ _realPathCache.set(input, real);
137
+ return real;
138
+ }
139
+ /** Resolve an array of paths to their canonical forms; deduplicate. */
140
+ function resolveRealPaths(paths) {
141
+ const seen = new Set();
142
+ const out = [];
143
+ for (const p of paths) {
144
+ const real = resolveRealPath(p);
145
+ if (!seen.has(real)) {
146
+ seen.add(real);
147
+ out.push(real);
148
+ }
149
+ }
150
+ return out;
151
+ }
152
+ /** Public for tests — clears the realpath cache so env-var changes
153
+ * in test isolation pick up fresh resolutions. Production code never
154
+ * calls this. */
155
+ function _clearRealPathCacheForTests() {
156
+ _realPathCache.clear();
157
+ }
158
+ // ── Env-var parsing helpers ─────────────────────────────────────────────────
159
+ function parseList(raw) {
160
+ if (!raw)
161
+ return [];
162
+ return raw.split(':').map((s) => s.trim()).filter((s) => s.length > 0);
163
+ }
164
+ function parseIntSafe(raw, fallback) {
165
+ if (raw === undefined || raw === null || raw === '')
166
+ return fallback;
167
+ const n = Number.parseInt(raw, 10);
168
+ return Number.isFinite(n) && n > 0 ? n : fallback;
169
+ }
170
+ function parseNetworkMode(raw) {
171
+ if (raw === 'none')
172
+ return 'none';
173
+ return 'bridge'; // bridge for unset / 'bridge' / junk
174
+ }
175
+ // ── Risk-tier inference ─────────────────────────────────────────────────────
176
+ /**
177
+ * Default risk tier for a tool that doesn't carry an explicit
178
+ * `riskTier` annotation. Leverages the existing `mutates: boolean`
179
+ * field — mutating tools default to `caution`, read-only tools
180
+ * default to `safe`. Plugin tools without annotation get a safe
181
+ * default for free.
182
+ *
183
+ * Phase 5 ApprovalEngine integration treats explicit annotations as
184
+ * a FLOOR — DANGEROUS_PATTERNS can escalate but never demote. The
185
+ * inference here is the floor when no annotation exists at all.
186
+ */
187
+ function inferDefaultRiskTier(mutates) {
188
+ return mutates ? 'caution' : 'safe';
189
+ }
190
+ // ── Reader ──────────────────────────────────────────────────────────────────
191
+ /**
192
+ * Pure factory. Reads env vars + defaults into a frozen-snapshot
193
+ * SandboxConfig. Idempotent for a given env. The CLI calls this
194
+ * once at boot via the singleton factory below; tests pass a custom
195
+ * `env` argument.
196
+ */
197
+ function readSandboxConfig(env = process.env) {
198
+ // v4.4 Phase 6 — default-on transition. AIDEN_SANDBOX is now
199
+ // enabled by default; users opt out with `AIDEN_SANDBOX=0`.
200
+ //
201
+ // v4.5 Phase 8a — the actual read goes through the runtimeToggles
202
+ // singleton so /sandbox slash-command flips and config.yaml
203
+ // overrides take effect without restart. When the singleton was
204
+ // initialised by the CLI it consults env > config.yaml > default;
205
+ // when not initialised (test bench) it falls back to env > default
206
+ // — the existing semantics.
207
+ // unset / '1' / 'true' / junk → enabled
208
+ // '0' → disabled
209
+ // Mirrors v4.2 Phase 6 (TCE) + v4.3 Phase 6 (browser depth)
210
+ // semantics exactly. The strict `!== '0'` check matches the
211
+ // pattern those phases set: any explicit "off" value disables;
212
+ // everything else (including unset) enables.
213
+ // v4.5 Phase 8a — when env (the readSandboxConfig input) is not the
214
+ // process env (test bench passing a custom env), honour the input
215
+ // directly to keep test isolation. Otherwise route through the
216
+ // runtimeToggles singleton.
217
+ const enabled = env === process.env
218
+ ? (0, runtimeToggles_1.getRuntimeToggles)().isEnabled('sandbox')
219
+ : env.AIDEN_SANDBOX !== '0';
220
+ // Allow/deny lists: defaults + user-provided extensions.
221
+ const customAllow = parseList(env.AIDEN_SANDBOX_ALLOW).map(resolveRealPath);
222
+ const customDeny = parseList(env.AIDEN_SANDBOX_DENY).map(resolveRealPath);
223
+ const fsAllowList = Array.from(new Set([...buildDefaultAllowList(), ...customAllow]));
224
+ const fsDenyList = Array.from(new Set([...buildDefaultDenyList(), ...customDeny]));
225
+ // Resource limits — string values pass through Docker as-is.
226
+ const resourceLimits = {
227
+ memory: env.AIDEN_SANDBOX_MEMORY ?? DEFAULT_RESOURCE_LIMITS.memory,
228
+ cpus: env.AIDEN_SANDBOX_CPUS ?? DEFAULT_RESOURCE_LIMITS.cpus,
229
+ pidsLimit: parseIntSafe(env.AIDEN_SANDBOX_PIDS, DEFAULT_RESOURCE_LIMITS.pidsLimit),
230
+ };
231
+ const networkMode = parseNetworkMode(env.AIDEN_SANDBOX_NETWORK);
232
+ const persistent = env.AIDEN_SANDBOX_PERSISTENT !== '0'; // default true
233
+ const idleReaperMs = parseIntSafe(env.AIDEN_SANDBOX_IDLE_MS, DEFAULT_IDLE_REAPER_MS);
234
+ const dryRun = env.AIDEN_DRYRUN === '1';
235
+ const image = typeof env.AIDEN_SANDBOX_IMAGE === 'string' && env.AIDEN_SANDBOX_IMAGE.trim().length > 0
236
+ ? env.AIDEN_SANDBOX_IMAGE.trim()
237
+ : DEFAULT_IMAGE;
238
+ // Phase 3 will route to 'docker' when enabled AND docker is
239
+ // available. Phase 1 reports the abstract default — Phase 3's
240
+ // runtime probe decides the actual route.
241
+ const defaultBackend = enabled ? 'docker' : 'local';
242
+ return {
243
+ enabled,
244
+ fsAllowList,
245
+ fsDenyList,
246
+ defaultBackend,
247
+ persistent,
248
+ resourceLimits,
249
+ networkMode,
250
+ idleReaperMs,
251
+ dryRun,
252
+ image,
253
+ };
254
+ }
255
+ // ── Singleton ───────────────────────────────────────────────────────────────
256
+ let _singleton = null;
257
+ let _toggleHookInstalled = false;
258
+ /**
259
+ * Read the singleton sandbox config. Initialized on first call from
260
+ * `process.env` (matches v4.2 TurnState / v4.3 BrowserState lifecycle).
261
+ * Tests construct fresh configs via `readSandboxConfig(env)` directly
262
+ * — the singleton path is for production CLI startup.
263
+ */
264
+ function getSandboxConfig() {
265
+ if (!_toggleHookInstalled) {
266
+ // v4.5 Phase 8a — invalidate the singleton when /sandbox toggles.
267
+ // Registered lazily on first read so test benches that build
268
+ // their own runtimeToggles + reset between cases don't carry
269
+ // hooks across instances.
270
+ try {
271
+ (0, runtimeToggles_1.getRuntimeToggles)().onChange('sandbox', () => { _singleton = null; });
272
+ _toggleHookInstalled = true;
273
+ }
274
+ catch { /* runtimeToggles import / init race — best-effort */ }
275
+ }
276
+ if (!_singleton)
277
+ _singleton = readSandboxConfig();
278
+ return _singleton;
279
+ }
280
+ /** Reset the singleton for test isolation. Production code never calls this. */
281
+ function _resetSandboxConfigForTests() {
282
+ _singleton = null;
283
+ _toggleHookInstalled = false;
284
+ _clearRealPathCacheForTests();
285
+ }
@@ -0,0 +1,316 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/sandboxFs.ts — v4.4 Phase 2: filesystem allowlist enforcement.
10
+ *
11
+ * Pure in-process path policy decision module. Consulted by the six
12
+ * file_* tools (file_read, file_list, file_write, file_patch,
13
+ * file_copy, file_move, file_delete) BEFORE any disk I/O, so a
14
+ * decision can be returned to the agent without ever touching the
15
+ * filesystem when the answer is "no".
16
+ *
17
+ * Two-list model (mirrors Phase 1's `SandboxConfig`):
18
+ * - fsDenyList — sensitive paths the user never wants touched
19
+ * (.ssh, .aws, .env, /etc, /var, ...). Wins for
20
+ * BOTH read and write operations. Denylist always
21
+ * takes precedence over allowlist.
22
+ * - fsAllowList — write-permitted roots (cwd, ~/Documents,
23
+ * ~/Downloads, ~/Desktop, os.tmpdir()). Writes /
24
+ * deletes outside these roots are refused. Reads
25
+ * are NOT constrained by the allowlist — reads
26
+ * only have to clear the denylist.
27
+ *
28
+ * Symlink defense:
29
+ * - Realpath the target (or its first existing ancestor for
30
+ * non-existent paths like file_write destinations). Symlink
31
+ * bypass on allowlist roots is the canonical attack vector;
32
+ * this module canonicalizes before checking.
33
+ * - A path that is LEXICALLY under an allowlist root but
34
+ * REALPATH'd outside (via a symlink) yields a distinct
35
+ * `fs.symlink_escape` violation code.
36
+ *
37
+ * Non-existent path handling (Q-P2-3 default):
38
+ * - Walk up to the first existing ancestor, realpath that, then
39
+ * rejoin the trailing segments. This defends against
40
+ * `<allowlist-root>/<symlinked-dir>/new-file.txt` writes.
41
+ *
42
+ * TOCTOU posture (Q-P2-5 default):
43
+ * - Phase 2 is in-process. A racing actor could rename a
44
+ * directory between our policy check and the tool's open()
45
+ * call. Phase 3 (Docker sandbox) closes this gap at the OS
46
+ * layer via container isolation. Phase 2 documents the gap
47
+ * and accepts it for the strict-opt-in `AIDEN_SANDBOX=1`
48
+ * period (default-off until Phase 6).
49
+ *
50
+ * Short-circuit semantics:
51
+ * - When `config.enabled === false` (default through Phase 5),
52
+ * `isPathAllowed` returns `{ allowed: true, resolvedPath: ...,
53
+ * ... }` with no denylist/allowlist evaluation. Zero overhead
54
+ * for users who haven't opted in. Phase 6 flips the gate but
55
+ * the wire-in stays.
56
+ *
57
+ * The returned `PathPolicyDecision` shape is also forward-compatible
58
+ * with Phase 5's `FailureCategory.sandbox_violation` — the
59
+ * `violation.category` field is pre-populated with the constant
60
+ * Phase 5 will register in FailureClassifier.
61
+ */
62
+ var __importDefault = (this && this.__importDefault) || function (mod) {
63
+ return (mod && mod.__esModule) ? mod : { "default": mod };
64
+ };
65
+ Object.defineProperty(exports, "__esModule", { value: true });
66
+ exports.isWithin = isWithin;
67
+ exports.realpathWithFallback = realpathWithFallback;
68
+ exports.isPathAllowed = isPathAllowed;
69
+ exports.violationEnvelope = violationEnvelope;
70
+ const node_path_1 = __importDefault(require("node:path"));
71
+ const node_fs_1 = __importDefault(require("node:fs"));
72
+ const node_os_1 = __importDefault(require("node:os"));
73
+ const sandboxConfig_1 = require("./sandboxConfig");
74
+ // ── Internal helpers ────────────────────────────────────────────────────────
75
+ /**
76
+ * `tools/v4/utils/paths.ts#expandPath` re-implemented locally so the
77
+ * core module doesn't depend on a tools-layer helper. Keeps the
78
+ * import direction core ← tools (never the reverse).
79
+ */
80
+ function expandPathInline(input, cwd) {
81
+ const home = node_os_1.default.homedir();
82
+ let p = input;
83
+ if (/^~[\\/]/i.test(p))
84
+ p = home + p.slice(1);
85
+ else if (/^Desktop[\\/]?$/i.test(p))
86
+ p = node_path_1.default.join(home, 'Desktop');
87
+ else if (/^Desktop[\\/]/i.test(p))
88
+ p = node_path_1.default.join(home, 'Desktop', p.slice(8));
89
+ if (node_path_1.default.isAbsolute(p))
90
+ return p;
91
+ if (/^[A-Z]:/i.test(p))
92
+ return p;
93
+ return node_path_1.default.join(cwd, p);
94
+ }
95
+ /**
96
+ * Boundary-aware containment check. `path.relative` avoids the
97
+ * `/home/user-evil` vs `/home/user` false positive that a naive
98
+ * `startsWith` would produce.
99
+ */
100
+ function isWithin(child, parent) {
101
+ if (!child || !parent)
102
+ return false;
103
+ const rel = node_path_1.default.relative(parent, child);
104
+ return rel === '' || (!rel.startsWith('..') && !node_path_1.default.isAbsolute(rel));
105
+ }
106
+ /**
107
+ * Realpath a path that may not yet exist. Walks up to the first
108
+ * existing ancestor, realpaths that, then rejoins the trailing
109
+ * segments. Defends against `<allowlist>/<symlink>/new-file.txt`
110
+ * writes where the symlink mid-path points outside the allowlist.
111
+ *
112
+ * Idempotent: existing paths are realpath'd directly (single
113
+ * `resolveRealPath` call).
114
+ */
115
+ function realpathWithFallback(input) {
116
+ // First, resolve the whole thing optimistically — if it exists,
117
+ // realpath handles it directly and we're done.
118
+ const resolved = node_path_1.default.resolve(input);
119
+ try {
120
+ if (node_fs_1.default.existsSync(resolved)) {
121
+ return (0, sandboxConfig_1.resolveRealPath)(resolved);
122
+ }
123
+ }
124
+ catch {
125
+ // existsSync shouldn't throw, but a permissions error on a
126
+ // parent dir could surface here on Windows. Fall through.
127
+ }
128
+ // Path doesn't exist yet — walk up.
129
+ let cur = resolved;
130
+ let tail = '';
131
+ // Guard against infinite loop on malformed paths (path.dirname
132
+ // of a root returns the root itself).
133
+ for (let i = 0; i < 4096; i++) {
134
+ const parent = node_path_1.default.dirname(cur);
135
+ if (parent === cur) {
136
+ // Reached filesystem root without finding any existing
137
+ // ancestor. Return the lexical resolve.
138
+ return resolved;
139
+ }
140
+ let parentExists = false;
141
+ try {
142
+ parentExists = node_fs_1.default.existsSync(parent);
143
+ }
144
+ catch {
145
+ parentExists = false;
146
+ }
147
+ if (parentExists) {
148
+ const parentReal = (0, sandboxConfig_1.resolveRealPath)(parent);
149
+ tail = tail ? node_path_1.default.join(node_path_1.default.basename(cur), tail) : node_path_1.default.basename(cur);
150
+ return node_path_1.default.join(parentReal, tail);
151
+ }
152
+ tail = tail ? node_path_1.default.join(node_path_1.default.basename(cur), tail) : node_path_1.default.basename(cur);
153
+ cur = parent;
154
+ }
155
+ return resolved;
156
+ }
157
+ function fmtList(list, max = 5) {
158
+ if (list.length === 0)
159
+ return '(none)';
160
+ if (list.length <= max)
161
+ return list.join(', ');
162
+ return list.slice(0, max).join(', ') + `, ... (${list.length - max} more)`;
163
+ }
164
+ // ── Public API ──────────────────────────────────────────────────────────────
165
+ /**
166
+ * Evaluate a path against the active sandbox policy.
167
+ *
168
+ * @param rawPath The original path string from tool args (untrusted).
169
+ * @param op 'read' | 'write' | 'delete' — determines whether the
170
+ * allowlist applies. 'read' = deny-only; 'write' /
171
+ * 'delete' = allowlist required.
172
+ * @param cwd The tool context's working directory, used to resolve
173
+ * relative paths the same way the file tools do.
174
+ * @param config Optional config override (default = singleton from
175
+ * `getSandboxConfig()`). Tests pass a custom config.
176
+ *
177
+ * @returns `{ allowed: true, ... }` when the operation may proceed,
178
+ * or `{ allowed: false, violation: {...}, ... }` with a
179
+ * structured `FsViolation` when it must be refused. Tools
180
+ * should forward the violation into a `sandbox_violation`
181
+ * envelope on the result object alongside `success: false`.
182
+ *
183
+ * Behavior when `config.enabled === false` (Phase 1-5 default):
184
+ * the function still resolves the path (so the caller can use
185
+ * `resolvedPath` uniformly), but returns `allowed: true` without
186
+ * evaluating either list. Zero runtime cost beyond expand+resolve.
187
+ */
188
+ function isPathAllowed(rawPath, op, cwd, config = (0, sandboxConfig_1.getSandboxConfig)()) {
189
+ const requestedPath = rawPath;
190
+ const expandedPath = expandPathInline(rawPath, cwd);
191
+ // Short-circuit when sandbox is disabled. Still produce a useful
192
+ // resolvedPath so the tool can keep its current single-codepath
193
+ // structure (resolve once, use the resolved path).
194
+ if (!config.enabled) {
195
+ return {
196
+ allowed: true,
197
+ resolvedPath: expandedPath,
198
+ requestedPath,
199
+ expandedPath,
200
+ op,
201
+ };
202
+ }
203
+ // Lexical path-traversal sniff: if `rawPath` was a relative input
204
+ // that escapes `cwd` via `..`, refuse with a clear code BEFORE
205
+ // realpath touches the disk. This is belt-and-suspenders — realpath
206
+ // would catch most cases via the symlink-escape branch — but a
207
+ // structured `fs.path_traversal` reads more cleanly in logs.
208
+ if (!node_path_1.default.isAbsolute(rawPath) && !/^[A-Z]:/i.test(rawPath) && !/^~[\\/]/i.test(rawPath)) {
209
+ const cwdReal = (0, sandboxConfig_1.resolveRealPath)(cwd);
210
+ const expReal = node_path_1.default.resolve(cwd, rawPath);
211
+ if (!isWithin(expReal, cwdReal) && rawPath.includes('..')) {
212
+ // Don't treat this as fatal yet — a relative path can legitimately
213
+ // escape cwd (e.g. `../tmp/x` when cwd is under tmp). We only flag
214
+ // it if it ALSO lands outside both lists, which the standard checks
215
+ // below will catch. Leaving the sniff in as a `path_traversal`
216
+ // upgrade for the escape-with-no-allowlist-hit case.
217
+ }
218
+ }
219
+ const resolvedPath = realpathWithFallback(expandedPath);
220
+ const base = {
221
+ resolvedPath,
222
+ requestedPath,
223
+ expandedPath,
224
+ op,
225
+ };
226
+ // ── Denylist (always wins, both read and write) ───────────────────────
227
+ for (const denied of config.fsDenyList) {
228
+ if (isWithin(resolvedPath, denied) || resolvedPath === denied) {
229
+ return {
230
+ ...base,
231
+ allowed: false,
232
+ violation: {
233
+ code: 'fs.sensitive_path',
234
+ matchedPolicy: denied,
235
+ category: 'sandbox_violation',
236
+ retryable: false,
237
+ message: `Sandbox: path "${resolvedPath}" is under the sensitive denylist entry ` +
238
+ `"${denied}". Reads and writes are both refused. ` +
239
+ `(Override by extending AIDEN_SANDBOX_ALLOW is not sufficient — the ` +
240
+ `denylist takes precedence.)`,
241
+ },
242
+ };
243
+ }
244
+ }
245
+ // ── Allowlist (write/delete only — reads pass through after denylist) ─
246
+ if (op === 'read') {
247
+ return { ...base, allowed: true };
248
+ }
249
+ // Symlink-escape detection: resolvedPath escaped the allowlist
250
+ // tree, but expandedPath was LEXICALLY under it. That's the
251
+ // classic allowlist-bypass-via-symlink pattern.
252
+ let lexicalUnderAllow = false;
253
+ let realUnderAllow = false;
254
+ let matchedAllow = '';
255
+ for (const allowed of config.fsAllowList) {
256
+ if (isWithin(expandedPath, allowed) || expandedPath === allowed) {
257
+ lexicalUnderAllow = true;
258
+ }
259
+ if (isWithin(resolvedPath, allowed) || resolvedPath === allowed) {
260
+ realUnderAllow = true;
261
+ matchedAllow = allowed;
262
+ break;
263
+ }
264
+ }
265
+ if (realUnderAllow) {
266
+ return { ...base, allowed: true };
267
+ }
268
+ if (lexicalUnderAllow) {
269
+ return {
270
+ ...base,
271
+ allowed: false,
272
+ violation: {
273
+ code: 'fs.symlink_escape',
274
+ matchedPolicy: '',
275
+ category: 'sandbox_violation',
276
+ retryable: false,
277
+ message: `Sandbox: path "${expandedPath}" appears to live under an allowlisted ` +
278
+ `root, but its real path "${resolvedPath}" is outside every allowlist ` +
279
+ `entry. A symlink in the path likely points outside the sandbox.`,
280
+ },
281
+ };
282
+ }
283
+ // Plain write-outside-allowlist. Most common refusal.
284
+ return {
285
+ ...base,
286
+ allowed: false,
287
+ violation: {
288
+ code: 'fs.write_outside_allowlist',
289
+ matchedPolicy: matchedAllow,
290
+ category: 'sandbox_violation',
291
+ retryable: false,
292
+ message: `Sandbox: ${op} target "${resolvedPath}" is not under any allowlisted ` +
293
+ `directory. Allowed roots: ${fmtList(config.fsAllowList)}. ` +
294
+ `(Extend via AIDEN_SANDBOX_ALLOW=<colon-separated-paths>.)`,
295
+ },
296
+ };
297
+ }
298
+ /**
299
+ * Convenience: build the structured envelope the file tools attach
300
+ * to their result objects when a policy denial occurs. Centralises
301
+ * the wire format so all six tools serialise the same shape.
302
+ */
303
+ function violationEnvelope(decision) {
304
+ if (!decision.violation) {
305
+ // Defensive — callers should only call this on denied decisions.
306
+ throw new Error('violationEnvelope called on allowed decision');
307
+ }
308
+ return {
309
+ code: decision.violation.code,
310
+ matched_policy: decision.violation.matchedPolicy,
311
+ requested_path: decision.requestedPath,
312
+ resolved_path: decision.resolvedPath,
313
+ retryable: false,
314
+ category: 'sandbox_violation',
315
+ };
316
+ }
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ /**
3
+ * Copyright (c) 2026 Shiva Deore (Taracod).
4
+ * Licensed under AGPL-3.0. See LICENSE for details.
5
+ *
6
+ * Aiden — local-first agent.
7
+ */
8
+ /**
9
+ * core/v4/suggestionCatalog.ts — v4.5 Phase 8b.
10
+ *
11
+ * Pre-rendered user-facing copy for each contextual suggestion slot.
12
+ * Centralised here so future tone tuning / i18n can change one file
13
+ * without touching the engine.
14
+ *
15
+ * Style guide (approved tone Q-P8b-5(b)):
16
+ * - One line per tip.
17
+ * - ≤ 90 visible chars (room for indentation + emoji).
18
+ * - "💡 tip:" prefix, lowercase command + brief why.
19
+ * - No trailing punctuation gymnastics.
20
+ * - No second-person preaching ("you should" → drop).
21
+ */
22
+ Object.defineProperty(exports, "__esModule", { value: true });
23
+ exports.SUGGESTION_COPY = void 0;
24
+ exports.suggestionMessageFor = suggestionMessageFor;
25
+ /**
26
+ * Slot → message map. The engine resolves the slot and the
27
+ * catalog renders the matching copy.
28
+ */
29
+ exports.SUGGESTION_COPY = Object.freeze({
30
+ sandbox: '💡 tip: enable /sandbox on for a safer guardrail against destructive ops.',
31
+ browser_depth: '💡 tip: enable /browser-depth on to capture page state + auto-retry stale refs.',
32
+ daemon_scheduling: '💡 tip: this looks like a recurring task — `aiden cron add` or `aiden trigger add file` can run it on a schedule.',
33
+ tce_recovery: '💡 tip: enable /tce on so Aiden classifies tool failures + auto-recovers.',
34
+ });
35
+ /**
36
+ * Look up the rendered tip string for a slot. Pure — caller decides
37
+ * when to display (`display.dim()` is the conventional sink).
38
+ */
39
+ function suggestionMessageFor(slot) {
40
+ return exports.SUGGESTION_COPY[slot];
41
+ }