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.
- package/README.md +250 -847
- package/dist/api/server.js +32 -5
- package/dist/cli/v4/aidenCLI.js +351 -53
- package/dist/cli/v4/callbacks.js +170 -0
- package/dist/cli/v4/chatSession.js +138 -3
- package/dist/cli/v4/commands/_runtimeToggleHelpers.js +92 -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/help.js +7 -0
- package/dist/cli/v4/commands/index.js +20 -1
- package/dist/cli/v4/commands/runs.js +203 -0
- package/dist/cli/v4/commands/sandbox.js +48 -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 +142 -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 +308 -4
- package/dist/core/v4/browserState.js +436 -0
- package/dist/core/v4/checkpoint.js +79 -0
- package/dist/core/v4/daemon/bootstrap.js +604 -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 +296 -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 +114 -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/recoveryReport.js +449 -0
- package/dist/core/v4/runtimeToggles.js +187 -0
- package/dist/core/v4/sandboxConfig.js +285 -0
- package/dist/core/v4/sandboxFs.js +316 -0
- package/dist/core/v4/suggestionCatalog.js +41 -0
- package/dist/core/v4/suggestionEngine.js +210 -0
- package/dist/core/v4/toolRegistry.js +18 -0
- 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/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 +71 -58
- 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 +2 -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/subagentFanout.js +1 -0
- 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 +7 -1
|
@@ -0,0 +1,76 @@
|
|
|
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/daemon/triggers/webhookSpec.ts — v4.5 Phase 3.
|
|
10
|
+
*
|
|
11
|
+
* Typed spec stored in triggers.spec_json for source='webhook'.
|
|
12
|
+
*
|
|
13
|
+
* Secret handling (corrected design):
|
|
14
|
+
* HMAC verification REQUIRES the raw secret at request time, so
|
|
15
|
+
* we store it RAW. Daemon.db sits under user-private
|
|
16
|
+
* %LOCALAPPDATA%/aiden/daemon/ on Windows and is chmod 600 on POSIX
|
|
17
|
+
* (see db/connection.ts). The raw secret is surfaced to the user
|
|
18
|
+
* ONLY on creation by `aiden trigger add webhook` with an explicit
|
|
19
|
+
* "save this now" warning.
|
|
20
|
+
*
|
|
21
|
+
* INSECURE_NO_AUTH sentinel: literal string the user can place in
|
|
22
|
+
* spec.secret to disable HMAC verification entirely. Only usable
|
|
23
|
+
* when the daemon is bound to loopback (127.0.0.1). Phase 3
|
|
24
|
+
* refuses to start a public-bound daemon with any INSECURE_NO_AUTH
|
|
25
|
+
* route configured.
|
|
26
|
+
*/
|
|
27
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
28
|
+
exports.DEFAULT_WEBHOOK_SPEC = exports.INSECURE_NO_AUTH = void 0;
|
|
29
|
+
exports.parseWebhookSpec = parseWebhookSpec;
|
|
30
|
+
exports.INSECURE_NO_AUTH = '__INSECURE_NO_AUTH__';
|
|
31
|
+
exports.DEFAULT_WEBHOOK_SPEC = {
|
|
32
|
+
hmacFormat: 'generic',
|
|
33
|
+
rateLimit: { perMinute: 30 },
|
|
34
|
+
maxBodyBytes: 1048576, // 1 MiB
|
|
35
|
+
idempotencyTtlMs: 60 * 60 * 1000, // 1 hour
|
|
36
|
+
deliverOnly: false,
|
|
37
|
+
publicBound: false,
|
|
38
|
+
};
|
|
39
|
+
function parseWebhookSpec(raw) {
|
|
40
|
+
const obj = typeof raw === 'string' ? JSON.parse(raw) : raw;
|
|
41
|
+
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
42
|
+
throw new Error('WebhookSpec: input must be an object');
|
|
43
|
+
}
|
|
44
|
+
const o = obj;
|
|
45
|
+
const name = typeof o.name === 'string' ? o.name : '';
|
|
46
|
+
if (!name)
|
|
47
|
+
throw new Error('WebhookSpec: name required');
|
|
48
|
+
const secret = typeof o.secret === 'string' ? o.secret : '';
|
|
49
|
+
if (!secret)
|
|
50
|
+
throw new Error('WebhookSpec: secret required (use INSECURE_NO_AUTH sentinel for loopback testing)');
|
|
51
|
+
const hmacFormat = sanitizeHmacFormat(o.hmacFormat);
|
|
52
|
+
const rateLimitObj = (o.rateLimit && typeof o.rateLimit === 'object') ? o.rateLimit : {};
|
|
53
|
+
const perMinute = sanitizeNum(rateLimitObj.perMinute, exports.DEFAULT_WEBHOOK_SPEC.rateLimit.perMinute, 1);
|
|
54
|
+
return {
|
|
55
|
+
name,
|
|
56
|
+
secret,
|
|
57
|
+
hmacFormat,
|
|
58
|
+
allowedEvents: Array.isArray(o.allowedEvents) ? o.allowedEvents.filter((s) => typeof s === 'string') : undefined,
|
|
59
|
+
rateLimit: { perMinute },
|
|
60
|
+
maxBodyBytes: sanitizeNum(o.maxBodyBytes, exports.DEFAULT_WEBHOOK_SPEC.maxBodyBytes, 1),
|
|
61
|
+
idempotencyTtlMs: sanitizeNum(o.idempotencyTtlMs, exports.DEFAULT_WEBHOOK_SPEC.idempotencyTtlMs, 1),
|
|
62
|
+
deliverOnly: typeof o.deliverOnly === 'boolean' ? o.deliverOnly : exports.DEFAULT_WEBHOOK_SPEC.deliverOnly,
|
|
63
|
+
promptTemplate: typeof o.promptTemplate === 'string' ? o.promptTemplate : undefined,
|
|
64
|
+
publicBound: typeof o.publicBound === 'boolean' ? o.publicBound : exports.DEFAULT_WEBHOOK_SPEC.publicBound,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function sanitizeHmacFormat(v) {
|
|
68
|
+
if (v === 'github' || v === 'gitlab' || v === 'generic')
|
|
69
|
+
return v;
|
|
70
|
+
return exports.DEFAULT_WEBHOOK_SPEC.hmacFormat;
|
|
71
|
+
}
|
|
72
|
+
function sanitizeNum(v, fallback, min) {
|
|
73
|
+
if (typeof v !== 'number' || !Number.isFinite(v) || v < min)
|
|
74
|
+
return fallback;
|
|
75
|
+
return v;
|
|
76
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
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/daemon/triggers/webhookVerifier.ts — v4.5 Phase 3.
|
|
10
|
+
*
|
|
11
|
+
* Per-format HMAC verification. Constant-time comparison via
|
|
12
|
+
* `crypto.timingSafeEqual` to defeat timing attacks.
|
|
13
|
+
*
|
|
14
|
+
* Three formats supported in Phase 3:
|
|
15
|
+
* github — X-Hub-Signature-256: sha256=<hex>
|
|
16
|
+
* HMAC-SHA256(secret, body) == hex
|
|
17
|
+
*
|
|
18
|
+
* gitlab — X-Gitlab-Token: <plain>
|
|
19
|
+
* Plain shared-secret comparison. No HMAC; the token
|
|
20
|
+
* header IS the secret. Constant-time compared.
|
|
21
|
+
*
|
|
22
|
+
* generic — X-Webhook-Signature: <hex>
|
|
23
|
+
* HMAC-SHA256(secret, body) == hex
|
|
24
|
+
*
|
|
25
|
+
* Phase 3.x extensibility: add new entries to `VERIFIERS` map.
|
|
26
|
+
* Stripe (timestamped) and Slack (timestamped) would each get a
|
|
27
|
+
* function that pulls timestamp + signature from headers and
|
|
28
|
+
* verifies HMAC-SHA256(secret, `${ts}.${body}`).
|
|
29
|
+
*/
|
|
30
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
31
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
32
|
+
};
|
|
33
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.verifyWebhookSignature = verifyWebhookSignature;
|
|
35
|
+
exports.deriveEventName = deriveEventName;
|
|
36
|
+
const node_crypto_1 = __importDefault(require("node:crypto"));
|
|
37
|
+
const webhookSpec_1 = require("./webhookSpec");
|
|
38
|
+
function verifyWebhookSignature(opts) {
|
|
39
|
+
// Loopback-only insecure mode — the SAFETY of this depends on
|
|
40
|
+
// the daemon being bound to 127.0.0.1, enforced by bootstrap.
|
|
41
|
+
if (opts.secret === webhookSpec_1.INSECURE_NO_AUTH)
|
|
42
|
+
return true;
|
|
43
|
+
const fn = VERIFIERS[opts.format];
|
|
44
|
+
if (!fn)
|
|
45
|
+
return false;
|
|
46
|
+
try {
|
|
47
|
+
return fn(opts);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const VERIFIERS = {
|
|
54
|
+
github(o) {
|
|
55
|
+
const raw = pickHeader(o.headers, 'x-hub-signature-256');
|
|
56
|
+
if (!raw)
|
|
57
|
+
return false;
|
|
58
|
+
// GitHub header is `sha256=<hex>`. Strip the prefix.
|
|
59
|
+
const m = raw.match(/^sha256=([0-9a-fA-F]+)$/);
|
|
60
|
+
if (!m)
|
|
61
|
+
return false;
|
|
62
|
+
const expected = hmacHex(o.secret, o.body);
|
|
63
|
+
return safeEqualHex(expected, m[1]);
|
|
64
|
+
},
|
|
65
|
+
gitlab(o) {
|
|
66
|
+
const received = pickHeader(o.headers, 'x-gitlab-token');
|
|
67
|
+
if (!received)
|
|
68
|
+
return false;
|
|
69
|
+
// Plain shared-secret. Compare as bytes; constant-time.
|
|
70
|
+
return safeEqualString(o.secret, received);
|
|
71
|
+
},
|
|
72
|
+
generic(o) {
|
|
73
|
+
const raw = pickHeader(o.headers, 'x-webhook-signature');
|
|
74
|
+
if (!raw)
|
|
75
|
+
return false;
|
|
76
|
+
const expected = hmacHex(o.secret, o.body);
|
|
77
|
+
return safeEqualHex(expected, raw.trim());
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
function hmacHex(secret, body) {
|
|
81
|
+
return node_crypto_1.default.createHmac('sha256', secret).update(body).digest('hex');
|
|
82
|
+
}
|
|
83
|
+
function safeEqualHex(a, b) {
|
|
84
|
+
// Both must be same hex length to call timingSafeEqual without
|
|
85
|
+
// throwing.
|
|
86
|
+
if (a.length !== b.length)
|
|
87
|
+
return false;
|
|
88
|
+
try {
|
|
89
|
+
return node_crypto_1.default.timingSafeEqual(Buffer.from(a, 'hex'), Buffer.from(b, 'hex'));
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function safeEqualString(a, b) {
|
|
96
|
+
const ab = Buffer.from(a, 'utf-8');
|
|
97
|
+
const bb = Buffer.from(b, 'utf-8');
|
|
98
|
+
if (ab.length !== bb.length)
|
|
99
|
+
return false;
|
|
100
|
+
try {
|
|
101
|
+
return node_crypto_1.default.timingSafeEqual(ab, bb);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function pickHeader(headers, name) {
|
|
108
|
+
// Express lowercases header names. Be defensive in case caller
|
|
109
|
+
// passes uppercase.
|
|
110
|
+
const k = name.toLowerCase();
|
|
111
|
+
const v = headers[k] ?? headers[name];
|
|
112
|
+
if (Array.isArray(v))
|
|
113
|
+
return v[0] ?? null;
|
|
114
|
+
if (typeof v === 'string')
|
|
115
|
+
return v;
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Map an event name out of the request headers, per format. Used
|
|
120
|
+
* for the optional spec.allowedEvents filter.
|
|
121
|
+
*/
|
|
122
|
+
function deriveEventName(format, headers) {
|
|
123
|
+
if (format === 'github')
|
|
124
|
+
return pickHeader(headers, 'x-github-event') ?? '';
|
|
125
|
+
if (format === 'gitlab')
|
|
126
|
+
return pickHeader(headers, 'x-gitlab-event') ?? '';
|
|
127
|
+
return pickHeader(headers, 'x-webhook-event') ?? '';
|
|
128
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
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/daemon/types.ts — v4.5 Phase 1: shared type surface.
|
|
10
|
+
*
|
|
11
|
+
* Pure types only. No I/O, no imports of side-effecting modules.
|
|
12
|
+
* Keeps the dependency graph one-way (every other daemon module
|
|
13
|
+
* imports from here, but this imports nothing of consequence).
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,461 @@
|
|
|
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/dockerSession.ts — v4.4 Phase 3: long-lived sandbox container
|
|
10
|
+
* lifecycle + reuse cache.
|
|
11
|
+
*
|
|
12
|
+
* One long-lived `docker run -d ... sleep` container is created per
|
|
13
|
+
* session and reused across every `shell_exec` call within that
|
|
14
|
+
* session. Per-command execution goes through `docker exec` — orders
|
|
15
|
+
* of magnitude faster than the old single-shot `docker run --rm`
|
|
16
|
+
* pattern (no image-resolution, no namespace setup, no teardown
|
|
17
|
+
* per call).
|
|
18
|
+
*
|
|
19
|
+
* Lifecycle:
|
|
20
|
+
* - first exec for a sessionId → start container (`docker run -d`),
|
|
21
|
+
* cache the handle, run `docker exec`.
|
|
22
|
+
* - subsequent execs → reuse the cached container.
|
|
23
|
+
* - idle past `idleReaperMs` → background reaper stops + removes.
|
|
24
|
+
* - SIGINT / SIGTERM / beforeExit → reapAllContainers() (parallel
|
|
25
|
+
* best-effort, never blocks shutdown).
|
|
26
|
+
*
|
|
27
|
+
* Concurrency:
|
|
28
|
+
* - Stampede defense: every handle carries an in-flight `starting`
|
|
29
|
+
* promise. A second caller that finds an existing handle with
|
|
30
|
+
* `starting != null` awaits it before proceeding.
|
|
31
|
+
* - Reaper is fire-and-forget; mark the handle `reaped: true` first,
|
|
32
|
+
* then `docker stop` async. Racing callers either find `reaped`
|
|
33
|
+
* and create a new container, or hit a dead container in
|
|
34
|
+
* `docker exec` and we restart.
|
|
35
|
+
*
|
|
36
|
+
* Local fallback (Q-P3-5):
|
|
37
|
+
* - When `isDockerAvailable() === false`, route through
|
|
38
|
+
* `localBackendExecute` and emit a one-time warning per session.
|
|
39
|
+
* The returned `backend` field stays `'local'` so traces are
|
|
40
|
+
* honest about what actually ran.
|
|
41
|
+
*
|
|
42
|
+
* Hardening flags applied unconditionally:
|
|
43
|
+
* --cap-drop ALL
|
|
44
|
+
* --security-opt no-new-privileges
|
|
45
|
+
* --pids-limit <config>
|
|
46
|
+
* --memory <config>
|
|
47
|
+
* --cpus <config>
|
|
48
|
+
* --tmpfs /tmp:rw,size=256m
|
|
49
|
+
* --tmpfs /var/tmp:rw,size=64m
|
|
50
|
+
* --tmpfs /run:rw,size=16m
|
|
51
|
+
* --network <bridge|none>
|
|
52
|
+
* -v cwd:/workspace (only when persistent === true)
|
|
53
|
+
* --tmpfs /workspace:rw,... (only when persistent === false)
|
|
54
|
+
*
|
|
55
|
+
* Gated by `config.enabled` (AIDEN_SANDBOX=1 strict in Phase 1-5).
|
|
56
|
+
* The status-quo single-shot `dockerBackendExecute` in
|
|
57
|
+
* `tools/v4/backends/docker.ts` is retained for the `AIDEN_SANDBOX=0
|
|
58
|
+
* + ctx.terminalBackend='docker'` path — zero regression there.
|
|
59
|
+
*/
|
|
60
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
61
|
+
exports._clearDockerAvailCacheForTests = _clearDockerAvailCacheForTests;
|
|
62
|
+
exports.dockerSessionExec = dockerSessionExec;
|
|
63
|
+
exports.reapSessionContainer = reapSessionContainer;
|
|
64
|
+
exports.reapAllContainers = reapAllContainers;
|
|
65
|
+
exports._resetDockerSessionForTests = _resetDockerSessionForTests;
|
|
66
|
+
exports._inspectDockerSessionsForTests = _inspectDockerSessionsForTests;
|
|
67
|
+
exports._setDockerAvailableForTests = _setDockerAvailableForTests;
|
|
68
|
+
const node_child_process_1 = require("node:child_process");
|
|
69
|
+
const node_crypto_1 = require("node:crypto");
|
|
70
|
+
const sandboxConfig_1 = require("./sandboxConfig");
|
|
71
|
+
const local_1 = require("../../tools/v4/backends/local");
|
|
72
|
+
// ── Module state ────────────────────────────────────────────────────────────
|
|
73
|
+
const _containers = new Map();
|
|
74
|
+
/** Sessions for which we already logged the local-fallback warning. */
|
|
75
|
+
const _warnedFallback = new Set();
|
|
76
|
+
let _reaperInterval = null;
|
|
77
|
+
let _shutdownHookInstalled = false;
|
|
78
|
+
// ── Docker availability cache (60s) ─────────────────────────────────────────
|
|
79
|
+
let _dockerAvailCache = null;
|
|
80
|
+
const DOCKER_AVAIL_TTL = 60000;
|
|
81
|
+
function isDockerAvailableCached() {
|
|
82
|
+
const now = Date.now();
|
|
83
|
+
if (_dockerAvailCache && now - _dockerAvailCache.ts < DOCKER_AVAIL_TTL) {
|
|
84
|
+
return _dockerAvailCache.value;
|
|
85
|
+
}
|
|
86
|
+
let value = false;
|
|
87
|
+
try {
|
|
88
|
+
const r = (0, node_child_process_1.spawnSync)('docker', ['version', '--format', '{{.Server.Version}}'], {
|
|
89
|
+
timeout: 3000,
|
|
90
|
+
stdio: 'pipe',
|
|
91
|
+
});
|
|
92
|
+
value = r.status === 0;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
value = false;
|
|
96
|
+
}
|
|
97
|
+
_dockerAvailCache = { ts: now, value };
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
/** Test-only — clears the docker-availability cache. */
|
|
101
|
+
function _clearDockerAvailCacheForTests() {
|
|
102
|
+
_dockerAvailCache = null;
|
|
103
|
+
}
|
|
104
|
+
// ── Container start ─────────────────────────────────────────────────────────
|
|
105
|
+
function shortId() {
|
|
106
|
+
return (0, node_crypto_1.randomBytes)(4).toString('hex');
|
|
107
|
+
}
|
|
108
|
+
function buildRunArgs(opts) {
|
|
109
|
+
const args = [
|
|
110
|
+
'run',
|
|
111
|
+
'-d',
|
|
112
|
+
'--name', opts.name,
|
|
113
|
+
'--cap-drop', 'ALL',
|
|
114
|
+
'--security-opt', 'no-new-privileges',
|
|
115
|
+
'--pids-limit', String(opts.config.resourceLimits.pidsLimit),
|
|
116
|
+
'--memory', opts.config.resourceLimits.memory,
|
|
117
|
+
'--cpus', opts.config.resourceLimits.cpus,
|
|
118
|
+
'--tmpfs', '/tmp:rw,size=256m',
|
|
119
|
+
'--tmpfs', '/var/tmp:rw,size=64m',
|
|
120
|
+
'--tmpfs', '/run:rw,size=16m',
|
|
121
|
+
'--network', opts.config.networkMode,
|
|
122
|
+
];
|
|
123
|
+
if (opts.persistent) {
|
|
124
|
+
args.push('-v', `${opts.cwd}:/workspace`);
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
args.push('--tmpfs', '/workspace:rw,size=512m');
|
|
128
|
+
}
|
|
129
|
+
args.push('-w', '/workspace');
|
|
130
|
+
args.push(opts.image);
|
|
131
|
+
// sleep loop — busybox `sleep infinity` isn't portable; use a loop
|
|
132
|
+
// so plain alpine images work without GNU coreutils.
|
|
133
|
+
args.push('sh', '-c', 'while true; do sleep 3600; done');
|
|
134
|
+
return args;
|
|
135
|
+
}
|
|
136
|
+
async function startContainer(sessionId, image, cwd, persistent, config) {
|
|
137
|
+
const name = `aiden-sbx-${sessionId.replace(/[^a-zA-Z0-9_.-]/g, '_').slice(0, 32)}-${shortId()}`;
|
|
138
|
+
const args = buildRunArgs({ config, image, cwd, name, persistent });
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
const child = (0, node_child_process_1.spawn)('docker', args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
141
|
+
let stdout = '';
|
|
142
|
+
let stderr = '';
|
|
143
|
+
let timedOut = false;
|
|
144
|
+
const timer = setTimeout(() => {
|
|
145
|
+
timedOut = true;
|
|
146
|
+
try {
|
|
147
|
+
child.kill('SIGTERM');
|
|
148
|
+
}
|
|
149
|
+
catch { /* ignore */ }
|
|
150
|
+
}, 120000); // first run may pull the image
|
|
151
|
+
child.stdout?.on('data', (b) => { stdout += b.toString(); });
|
|
152
|
+
child.stderr?.on('data', (b) => { stderr += b.toString(); });
|
|
153
|
+
child.on('error', (err) => {
|
|
154
|
+
clearTimeout(timer);
|
|
155
|
+
reject(new Error(`docker run failed: ${err.message}`));
|
|
156
|
+
});
|
|
157
|
+
child.on('close', (code) => {
|
|
158
|
+
clearTimeout(timer);
|
|
159
|
+
if (timedOut) {
|
|
160
|
+
reject(new Error('docker run timed out (120s)'));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (code !== 0) {
|
|
164
|
+
reject(new Error(`docker run exited ${code}: ${stderr.trim()}`));
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
const id = stdout.trim().split(/\s+/)[0];
|
|
168
|
+
if (!id) {
|
|
169
|
+
reject(new Error('docker run produced no container id'));
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
resolve(id);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
// ── Container exec ──────────────────────────────────────────────────────────
|
|
177
|
+
function execInContainer(handle, args, cb) {
|
|
178
|
+
handle.lastUsedAt = Date.now();
|
|
179
|
+
const timeoutMs = args.timeoutMs ?? 30000;
|
|
180
|
+
const capture = args.captureOutput ?? true;
|
|
181
|
+
const start = Date.now();
|
|
182
|
+
const execArgs = ['exec', '-i'];
|
|
183
|
+
if (args.env) {
|
|
184
|
+
for (const [k, v] of Object.entries(args.env)) {
|
|
185
|
+
execArgs.push('-e', `${k}=${v}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
execArgs.push(handle.id, 'sh', '-c', args.command);
|
|
189
|
+
return new Promise((resolve) => {
|
|
190
|
+
const child = (0, node_child_process_1.spawn)('docker', execArgs, {
|
|
191
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
192
|
+
});
|
|
193
|
+
let stdout = '';
|
|
194
|
+
let stderr = '';
|
|
195
|
+
let timedOut = false;
|
|
196
|
+
if (capture) {
|
|
197
|
+
child.stdout?.on('data', (b) => {
|
|
198
|
+
const s = b.toString();
|
|
199
|
+
stdout += s;
|
|
200
|
+
cb.log?.('info', s.slice(0, 200));
|
|
201
|
+
});
|
|
202
|
+
child.stderr?.on('data', (b) => {
|
|
203
|
+
const s = b.toString();
|
|
204
|
+
stderr += s;
|
|
205
|
+
cb.log?.('warn', s.slice(0, 200));
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
child.stdout?.resume();
|
|
210
|
+
child.stderr?.resume();
|
|
211
|
+
}
|
|
212
|
+
const timer = setTimeout(() => {
|
|
213
|
+
timedOut = true;
|
|
214
|
+
try {
|
|
215
|
+
child.kill('SIGTERM');
|
|
216
|
+
}
|
|
217
|
+
catch { /* ignore */ }
|
|
218
|
+
setTimeout(() => {
|
|
219
|
+
try {
|
|
220
|
+
child.kill('SIGKILL');
|
|
221
|
+
}
|
|
222
|
+
catch { /* ignore */ }
|
|
223
|
+
}, 2000);
|
|
224
|
+
}, timeoutMs);
|
|
225
|
+
child.on('error', (err) => {
|
|
226
|
+
clearTimeout(timer);
|
|
227
|
+
resolve({
|
|
228
|
+
exitCode: -1,
|
|
229
|
+
stdout,
|
|
230
|
+
stderr: stderr || err.message,
|
|
231
|
+
durationMs: Date.now() - start,
|
|
232
|
+
timedOut,
|
|
233
|
+
backend: 'docker',
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
child.on('close', (code) => {
|
|
237
|
+
clearTimeout(timer);
|
|
238
|
+
resolve({
|
|
239
|
+
exitCode: typeof code === 'number' ? code : -1,
|
|
240
|
+
stdout,
|
|
241
|
+
stderr,
|
|
242
|
+
durationMs: Date.now() - start,
|
|
243
|
+
timedOut,
|
|
244
|
+
backend: 'docker',
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
// ── Public exec entry point ─────────────────────────────────────────────────
|
|
250
|
+
/**
|
|
251
|
+
* Execute a command inside the long-lived sandbox container for the
|
|
252
|
+
* given session. Starts a new container if none exists, reuses
|
|
253
|
+
* otherwise. Falls back to the local backend when Docker isn't
|
|
254
|
+
* available, warning once per session.
|
|
255
|
+
*/
|
|
256
|
+
async function dockerSessionExec(args, cb = {}) {
|
|
257
|
+
const config = (0, sandboxConfig_1.getSandboxConfig)();
|
|
258
|
+
const sessionId = args.sessionId ?? 'default';
|
|
259
|
+
const cwd = args.cwd ?? process.cwd();
|
|
260
|
+
const image = args.image ?? config.image;
|
|
261
|
+
// Local fallback when Docker isn't reachable.
|
|
262
|
+
if (!isDockerAvailableCached()) {
|
|
263
|
+
if (!_warnedFallback.has(sessionId)) {
|
|
264
|
+
_warnedFallback.add(sessionId);
|
|
265
|
+
cb.log?.('warn', 'Sandbox: Docker is not running or unreachable. ' +
|
|
266
|
+
'Falling back to local backend for this session — resource ' +
|
|
267
|
+
'limits and isolation are NOT enforced.');
|
|
268
|
+
}
|
|
269
|
+
return (0, local_1.localBackendExecute)({
|
|
270
|
+
command: args.command,
|
|
271
|
+
cwd,
|
|
272
|
+
env: args.env,
|
|
273
|
+
timeoutMs: args.timeoutMs,
|
|
274
|
+
captureOutput: args.captureOutput,
|
|
275
|
+
}, cb);
|
|
276
|
+
}
|
|
277
|
+
// Opportunistic idle sweep on the active path (Q-P3-4 hybrid).
|
|
278
|
+
sweepIdleAsync(config);
|
|
279
|
+
// Hot path — reuse existing container if any.
|
|
280
|
+
let handle = _containers.get(sessionId);
|
|
281
|
+
if (handle && !handle.reaped) {
|
|
282
|
+
if (handle.starting) {
|
|
283
|
+
try {
|
|
284
|
+
await handle.starting;
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
// start failed; drop the handle and fall through to restart.
|
|
288
|
+
_containers.delete(sessionId);
|
|
289
|
+
handle = undefined;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (!handle || handle.reaped) {
|
|
294
|
+
// Start a new container.
|
|
295
|
+
const newHandle = {
|
|
296
|
+
id: '',
|
|
297
|
+
sessionId,
|
|
298
|
+
image,
|
|
299
|
+
cwd,
|
|
300
|
+
persistent: config.persistent,
|
|
301
|
+
startedAt: Date.now(),
|
|
302
|
+
lastUsedAt: Date.now(),
|
|
303
|
+
reaped: false,
|
|
304
|
+
warnedFallback: false,
|
|
305
|
+
};
|
|
306
|
+
let resolveStart = () => undefined;
|
|
307
|
+
let rejectStart = () => undefined;
|
|
308
|
+
newHandle.starting = new Promise((res, rej) => {
|
|
309
|
+
resolveStart = res;
|
|
310
|
+
rejectStart = rej;
|
|
311
|
+
});
|
|
312
|
+
_containers.set(sessionId, newHandle);
|
|
313
|
+
armReaperIfNeeded();
|
|
314
|
+
try {
|
|
315
|
+
const id = await startContainer(sessionId, image, cwd, config.persistent, config);
|
|
316
|
+
newHandle.id = id;
|
|
317
|
+
newHandle.starting = undefined;
|
|
318
|
+
resolveStart();
|
|
319
|
+
}
|
|
320
|
+
catch (e) {
|
|
321
|
+
const err = e instanceof Error ? e : new Error(String(e));
|
|
322
|
+
_containers.delete(sessionId);
|
|
323
|
+
rejectStart(err);
|
|
324
|
+
return {
|
|
325
|
+
exitCode: -1,
|
|
326
|
+
stdout: '',
|
|
327
|
+
stderr: `Sandbox: failed to start container: ${err.message}`,
|
|
328
|
+
durationMs: 0,
|
|
329
|
+
timedOut: false,
|
|
330
|
+
backend: 'docker',
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
handle = newHandle;
|
|
334
|
+
}
|
|
335
|
+
return execInContainer(handle, args, cb);
|
|
336
|
+
}
|
|
337
|
+
// ── Idle reaper ─────────────────────────────────────────────────────────────
|
|
338
|
+
function sweepIdleAsync(config) {
|
|
339
|
+
const now = Date.now();
|
|
340
|
+
for (const handle of _containers.values()) {
|
|
341
|
+
if (handle.starting)
|
|
342
|
+
continue;
|
|
343
|
+
if (handle.reaped)
|
|
344
|
+
continue;
|
|
345
|
+
if (now - handle.lastUsedAt > config.idleReaperMs) {
|
|
346
|
+
// Fire-and-forget.
|
|
347
|
+
void reapSessionContainer(handle.sessionId);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
function armReaperIfNeeded() {
|
|
352
|
+
if (_reaperInterval)
|
|
353
|
+
return;
|
|
354
|
+
_reaperInterval = setInterval(() => {
|
|
355
|
+
try {
|
|
356
|
+
sweepIdleAsync((0, sandboxConfig_1.getSandboxConfig)());
|
|
357
|
+
if (_containers.size === 0 && _reaperInterval) {
|
|
358
|
+
clearInterval(_reaperInterval);
|
|
359
|
+
_reaperInterval = null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
/* never let the reaper crash the process */
|
|
364
|
+
}
|
|
365
|
+
}, 30000);
|
|
366
|
+
// Don't keep the process alive just for the reaper.
|
|
367
|
+
if (typeof _reaperInterval.unref === 'function') {
|
|
368
|
+
_reaperInterval.unref();
|
|
369
|
+
}
|
|
370
|
+
installShutdownHook();
|
|
371
|
+
}
|
|
372
|
+
function installShutdownHook() {
|
|
373
|
+
if (_shutdownHookInstalled)
|
|
374
|
+
return;
|
|
375
|
+
_shutdownHookInstalled = true;
|
|
376
|
+
const handler = () => {
|
|
377
|
+
// Fire-and-forget — never block shutdown.
|
|
378
|
+
void reapAllContainers();
|
|
379
|
+
};
|
|
380
|
+
process.once('beforeExit', handler);
|
|
381
|
+
process.once('SIGINT', () => { handler(); });
|
|
382
|
+
process.once('SIGTERM', () => { handler(); });
|
|
383
|
+
}
|
|
384
|
+
// ── Reap APIs ───────────────────────────────────────────────────────────────
|
|
385
|
+
function dockerStopRemove(id) {
|
|
386
|
+
return new Promise((resolve) => {
|
|
387
|
+
// -t 2: 2-second grace period before SIGKILL.
|
|
388
|
+
const stop = (0, node_child_process_1.spawn)('docker', ['stop', '-t', '2', id], { stdio: 'ignore' });
|
|
389
|
+
const timer = setTimeout(() => {
|
|
390
|
+
try {
|
|
391
|
+
stop.kill('SIGKILL');
|
|
392
|
+
}
|
|
393
|
+
catch { /* ignore */ }
|
|
394
|
+
}, 5000);
|
|
395
|
+
stop.on('close', () => {
|
|
396
|
+
clearTimeout(timer);
|
|
397
|
+
const rm = (0, node_child_process_1.spawn)('docker', ['rm', '-f', id], { stdio: 'ignore' });
|
|
398
|
+
const rmTimer = setTimeout(() => {
|
|
399
|
+
try {
|
|
400
|
+
rm.kill('SIGKILL');
|
|
401
|
+
}
|
|
402
|
+
catch { /* ignore */ }
|
|
403
|
+
}, 3000);
|
|
404
|
+
rm.on('close', () => { clearTimeout(rmTimer); resolve(); });
|
|
405
|
+
rm.on('error', () => { clearTimeout(rmTimer); resolve(); });
|
|
406
|
+
});
|
|
407
|
+
stop.on('error', () => { clearTimeout(timer); resolve(); });
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
/** Reap one session's container. Idempotent + fire-and-forget safe. */
|
|
411
|
+
async function reapSessionContainer(sessionId) {
|
|
412
|
+
const handle = _containers.get(sessionId);
|
|
413
|
+
if (!handle)
|
|
414
|
+
return;
|
|
415
|
+
if (handle.reaped)
|
|
416
|
+
return;
|
|
417
|
+
handle.reaped = true;
|
|
418
|
+
_containers.delete(sessionId);
|
|
419
|
+
if (handle.id) {
|
|
420
|
+
try {
|
|
421
|
+
await dockerStopRemove(handle.id);
|
|
422
|
+
}
|
|
423
|
+
catch { /* never throw on cleanup */ }
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/** Reap every cached container. Used by shutdown hooks. */
|
|
427
|
+
async function reapAllContainers() {
|
|
428
|
+
const ids = Array.from(_containers.keys());
|
|
429
|
+
await Promise.all(ids.map((sid) => reapSessionContainer(sid)));
|
|
430
|
+
if (_reaperInterval) {
|
|
431
|
+
clearInterval(_reaperInterval);
|
|
432
|
+
_reaperInterval = null;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
// ── Test helpers ────────────────────────────────────────────────────────────
|
|
436
|
+
/** Test-only — synchronously wipe in-memory state. Does NOT call docker. */
|
|
437
|
+
function _resetDockerSessionForTests() {
|
|
438
|
+
_containers.clear();
|
|
439
|
+
_warnedFallback.clear();
|
|
440
|
+
if (_reaperInterval) {
|
|
441
|
+
clearInterval(_reaperInterval);
|
|
442
|
+
_reaperInterval = null;
|
|
443
|
+
}
|
|
444
|
+
_dockerAvailCache = null;
|
|
445
|
+
}
|
|
446
|
+
/** Test-only — inspect the cache state. */
|
|
447
|
+
function _inspectDockerSessionsForTests() {
|
|
448
|
+
return {
|
|
449
|
+
count: _containers.size,
|
|
450
|
+
sessionIds: Array.from(_containers.keys()),
|
|
451
|
+
warnedSessions: Array.from(_warnedFallback),
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Test-only — inject a fake docker-availability decision so unit
|
|
456
|
+
* tests can exercise the local-fallback warn-once path without
|
|
457
|
+
* actually probing docker.
|
|
458
|
+
*/
|
|
459
|
+
function _setDockerAvailableForTests(value) {
|
|
460
|
+
_dockerAvailCache = { ts: Date.now(), value };
|
|
461
|
+
}
|