aiden-runtime 4.8.1 → 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.
Files changed (93) hide show
  1. package/README.md +88 -1
  2. package/dist/cli/v4/aidenCLI.js +35 -4
  3. package/dist/cli/v4/chatSession.js +34 -9
  4. package/dist/cli/v4/commands/daemon.js +47 -2
  5. package/dist/cli/v4/commands/daemonDoctor.js +212 -0
  6. package/dist/cli/v4/commands/daemonStatus.js +1 -1
  7. package/dist/cli/v4/commands/help.js +2 -0
  8. package/dist/cli/v4/commands/hooks.js +428 -0
  9. package/dist/cli/v4/commands/index.js +5 -1
  10. package/dist/cli/v4/commands/mcp.js +89 -1
  11. package/dist/cli/v4/commands/mcpClientInstall.js +359 -0
  12. package/dist/cli/v4/commands/memory.js +702 -0
  13. package/dist/cli/v4/commands/recovery.js +1 -1
  14. package/dist/cli/v4/commands/skin.js +7 -0
  15. package/dist/cli/v4/commands/theme.js +217 -0
  16. package/dist/cli/v4/commands/trigger.js +1 -1
  17. package/dist/cli/v4/design/tokens.js +52 -4
  18. package/dist/cli/v4/display.js +39 -26
  19. package/dist/cli/v4/replyRenderer.js +6 -5
  20. package/dist/cli/v4/skinEngine.js +67 -0
  21. package/dist/core/v4/aidenAgent.js +45 -2
  22. package/dist/core/v4/daemon/api/runs.js +131 -0
  23. package/dist/core/v4/daemon/bootstrap.js +368 -13
  24. package/dist/core/v4/daemon/db/migrations.js +169 -0
  25. package/dist/core/v4/daemon/idempotency/runIdempotencyStore.js +128 -0
  26. package/dist/core/v4/daemon/incarnationStore.js +47 -0
  27. package/dist/core/v4/daemon/runs/attemptStore.js +67 -0
  28. package/dist/core/v4/daemon/runs/reclaim.js +88 -0
  29. package/dist/core/v4/daemon/runs/retryPolicy.js +73 -0
  30. package/dist/core/v4/daemon/runs/runWithRetry.js +80 -0
  31. package/dist/core/v4/daemon/runs/stuckAttemptWatchdog.js +72 -0
  32. package/dist/core/v4/daemon/spans/spanHelpers.js +272 -0
  33. package/dist/core/v4/daemon/spans/spanStore.js +113 -0
  34. package/dist/core/v4/daemon/triggerBus.js +50 -19
  35. package/dist/core/v4/hooks/auditQuery.js +67 -0
  36. package/dist/core/v4/hooks/dispatcher.js +286 -0
  37. package/dist/core/v4/hooks/index.js +46 -0
  38. package/dist/core/v4/hooks/lifecycle.js +27 -0
  39. package/dist/core/v4/hooks/manifest.js +142 -0
  40. package/dist/core/v4/hooks/registry.js +149 -0
  41. package/dist/core/v4/hooks/runtime/subprocessRunner.js +188 -0
  42. package/dist/core/v4/hooks/toolHookGate.js +76 -0
  43. package/dist/core/v4/hooks/trust.js +14 -0
  44. package/dist/core/v4/identity/contextManager.js +83 -0
  45. package/dist/core/v4/identity/daemonId.js +85 -0
  46. package/dist/core/v4/identity/enforcement.js +103 -0
  47. package/dist/core/v4/identity/executionContext.js +153 -0
  48. package/dist/core/v4/identity/hookExecution.js +62 -0
  49. package/dist/core/v4/identity/httpContext.js +68 -0
  50. package/dist/core/v4/identity/ids.js +185 -0
  51. package/dist/core/v4/identity/index.js +60 -0
  52. package/dist/core/v4/identity/subprocessContext.js +98 -0
  53. package/dist/core/v4/identity/traceparent.js +114 -0
  54. package/dist/core/v4/logger/index.js +3 -1
  55. package/dist/core/v4/logger/logger.js +28 -1
  56. package/dist/core/v4/logger/redact.js +149 -0
  57. package/dist/core/v4/logger/sinks/fileSink.js +13 -0
  58. package/dist/core/v4/logger/sinks/stdSink.js +19 -1
  59. package/dist/core/v4/mcp/install/backup.js +78 -0
  60. package/dist/core/v4/mcp/install/clientPaths.js +90 -0
  61. package/dist/core/v4/mcp/install/clients.js +203 -0
  62. package/dist/core/v4/mcp/install/healthCheck.js +83 -0
  63. package/dist/core/v4/mcp/install/jsoncMerge.js +174 -0
  64. package/dist/core/v4/mcp/install/profiles.js +109 -0
  65. package/dist/core/v4/mcp/install/wslDetect.js +62 -0
  66. package/dist/core/v4/memory/namespaceRegistry.js +117 -0
  67. package/dist/core/v4/memory/projectRoot.js +76 -0
  68. package/dist/core/v4/memory/reviewer/index.js +162 -0
  69. package/dist/core/v4/memory/reviewer/pendingStore.js +136 -0
  70. package/dist/core/v4/memory/reviewer/prompt.js +105 -0
  71. package/dist/core/v4/memory/reviewer/skipRules.js +92 -0
  72. package/dist/core/v4/memoryManager.js +57 -10
  73. package/dist/core/v4/paths.js +2 -0
  74. package/dist/core/v4/subagent/spawnSubAgent.js +20 -7
  75. package/dist/core/v4/theme/bundledThemes.js +106 -0
  76. package/dist/core/v4/theme/themeLoader.js +160 -0
  77. package/dist/core/v4/theme/themeRegistry.js +97 -0
  78. package/dist/core/v4/theme/themeWatcher.js +95 -0
  79. package/dist/core/v4/toolRegistry.js +71 -8
  80. package/dist/moat/approvalEngine.js +4 -0
  81. package/dist/moat/memoryGuard.js +8 -1
  82. package/dist/providers/v4/anthropicAdapter.js +10 -4
  83. package/dist/tools/v4/backends/local.js +19 -2
  84. package/dist/tools/v4/sessions/recallSession.js +6 -1
  85. package/package.json +3 -1
  86. package/plugins/aiden-plugin-chatgpt-plus/index.js +10 -1
  87. package/themes/default.yaml +52 -0
  88. package/themes/dracula.yaml +32 -0
  89. package/themes/light.yaml +32 -0
  90. package/themes/monochrome.yaml +31 -0
  91. package/themes/tokyo-night.yaml +32 -0
  92. package/dist/core/pluginSystem.js +0 -121
  93. 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
+ }