aiden-runtime 4.8.0 → 4.9.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 +88 -1
- package/dist/cli/v4/aidenCLI.js +35 -4
- package/dist/cli/v4/chatSession.js +43 -16
- package/dist/cli/v4/commands/daemon.js +47 -2
- package/dist/cli/v4/commands/daemonDoctor.js +212 -0
- package/dist/cli/v4/commands/daemonStatus.js +1 -1
- package/dist/cli/v4/commands/help.js +2 -0
- package/dist/cli/v4/commands/hooks.js +428 -0
- package/dist/cli/v4/commands/index.js +5 -1
- package/dist/cli/v4/commands/mcp.js +89 -1
- package/dist/cli/v4/commands/mcpClientInstall.js +359 -0
- package/dist/cli/v4/commands/memory.js +702 -0
- package/dist/cli/v4/commands/recovery.js +1 -1
- package/dist/cli/v4/commands/skin.js +7 -0
- package/dist/cli/v4/commands/theme.js +217 -0
- package/dist/cli/v4/commands/trigger.js +1 -1
- package/dist/cli/v4/commands/update.js +14 -2
- package/dist/cli/v4/design/tokens.js +52 -4
- package/dist/cli/v4/display.js +102 -46
- package/dist/cli/v4/pasteIntercept.js +214 -70
- package/dist/cli/v4/replyRenderer.js +145 -5
- package/dist/cli/v4/skinEngine.js +67 -0
- package/dist/core/v4/aidenAgent.js +45 -2
- package/dist/core/v4/daemon/api/runs.js +131 -0
- package/dist/core/v4/daemon/bootstrap.js +368 -13
- package/dist/core/v4/daemon/db/migrations.js +169 -0
- package/dist/core/v4/daemon/idempotency/runIdempotencyStore.js +128 -0
- package/dist/core/v4/daemon/incarnationStore.js +47 -0
- package/dist/core/v4/daemon/runs/attemptStore.js +67 -0
- package/dist/core/v4/daemon/runs/reclaim.js +88 -0
- package/dist/core/v4/daemon/runs/retryPolicy.js +73 -0
- package/dist/core/v4/daemon/runs/runWithRetry.js +80 -0
- package/dist/core/v4/daemon/runs/stuckAttemptWatchdog.js +72 -0
- package/dist/core/v4/daemon/spans/spanHelpers.js +272 -0
- package/dist/core/v4/daemon/spans/spanStore.js +113 -0
- package/dist/core/v4/daemon/triggerBus.js +50 -19
- package/dist/core/v4/hooks/auditQuery.js +67 -0
- package/dist/core/v4/hooks/dispatcher.js +286 -0
- package/dist/core/v4/hooks/index.js +46 -0
- package/dist/core/v4/hooks/lifecycle.js +27 -0
- package/dist/core/v4/hooks/manifest.js +142 -0
- package/dist/core/v4/hooks/registry.js +149 -0
- package/dist/core/v4/hooks/runtime/subprocessRunner.js +188 -0
- package/dist/core/v4/hooks/toolHookGate.js +76 -0
- package/dist/core/v4/hooks/trust.js +14 -0
- package/dist/core/v4/identity/contextManager.js +83 -0
- package/dist/core/v4/identity/daemonId.js +85 -0
- package/dist/core/v4/identity/enforcement.js +103 -0
- package/dist/core/v4/identity/executionContext.js +153 -0
- package/dist/core/v4/identity/hookExecution.js +62 -0
- package/dist/core/v4/identity/httpContext.js +68 -0
- package/dist/core/v4/identity/ids.js +185 -0
- package/dist/core/v4/identity/index.js +60 -0
- package/dist/core/v4/identity/subprocessContext.js +98 -0
- package/dist/core/v4/identity/traceparent.js +114 -0
- package/dist/core/v4/logger/index.js +3 -1
- package/dist/core/v4/logger/logger.js +28 -1
- package/dist/core/v4/logger/redact.js +149 -0
- package/dist/core/v4/logger/sinks/fileSink.js +13 -0
- package/dist/core/v4/logger/sinks/stdSink.js +19 -1
- package/dist/core/v4/mcp/install/backup.js +78 -0
- package/dist/core/v4/mcp/install/clientPaths.js +90 -0
- package/dist/core/v4/mcp/install/clients.js +203 -0
- package/dist/core/v4/mcp/install/healthCheck.js +83 -0
- package/dist/core/v4/mcp/install/jsoncMerge.js +174 -0
- package/dist/core/v4/mcp/install/profiles.js +109 -0
- package/dist/core/v4/mcp/install/wslDetect.js +62 -0
- package/dist/core/v4/memory/namespaceRegistry.js +117 -0
- package/dist/core/v4/memory/projectRoot.js +76 -0
- package/dist/core/v4/memory/reviewer/index.js +162 -0
- package/dist/core/v4/memory/reviewer/pendingStore.js +136 -0
- package/dist/core/v4/memory/reviewer/prompt.js +105 -0
- package/dist/core/v4/memory/reviewer/skipRules.js +92 -0
- package/dist/core/v4/memoryManager.js +57 -10
- package/dist/core/v4/paths.js +2 -0
- package/dist/core/v4/promptBuilder.js +6 -0
- package/dist/core/v4/subagent/spawnSubAgent.js +20 -7
- package/dist/core/v4/theme/bundledThemes.js +106 -0
- package/dist/core/v4/theme/themeLoader.js +160 -0
- package/dist/core/v4/theme/themeRegistry.js +97 -0
- package/dist/core/v4/theme/themeWatcher.js +95 -0
- package/dist/core/v4/toolRegistry.js +71 -8
- package/dist/core/v4/update/executeInstall.js +10 -6
- package/dist/core/v4/update/installMethodDetect.js +7 -0
- package/dist/core/version.js +67 -2
- package/dist/moat/approvalEngine.js +4 -0
- package/dist/moat/memoryGuard.js +8 -1
- package/dist/providers/v4/anthropicAdapter.js +10 -4
- package/dist/tools/v4/backends/local.js +19 -2
- package/dist/tools/v4/sessions/recallSession.js +6 -1
- package/package.json +3 -3
- package/plugins/aiden-plugin-chatgpt-plus/index.js +10 -1
- package/themes/default.yaml +52 -0
- package/themes/dracula.yaml +32 -0
- package/themes/light.yaml +32 -0
- package/themes/monochrome.yaml +31 -0
- package/themes/tokyo-night.yaml +32 -0
- package/dist/core/pluginSystem.js +0 -121
- package/dist/tools/v4/ui/_uiSmokeTool.js +0 -60
|
@@ -0,0 +1,188 @@
|
|
|
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/hooks/runtime/subprocessRunner.ts — v4.9.0 Slice 12a.
|
|
10
|
+
*
|
|
11
|
+
* Spawn a hook entrypoint in a child process with a scrubbed
|
|
12
|
+
* environment, pipe JSON to stdin, capture stdout (≤64 KB) and
|
|
13
|
+
* stderr (≤16 KB), enforce a timeout via SIGKILL, parse the JSON
|
|
14
|
+
* response, and return a structured outcome envelope.
|
|
15
|
+
*
|
|
16
|
+
* `shell: false` always — no shell expansion, the manifest argv is
|
|
17
|
+
* exec'd directly. Only a curated allowlist of env vars is exported
|
|
18
|
+
* to the child; Aiden's API keys, OAuth tokens, and AIDEN_* config
|
|
19
|
+
* vars are NEVER inherited.
|
|
20
|
+
*/
|
|
21
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
22
|
+
exports.runHookSubprocess = runHookSubprocess;
|
|
23
|
+
const node_child_process_1 = require("node:child_process");
|
|
24
|
+
const node_crypto_1 = require("node:crypto");
|
|
25
|
+
const STDOUT_CAP = 64 * 1024;
|
|
26
|
+
const STDERR_CAP = 16 * 1024;
|
|
27
|
+
/**
|
|
28
|
+
* Env keys that are SAFE to pass through to a hook subprocess. Anything
|
|
29
|
+
* not on this list is dropped. PATH is whitelisted but reset to the
|
|
30
|
+
* system PATH (no Aiden-prepended dirs). The four AIDEN_* keys are the
|
|
31
|
+
* minimal correlation identifiers the hook needs; api keys / oauth
|
|
32
|
+
* tokens / model config never reach the subprocess.
|
|
33
|
+
*/
|
|
34
|
+
const SAFE_ENV_KEYS = new Set([
|
|
35
|
+
'PATH', 'SystemRoot', 'COMSPEC', 'TEMP', 'TMP', 'TMPDIR',
|
|
36
|
+
'HOME', 'USERPROFILE', 'LANG', 'LC_ALL', 'LC_CTYPE', 'LC_MESSAGES',
|
|
37
|
+
// Allowed but populated by the runner, not inherited:
|
|
38
|
+
// AIDEN_HOOK_EVENT, AIDEN_HOOK_ID, AIDEN_RUN_ID,
|
|
39
|
+
// AIDEN_TRACE_ID, AIDEN_PARENT_SPAN_ID
|
|
40
|
+
]);
|
|
41
|
+
/**
|
|
42
|
+
* Build the env block the child sees. Whitelist-only — anything not
|
|
43
|
+
* in `SAFE_ENV_KEYS` is dropped. The 5 AIDEN_HOOK_* keys are stamped
|
|
44
|
+
* fresh from `input` (NOT read from `process.env`, so a parent-leaked
|
|
45
|
+
* AIDEN_API_KEY can never propagate).
|
|
46
|
+
*/
|
|
47
|
+
function buildChildEnv(input) {
|
|
48
|
+
const out = {};
|
|
49
|
+
for (const k of Object.keys(process.env)) {
|
|
50
|
+
if (SAFE_ENV_KEYS.has(k))
|
|
51
|
+
out[k] = process.env[k];
|
|
52
|
+
}
|
|
53
|
+
if (input.event)
|
|
54
|
+
out.AIDEN_HOOK_EVENT = input.event;
|
|
55
|
+
if (input.hookId)
|
|
56
|
+
out.AIDEN_HOOK_ID = input.hookId;
|
|
57
|
+
if (input.runId)
|
|
58
|
+
out.AIDEN_RUN_ID = input.runId;
|
|
59
|
+
if (input.traceId)
|
|
60
|
+
out.AIDEN_TRACE_ID = input.traceId;
|
|
61
|
+
if (input.parentSpanId)
|
|
62
|
+
out.AIDEN_PARENT_SPAN_ID = input.parentSpanId;
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
function clip(s, cap) {
|
|
66
|
+
return s.length <= cap ? s : s.slice(0, cap) + `…(${s.length - cap} bytes elided)`;
|
|
67
|
+
}
|
|
68
|
+
/** SHA-256 hex of a UTF-8 string. */
|
|
69
|
+
function sha256(s) {
|
|
70
|
+
return (0, node_crypto_1.createHash)('sha256').update(s, 'utf8').digest('hex');
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Execute one hook invocation. Resolves with a structured outcome —
|
|
74
|
+
* never throws. The dispatcher branches on `status` to apply the
|
|
75
|
+
* subscription's `on_error` / `on_timeout` policy.
|
|
76
|
+
*/
|
|
77
|
+
function runHookSubprocess(input) {
|
|
78
|
+
return new Promise((resolve) => {
|
|
79
|
+
const stdinJson = JSON.stringify(input.payload);
|
|
80
|
+
const started = Date.now();
|
|
81
|
+
const payloadHash = sha256(stdinJson);
|
|
82
|
+
let stdout = '';
|
|
83
|
+
let stderr = '';
|
|
84
|
+
let timedOut = false;
|
|
85
|
+
const child = (0, node_child_process_1.spawn)(input.argv[0], input.argv.slice(1), {
|
|
86
|
+
cwd: input.cwd,
|
|
87
|
+
env: buildChildEnv(input),
|
|
88
|
+
shell: false,
|
|
89
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
90
|
+
});
|
|
91
|
+
const timer = setTimeout(() => {
|
|
92
|
+
timedOut = true;
|
|
93
|
+
try {
|
|
94
|
+
child.kill('SIGKILL');
|
|
95
|
+
}
|
|
96
|
+
catch { /* noop */ }
|
|
97
|
+
}, input.timeoutMs);
|
|
98
|
+
if (typeof timer.unref === 'function')
|
|
99
|
+
timer.unref();
|
|
100
|
+
child.stdout?.on('data', (b) => {
|
|
101
|
+
if (stdout.length < STDOUT_CAP)
|
|
102
|
+
stdout += b.toString('utf8');
|
|
103
|
+
});
|
|
104
|
+
child.stderr?.on('data', (b) => {
|
|
105
|
+
if (stderr.length < STDERR_CAP)
|
|
106
|
+
stderr += b.toString('utf8');
|
|
107
|
+
});
|
|
108
|
+
child.on('error', (e) => {
|
|
109
|
+
clearTimeout(timer);
|
|
110
|
+
resolve({
|
|
111
|
+
status: 'crash',
|
|
112
|
+
exitCode: null,
|
|
113
|
+
stdoutPreview: clip(stdout, STDOUT_CAP),
|
|
114
|
+
stderrPreview: clip(stderr, STDERR_CAP),
|
|
115
|
+
elapsedMs: Date.now() - started,
|
|
116
|
+
payloadHash,
|
|
117
|
+
errorKind: 'SpawnError',
|
|
118
|
+
errorMessage: e.message,
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
child.on('exit', (code) => {
|
|
122
|
+
clearTimeout(timer);
|
|
123
|
+
const elapsed = Date.now() - started;
|
|
124
|
+
if (timedOut) {
|
|
125
|
+
resolve({
|
|
126
|
+
status: 'timeout', exitCode: code, elapsedMs: elapsed, payloadHash,
|
|
127
|
+
stdoutPreview: clip(stdout, STDOUT_CAP),
|
|
128
|
+
stderrPreview: clip(stderr, STDERR_CAP),
|
|
129
|
+
errorKind: 'HookTimeout', errorMessage: `exceeded ${input.timeoutMs}ms`,
|
|
130
|
+
});
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (code !== 0) {
|
|
134
|
+
resolve({
|
|
135
|
+
status: 'crash', exitCode: code, elapsedMs: elapsed, payloadHash,
|
|
136
|
+
stdoutPreview: clip(stdout, STDOUT_CAP),
|
|
137
|
+
stderrPreview: clip(stderr, STDERR_CAP),
|
|
138
|
+
errorKind: 'NonZeroExit', errorMessage: `exit code ${code}`,
|
|
139
|
+
});
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
// exit 0 — parse stdout as JSON.
|
|
143
|
+
const trimmed = stdout.trim();
|
|
144
|
+
if (trimmed.length === 0) {
|
|
145
|
+
// Empty stdout treated as `{}` — "no opinion".
|
|
146
|
+
resolve({
|
|
147
|
+
status: 'ok', exitCode: code, elapsedMs: elapsed, payloadHash,
|
|
148
|
+
response: { decision: 'none' },
|
|
149
|
+
stdoutPreview: '', stderrPreview: clip(stderr, STDERR_CAP),
|
|
150
|
+
responseHash: sha256(''),
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
try {
|
|
155
|
+
const parsed = JSON.parse(trimmed);
|
|
156
|
+
resolve({
|
|
157
|
+
status: 'ok', exitCode: code, elapsedMs: elapsed, payloadHash,
|
|
158
|
+
response: parsed ?? { decision: 'none' },
|
|
159
|
+
stdoutPreview: clip(stdout, STDOUT_CAP),
|
|
160
|
+
stderrPreview: clip(stderr, STDERR_CAP),
|
|
161
|
+
responseHash: sha256(trimmed),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
catch (e) {
|
|
165
|
+
resolve({
|
|
166
|
+
status: 'malformed_output', exitCode: code, elapsedMs: elapsed, payloadHash,
|
|
167
|
+
stdoutPreview: clip(stdout, STDOUT_CAP),
|
|
168
|
+
stderrPreview: clip(stderr, STDERR_CAP),
|
|
169
|
+
errorKind: 'JSONParseError',
|
|
170
|
+
errorMessage: e instanceof Error ? e.message : String(e),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
// Write payload then close stdin.
|
|
175
|
+
try {
|
|
176
|
+
child.stdin?.write(stdinJson);
|
|
177
|
+
child.stdin?.end();
|
|
178
|
+
}
|
|
179
|
+
catch (e) {
|
|
180
|
+
clearTimeout(timer);
|
|
181
|
+
resolve({
|
|
182
|
+
status: 'crash', exitCode: null, elapsedMs: Date.now() - started, payloadHash,
|
|
183
|
+
stdoutPreview: '', stderrPreview: '',
|
|
184
|
+
errorKind: 'StdinError', errorMessage: e instanceof Error ? e.message : String(e),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
@@ -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/hooks/toolHookGate.ts — v4.9.0 Slice 12a Phase 3.
|
|
10
|
+
*
|
|
11
|
+
* Bridge between the tool dispatcher and the hook subsystem. Wraps a
|
|
12
|
+
* tool call with:
|
|
13
|
+
*
|
|
14
|
+
* 1. `tool.call.pre` dispatch — if any `mandatory_policy` block
|
|
15
|
+
* decision lands, the handler is NOT executed and a
|
|
16
|
+
* `HookBlockedError` is thrown so the executor's catch path
|
|
17
|
+
* surfaces it as a structured `ToolCallResult.error`.
|
|
18
|
+
* 2. Input transform — patches from `transform_input` hooks merge
|
|
19
|
+
* into the args before the handler runs.
|
|
20
|
+
* 3. Handler execution — caller-supplied async fn.
|
|
21
|
+
* 4. `tool.call.post` dispatch — fires informational + output
|
|
22
|
+
* transform hooks. `transform_output` patches the result.
|
|
23
|
+
* A post-hook block is recorded in `hook_executions` but the
|
|
24
|
+
* tool result still returns (the handler already side-effected;
|
|
25
|
+
* block-after-the-fact would just hide that from the model).
|
|
26
|
+
*
|
|
27
|
+
* If `db` is null (e.g. the headless CLI mode without a daemon),
|
|
28
|
+
* runs the handler directly — no audit, no hooks. Keeps test paths
|
|
29
|
+
* that don't open a database fully working.
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.HookBlockedError = void 0;
|
|
33
|
+
exports.runToolWithHooks = runToolWithHooks;
|
|
34
|
+
const dispatcher_1 = require("./dispatcher");
|
|
35
|
+
/**
|
|
36
|
+
* Thrown when a `mandatory_policy` hook blocks `tool.call.pre`.
|
|
37
|
+
* The tool dispatcher's outer catch maps this to a structured
|
|
38
|
+
* `ToolCallResult.error` so the model sees the rejection.
|
|
39
|
+
*/
|
|
40
|
+
class HookBlockedError extends Error {
|
|
41
|
+
constructor(reason, userMessage, modelMessage) {
|
|
42
|
+
super(reason);
|
|
43
|
+
this.name = 'HookBlocked';
|
|
44
|
+
this.userMessage = userMessage;
|
|
45
|
+
this.modelMessage = modelMessage;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.HookBlockedError = HookBlockedError;
|
|
49
|
+
/**
|
|
50
|
+
* Wrap a tool handler call with pre/post hook dispatch. Returns
|
|
51
|
+
* the (possibly transformed) handler result. Throws `HookBlockedError`
|
|
52
|
+
* if a pre-hook with `mandatory_policy` decides block.
|
|
53
|
+
*/
|
|
54
|
+
async function runToolWithHooks(opts, runHandler) {
|
|
55
|
+
if (!opts.db) {
|
|
56
|
+
// No daemon DB → no hook subsystem. Pass-through.
|
|
57
|
+
return runHandler(opts.args);
|
|
58
|
+
}
|
|
59
|
+
const baseCtx = { ...opts.ctx, toolName: opts.toolName, toolCallId: opts.toolCallId };
|
|
60
|
+
// ── tool.call.pre ───────────────────────────────────────────────
|
|
61
|
+
const pre = await (0, dispatcher_1.dispatchHook)(opts.db, 'tool.call.pre', opts.args, baseCtx);
|
|
62
|
+
if (pre.decision === 'block') {
|
|
63
|
+
throw new HookBlockedError(pre.reason ?? 'tool call blocked by pre-hook', pre.user_message, pre.model_message);
|
|
64
|
+
}
|
|
65
|
+
const finalArgs = pre.payload;
|
|
66
|
+
// ── handler ─────────────────────────────────────────────────────
|
|
67
|
+
const result = await runHandler(finalArgs);
|
|
68
|
+
// ── tool.call.post ──────────────────────────────────────────────
|
|
69
|
+
// Wrap the output in a stable envelope so transform_output hooks
|
|
70
|
+
// have a known field name to patch. We discard the post-hook's
|
|
71
|
+
// block decision (the handler already ran — see file header).
|
|
72
|
+
const postPayload = { input: finalArgs, output: result };
|
|
73
|
+
const post = await (0, dispatcher_1.dispatchHook)(opts.db, 'tool.call.post', postPayload, baseCtx);
|
|
74
|
+
const patched = post.payload;
|
|
75
|
+
return ('output' in patched) ? patched.output : result;
|
|
76
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.markTrusted = markTrusted;
|
|
4
|
+
exports.markRevoked = markRevoked;
|
|
5
|
+
exports.markUntrusted = markUntrusted;
|
|
6
|
+
function markTrusted(db, hookId) {
|
|
7
|
+
db.prepare(`UPDATE hooks SET trust_state='trusted', enabled=1, updated_at=? WHERE hook_id = ?`).run(new Date().toISOString(), hookId);
|
|
8
|
+
}
|
|
9
|
+
function markRevoked(db, hookId) {
|
|
10
|
+
db.prepare(`UPDATE hooks SET trust_state='revoked', enabled=0, updated_at=? WHERE hook_id = ?`).run(new Date().toISOString(), hookId);
|
|
11
|
+
}
|
|
12
|
+
function markUntrusted(db, hookId) {
|
|
13
|
+
db.prepare(`UPDATE hooks SET trust_state='untrusted', enabled=0, updated_at=? WHERE hook_id = ?`).run(new Date().toISOString(), hookId);
|
|
14
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
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/identity/contextManager.ts — v4.9.0 Slice 4.
|
|
10
|
+
*
|
|
11
|
+
* Thin wrapper over `node:async_hooks`' `AsyncLocalStorage` exposing
|
|
12
|
+
* three primitives:
|
|
13
|
+
*
|
|
14
|
+
* - `runWithContext(ctx, fn)` — entry point; installs the context
|
|
15
|
+
* for the duration of `fn` (and every awaited continuation it
|
|
16
|
+
* spawns). Returns whatever `fn` returns. Synchronous closures
|
|
17
|
+
* and async functions both work.
|
|
18
|
+
*
|
|
19
|
+
* - `currentContext()` — returns the ambient context or `undefined`
|
|
20
|
+
* when called outside any `runWithContext` frame. Cheap; safe to
|
|
21
|
+
* call from anywhere (logger sinks, tool handlers, hooks).
|
|
22
|
+
*
|
|
23
|
+
* - `requireContext(kind?)` — same as `currentContext()` but throws
|
|
24
|
+
* when no context is active. Use this at boundaries that should
|
|
25
|
+
* never run un-contexted (e.g. inside a tool handler that depends
|
|
26
|
+
* on the runId for idempotency). The optional `kind` hint goes
|
|
27
|
+
* into the error message for easier debugging.
|
|
28
|
+
*
|
|
29
|
+
* Slice 4 only INSTALLS the storage + primitives. It does NOT wrap
|
|
30
|
+
* existing call sites — that's later-slice work. The logger picks up
|
|
31
|
+
* `currentContext()` automatically; if you don't enter a
|
|
32
|
+
* `runWithContext` frame, you get the pre-Slice-4 log shape exactly.
|
|
33
|
+
*/
|
|
34
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
35
|
+
exports.runWithContext = runWithContext;
|
|
36
|
+
exports.currentContext = currentContext;
|
|
37
|
+
exports.requireContext = requireContext;
|
|
38
|
+
const node_async_hooks_1 = require("node:async_hooks");
|
|
39
|
+
const als = new node_async_hooks_1.AsyncLocalStorage();
|
|
40
|
+
/**
|
|
41
|
+
* Install `ctx` as the ambient context for the synchronous and async
|
|
42
|
+
* lifetime of `fn`. Any code reached via `await` within `fn` (or via
|
|
43
|
+
* timer callbacks scheduled from inside `fn`) sees the same context.
|
|
44
|
+
*
|
|
45
|
+
* The store unwinds when `fn` completes (or throws). Use a sibling
|
|
46
|
+
* `runWithContext` call to switch contexts; `als.enterWith(...)` is
|
|
47
|
+
* deliberately NOT exposed — it leaks across boundaries and we want
|
|
48
|
+
* the scoping discipline.
|
|
49
|
+
*/
|
|
50
|
+
function runWithContext(ctx, fn) {
|
|
51
|
+
return als.run(ctx, fn);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Read the ambient context. Returns `undefined` outside a
|
|
55
|
+
* `runWithContext` frame.
|
|
56
|
+
*
|
|
57
|
+
* Never throws — by the project rule "no log formatter throws because
|
|
58
|
+
* context is missing", callers in the logging path consume the
|
|
59
|
+
* undefined and degrade gracefully.
|
|
60
|
+
*/
|
|
61
|
+
function currentContext() {
|
|
62
|
+
return als.getStore();
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Read the ambient context, throwing when none is active. Use this in
|
|
66
|
+
* code paths that depend on having an id (e.g. tool dispatch for
|
|
67
|
+
* idempotency, hook firing). The `kind` argument is purely diagnostic;
|
|
68
|
+
* it shows up in the error message so you can tell which call site
|
|
69
|
+
* was un-contexted.
|
|
70
|
+
*
|
|
71
|
+
* Slice 4 does NOT add `requireContext()` to any existing call site;
|
|
72
|
+
* the function is here for future slices to opt in. The additive-only
|
|
73
|
+
* constraint is intentional — Slice 4 must not break existing call
|
|
74
|
+
* sites that have no ambient context.
|
|
75
|
+
*/
|
|
76
|
+
function requireContext(kind) {
|
|
77
|
+
const ctx = als.getStore();
|
|
78
|
+
if (!ctx) {
|
|
79
|
+
const where = kind ? ` (required by: ${kind})` : '';
|
|
80
|
+
throw new Error(`requireContext: no ambient ExecutionContext${where}`);
|
|
81
|
+
}
|
|
82
|
+
return ctx;
|
|
83
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
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/identity/daemonId.ts — v4.9.0 Slice 4.
|
|
10
|
+
*
|
|
11
|
+
* The `daemonId` is the *persistent* identity of an Aiden install. It
|
|
12
|
+
* survives daemon restarts, schema migrations, and crashes — only a
|
|
13
|
+
* hard reset (deleting the file) gives a new identity. Each daemon
|
|
14
|
+
* process gets a fresh `incarnationId` per boot; the pair (daemon,
|
|
15
|
+
* incarnation) is what callers correlate against.
|
|
16
|
+
*
|
|
17
|
+
* Storage: a single-line file at `<aidenRoot>/daemon/daemon_id`. The
|
|
18
|
+
* file's content is exactly the ID string (e.g. `dmn_<32-hex>\n`).
|
|
19
|
+
*
|
|
20
|
+
* Atomic write semantics: tmp + rename. SQLite-style — if the rename
|
|
21
|
+
* crashes mid-flight the prior file is untouched. We don't fsync the
|
|
22
|
+
* containing directory (Windows doesn't support directory fsync via
|
|
23
|
+
* Node, and our acceptable failure mode is "one boot might generate a
|
|
24
|
+
* new id"; the next boot picks up the persisted one).
|
|
25
|
+
*/
|
|
26
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
27
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
28
|
+
};
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.daemonIdFilePath = daemonIdFilePath;
|
|
31
|
+
exports.loadOrCreateDaemonId = loadOrCreateDaemonId;
|
|
32
|
+
const node_fs_1 = require("node:fs");
|
|
33
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
34
|
+
const ids_1 = require("./ids");
|
|
35
|
+
/** Filesystem path the daemon id lives at, given an aiden root. */
|
|
36
|
+
function daemonIdFilePath(aidenRoot) {
|
|
37
|
+
return node_path_1.default.join(aidenRoot, 'daemon', 'daemon_id');
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Read the persisted daemon id, or generate + write a fresh one on
|
|
41
|
+
* first boot. Returns the canonical `dmn_<hex>` string.
|
|
42
|
+
*
|
|
43
|
+
* Defensive: if the file exists but is unparseable (corrupted /
|
|
44
|
+
* truncated), we treat it as missing and write a new one. The old
|
|
45
|
+
* content is saved to `daemon_id.broken-<ts>` for postmortem.
|
|
46
|
+
*/
|
|
47
|
+
function loadOrCreateDaemonId(aidenRoot) {
|
|
48
|
+
const filePath = daemonIdFilePath(aidenRoot);
|
|
49
|
+
const dir = node_path_1.default.dirname(filePath);
|
|
50
|
+
if ((0, node_fs_1.existsSync)(filePath)) {
|
|
51
|
+
try {
|
|
52
|
+
const raw = (0, node_fs_1.readFileSync)(filePath, 'utf8').trim();
|
|
53
|
+
const parsed = (0, ids_1.parseId)(raw);
|
|
54
|
+
if (parsed && parsed.prefix === 'dmn') {
|
|
55
|
+
return raw;
|
|
56
|
+
}
|
|
57
|
+
// Corrupted — quarantine + fall through to regenerate.
|
|
58
|
+
try {
|
|
59
|
+
(0, node_fs_1.renameSync)(filePath, `${filePath}.broken-${Date.now()}`);
|
|
60
|
+
}
|
|
61
|
+
catch { /* best effort */ }
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
/* permission / IO — fall through to regenerate */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Generate + persist atomically.
|
|
68
|
+
if (!(0, node_fs_1.existsSync)(dir))
|
|
69
|
+
(0, node_fs_1.mkdirSync)(dir, { recursive: true });
|
|
70
|
+
const id = (0, ids_1.newDaemonId)();
|
|
71
|
+
const tmp = `${filePath}.tmp-${process.pid}`;
|
|
72
|
+
(0, node_fs_1.writeFileSync)(tmp, `${id}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
73
|
+
try {
|
|
74
|
+
const fd = (0, node_fs_1.openSync)(tmp, 'r+');
|
|
75
|
+
try {
|
|
76
|
+
(0, node_fs_1.fsyncSync)(fd);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
(0, node_fs_1.closeSync)(fd);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch { /* fsync best-effort */ }
|
|
83
|
+
(0, node_fs_1.renameSync)(tmp, filePath);
|
|
84
|
+
return id;
|
|
85
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
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/identity/enforcement.ts — v4.9.0 Slice 8.
|
|
10
|
+
*
|
|
11
|
+
* Wires the "context missing" event from Slices 6 + 7 into an
|
|
12
|
+
* observable + tunable layer. Default mode is `'warn'` — every
|
|
13
|
+
* fall-through path that today silently no-ops now logs a single
|
|
14
|
+
* warning per kind (deduplicated by a small in-process cooldown) and
|
|
15
|
+
* increments a telemetry counter. `'strict'` mode throws so a dev
|
|
16
|
+
* shaking down a new code path can find missing `runWithContext`
|
|
17
|
+
* frames; `'silent'` mode is the production knob for callers that
|
|
18
|
+
* legitimately operate outside the daemon (CLI one-shots, scripts).
|
|
19
|
+
*
|
|
20
|
+
* Read from env at module load:
|
|
21
|
+
* AIDEN_CONTEXT_ENFORCEMENT — default 'warn'
|
|
22
|
+
* AIDEN_CONTEXT_ENFORCEMENT_TOOL — per-kind override
|
|
23
|
+
* AIDEN_CONTEXT_ENFORCEMENT_LLM
|
|
24
|
+
* AIDEN_CONTEXT_ENFORCEMENT_HTTP_OUTBOUND
|
|
25
|
+
* AIDEN_CONTEXT_ENFORCEMENT_SUBPROCESS
|
|
26
|
+
* AIDEN_CONTEXT_ENFORCEMENT_MEMORY_WRITE
|
|
27
|
+
* AIDEN_CONTEXT_ENFORCEMENT_HOOK
|
|
28
|
+
*/
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.ContextMissingError = void 0;
|
|
31
|
+
exports.getEnforcementMode = getEnforcementMode;
|
|
32
|
+
exports.getContextMissingCounter = getContextMissingCounter;
|
|
33
|
+
exports.getAllContextMissingCounters = getAllContextMissingCounters;
|
|
34
|
+
exports._resetContextMissingCountersForTests = _resetContextMissingCountersForTests;
|
|
35
|
+
exports.reportMissingContext = reportMissingContext;
|
|
36
|
+
const VALID = new Set(['strict', 'warn', 'silent']);
|
|
37
|
+
function readMode(envVal, fallback) {
|
|
38
|
+
if (!envVal)
|
|
39
|
+
return fallback;
|
|
40
|
+
const v = envVal.toLowerCase();
|
|
41
|
+
return VALID.has(v) ? v : fallback;
|
|
42
|
+
}
|
|
43
|
+
function kindEnvKey(kind) {
|
|
44
|
+
return `AIDEN_CONTEXT_ENFORCEMENT_${kind.toUpperCase()}`;
|
|
45
|
+
}
|
|
46
|
+
/** Resolve the effective mode for a kind. Re-reads env per call so test envs work. */
|
|
47
|
+
function getEnforcementMode(kind) {
|
|
48
|
+
const global = readMode(process.env.AIDEN_CONTEXT_ENFORCEMENT, 'warn');
|
|
49
|
+
return readMode(process.env[kindEnvKey(kind)], global);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Telemetry counter `context_missing_total{kind}` — incremented on
|
|
53
|
+
* every report regardless of mode. Exposed via the daemon's
|
|
54
|
+
* `/metrics` endpoint (Slice 3 + the existing telemetry surface).
|
|
55
|
+
*/
|
|
56
|
+
const _counters = Object.create(null);
|
|
57
|
+
function getContextMissingCounter(kind) {
|
|
58
|
+
return _counters[kind] ?? 0;
|
|
59
|
+
}
|
|
60
|
+
function getAllContextMissingCounters() {
|
|
61
|
+
return { ..._counters };
|
|
62
|
+
}
|
|
63
|
+
function _resetContextMissingCountersForTests() {
|
|
64
|
+
for (const k of Object.keys(_counters))
|
|
65
|
+
delete _counters[k];
|
|
66
|
+
}
|
|
67
|
+
/** Warn-mode dedup so a tight loop with no context doesn't spam. */
|
|
68
|
+
const _lastWarnAt = Object.create(null);
|
|
69
|
+
const WARN_DEDUP_MS = 30000;
|
|
70
|
+
class ContextMissingError extends Error {
|
|
71
|
+
constructor(kind, hint) {
|
|
72
|
+
super(`[context-enforcement] ${kind}: no ambient ExecutionContext` + (hint ? ` (${hint})` : ''));
|
|
73
|
+
this.name = 'ContextMissingError';
|
|
74
|
+
this.kind = kind;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
exports.ContextMissingError = ContextMissingError;
|
|
78
|
+
/**
|
|
79
|
+
* Record a "context missing" event. Behaviour per resolved mode:
|
|
80
|
+
* 'strict' — throws ContextMissingError
|
|
81
|
+
* 'warn' — increments counter; logs via sinks.warn (dedup'd)
|
|
82
|
+
* 'silent' — increments counter only
|
|
83
|
+
*/
|
|
84
|
+
function reportMissingContext(kind, hint, sinks = {}) {
|
|
85
|
+
_counters[kind] = (_counters[kind] ?? 0) + 1;
|
|
86
|
+
const mode = getEnforcementMode(kind);
|
|
87
|
+
if (mode === 'strict') {
|
|
88
|
+
throw new ContextMissingError(kind, hint);
|
|
89
|
+
}
|
|
90
|
+
if (mode === 'warn') {
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
if ((now - (_lastWarnAt[kind] ?? 0)) > WARN_DEDUP_MS) {
|
|
93
|
+
_lastWarnAt[kind] = now;
|
|
94
|
+
if (sinks.warn) {
|
|
95
|
+
try {
|
|
96
|
+
sinks.warn(`[context-enforcement] ${kind} missing context` + (hint ? ` (${hint})` : ''));
|
|
97
|
+
}
|
|
98
|
+
catch { /* noop */ }
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
// silent: counter only.
|
|
103
|
+
}
|