aiden-runtime 4.1.5 → 4.6.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 +265 -847
- package/dist/api/server.js +32 -5
- package/dist/cli/v4/aidenCLI.js +536 -152
- package/dist/cli/v4/callbacks.js +170 -0
- package/dist/cli/v4/chatSession.js +245 -3
- package/dist/cli/v4/commands/_runtimeToggleHelpers.js +94 -0
- package/dist/cli/v4/commands/browserDepth.js +45 -0
- package/dist/cli/v4/commands/cron.js +264 -0
- package/dist/cli/v4/commands/daemon.js +541 -0
- package/dist/cli/v4/commands/daemonStatus.js +253 -0
- package/dist/cli/v4/commands/fanout.js +42 -59
- package/dist/cli/v4/commands/help.js +13 -0
- package/dist/cli/v4/commands/index.js +35 -1
- package/dist/cli/v4/commands/mcp.js +80 -54
- package/dist/cli/v4/commands/plannerGuard.js +53 -0
- package/dist/cli/v4/commands/recovery.js +122 -0
- package/dist/cli/v4/commands/runs.js +223 -0
- package/dist/cli/v4/commands/sandbox.js +48 -0
- package/dist/cli/v4/commands/spawnPause.js +93 -0
- package/dist/cli/v4/commands/suggestions.js +68 -0
- package/dist/cli/v4/commands/tce.js +41 -0
- package/dist/cli/v4/commands/trigger.js +378 -0
- package/dist/cli/v4/commands/update.js +95 -3
- package/dist/cli/v4/daemonAgentBuilder.js +145 -0
- package/dist/cli/v4/defaultSoul.js +1 -1
- package/dist/cli/v4/display/capabilityCard.js +26 -0
- package/dist/cli/v4/display.js +18 -8
- package/dist/cli/v4/replyRenderer.js +31 -23
- package/dist/cli/v4/updateBootPrompt.js +170 -0
- package/dist/core/playwrightBridge.js +129 -0
- package/dist/core/v4/aidenAgent.js +527 -5
- package/dist/core/v4/browserState.js +436 -0
- package/dist/core/v4/checkpoint.js +79 -0
- package/dist/core/v4/daemon/bootstrap.js +651 -0
- package/dist/core/v4/daemon/cleanShutdown.js +154 -0
- package/dist/core/v4/daemon/cron/cronBridge.js +126 -0
- package/dist/core/v4/daemon/cron/cronEmitter.js +173 -0
- package/dist/core/v4/daemon/cron/migration.js +199 -0
- package/dist/core/v4/daemon/cron/misfirePolicy.js +115 -0
- package/dist/core/v4/daemon/daemonConfig.js +90 -0
- package/dist/core/v4/daemon/db/connection.js +106 -0
- package/dist/core/v4/daemon/db/migrations.js +362 -0
- package/dist/core/v4/daemon/db/schema/v1.spec.js +18 -0
- package/dist/core/v4/daemon/dispatcher/agentRunner.js +98 -0
- package/dist/core/v4/daemon/dispatcher/budgetGate.js +127 -0
- package/dist/core/v4/daemon/dispatcher/daemonApproval.js +113 -0
- package/dist/core/v4/daemon/dispatcher/dailyBudgetTracker.js +120 -0
- package/dist/core/v4/daemon/dispatcher/dispatcher.js +389 -0
- package/dist/core/v4/daemon/dispatcher/fireRateLimiter.js +113 -0
- package/dist/core/v4/daemon/dispatcher/index.js +53 -0
- package/dist/core/v4/daemon/dispatcher/promptTemplate.js +95 -0
- package/dist/core/v4/daemon/dispatcher/realAgentRunner.js +356 -0
- package/dist/core/v4/daemon/dispatcher/resolveModel.js +93 -0
- package/dist/core/v4/daemon/dispatcher/sessionId.js +93 -0
- package/dist/core/v4/daemon/drain.js +156 -0
- package/dist/core/v4/daemon/eventLoopLag.js +73 -0
- package/dist/core/v4/daemon/health.js +159 -0
- package/dist/core/v4/daemon/idempotencyStore.js +204 -0
- package/dist/core/v4/daemon/index.js +179 -0
- package/dist/core/v4/daemon/instanceTracker.js +99 -0
- package/dist/core/v4/daemon/resourceRegistry.js +150 -0
- package/dist/core/v4/daemon/restartCode.js +32 -0
- package/dist/core/v4/daemon/restartFailureCounter.js +77 -0
- package/dist/core/v4/daemon/runStore.js +144 -0
- package/dist/core/v4/daemon/runtimeLock.js +167 -0
- package/dist/core/v4/daemon/signals.js +50 -0
- package/dist/core/v4/daemon/supervisor.js +272 -0
- package/dist/core/v4/daemon/triggerBus.js +279 -0
- package/dist/core/v4/daemon/triggers/email/allowlist.js +70 -0
- package/dist/core/v4/daemon/triggers/email/automatedSender.js +78 -0
- package/dist/core/v4/daemon/triggers/email/bodyExtractor.js +0 -0
- package/dist/core/v4/daemon/triggers/email/emailSeenStore.js +99 -0
- package/dist/core/v4/daemon/triggers/email/emailSpec.js +107 -0
- package/dist/core/v4/daemon/triggers/email/imapConnection.js +211 -0
- package/dist/core/v4/daemon/triggers/email/index.js +332 -0
- package/dist/core/v4/daemon/triggers/email/seenUids.js +60 -0
- package/dist/core/v4/daemon/triggers/fileObservationsStore.js +93 -0
- package/dist/core/v4/daemon/triggers/fileWatcher.js +253 -0
- package/dist/core/v4/daemon/triggers/fileWatcherSpec.js +88 -0
- package/dist/core/v4/daemon/triggers/fsIdentity.js +42 -0
- package/dist/core/v4/daemon/triggers/globMatcher.js +100 -0
- package/dist/core/v4/daemon/triggers/reconcile.js +206 -0
- package/dist/core/v4/daemon/triggers/settleStat.js +81 -0
- package/dist/core/v4/daemon/triggers/webhook.js +376 -0
- package/dist/core/v4/daemon/triggers/webhookDeliveriesStore.js +109 -0
- package/dist/core/v4/daemon/triggers/webhookIdempotency.js +72 -0
- package/dist/core/v4/daemon/triggers/webhookRateLimit.js +56 -0
- package/dist/core/v4/daemon/triggers/webhookSpec.js +76 -0
- package/dist/core/v4/daemon/triggers/webhookVerifier.js +128 -0
- package/dist/core/v4/daemon/types.js +15 -0
- package/dist/core/v4/dockerSession.js +461 -0
- package/dist/core/v4/dryRun.js +117 -0
- package/dist/core/v4/failureClassifier.js +779 -0
- package/dist/core/v4/providerFallback.js +35 -2
- package/dist/core/v4/recoveryReport.js +449 -0
- package/dist/core/v4/runtimeToggles.js +214 -0
- package/dist/core/v4/sandboxConfig.js +285 -0
- package/dist/core/v4/sandboxFs.js +316 -0
- package/dist/core/v4/selfimprovement/recoveryStore.js +307 -0
- package/dist/core/v4/selfimprovement/signatureBuilder.js +158 -0
- package/dist/core/v4/subagent/childBuilder.js +391 -0
- package/dist/core/v4/subagent/fanout.js +75 -51
- package/dist/core/v4/subagent/spawnPause.js +191 -0
- package/dist/core/v4/subagent/spawnSubAgent.js +310 -0
- package/dist/core/v4/suggestionCatalog.js +41 -0
- package/dist/core/v4/suggestionEngine.js +210 -0
- package/dist/core/v4/toolRegistry.js +37 -3
- package/dist/core/v4/turnState.js +587 -0
- package/dist/core/v4/update/checkUpdate.js +63 -3
- package/dist/core/v4/update/installMethodDetect.js +115 -0
- package/dist/core/v4/update/registryClient.js +121 -0
- package/dist/core/v4/update/skipState.js +75 -0
- package/dist/core/v4/verifier.js +448 -0
- package/dist/core/version.js +1 -1
- package/dist/moat/plannerGuard.js +29 -0
- package/dist/providers/v4/anthropicAdapter.js +31 -3
- package/dist/providers/v4/chatCompletionsAdapter.js +26 -3
- package/dist/providers/v4/codexResponsesAdapter.js +25 -2
- package/dist/providers/v4/ollamaPromptToolsAdapter.js +57 -2
- package/dist/tools/v4/browser/_observer.js +224 -0
- package/dist/tools/v4/browser/browserBlocker.js +396 -0
- package/dist/tools/v4/browser/browserClick.js +18 -1
- package/dist/tools/v4/browser/browserClose.js +18 -1
- package/dist/tools/v4/browser/browserExtract.js +5 -1
- package/dist/tools/v4/browser/browserFill.js +17 -1
- package/dist/tools/v4/browser/browserGetUrl.js +5 -1
- package/dist/tools/v4/browser/browserNavigate.js +16 -1
- package/dist/tools/v4/browser/browserScreenshot.js +5 -1
- package/dist/tools/v4/browser/browserScroll.js +18 -1
- package/dist/tools/v4/browser/browserType.js +17 -1
- package/dist/tools/v4/browser/captchaCheck.js +5 -1
- package/dist/tools/v4/executeCode.js +1 -0
- package/dist/tools/v4/files/fileCopy.js +56 -2
- package/dist/tools/v4/files/fileDelete.js +38 -1
- package/dist/tools/v4/files/fileList.js +12 -1
- package/dist/tools/v4/files/fileMove.js +59 -2
- package/dist/tools/v4/files/filePatch.js +43 -1
- package/dist/tools/v4/files/fileRead.js +12 -1
- package/dist/tools/v4/files/fileWrite.js +41 -1
- package/dist/tools/v4/index.js +88 -61
- package/dist/tools/v4/memory/memoryAdd.js +14 -0
- package/dist/tools/v4/memory/memoryRemove.js +14 -0
- package/dist/tools/v4/memory/memoryReplace.js +15 -0
- package/dist/tools/v4/memory/sessionSummary.js +12 -0
- package/dist/tools/v4/process/processKill.js +19 -0
- package/dist/tools/v4/process/processList.js +1 -0
- package/dist/tools/v4/process/processLogRead.js +1 -0
- package/dist/tools/v4/process/processSpawn.js +13 -0
- package/dist/tools/v4/process/processWait.js +1 -0
- package/dist/tools/v4/sessions/recallSession.js +1 -0
- package/dist/tools/v4/sessions/sessionList.js +1 -0
- package/dist/tools/v4/sessions/sessionSearch.js +1 -0
- package/dist/tools/v4/skills/lookupToolSchema.js +7 -0
- package/dist/tools/v4/skills/skillManage.js +13 -0
- package/dist/tools/v4/skills/skillView.js +1 -0
- package/dist/tools/v4/skills/skillsList.js +1 -0
- package/dist/tools/v4/subagent/spawnSubAgentTool.js +334 -0
- package/dist/tools/v4/subagent/subagentFanout.js +54 -1
- package/dist/tools/v4/system/aidenSelfUpdate.js +16 -0
- package/dist/tools/v4/system/appClose.js +13 -0
- package/dist/tools/v4/system/appInput.js +13 -0
- package/dist/tools/v4/system/appLaunch.js +13 -0
- package/dist/tools/v4/system/clipboardRead.js +1 -0
- package/dist/tools/v4/system/clipboardWrite.js +14 -0
- package/dist/tools/v4/system/mediaKey.js +12 -0
- package/dist/tools/v4/system/mediaSessions.js +1 -0
- package/dist/tools/v4/system/mediaTransport.js +13 -0
- package/dist/tools/v4/system/naturalEvents.js +1 -0
- package/dist/tools/v4/system/nowPlaying.js +1 -0
- package/dist/tools/v4/system/osProcessList.js +1 -0
- package/dist/tools/v4/system/screenshot.js +1 -0
- package/dist/tools/v4/system/systemInfo.js +1 -0
- package/dist/tools/v4/system/volumeSet.js +17 -0
- package/dist/tools/v4/terminal/shellExec.js +81 -9
- package/dist/tools/v4/web/deepResearch.js +1 -0
- package/dist/tools/v4/web/openUrl.js +1 -0
- package/dist/tools/v4/web/webFetch.js +1 -0
- package/dist/tools/v4/web/webPage.js +1 -0
- package/dist/tools/v4/web/webSearch.js +1 -0
- package/dist/tools/v4/web/youtubeSearch.js +1 -0
- package/package.json +13 -3
|
@@ -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,307 @@
|
|
|
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/selfimprovement/recoveryStore.ts — v4.6 Phase 3b.
|
|
10
|
+
*
|
|
11
|
+
* Durable cross-session failure ledger + recovery report writer.
|
|
12
|
+
* Backed by the v7 schema's `failure_signatures` + `recovery_reports`
|
|
13
|
+
* tables. The store is the single write path for both halves of the
|
|
14
|
+
* self-improvement loop:
|
|
15
|
+
*
|
|
16
|
+
* 1. `recordFailureOccurrence(...)` — called on every classified
|
|
17
|
+
* failure (TCE write-through at the aidenAgent classify site).
|
|
18
|
+
* Upserts the signature row, increments `occurrences`, updates
|
|
19
|
+
* `last_seen_at`.
|
|
20
|
+
*
|
|
21
|
+
* 2. `recordRecovery(...)` — called when a previously-failed
|
|
22
|
+
* signature is observed succeeding (or when the agent's TCE
|
|
23
|
+
* surfaces a structured recovery report at turn end).
|
|
24
|
+
* Inserts a `recovery_reports` row + bumps the signature's
|
|
25
|
+
* `recovered_count` + sets `last_recovery_report_id`.
|
|
26
|
+
*
|
|
27
|
+
* Reads:
|
|
28
|
+
*
|
|
29
|
+
* * `listTopFailures(limit)` — operator dashboard query.
|
|
30
|
+
* * `getBySignature(signature)` — `/recovery show` detail surface.
|
|
31
|
+
* * `listForSession(sessionId)` — used by future plugin hooks that
|
|
32
|
+
* want per-session summaries (currently unused; the operator
|
|
33
|
+
* command path goes via `listTopFailures` + `getBySignature`).
|
|
34
|
+
* * `listReportsForSignature(signatureId, limit)` — the recovery
|
|
35
|
+
* history for one signature.
|
|
36
|
+
*
|
|
37
|
+
* Singleton pattern mirrors `spawnPause` (Phase 3a): `initRecoveryStore({db})`
|
|
38
|
+
* at boot; `getRecoveryStore()` thereafter. Production wiring opens the
|
|
39
|
+
* daemon DB once at REPL/daemon/MCP boot and re-uses the singleton
|
|
40
|
+
* across REPL turns and daemon-fired turns. Tests reset via
|
|
41
|
+
* `_resetRecoveryStoreForTests()`.
|
|
42
|
+
*
|
|
43
|
+
* Failure mode: NEVER throws. A persistence failure (locked DB,
|
|
44
|
+
* schema drift, etc.) returns 0 / null and logs a warning. The
|
|
45
|
+
* TCE write-through path treats the store as best-effort — losing
|
|
46
|
+
* a signature increment does not break a turn.
|
|
47
|
+
*/
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
exports.RecoveryStore = void 0;
|
|
50
|
+
exports.initRecoveryStore = initRecoveryStore;
|
|
51
|
+
exports.getRecoveryStore = getRecoveryStore;
|
|
52
|
+
exports._resetRecoveryStoreForTests = _resetRecoveryStoreForTests;
|
|
53
|
+
// ── Implementation ───────────────────────────────────────────────────────
|
|
54
|
+
/**
|
|
55
|
+
* SQLite-backed store. Constructed with a `better-sqlite3` Database
|
|
56
|
+
* handle that already has the v7 migration applied. The store does
|
|
57
|
+
* NOT run migrations itself — that's the caller's job (typically
|
|
58
|
+
* `openDaemonDb` + `runMigrations`).
|
|
59
|
+
*/
|
|
60
|
+
class RecoveryStore {
|
|
61
|
+
constructor(db) {
|
|
62
|
+
this.db = db;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Upsert a failure signature + bump occurrences. Returns the
|
|
66
|
+
* signature row id, or 0 on persistence failure (logged). The
|
|
67
|
+
* caller (TCE write-through path) treats the return as best-effort.
|
|
68
|
+
*/
|
|
69
|
+
recordFailureOccurrence(opts) {
|
|
70
|
+
const now = opts.now ?? Date.now;
|
|
71
|
+
const ts = now();
|
|
72
|
+
try {
|
|
73
|
+
// SQLite-native UPSERT — single round trip per failure. The
|
|
74
|
+
// `excluded.x` syntax references the row we tried to insert.
|
|
75
|
+
const r = this.db.prepare(`
|
|
76
|
+
INSERT INTO failure_signatures
|
|
77
|
+
(signature, tool_name, failure_category, args_hash,
|
|
78
|
+
first_seen_at, last_seen_at, occurrences, recovered_count)
|
|
79
|
+
VALUES (?, ?, ?, ?, ?, ?, 1, 0)
|
|
80
|
+
ON CONFLICT(signature) DO UPDATE SET
|
|
81
|
+
last_seen_at = excluded.last_seen_at,
|
|
82
|
+
occurrences = failure_signatures.occurrences + 1
|
|
83
|
+
RETURNING id
|
|
84
|
+
`).get(opts.signature, opts.toolName, opts.category, opts.argsHash ?? null, ts, ts);
|
|
85
|
+
return r?.id ?? 0;
|
|
86
|
+
}
|
|
87
|
+
catch (err) {
|
|
88
|
+
// eslint-disable-next-line no-console
|
|
89
|
+
console.warn('[recoveryStore] recordFailureOccurrence failed:', err instanceof Error ? err.message : String(err));
|
|
90
|
+
return 0;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Record a successful recovery. Inserts a `recovery_reports`
|
|
95
|
+
* row + atomically bumps the signature's `recovered_count` and
|
|
96
|
+
* `last_recovery_report_id`. Returns the new report id, or 0 on
|
|
97
|
+
* failure.
|
|
98
|
+
*/
|
|
99
|
+
recordRecovery(opts) {
|
|
100
|
+
const now = opts.now ?? Date.now;
|
|
101
|
+
const ts = now();
|
|
102
|
+
try {
|
|
103
|
+
// Two-statement transaction — insert then update — keeps the
|
|
104
|
+
// signature's `last_recovery_report_id` consistent with the
|
|
105
|
+
// newly-inserted report row.
|
|
106
|
+
const txn = this.db.transaction(() => {
|
|
107
|
+
const ins = this.db.prepare(`
|
|
108
|
+
INSERT INTO recovery_reports
|
|
109
|
+
(signature_id, run_id, session_id, failed_attempts,
|
|
110
|
+
successful_strategy, changed_parameters, verification,
|
|
111
|
+
created_at, notes)
|
|
112
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
113
|
+
`).run(opts.signatureId, opts.runId ?? null, opts.sessionId ?? null, opts.failedAttempts, opts.successfulStrategy, opts.changedParameters ? JSON.stringify(opts.changedParameters) : null, opts.verification ?? null, ts, opts.notes ?? null);
|
|
114
|
+
const reportId = Number(ins.lastInsertRowid);
|
|
115
|
+
this.db.prepare(`
|
|
116
|
+
UPDATE failure_signatures
|
|
117
|
+
SET recovered_count = recovered_count + 1,
|
|
118
|
+
last_recovery_report_id = ?
|
|
119
|
+
WHERE id = ?
|
|
120
|
+
`).run(reportId, opts.signatureId);
|
|
121
|
+
return reportId;
|
|
122
|
+
});
|
|
123
|
+
return txn();
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
// eslint-disable-next-line no-console
|
|
127
|
+
console.warn('[recoveryStore] recordRecovery failed:', err instanceof Error ? err.message : String(err));
|
|
128
|
+
return 0;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Top N recurring failure signatures, sorted by occurrence count
|
|
133
|
+
* descending. Backs `/recovery list`. Joins the most recent
|
|
134
|
+
* recovery_report so the operator sees what worked last.
|
|
135
|
+
*/
|
|
136
|
+
listTopFailures(limit = 10) {
|
|
137
|
+
const cap = Math.max(1, Math.min(limit, 500));
|
|
138
|
+
try {
|
|
139
|
+
const rows = this.db.prepare(`
|
|
140
|
+
SELECT
|
|
141
|
+
s.signature AS signature,
|
|
142
|
+
s.tool_name AS toolName,
|
|
143
|
+
s.failure_category AS failureCategory,
|
|
144
|
+
s.occurrences AS occurrences,
|
|
145
|
+
s.recovered_count AS recoveredCount,
|
|
146
|
+
s.last_seen_at AS lastSeenAt,
|
|
147
|
+
r.successful_strategy AS lastRecoveryStrategy
|
|
148
|
+
FROM failure_signatures s
|
|
149
|
+
LEFT JOIN recovery_reports r
|
|
150
|
+
ON r.id = s.last_recovery_report_id
|
|
151
|
+
ORDER BY s.occurrences DESC, s.last_seen_at DESC
|
|
152
|
+
LIMIT ?
|
|
153
|
+
`).all(cap);
|
|
154
|
+
return rows;
|
|
155
|
+
}
|
|
156
|
+
catch (err) {
|
|
157
|
+
// eslint-disable-next-line no-console
|
|
158
|
+
console.warn('[recoveryStore] listTopFailures failed:', err instanceof Error ? err.message : String(err));
|
|
159
|
+
return [];
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Lookup one signature by its canonical string. Backs `/recovery
|
|
164
|
+
* show`. Returns null when no signature row exists yet.
|
|
165
|
+
*/
|
|
166
|
+
getBySignature(signature) {
|
|
167
|
+
try {
|
|
168
|
+
const row = this.db.prepare(`
|
|
169
|
+
SELECT
|
|
170
|
+
id AS id,
|
|
171
|
+
signature AS signature,
|
|
172
|
+
tool_name AS toolName,
|
|
173
|
+
failure_category AS failureCategory,
|
|
174
|
+
args_hash AS argsHash,
|
|
175
|
+
first_seen_at AS firstSeenAt,
|
|
176
|
+
last_seen_at AS lastSeenAt,
|
|
177
|
+
occurrences AS occurrences,
|
|
178
|
+
recovered_count AS recoveredCount,
|
|
179
|
+
last_recovery_report_id AS lastRecoveryReportId
|
|
180
|
+
FROM failure_signatures WHERE signature = ?
|
|
181
|
+
`).get(signature);
|
|
182
|
+
return row ?? null;
|
|
183
|
+
}
|
|
184
|
+
catch (err) {
|
|
185
|
+
// eslint-disable-next-line no-console
|
|
186
|
+
console.warn('[recoveryStore] getBySignature failed:', err instanceof Error ? err.message : String(err));
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Recovery reports linked to one signature, most recent first.
|
|
192
|
+
* Used by `/recovery show` to render the recovery history below
|
|
193
|
+
* the signature header.
|
|
194
|
+
*/
|
|
195
|
+
listReportsForSignature(signatureId, limit = 50) {
|
|
196
|
+
const cap = Math.max(1, Math.min(limit, 500));
|
|
197
|
+
try {
|
|
198
|
+
const rows = this.db.prepare(`
|
|
199
|
+
SELECT
|
|
200
|
+
id AS id,
|
|
201
|
+
signature_id AS signatureId,
|
|
202
|
+
run_id AS runId,
|
|
203
|
+
session_id AS sessionId,
|
|
204
|
+
failed_attempts AS failedAttempts,
|
|
205
|
+
successful_strategy AS successfulStrategy,
|
|
206
|
+
changed_parameters AS changedParameters,
|
|
207
|
+
verification AS verification,
|
|
208
|
+
created_at AS createdAt,
|
|
209
|
+
notes AS notes
|
|
210
|
+
FROM recovery_reports
|
|
211
|
+
WHERE signature_id = ?
|
|
212
|
+
ORDER BY created_at DESC
|
|
213
|
+
LIMIT ?
|
|
214
|
+
`).all(signatureId, cap);
|
|
215
|
+
return rows;
|
|
216
|
+
}
|
|
217
|
+
catch (err) {
|
|
218
|
+
// eslint-disable-next-line no-console
|
|
219
|
+
console.warn('[recoveryStore] listReportsForSignature failed:', err instanceof Error ? err.message : String(err));
|
|
220
|
+
return [];
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Recovery reports written during one session, used by future
|
|
225
|
+
* plugin hooks + the `/recovery` command's per-session view.
|
|
226
|
+
* Wraps a single SELECT — no aggregation. Empty array when no
|
|
227
|
+
* recoveries happened.
|
|
228
|
+
*/
|
|
229
|
+
listForSession(sessionId) {
|
|
230
|
+
try {
|
|
231
|
+
const rows = this.db.prepare(`
|
|
232
|
+
SELECT
|
|
233
|
+
id AS id,
|
|
234
|
+
signature_id AS signatureId,
|
|
235
|
+
run_id AS runId,
|
|
236
|
+
session_id AS sessionId,
|
|
237
|
+
failed_attempts AS failedAttempts,
|
|
238
|
+
successful_strategy AS successfulStrategy,
|
|
239
|
+
changed_parameters AS changedParameters,
|
|
240
|
+
verification AS verification,
|
|
241
|
+
created_at AS createdAt,
|
|
242
|
+
notes AS notes
|
|
243
|
+
FROM recovery_reports
|
|
244
|
+
WHERE session_id = ?
|
|
245
|
+
ORDER BY created_at DESC
|
|
246
|
+
`).all(sessionId);
|
|
247
|
+
return rows;
|
|
248
|
+
}
|
|
249
|
+
catch (err) {
|
|
250
|
+
// eslint-disable-next-line no-console
|
|
251
|
+
console.warn('[recoveryStore] listForSession failed:', err instanceof Error ? err.message : String(err));
|
|
252
|
+
return [];
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Operator escape hatch — `/recovery clear <signature>` lets the
|
|
257
|
+
* operator say "this is fixed, stop counting it." Cascades to
|
|
258
|
+
* the linked recovery_reports rows so the signature genuinely
|
|
259
|
+
* disappears. Returns true when a row was deleted.
|
|
260
|
+
*/
|
|
261
|
+
clearSignature(signature) {
|
|
262
|
+
try {
|
|
263
|
+
const sig = this.getBySignature(signature);
|
|
264
|
+
if (!sig)
|
|
265
|
+
return false;
|
|
266
|
+
const txn = this.db.transaction(() => {
|
|
267
|
+
this.db.prepare(`DELETE FROM recovery_reports WHERE signature_id = ?`).run(sig.id);
|
|
268
|
+
this.db.prepare(`DELETE FROM failure_signatures WHERE id = ?`).run(sig.id);
|
|
269
|
+
});
|
|
270
|
+
txn();
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
catch (err) {
|
|
274
|
+
// eslint-disable-next-line no-console
|
|
275
|
+
console.warn('[recoveryStore] clearSignature failed:', err instanceof Error ? err.message : String(err));
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
exports.RecoveryStore = RecoveryStore;
|
|
281
|
+
// ── Module-level singleton ───────────────────────────────────────────────
|
|
282
|
+
let _singleton = null;
|
|
283
|
+
/**
|
|
284
|
+
* Initialise the process-wide store. Called once at REPL / daemon /
|
|
285
|
+
* MCP boot, after `runMigrations` has applied v7. Re-init replaces
|
|
286
|
+
* the singleton so tests can swap DBs cleanly.
|
|
287
|
+
*/
|
|
288
|
+
function initRecoveryStore(opts) {
|
|
289
|
+
_singleton = new RecoveryStore(opts.db);
|
|
290
|
+
return _singleton;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Read the current singleton. Returns null when not initialised so
|
|
294
|
+
* callers on the hot path (TCE write-through) can no-op silently
|
|
295
|
+
* instead of throwing. The slash command path (`/recovery list`)
|
|
296
|
+
* does its own "not initialised" error reporting.
|
|
297
|
+
*/
|
|
298
|
+
function getRecoveryStore() {
|
|
299
|
+
return _singleton;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Test-only — drop the singleton so the next `initRecoveryStore`
|
|
303
|
+
* call wires a fresh state.
|
|
304
|
+
*/
|
|
305
|
+
function _resetRecoveryStoreForTests() {
|
|
306
|
+
_singleton = null;
|
|
307
|
+
}
|