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,98 @@
|
|
|
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/dispatcher/agentRunner.ts — v4.5 Phase 5a.
|
|
10
|
+
*
|
|
11
|
+
* The seam between the dispatcher's claim/markDone loop and an
|
|
12
|
+
* AidenAgent invocation. Keeps the dispatcher testable without
|
|
13
|
+
* dragging in the full agent / provider / tool stack.
|
|
14
|
+
*
|
|
15
|
+
* Two responsibilities:
|
|
16
|
+
* 1. Define the `DaemonAgentRunner` interface (one `invoke` method).
|
|
17
|
+
* 2. Define `DaemonAgentInput` / `DaemonAgentResult` so the
|
|
18
|
+
* dispatcher and bootstrap-side wiring agree on shape.
|
|
19
|
+
*
|
|
20
|
+
* Bootstrap is responsible for building a runner that holds the
|
|
21
|
+
* AidenAgent + provider + toolExecutor + plumbing. Tests pass a
|
|
22
|
+
* stub runner.
|
|
23
|
+
*
|
|
24
|
+
* Two pure helpers also live here:
|
|
25
|
+
* - `buildInitialHistory(input)` — turns a `DaemonAgentInput`
|
|
26
|
+
* into the `Message[]` the agent's `runConversation` expects.
|
|
27
|
+
* Used by both the real runner and integration tests that
|
|
28
|
+
* assert the dispatched message shape.
|
|
29
|
+
* - `deliverOnlyStub(input, runStore, instanceId)` — the
|
|
30
|
+
* deliver-only short-circuit. Q-P5-4(a): logs delivery via
|
|
31
|
+
* a run_event row and returns immediately without invoking
|
|
32
|
+
* the agent. Channel-adapter integration (Telegram, Discord,
|
|
33
|
+
* etc.) is deferred to a future phase.
|
|
34
|
+
*
|
|
35
|
+
* The runner returns the `runId` written into `runs`. The
|
|
36
|
+
* dispatcher passes that to `triggerBus.markDone(eventId,
|
|
37
|
+
* claimToken, runId)` so the link FK is populated.
|
|
38
|
+
*/
|
|
39
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
40
|
+
exports.buildInitialHistory = buildInitialHistory;
|
|
41
|
+
exports.deliverOnlyStub = deliverOnlyStub;
|
|
42
|
+
exports.makeRunner = makeRunner;
|
|
43
|
+
// ── Pure helpers ───────────────────────────────────────────────────────────
|
|
44
|
+
/**
|
|
45
|
+
* Construct the initial `Message[]` history fed to
|
|
46
|
+
* `AidenAgent.runConversation`. Single user message carrying
|
|
47
|
+
* the rendered initial message. The system prompt is layered
|
|
48
|
+
* on by the agent itself.
|
|
49
|
+
*
|
|
50
|
+
* Pure — no side effects, deterministic per input.
|
|
51
|
+
*/
|
|
52
|
+
function buildInitialHistory(input) {
|
|
53
|
+
return [
|
|
54
|
+
{
|
|
55
|
+
role: 'user',
|
|
56
|
+
content: input.initialMessage,
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Q-P5-4(a) deliver-only short-circuit.
|
|
62
|
+
*
|
|
63
|
+
* Creates a `runs` row with status `completed`, emits a
|
|
64
|
+
* `delivered` run_event capturing the rendered message, and
|
|
65
|
+
* returns a `DaemonAgentResult` with finishReason='delivered'.
|
|
66
|
+
*
|
|
67
|
+
* Channel-adapter integration (Telegram / Discord / Slack
|
|
68
|
+
* webhook target / etc.) is deferred. The stub records that
|
|
69
|
+
* "delivery" happened (logs only) so operators can verify the
|
|
70
|
+
* code path via run_events without yet wiring a transport.
|
|
71
|
+
*/
|
|
72
|
+
function deliverOnlyStub(input, runStore) {
|
|
73
|
+
const runId = runStore.create({
|
|
74
|
+
sessionId: input.sessionId,
|
|
75
|
+
instanceId: input.instanceId,
|
|
76
|
+
triggerEventId: input.triggerEventId,
|
|
77
|
+
status: 'running',
|
|
78
|
+
});
|
|
79
|
+
runStore.emitEvent(runId, 'delivered', {
|
|
80
|
+
source: input.triggerContext.source,
|
|
81
|
+
triggerId: input.triggerContext.triggerId,
|
|
82
|
+
eventId: input.triggerEventId,
|
|
83
|
+
messageBytes: input.initialMessage.length,
|
|
84
|
+
deliverOnly: true,
|
|
85
|
+
/* future: target channel, adapter, response */
|
|
86
|
+
});
|
|
87
|
+
runStore.setStatus(runId, 'completed', { finishReason: 'delivered' });
|
|
88
|
+
return { runId, finishReason: 'delivered' };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Wrap a function-shaped invocation into the `DaemonAgentRunner`
|
|
92
|
+
* interface. Convenience for tests + simple wiring.
|
|
93
|
+
*
|
|
94
|
+
* const runner = makeRunner(async (input) => ({ runId, finishReason: 'stop' }));
|
|
95
|
+
*/
|
|
96
|
+
function makeRunner(invoke) {
|
|
97
|
+
return { invoke };
|
|
98
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
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/dispatcher/budgetGate.ts — v4.5 Phase 7.
|
|
10
|
+
*
|
|
11
|
+
* Two-layer cost guardrail for daemon-fired agent turns:
|
|
12
|
+
*
|
|
13
|
+
* 1. PER-TRIGGER (soft) — `spec.maxTokensPerFire`. Each turn
|
|
14
|
+
* tracks token usage via `AidenAgent.onBudgetWarning`. When
|
|
15
|
+
* crossed, the turn ABORTS via the wrap-controller's abort
|
|
16
|
+
* signal; finishReason becomes `budget_exhausted`. NO bus-
|
|
17
|
+
* level retry — that would just re-trigger the same blowup.
|
|
18
|
+
*
|
|
19
|
+
* 2. GLOBAL (hard) — `AIDEN_DAEMON_DAILY_BUDGET=<tokens>` env.
|
|
20
|
+
* Pre-flight check before invoking the agent at all. When
|
|
21
|
+
* hit, the dispatcher rejects new claims with a
|
|
22
|
+
* `trigger_quota` tag (classifier maps to that category;
|
|
23
|
+
* dispatcher dead-letters by retry-matrix).
|
|
24
|
+
*
|
|
25
|
+
* The gate exposes a single `evaluatePreTurn()` that combines
|
|
26
|
+
* both layers + returns a structured verdict. Callers wrap the
|
|
27
|
+
* agent loop in an `AbortController` whose `signal` the per-trigger
|
|
28
|
+
* watcher trips on threshold cross.
|
|
29
|
+
*
|
|
30
|
+
* Token accounting is best-effort — the agent's onToolCall +
|
|
31
|
+
* provider response token counts feed the watcher. Providers
|
|
32
|
+
* that don't surface usage (rare) get a zero-cost turn and
|
|
33
|
+
* neither cap fires. That's acceptable: the global daily budget
|
|
34
|
+
* is the strict safety net regardless.
|
|
35
|
+
*/
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.evaluatePreTurn = evaluatePreTurn;
|
|
38
|
+
exports.consumePostTurn = consumePostTurn;
|
|
39
|
+
exports.createPerTurnBudgetWatcher = createPerTurnBudgetWatcher;
|
|
40
|
+
/**
|
|
41
|
+
* Pre-flight check before invoking the agent. When the daily
|
|
42
|
+
* budget is already exhausted, returns allowed=false WITHOUT
|
|
43
|
+
* consuming. When estimated tokens would push over the cap,
|
|
44
|
+
* also rejects pre-emptively. When the estimate fits (or
|
|
45
|
+
* unknown), allows — the per-trigger watcher catches in-flight
|
|
46
|
+
* overruns.
|
|
47
|
+
*
|
|
48
|
+
* Does NOT consume tokens itself. Token consumption happens
|
|
49
|
+
* mid/post-turn via `consumePostTurn()`.
|
|
50
|
+
*/
|
|
51
|
+
function evaluatePreTurn(input) {
|
|
52
|
+
const snapshot = input.tracker.peek({ budget: input.dailyBudget, now: input.now });
|
|
53
|
+
if (snapshot.exhausted) {
|
|
54
|
+
return {
|
|
55
|
+
allowed: false,
|
|
56
|
+
daily: snapshot,
|
|
57
|
+
reason: `trigger_quota: daily_budget_exhausted (used=${snapshot.used}/${snapshot.budget})`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (input.estimatedTokens && input.estimatedTokens > 0 && snapshot.budget !== null && snapshot.budget > 0) {
|
|
61
|
+
if (snapshot.used + input.estimatedTokens > snapshot.budget) {
|
|
62
|
+
return {
|
|
63
|
+
allowed: false,
|
|
64
|
+
daily: snapshot,
|
|
65
|
+
reason: `trigger_quota: estimated ${input.estimatedTokens} tokens exceeds remaining ${snapshot.remaining}`,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return { allowed: true, daily: snapshot };
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Post-turn consumption. Caller passes the ACTUAL tokens the
|
|
73
|
+
* agent reported (or 0 when the provider didn't surface usage).
|
|
74
|
+
* Returns the post-consume snapshot for the dispatcher:completed
|
|
75
|
+
* event payload. Never throws — over-budget consume is just
|
|
76
|
+
* recorded (the next pre-turn check will reject the next claim).
|
|
77
|
+
*/
|
|
78
|
+
function consumePostTurn(input) {
|
|
79
|
+
if (input.actualTokens <= 0)
|
|
80
|
+
return input.tracker.peek({ budget: input.dailyBudget, now: input.now });
|
|
81
|
+
const r = input.tracker.addAndCheck(input.actualTokens, {
|
|
82
|
+
budget: input.dailyBudget,
|
|
83
|
+
now: input.now,
|
|
84
|
+
});
|
|
85
|
+
return r.snapshot;
|
|
86
|
+
}
|
|
87
|
+
function createPerTurnBudgetWatcher(opts) {
|
|
88
|
+
const limit = opts.maxTokensPerFire ?? null;
|
|
89
|
+
const ctl = new AbortController();
|
|
90
|
+
let used = 0;
|
|
91
|
+
let hit = false;
|
|
92
|
+
let reason = null;
|
|
93
|
+
function check() {
|
|
94
|
+
if (hit)
|
|
95
|
+
return;
|
|
96
|
+
if (limit === null || limit <= 0)
|
|
97
|
+
return;
|
|
98
|
+
if (used >= limit) {
|
|
99
|
+
hit = true;
|
|
100
|
+
reason = `per_trigger_budget_exhausted: ${used}/${limit}`;
|
|
101
|
+
try {
|
|
102
|
+
ctl.abort(reason);
|
|
103
|
+
}
|
|
104
|
+
catch { /* abort may throw on older Node; safe to swallow */ }
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
tally(tokens) {
|
|
109
|
+
if (tokens > 0) {
|
|
110
|
+
used += tokens;
|
|
111
|
+
check();
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
used() { return used; },
|
|
115
|
+
hit() { return hit; },
|
|
116
|
+
reason() { return reason; },
|
|
117
|
+
signal: ctl.signal,
|
|
118
|
+
abort(r) {
|
|
119
|
+
hit = true;
|
|
120
|
+
reason = r ?? 'manual_abort';
|
|
121
|
+
try {
|
|
122
|
+
ctl.abort(reason);
|
|
123
|
+
}
|
|
124
|
+
catch { /* noop */ }
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
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/dispatcher/daemonApproval.ts — v4.5 Phase 7.
|
|
10
|
+
*
|
|
11
|
+
* Non-interactive approval callbacks for daemon-mode agent turns.
|
|
12
|
+
* Interactive `approvalEngine.promptUser` requires a TTY; daemon-
|
|
13
|
+
* fired turns have no user, so we hand the engine an auto-decider
|
|
14
|
+
* keyed on the per-trigger policy (Q-P7-1a default: 'safe-only').
|
|
15
|
+
*
|
|
16
|
+
* Decision table:
|
|
17
|
+
*
|
|
18
|
+
* policy safe caution dangerous
|
|
19
|
+
* --------------- ------ -------- ---------
|
|
20
|
+
* safe-only allow DENY DENY ← default
|
|
21
|
+
* caution-ok allow allow DENY
|
|
22
|
+
* dangerous-ok allow allow allow
|
|
23
|
+
*
|
|
24
|
+
* Every decision emits an `approval_decision` run_event with
|
|
25
|
+
* `{toolName, riskTier, decision, policy}` so the operator can
|
|
26
|
+
* audit what was let through and what was blocked.
|
|
27
|
+
*
|
|
28
|
+
* Denied tools surface to the agent as an `approval_denied`
|
|
29
|
+
* envelope; the daemon:dispatcher classifier maps these to
|
|
30
|
+
* `trigger_misconfigured` (already wired in failureClassifier.ts).
|
|
31
|
+
* Per-trigger override is loaded by the runner via
|
|
32
|
+
* `triggers.spec_json.daemonApproval` and passed to this builder.
|
|
33
|
+
*/
|
|
34
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
+
exports.DEFAULT_DAEMON_APPROVAL_POLICY = void 0;
|
|
36
|
+
exports.buildDaemonApprovalCallbacks = buildDaemonApprovalCallbacks;
|
|
37
|
+
exports.decideForPolicy = decideForPolicy;
|
|
38
|
+
exports.isDaemonApprovalPolicy = isDaemonApprovalPolicy;
|
|
39
|
+
exports.DEFAULT_DAEMON_APPROVAL_POLICY = 'safe-only';
|
|
40
|
+
/**
|
|
41
|
+
* Build a non-interactive `ApprovalCallbacks` instance for use by
|
|
42
|
+
* a daemon-mode `ApprovalEngine`. Returns the same shape the
|
|
43
|
+
* interactive REPL builds — only `promptUser` is auto-deciding
|
|
44
|
+
* (instead of asking the user). `riskAssess` is intentionally
|
|
45
|
+
* absent: smart-mode auxiliary classifier doesn't fit a non-
|
|
46
|
+
* interactive turn (it'd burn an extra LLM call per tool and the
|
|
47
|
+
* tier table already encodes the policy).
|
|
48
|
+
*/
|
|
49
|
+
function buildDaemonApprovalCallbacks(input) {
|
|
50
|
+
const policy = input.policy;
|
|
51
|
+
const log = input.log ?? (() => { });
|
|
52
|
+
return {
|
|
53
|
+
/**
|
|
54
|
+
* The agent loop only invokes `promptUser` when the engine
|
|
55
|
+
* can't auto-decide via mode + tier. Daemon callbacks NEVER
|
|
56
|
+
* defer to a human — every call resolves here synchronously
|
|
57
|
+
* (well, via Promise.resolve for shape parity).
|
|
58
|
+
*/
|
|
59
|
+
promptUser: async (req) => {
|
|
60
|
+
const decision = decideForPolicy(policy, req.riskTier ?? 'caution');
|
|
61
|
+
return decision;
|
|
62
|
+
},
|
|
63
|
+
/**
|
|
64
|
+
* `onDecision` fires AFTER every decision (allow + deny). We
|
|
65
|
+
* use it as the emission hook for the `approval_decision`
|
|
66
|
+
* run_event so EVERY decision lands in the audit log — not
|
|
67
|
+
* just the deferred-to-promptUser ones.
|
|
68
|
+
*/
|
|
69
|
+
onDecision: (req, decision) => {
|
|
70
|
+
try {
|
|
71
|
+
input.runStore.emitEvent(input.runId, 'approval_decision', {
|
|
72
|
+
toolName: req.toolName,
|
|
73
|
+
category: req.category,
|
|
74
|
+
riskTier: req.riskTier ?? 'caution',
|
|
75
|
+
reason: req.reason ?? null,
|
|
76
|
+
policy,
|
|
77
|
+
decision,
|
|
78
|
+
});
|
|
79
|
+
if (decision === 'deny') {
|
|
80
|
+
log('warn', `[daemon-approval] denied ${req.toolName} (tier=${req.riskTier ?? 'caution'} policy=${policy})`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
catch { /* never let logging crash the agent loop */ }
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Pure decision function. Public for testing.
|
|
89
|
+
*
|
|
90
|
+
* safe-only → safe → allow_session, caution|dangerous → deny
|
|
91
|
+
* caution-ok → safe|caution → allow_session, dangerous → deny
|
|
92
|
+
* dangerous-ok → safe|caution|dangerous → allow_session
|
|
93
|
+
*
|
|
94
|
+
* Returns `allow_session` (not `allow_always`) so the agent's
|
|
95
|
+
* session-scoped allowlist doesn't grow unbounded across daemon
|
|
96
|
+
* turns of the same sessionId. The session-scoped allowlist DOES
|
|
97
|
+
* persist across multiple tool calls within ONE runConversation,
|
|
98
|
+
* which is the optimization we want: a webhook turn that touches
|
|
99
|
+
* `file_read` twice doesn't pay the auto-decide cost twice.
|
|
100
|
+
*/
|
|
101
|
+
function decideForPolicy(policy, tier) {
|
|
102
|
+
if (tier === 'safe')
|
|
103
|
+
return 'allow_session';
|
|
104
|
+
if (tier === 'caution') {
|
|
105
|
+
return policy === 'safe-only' ? 'deny' : 'allow_session';
|
|
106
|
+
}
|
|
107
|
+
// dangerous
|
|
108
|
+
return policy === 'dangerous-ok' ? 'allow_session' : 'deny';
|
|
109
|
+
}
|
|
110
|
+
/** Type-guard for runtime spec validation. */
|
|
111
|
+
function isDaemonApprovalPolicy(s) {
|
|
112
|
+
return s === 'safe-only' || s === 'caution-ok' || s === 'dangerous-ok';
|
|
113
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
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/dispatcher/dailyBudgetTracker.ts — v4.5 Phase 7.
|
|
10
|
+
*
|
|
11
|
+
* Persistent daily-token-budget counter for daemon-mode agent
|
|
12
|
+
* turns. Defends against "I set up 50 triggers and the bill
|
|
13
|
+
* exploded" (Q-P7-4a).
|
|
14
|
+
*
|
|
15
|
+
* Storage: piggyback on the existing `idempotency_keys` table
|
|
16
|
+
* via `scope='daemon_budget'` rows — no schema bump. One row per
|
|
17
|
+
* UTC day, `key=<yyyy-mm-dd>`, `response_json` carries the
|
|
18
|
+
* running total. `status_code` unused (200). `expires_at` set 7
|
|
19
|
+
* days out so the L2 sweep cleans old rows automatically.
|
|
20
|
+
*
|
|
21
|
+
* Threading: synchronous SQL writes serialize the counter at
|
|
22
|
+
* the SQLite level. Concurrency-safe against multiple producer
|
|
23
|
+
* threads in the same process. Cross-process daemons would need
|
|
24
|
+
* a separate locking step (out of scope — single-daemon-per-host
|
|
25
|
+
* is the v4.5 invariant).
|
|
26
|
+
*
|
|
27
|
+
* Cap evaluation: `addAndCheck(tokens)` returns `{allowed,
|
|
28
|
+
* remaining, used}`. Allowed=false means EITHER the add would
|
|
29
|
+
* exceed the budget OR the budget is already exhausted. Callers
|
|
30
|
+
* (`budgetGate.ts`) handle the reject path.
|
|
31
|
+
*/
|
|
32
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
33
|
+
exports.createDailyBudgetTracker = createDailyBudgetTracker;
|
|
34
|
+
exports.utcDateKey = utcDateKey;
|
|
35
|
+
const BUDGET_SCOPE = 'daemon_budget';
|
|
36
|
+
const ROW_TTL_DAYS = 7;
|
|
37
|
+
function createDailyBudgetTracker(opts) {
|
|
38
|
+
const db = opts.db;
|
|
39
|
+
const configuredBudget = opts.budget ?? null;
|
|
40
|
+
function readRow(date) {
|
|
41
|
+
const row = db.prepare(`SELECT response_json FROM idempotency_keys WHERE scope = ? AND key = ?`).get(BUDGET_SCOPE, date);
|
|
42
|
+
if (!row)
|
|
43
|
+
return 0;
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(row.response_json);
|
|
46
|
+
return typeof parsed.used === 'number' ? parsed.used : 0;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function writeRow(date, used, now) {
|
|
53
|
+
const expiresAt = now + ROW_TTL_DAYS * 24 * 60 * 60 * 1000;
|
|
54
|
+
db.prepare(`INSERT OR REPLACE INTO idempotency_keys
|
|
55
|
+
(scope, key, fingerprint, response_json, status_code, created_at, expires_at)
|
|
56
|
+
VALUES (?, ?, NULL, ?, 200, ?, ?)`).run(BUDGET_SCOPE, date, JSON.stringify({ used }), now, expiresAt);
|
|
57
|
+
}
|
|
58
|
+
function buildSnapshot(date, used, budget) {
|
|
59
|
+
return {
|
|
60
|
+
date,
|
|
61
|
+
used,
|
|
62
|
+
budget,
|
|
63
|
+
remaining: budget !== null && budget > 0 ? Math.max(0, budget - used) : Number.POSITIVE_INFINITY,
|
|
64
|
+
exhausted: budget !== null && budget > 0 && used >= budget,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
addAndCheck(tokens, opts2 = {}) {
|
|
69
|
+
const now = opts2.now ?? Date.now();
|
|
70
|
+
const date = utcDateKey(now);
|
|
71
|
+
const budget = opts2.budget !== undefined ? opts2.budget : configuredBudget;
|
|
72
|
+
const tx = db.transaction(() => {
|
|
73
|
+
const used = readRow(date);
|
|
74
|
+
// Refuse to consume when already at/over cap.
|
|
75
|
+
if (budget !== null && budget > 0 && used >= budget) {
|
|
76
|
+
return {
|
|
77
|
+
allowed: false,
|
|
78
|
+
snapshot: buildSnapshot(date, used, budget),
|
|
79
|
+
reason: `daily_budget_exhausted: ${used}/${budget} tokens used today (${date})`,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// Refuse to consume when this call would push over cap.
|
|
83
|
+
const next = used + Math.max(0, tokens);
|
|
84
|
+
if (budget !== null && budget > 0 && next > budget) {
|
|
85
|
+
return {
|
|
86
|
+
allowed: false,
|
|
87
|
+
snapshot: buildSnapshot(date, used, budget),
|
|
88
|
+
reason: `daily_budget_would_exceed: ${tokens} > remaining ${budget - used}`,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
writeRow(date, next, now);
|
|
92
|
+
return {
|
|
93
|
+
allowed: true,
|
|
94
|
+
snapshot: buildSnapshot(date, next, budget),
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
return tx();
|
|
98
|
+
},
|
|
99
|
+
peek(opts2 = {}) {
|
|
100
|
+
const now = opts2.now ?? Date.now();
|
|
101
|
+
const date = utcDateKey(now);
|
|
102
|
+
const budget = opts2.budget !== undefined ? opts2.budget : configuredBudget;
|
|
103
|
+
const used = readRow(date);
|
|
104
|
+
return buildSnapshot(date, used, budget);
|
|
105
|
+
},
|
|
106
|
+
reset(opts2 = {}) {
|
|
107
|
+
const now = opts2.now ?? Date.now();
|
|
108
|
+
const date = utcDateKey(now);
|
|
109
|
+
writeRow(date, 0, now);
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
/** `2026-05-17` for the day midnight UTC of `nowMs`. Public for tests. */
|
|
114
|
+
function utcDateKey(nowMs) {
|
|
115
|
+
const d = new Date(nowMs);
|
|
116
|
+
const y = d.getUTCFullYear();
|
|
117
|
+
const m = String(d.getUTCMonth() + 1).padStart(2, '0');
|
|
118
|
+
const day = String(d.getUTCDate()).padStart(2, '0');
|
|
119
|
+
return `${y}-${m}-${day}`;
|
|
120
|
+
}
|