@lumenflow/kernel 3.0.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 (127) hide show
  1. package/LICENSE.md +190 -0
  2. package/README.md +26 -0
  3. package/dist/canonical-json.d.ts +7 -0
  4. package/dist/canonical-json.d.ts.map +1 -0
  5. package/dist/canonical-json.js +50 -0
  6. package/dist/canonical-json.js.map +1 -0
  7. package/dist/event-kinds.d.ts +32 -0
  8. package/dist/event-kinds.d.ts.map +1 -0
  9. package/dist/event-kinds.js +49 -0
  10. package/dist/event-kinds.js.map +1 -0
  11. package/dist/event-store/index.d.ts +64 -0
  12. package/dist/event-store/index.d.ts.map +1 -0
  13. package/dist/event-store/index.js +634 -0
  14. package/dist/event-store/index.js.map +1 -0
  15. package/dist/evidence/evidence-store.d.ts +78 -0
  16. package/dist/evidence/evidence-store.d.ts.map +1 -0
  17. package/dist/evidence/evidence-store.js +409 -0
  18. package/dist/evidence/evidence-store.js.map +1 -0
  19. package/dist/evidence/fs-helpers.d.ts +13 -0
  20. package/dist/evidence/fs-helpers.d.ts.map +1 -0
  21. package/dist/evidence/fs-helpers.js +38 -0
  22. package/dist/evidence/fs-helpers.js.map +1 -0
  23. package/dist/evidence/index.d.ts +3 -0
  24. package/dist/evidence/index.d.ts.map +1 -0
  25. package/dist/evidence/index.js +5 -0
  26. package/dist/evidence/index.js.map +1 -0
  27. package/dist/index.d.ts +17 -0
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +19 -0
  30. package/dist/index.js.map +1 -0
  31. package/dist/kernel.schemas.d.ts +642 -0
  32. package/dist/kernel.schemas.d.ts.map +1 -0
  33. package/dist/kernel.schemas.js +331 -0
  34. package/dist/kernel.schemas.js.map +1 -0
  35. package/dist/pack/hash.d.ts +7 -0
  36. package/dist/pack/hash.d.ts.map +1 -0
  37. package/dist/pack/hash.js +56 -0
  38. package/dist/pack/hash.js.map +1 -0
  39. package/dist/pack/index.d.ts +4 -0
  40. package/dist/pack/index.d.ts.map +1 -0
  41. package/dist/pack/index.js +6 -0
  42. package/dist/pack/index.js.map +1 -0
  43. package/dist/pack/manifest.d.ts +100 -0
  44. package/dist/pack/manifest.d.ts.map +1 -0
  45. package/dist/pack/manifest.js +50 -0
  46. package/dist/pack/manifest.js.map +1 -0
  47. package/dist/pack/pack-loader.d.ts +108 -0
  48. package/dist/pack/pack-loader.d.ts.map +1 -0
  49. package/dist/pack/pack-loader.js +282 -0
  50. package/dist/pack/pack-loader.js.map +1 -0
  51. package/dist/policy/approval-event.d.ts +29 -0
  52. package/dist/policy/approval-event.d.ts.map +1 -0
  53. package/dist/policy/approval-event.js +17 -0
  54. package/dist/policy/approval-event.js.map +1 -0
  55. package/dist/policy/index.d.ts +3 -0
  56. package/dist/policy/index.d.ts.map +1 -0
  57. package/dist/policy/index.js +5 -0
  58. package/dist/policy/index.js.map +1 -0
  59. package/dist/policy/policy-engine.d.ts +52 -0
  60. package/dist/policy/policy-engine.d.ts.map +1 -0
  61. package/dist/policy/policy-engine.js +83 -0
  62. package/dist/policy/policy-engine.js.map +1 -0
  63. package/dist/runtime/index.d.ts +2 -0
  64. package/dist/runtime/index.d.ts.map +1 -0
  65. package/dist/runtime/index.js +4 -0
  66. package/dist/runtime/index.js.map +1 -0
  67. package/dist/runtime/kernel-runtime.d.ts +170 -0
  68. package/dist/runtime/kernel-runtime.d.ts.map +1 -0
  69. package/dist/runtime/kernel-runtime.js +751 -0
  70. package/dist/runtime/kernel-runtime.js.map +1 -0
  71. package/dist/sandbox/bwrap-invocation.d.ts +13 -0
  72. package/dist/sandbox/bwrap-invocation.d.ts.map +1 -0
  73. package/dist/sandbox/bwrap-invocation.js +105 -0
  74. package/dist/sandbox/bwrap-invocation.js.map +1 -0
  75. package/dist/sandbox/index.d.ts +5 -0
  76. package/dist/sandbox/index.d.ts.map +1 -0
  77. package/dist/sandbox/index.js +7 -0
  78. package/dist/sandbox/index.js.map +1 -0
  79. package/dist/sandbox/profile.d.ts +32 -0
  80. package/dist/sandbox/profile.d.ts.map +1 -0
  81. package/dist/sandbox/profile.js +101 -0
  82. package/dist/sandbox/profile.js.map +1 -0
  83. package/dist/sandbox/subprocess-dispatcher.d.ts +38 -0
  84. package/dist/sandbox/subprocess-dispatcher.d.ts.map +1 -0
  85. package/dist/sandbox/subprocess-dispatcher.js +145 -0
  86. package/dist/sandbox/subprocess-dispatcher.js.map +1 -0
  87. package/dist/sandbox/tool-runner-worker.d.ts +54 -0
  88. package/dist/sandbox/tool-runner-worker.d.ts.map +1 -0
  89. package/dist/sandbox/tool-runner-worker.js +159 -0
  90. package/dist/sandbox/tool-runner-worker.js.map +1 -0
  91. package/dist/shared-constants.d.ts +48 -0
  92. package/dist/shared-constants.d.ts.map +1 -0
  93. package/dist/shared-constants.js +49 -0
  94. package/dist/shared-constants.js.map +1 -0
  95. package/dist/state-machine/index.d.ts +30 -0
  96. package/dist/state-machine/index.d.ts.map +1 -0
  97. package/dist/state-machine/index.js +92 -0
  98. package/dist/state-machine/index.js.map +1 -0
  99. package/dist/tool-host/builtins/capabilities.d.ts +20 -0
  100. package/dist/tool-host/builtins/capabilities.d.ts.map +1 -0
  101. package/dist/tool-host/builtins/capabilities.js +211 -0
  102. package/dist/tool-host/builtins/capabilities.js.map +1 -0
  103. package/dist/tool-host/builtins/index.d.ts +2 -0
  104. package/dist/tool-host/builtins/index.d.ts.map +1 -0
  105. package/dist/tool-host/builtins/index.js +4 -0
  106. package/dist/tool-host/builtins/index.js.map +1 -0
  107. package/dist/tool-host/index.d.ts +5 -0
  108. package/dist/tool-host/index.d.ts.map +1 -0
  109. package/dist/tool-host/index.js +7 -0
  110. package/dist/tool-host/index.js.map +1 -0
  111. package/dist/tool-host/scope-intersection.d.ts +10 -0
  112. package/dist/tool-host/scope-intersection.d.ts.map +1 -0
  113. package/dist/tool-host/scope-intersection.js +188 -0
  114. package/dist/tool-host/scope-intersection.js.map +1 -0
  115. package/dist/tool-host/subprocess-dispatcher.d.ts +14 -0
  116. package/dist/tool-host/subprocess-dispatcher.d.ts.map +1 -0
  117. package/dist/tool-host/subprocess-dispatcher.js +14 -0
  118. package/dist/tool-host/subprocess-dispatcher.js.map +1 -0
  119. package/dist/tool-host/tool-host.d.ts +42 -0
  120. package/dist/tool-host/tool-host.d.ts.map +1 -0
  121. package/dist/tool-host/tool-host.js +395 -0
  122. package/dist/tool-host/tool-host.js.map +1 -0
  123. package/dist/tool-host/tool-registry.d.ts +9 -0
  124. package/dist/tool-host/tool-registry.d.ts.map +1 -0
  125. package/dist/tool-host/tool-registry.js +28 -0
  126. package/dist/tool-host/tool-registry.js.map +1 -0
  127. package/package.json +71 -0
@@ -0,0 +1,751 @@
1
+ // Copyright (c) 2026 Hellmai Ltd
2
+ // SPDX-License-Identifier: Apache-2.0
3
+ import { randomBytes } from 'node:crypto';
4
+ import { access, mkdir, open, readFile, rm } from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import YAML from 'yaml';
7
+ import { z } from 'zod';
8
+ import { canonical_json } from '../canonical-json.js';
9
+ import { KERNEL_EVENT_KINDS, TOOL_TRACE_KINDS, isRunLifecycleEventKind, } from '../event-kinds.js';
10
+ import { EXECUTION_METADATA_KEYS, KERNEL_POLICY_IDS, KERNEL_RUNTIME_EVENTS_DIR_NAME, KERNEL_RUNTIME_EVENTS_FILE_NAME, KERNEL_RUNTIME_EVENTS_LOCK_FILE_NAME, KERNEL_RUNTIME_EVIDENCE_DIR_NAME, KERNEL_RUNTIME_ROOT_DIR_NAME, KERNEL_RUNTIME_TASKS_DIR_NAME, LUMENFLOW_DIR_NAME, LUMENFLOW_SCOPE_NAME, PACKAGES_DIR_NAME, PACKS_DIR_NAME, UTF8_ENCODING, WORKSPACE_CONFIG_HASH_CONTEXT_KEYS, WORKSPACE_FILE_NAME, } from '../shared-constants.js';
11
+ import { EventStore, projectTaskState, } from '../event-store/index.js';
12
+ import { EvidenceStore } from '../evidence/evidence-store.js';
13
+ import { ExecutionContextSchema, RUN_STATUSES, RunSchema, TOOL_HANDLER_KINDS, TaskSpecSchema, WorkspaceSpecSchema, } from '../kernel.schemas.js';
14
+ import { PackLoader, resolvePackToolEntryPath } from '../pack/index.js';
15
+ import { POLICY_TRIGGERS, PolicyEngine, } from '../policy/index.js';
16
+ import { SandboxSubprocessDispatcher, } from '../sandbox/index.js';
17
+ import { assertTransition } from '../state-machine/index.js';
18
+ import { createBuiltinPolicyHook, registerBuiltinToolCapabilities, } from '../tool-host/builtins/index.js';
19
+ import { ToolHost } from '../tool-host/tool-host.js';
20
+ import { ToolRegistry } from '../tool-host/tool-registry.js';
21
+ const DEFAULT_RUNTIME_ROOT = path.join(LUMENFLOW_DIR_NAME, KERNEL_RUNTIME_ROOT_DIR_NAME);
22
+ const DEFAULT_PACKS_ROOT_CANDIDATES = [
23
+ PACKS_DIR_NAME,
24
+ path.join(PACKAGES_DIR_NAME, LUMENFLOW_SCOPE_NAME, PACKS_DIR_NAME),
25
+ ];
26
+ const DEFAULT_PACK_TOOL_INPUT_SCHEMA = z.record(z.string(), z.unknown());
27
+ const DEFAULT_PACK_TOOL_OUTPUT_SCHEMA = z.record(z.string(), z.unknown());
28
+ const RUNTIME_LOAD_STAGE_ERROR_PREFIX = 'Runtime load stage failed for pack';
29
+ const RUNTIME_REGISTRATION_STAGE_ERROR_PREFIX = 'Runtime registration stage failed for tool';
30
+ const WORKSPACE_UPDATED_INIT_SUMMARY = 'Workspace config hash initialized during runtime startup.';
31
+ const SPEC_TAMPERED_ERROR_CODE = 'SPEC_TAMPERED';
32
+ const SPEC_TAMPERED_WORKSPACE_MESSAGE = 'Workspace configuration hash mismatch detected; execution blocked.';
33
+ const SPEC_TAMPERED_WORKSPACE_MISSING_MESSAGE = 'Workspace configuration file is missing; execution blocked.';
34
+ function normalizeTimestamp(now, inputTimestamp) {
35
+ return inputTimestamp ?? now().toISOString();
36
+ }
37
+ export function defaultRunIdFactory(taskId, nextRunNumber) {
38
+ const suffix = randomBytes(4).toString('hex');
39
+ return `run-${taskId}-${nextRunNumber}-${suffix}`;
40
+ }
41
+ function isRunLifecycleEvent(event) {
42
+ return isRunLifecycleEventKind(event.kind);
43
+ }
44
+ function buildRunHistory(events) {
45
+ const sortedEvents = [...events].sort((left, right) => {
46
+ return Date.parse(left.timestamp) - Date.parse(right.timestamp);
47
+ });
48
+ const byRun = new Map();
49
+ for (const event of sortedEvents) {
50
+ if (!isRunLifecycleEvent(event)) {
51
+ continue;
52
+ }
53
+ const existing = byRun.get(event.run_id);
54
+ if (event.kind === KERNEL_EVENT_KINDS.RUN_STARTED) {
55
+ byRun.set(event.run_id, RunSchema.parse({
56
+ run_id: event.run_id,
57
+ task_id: event.task_id,
58
+ status: RUN_STATUSES.EXECUTING,
59
+ started_at: event.timestamp,
60
+ by: event.by,
61
+ session_id: event.session_id,
62
+ }));
63
+ continue;
64
+ }
65
+ const fallback = existing ??
66
+ RunSchema.parse({
67
+ run_id: event.run_id,
68
+ task_id: event.task_id,
69
+ status: RUN_STATUSES.PLANNED,
70
+ started_at: event.timestamp,
71
+ by: 'unknown',
72
+ session_id: 'unknown',
73
+ });
74
+ if (event.kind === KERNEL_EVENT_KINDS.RUN_PAUSED) {
75
+ byRun.set(event.run_id, RunSchema.parse({
76
+ ...fallback,
77
+ status: RUN_STATUSES.PAUSED,
78
+ }));
79
+ continue;
80
+ }
81
+ if (event.kind === KERNEL_EVENT_KINDS.RUN_FAILED) {
82
+ byRun.set(event.run_id, RunSchema.parse({
83
+ ...fallback,
84
+ status: RUN_STATUSES.FAILED,
85
+ completed_at: event.timestamp,
86
+ }));
87
+ continue;
88
+ }
89
+ byRun.set(event.run_id, RunSchema.parse({
90
+ ...fallback,
91
+ status: RUN_STATUSES.SUCCEEDED,
92
+ completed_at: event.timestamp,
93
+ }));
94
+ }
95
+ return [...byRun.values()].sort((left, right) => {
96
+ return Date.parse(left.started_at) - Date.parse(right.started_at);
97
+ });
98
+ }
99
+ function dedupePolicyDecisions(decisions) {
100
+ const byKey = new Map();
101
+ for (const decision of decisions) {
102
+ const key = `${decision.policy_id}|${decision.decision}|${decision.reason ?? ''}`;
103
+ if (!byKey.has(key)) {
104
+ byKey.set(key, decision);
105
+ }
106
+ }
107
+ return [...byKey.values()];
108
+ }
109
+ function toPolicyHookDecisions(evaluation) {
110
+ if (evaluation.decisions.length === 0) {
111
+ return [
112
+ {
113
+ policy_id: KERNEL_POLICY_IDS.RUNTIME_FALLBACK,
114
+ decision: evaluation.decision,
115
+ reason: 'Effective policy decision without explicit matching rules.',
116
+ },
117
+ ];
118
+ }
119
+ if (evaluation.decision === 'deny') {
120
+ const hasHardDeny = evaluation.decisions.some((decision) => decision.decision === 'deny');
121
+ if (!hasHardDeny) {
122
+ return [
123
+ ...evaluation.decisions,
124
+ {
125
+ policy_id: KERNEL_POLICY_IDS.RUNTIME_FALLBACK,
126
+ decision: 'deny',
127
+ reason: 'Effective policy decision is deny.',
128
+ },
129
+ ];
130
+ }
131
+ }
132
+ return evaluation.decisions;
133
+ }
134
+ function mergeStateAliases(loadedPacks) {
135
+ const aliases = {};
136
+ const validStates = new Set(['ready', 'active', 'blocked', 'waiting', 'done']);
137
+ for (const loadedPack of loadedPacks) {
138
+ for (const [state, alias] of Object.entries(loadedPack.manifest.state_aliases)) {
139
+ if (!validStates.has(state)) {
140
+ continue;
141
+ }
142
+ aliases[state] = alias;
143
+ }
144
+ }
145
+ return aliases;
146
+ }
147
+ function formatRuntimeLoadStageError(packId) {
148
+ return `${RUNTIME_LOAD_STAGE_ERROR_PREFIX} "${packId}"`;
149
+ }
150
+ function formatRuntimeRegistrationStageError(toolName, packId) {
151
+ return `${RUNTIME_REGISTRATION_STAGE_ERROR_PREFIX} "${toolName}" in pack "${packId}"`;
152
+ }
153
+ function buildPackToolDescription(toolName, packId) {
154
+ return `Pack tool ${toolName} declared by ${packId}`;
155
+ }
156
+ export async function defaultRuntimeToolCapabilityResolver(input) {
157
+ const resolvedEntry = resolvePackToolEntryPath(input.loadedPack.packRoot, input.tool.entry);
158
+ return {
159
+ name: input.tool.name,
160
+ domain: input.loadedPack.manifest.id,
161
+ version: input.loadedPack.manifest.version,
162
+ input_schema: DEFAULT_PACK_TOOL_INPUT_SCHEMA,
163
+ output_schema: DEFAULT_PACK_TOOL_OUTPUT_SCHEMA,
164
+ permission: input.tool.permission,
165
+ required_scopes: input.tool.required_scopes,
166
+ handler: {
167
+ kind: TOOL_HANDLER_KINDS.SUBPROCESS,
168
+ entry: resolvedEntry,
169
+ },
170
+ description: buildPackToolDescription(input.tool.name, input.loadedPack.manifest.id),
171
+ pack: input.loadedPack.pin.id,
172
+ };
173
+ }
174
+ async function fileExists(targetPath) {
175
+ try {
176
+ await access(targetPath);
177
+ return true;
178
+ }
179
+ catch (error) {
180
+ const nodeError = error;
181
+ if (nodeError.code === 'ENOENT') {
182
+ return false;
183
+ }
184
+ throw error;
185
+ }
186
+ }
187
+ async function resolvePacksRoot(options) {
188
+ if (options.packsRoot) {
189
+ return path.resolve(options.packsRoot);
190
+ }
191
+ const workspaceRoot = path.resolve(options.workspaceRoot);
192
+ for (const candidate of DEFAULT_PACKS_ROOT_CANDIDATES) {
193
+ const absoluteCandidate = path.resolve(workspaceRoot, candidate);
194
+ if (await fileExists(absoluteCandidate)) {
195
+ return absoluteCandidate;
196
+ }
197
+ }
198
+ const fallbackCandidate = DEFAULT_PACKS_ROOT_CANDIDATES[0];
199
+ if (!fallbackCandidate) {
200
+ throw new Error('No default packs root candidates are configured.');
201
+ }
202
+ return path.resolve(workspaceRoot, fallbackCandidate);
203
+ }
204
+ function resolveTaskSpecPath(taskSpecRoot, taskId) {
205
+ return path.join(taskSpecRoot, `${taskId}.yaml`);
206
+ }
207
+ async function readTaskSpecFromDisk(taskSpecRoot, taskId) {
208
+ const taskSpecPath = resolveTaskSpecPath(taskSpecRoot, taskId);
209
+ try {
210
+ const yamlText = await readFile(taskSpecPath, UTF8_ENCODING);
211
+ return TaskSpecSchema.parse(YAML.parse(yamlText));
212
+ }
213
+ catch (error) {
214
+ const nodeError = error;
215
+ if (nodeError.code === 'ENOENT') {
216
+ return null;
217
+ }
218
+ throw error;
219
+ }
220
+ }
221
+ async function writeTaskSpecImmutable(taskSpecRoot, task) {
222
+ await mkdir(taskSpecRoot, { recursive: true });
223
+ const taskSpecPath = resolveTaskSpecPath(taskSpecRoot, task.id);
224
+ const fileHandle = await open(taskSpecPath, 'wx');
225
+ try {
226
+ await fileHandle.writeFile(YAML.stringify(task), UTF8_ENCODING);
227
+ }
228
+ catch (writeError) {
229
+ await fileHandle.close();
230
+ await rm(taskSpecPath, { force: true });
231
+ throw writeError;
232
+ }
233
+ await fileHandle.close();
234
+ return taskSpecPath;
235
+ }
236
+ async function resolveWorkspaceSpec(options) {
237
+ const workspaceRoot = path.resolve(options.workspaceRoot);
238
+ const workspaceFilePath = path.resolve(options.workspaceFilePath ??
239
+ path.join(workspaceRoot, options.workspaceFileName ?? WORKSPACE_FILE_NAME));
240
+ const raw = await readFile(workspaceFilePath, UTF8_ENCODING);
241
+ const workspaceSpec = WorkspaceSpecSchema.parse(YAML.parse(raw));
242
+ return {
243
+ workspace_file_path: workspaceFilePath,
244
+ workspace_spec: workspaceSpec,
245
+ workspace_config_hash: canonical_json(raw),
246
+ };
247
+ }
248
+ function buildDefaultPolicyLayers(loadedPacks) {
249
+ const packRules = loadedPacks.flatMap((loadedPack) => {
250
+ return loadedPack.manifest.policies.map((policy) => ({
251
+ id: policy.id,
252
+ trigger: policy.trigger,
253
+ decision: policy.decision,
254
+ reason: policy.reason,
255
+ }));
256
+ });
257
+ return [
258
+ {
259
+ level: 'workspace',
260
+ default_decision: 'allow',
261
+ allow_loosening: true,
262
+ rules: [],
263
+ },
264
+ {
265
+ level: 'lane',
266
+ rules: [],
267
+ },
268
+ {
269
+ level: 'pack',
270
+ rules: packRules,
271
+ },
272
+ {
273
+ level: 'task',
274
+ rules: [],
275
+ },
276
+ ];
277
+ }
278
+ function createRuntimePolicyHook(policyEngine) {
279
+ const builtinPolicyHook = createBuiltinPolicyHook();
280
+ return async (input) => {
281
+ const builtinDecisions = await builtinPolicyHook(input);
282
+ if (builtinDecisions.some((decision) => decision.decision === 'deny')) {
283
+ return builtinDecisions;
284
+ }
285
+ const context = {
286
+ trigger: POLICY_TRIGGERS.ON_TOOL_REQUEST,
287
+ run_id: input.context.run_id,
288
+ task_id: input.context.task_id,
289
+ tool_name: input.capability.name,
290
+ pack_id: input.capability.pack,
291
+ };
292
+ const evaluation = await policyEngine.evaluate(context);
293
+ return [...builtinDecisions, ...toPolicyHookDecisions(evaluation)];
294
+ };
295
+ }
296
+ function resolveReplayTaskFilter(taskId) {
297
+ return {
298
+ taskId,
299
+ };
300
+ }
301
+ function resolveExecutionMetadata(context) {
302
+ if (!context.metadata || typeof context.metadata !== 'object') {
303
+ return {};
304
+ }
305
+ return context.metadata;
306
+ }
307
+ export class DefaultKernelRuntime {
308
+ workspaceSpec;
309
+ workspaceFilePath;
310
+ workspaceConfigHash;
311
+ loadedPacks;
312
+ taskSpecRoot;
313
+ eventStore;
314
+ evidenceStore;
315
+ toolHost;
316
+ policyEngine;
317
+ stateAliases;
318
+ now;
319
+ runIdFactory;
320
+ constructor(options) {
321
+ this.workspaceSpec = options.workspace_spec;
322
+ this.workspaceFilePath = options.workspace_file_path;
323
+ this.workspaceConfigHash = options.workspace_config_hash;
324
+ this.loadedPacks = options.loaded_packs;
325
+ this.taskSpecRoot = options.task_spec_root;
326
+ this.eventStore = options.event_store;
327
+ this.evidenceStore = options.evidence_store;
328
+ this.toolHost = options.tool_host;
329
+ this.policyEngine = options.policy_engine;
330
+ this.stateAliases = options.state_aliases ?? {};
331
+ this.now = options.now ?? (() => new Date());
332
+ this.runIdFactory = options.run_id_factory ?? defaultRunIdFactory;
333
+ }
334
+ getToolHost() {
335
+ return this.toolHost;
336
+ }
337
+ getPolicyEngine() {
338
+ return this.policyEngine;
339
+ }
340
+ async executeTool(name, input, ctx) {
341
+ const context = ExecutionContextSchema.parse(ctx);
342
+ const metadata = resolveExecutionMetadata(context);
343
+ const expectedHash = this.workspaceConfigHash;
344
+ let actualHash;
345
+ let missingWorkspaceFile = false;
346
+ try {
347
+ actualHash = await this.computeWorkspaceConfigHash();
348
+ }
349
+ catch (error) {
350
+ const nodeError = error;
351
+ if (nodeError.code !== 'ENOENT') {
352
+ throw error;
353
+ }
354
+ missingWorkspaceFile = true;
355
+ actualHash = canonical_json({
356
+ [WORKSPACE_CONFIG_HASH_CONTEXT_KEYS.WORKSPACE_FILE_MISSING]: this.workspaceFilePath,
357
+ });
358
+ }
359
+ if (actualHash !== expectedHash) {
360
+ const tamperedEvent = {
361
+ schema_version: 1,
362
+ kind: KERNEL_EVENT_KINDS.SPEC_TAMPERED,
363
+ spec: 'workspace',
364
+ id: this.workspaceSpec.id,
365
+ expected_hash: expectedHash,
366
+ actual_hash: actualHash,
367
+ timestamp: normalizeTimestamp(this.now),
368
+ };
369
+ await this.eventStore.append(tamperedEvent);
370
+ return {
371
+ success: false,
372
+ error: {
373
+ code: SPEC_TAMPERED_ERROR_CODE,
374
+ message: missingWorkspaceFile
375
+ ? SPEC_TAMPERED_WORKSPACE_MISSING_MESSAGE
376
+ : SPEC_TAMPERED_WORKSPACE_MESSAGE,
377
+ details: {
378
+ workspace_id: this.workspaceSpec.id,
379
+ workspace_file_path: this.workspaceFilePath,
380
+ [WORKSPACE_CONFIG_HASH_CONTEXT_KEYS.WORKSPACE_FILE_MISSING]: missingWorkspaceFile,
381
+ expected_hash: expectedHash,
382
+ actual_hash: actualHash,
383
+ },
384
+ },
385
+ };
386
+ }
387
+ const runtimeContext = ExecutionContextSchema.parse({
388
+ ...context,
389
+ metadata: {
390
+ ...metadata,
391
+ [EXECUTION_METADATA_KEYS.WORKSPACE_CONFIG_HASH]: actualHash,
392
+ },
393
+ });
394
+ return this.toolHost.execute(name, input, runtimeContext);
395
+ }
396
+ async createTask(taskSpec) {
397
+ const parsedTask = TaskSpecSchema.parse(taskSpec);
398
+ if (parsedTask.workspace_id !== this.workspaceSpec.id) {
399
+ throw new Error(`Task workspace mismatch: expected ${this.workspaceSpec.id}, got ${parsedTask.workspace_id}`);
400
+ }
401
+ const laneExists = this.workspaceSpec.lanes.some((lane) => lane.id === parsedTask.lane_id);
402
+ if (!laneExists) {
403
+ throw new Error(`Task lane "${parsedTask.lane_id}" is not declared in workspace lanes.`);
404
+ }
405
+ let taskSpecPath;
406
+ try {
407
+ taskSpecPath = await writeTaskSpecImmutable(this.taskSpecRoot, parsedTask);
408
+ }
409
+ catch (error) {
410
+ const nodeError = error;
411
+ if (nodeError.code === 'EEXIST') {
412
+ throw new Error(`Task spec already exists for ${parsedTask.id} and is immutable.`, {
413
+ cause: error,
414
+ });
415
+ }
416
+ throw error;
417
+ }
418
+ const createdEvent = {
419
+ schema_version: 1,
420
+ kind: KERNEL_EVENT_KINDS.TASK_CREATED,
421
+ task_id: parsedTask.id,
422
+ timestamp: normalizeTimestamp(this.now),
423
+ spec_hash: canonical_json(parsedTask),
424
+ };
425
+ try {
426
+ await this.eventStore.append(createdEvent);
427
+ }
428
+ catch (eventError) {
429
+ // Clean up the spec file if event emission fails (crash recovery).
430
+ // Without this, a spec file exists on disk with no corresponding event,
431
+ // and the task cannot be retried because 'wx' open will fail with EEXIST.
432
+ await rm(taskSpecPath, { force: true });
433
+ throw eventError;
434
+ }
435
+ return {
436
+ task: parsedTask,
437
+ task_spec_path: taskSpecPath,
438
+ event: createdEvent,
439
+ };
440
+ }
441
+ async claimTask(input) {
442
+ const task = await this.requireTaskSpec(input.task_id);
443
+ const projected = await this.projectTaskState(task.id);
444
+ assertTransition(projected.status, 'active', task.id, this.stateAliases);
445
+ const runId = this.runIdFactory(task.id, projected.run_count + 1);
446
+ const policy = await this.policyEngine.evaluate({
447
+ trigger: POLICY_TRIGGERS.ON_CLAIM,
448
+ run_id: runId,
449
+ task_id: task.id,
450
+ lane_id: task.lane_id,
451
+ pack_id: task.domain,
452
+ });
453
+ if (policy.decision === 'deny') {
454
+ throw new Error(`Policy denied claim for ${task.id}.`);
455
+ }
456
+ const claimedTimestamp = normalizeTimestamp(this.now, input.timestamp);
457
+ const claimedEvent = {
458
+ schema_version: 1,
459
+ kind: KERNEL_EVENT_KINDS.TASK_CLAIMED,
460
+ task_id: task.id,
461
+ timestamp: claimedTimestamp,
462
+ by: input.by,
463
+ session_id: input.session_id,
464
+ domain_data: input.domain_data,
465
+ };
466
+ const runStartedEvent = {
467
+ schema_version: 1,
468
+ kind: KERNEL_EVENT_KINDS.RUN_STARTED,
469
+ task_id: task.id,
470
+ run_id: runId,
471
+ timestamp: claimedTimestamp,
472
+ by: input.by,
473
+ session_id: input.session_id,
474
+ };
475
+ await this.eventStore.appendAll([claimedEvent, runStartedEvent]);
476
+ return {
477
+ task_id: task.id,
478
+ run: RunSchema.parse({
479
+ run_id: runId,
480
+ task_id: task.id,
481
+ status: RUN_STATUSES.EXECUTING,
482
+ started_at: runStartedEvent.timestamp,
483
+ by: input.by,
484
+ session_id: input.session_id,
485
+ }),
486
+ events: [claimedEvent, runStartedEvent],
487
+ policy,
488
+ };
489
+ }
490
+ async blockTask(input) {
491
+ const task = await this.requireTaskSpec(input.task_id);
492
+ const projected = await this.projectTaskState(task.id);
493
+ assertTransition(projected.status, 'blocked', task.id, this.stateAliases);
494
+ const reason = input.reason.trim();
495
+ if (reason.length === 0) {
496
+ throw new Error(`Cannot block ${task.id}: reason is required.`);
497
+ }
498
+ const blockedEvent = {
499
+ schema_version: 1,
500
+ kind: KERNEL_EVENT_KINDS.TASK_BLOCKED,
501
+ task_id: task.id,
502
+ timestamp: normalizeTimestamp(this.now, input.timestamp),
503
+ reason,
504
+ };
505
+ await this.eventStore.append(blockedEvent);
506
+ return {
507
+ task_id: task.id,
508
+ event: blockedEvent,
509
+ };
510
+ }
511
+ async unblockTask(input) {
512
+ const task = await this.requireTaskSpec(input.task_id);
513
+ const projected = await this.projectTaskState(task.id);
514
+ assertTransition(projected.status, 'active', task.id, this.stateAliases);
515
+ const unblockedEvent = {
516
+ schema_version: 1,
517
+ kind: KERNEL_EVENT_KINDS.TASK_UNBLOCKED,
518
+ task_id: task.id,
519
+ timestamp: normalizeTimestamp(this.now, input.timestamp),
520
+ };
521
+ await this.eventStore.append(unblockedEvent);
522
+ return {
523
+ task_id: task.id,
524
+ event: unblockedEvent,
525
+ };
526
+ }
527
+ async completeTask(input) {
528
+ const task = await this.requireTaskSpec(input.task_id);
529
+ const projected = await this.projectTaskState(task.id);
530
+ assertTransition(projected.status, 'done', task.id, this.stateAliases);
531
+ const runId = input.run_id ?? projected.current_run?.run_id;
532
+ if (!runId) {
533
+ throw new Error(`Cannot complete ${task.id}: no active run found.`);
534
+ }
535
+ const policy = await this.policyEngine.evaluate({
536
+ trigger: POLICY_TRIGGERS.ON_COMPLETION,
537
+ run_id: runId,
538
+ task_id: task.id,
539
+ lane_id: task.lane_id,
540
+ pack_id: task.domain,
541
+ });
542
+ if (policy.decision === 'deny') {
543
+ throw new Error(`Policy denied completion for ${task.id}.`);
544
+ }
545
+ const completedTimestamp = normalizeTimestamp(this.now, input.timestamp);
546
+ const runSucceededEvent = {
547
+ schema_version: 1,
548
+ kind: KERNEL_EVENT_KINDS.RUN_SUCCEEDED,
549
+ task_id: task.id,
550
+ run_id: runId,
551
+ timestamp: completedTimestamp,
552
+ evidence_refs: input.evidence_refs,
553
+ };
554
+ const taskCompletedEvent = {
555
+ schema_version: 1,
556
+ kind: KERNEL_EVENT_KINDS.TASK_COMPLETED,
557
+ task_id: task.id,
558
+ timestamp: completedTimestamp,
559
+ evidence_refs: input.evidence_refs,
560
+ };
561
+ await this.eventStore.appendAll([runSucceededEvent, taskCompletedEvent]);
562
+ await this.evidenceStore.pruneTask(task.id);
563
+ return {
564
+ task_id: task.id,
565
+ run_id: runId,
566
+ events: [runSucceededEvent, taskCompletedEvent],
567
+ policy,
568
+ };
569
+ }
570
+ async inspectTask(taskId) {
571
+ const task = await this.requireTaskSpec(taskId);
572
+ const replayResult = await this.eventStore.replay(resolveReplayTaskFilter(taskId));
573
+ const events = replayResult.events;
574
+ const state = projectTaskState(task, events);
575
+ const runHistory = buildRunHistory(events);
576
+ const receipts = await this.readReceiptsForTask(taskId);
577
+ const receiptDecisions = receipts.flatMap((receipt) => {
578
+ if (receipt.kind === TOOL_TRACE_KINDS.TOOL_CALL_FINISHED) {
579
+ return receipt.policy_decisions;
580
+ }
581
+ return [];
582
+ });
583
+ const completionDecisions = state.status === 'done' ? await this.evaluateCompletionPolicy(task, state) : [];
584
+ return {
585
+ task_id: taskId,
586
+ task,
587
+ state,
588
+ run_history: runHistory,
589
+ receipts,
590
+ policy_decisions: dedupePolicyDecisions([...receiptDecisions, ...completionDecisions]),
591
+ events,
592
+ };
593
+ }
594
+ async requireTaskSpec(taskId) {
595
+ const loaded = await readTaskSpecFromDisk(this.taskSpecRoot, taskId);
596
+ if (!loaded) {
597
+ throw new Error(`Task spec not found for ${taskId}`);
598
+ }
599
+ return loaded;
600
+ }
601
+ async projectTaskState(taskId) {
602
+ const task = await this.requireTaskSpec(taskId);
603
+ const { events } = await this.eventStore.replay(resolveReplayTaskFilter(taskId));
604
+ return projectTaskState(task, events);
605
+ }
606
+ async evaluateCompletionPolicy(task, state) {
607
+ const runId = state.current_run?.run_id;
608
+ if (!runId) {
609
+ return [];
610
+ }
611
+ const evaluation = await this.policyEngine.evaluate({
612
+ trigger: POLICY_TRIGGERS.ON_COMPLETION,
613
+ run_id: runId,
614
+ task_id: task.id,
615
+ lane_id: task.lane_id,
616
+ pack_id: task.domain,
617
+ });
618
+ return evaluation.decisions;
619
+ }
620
+ async computeWorkspaceConfigHash() {
621
+ const raw = await readFile(this.workspaceFilePath, UTF8_ENCODING);
622
+ return canonical_json(raw);
623
+ }
624
+ async readReceiptsForTask(taskId) {
625
+ const indexed = await this.evidenceStore.readTracesByTaskId(taskId);
626
+ if (indexed.length > 0) {
627
+ return indexed;
628
+ }
629
+ // After pruneTask (called during completeTask), the in-memory index is
630
+ // cleared but traces remain in orderedTraces. Fall back to a full scan
631
+ // so inspectTask still returns receipts for completed tasks.
632
+ const traces = await this.evidenceStore.readTraces();
633
+ const receiptIds = new Set();
634
+ for (const trace of traces) {
635
+ if (trace.kind !== TOOL_TRACE_KINDS.TOOL_CALL_STARTED) {
636
+ continue;
637
+ }
638
+ if (trace.task_id === taskId) {
639
+ receiptIds.add(trace.receipt_id);
640
+ }
641
+ }
642
+ return traces.filter((trace) => {
643
+ if (trace.kind === TOOL_TRACE_KINDS.TOOL_CALL_STARTED) {
644
+ return trace.task_id === taskId;
645
+ }
646
+ return receiptIds.has(trace.receipt_id);
647
+ });
648
+ }
649
+ }
650
+ export async function initializeKernelRuntime(options) {
651
+ const workspaceRoot = path.resolve(options.workspaceRoot);
652
+ const resolvedWorkspace = await resolveWorkspaceSpec(options);
653
+ const workspaceSpec = resolvedWorkspace.workspace_spec;
654
+ const packsRoot = await resolvePacksRoot(options);
655
+ const now = options.now ?? (() => new Date());
656
+ const taskSpecRoot = path.resolve(options.taskSpecRoot ??
657
+ path.join(workspaceRoot, DEFAULT_RUNTIME_ROOT, KERNEL_RUNTIME_TASKS_DIR_NAME));
658
+ const eventsFilePath = path.resolve(options.eventsFilePath ??
659
+ path.join(workspaceRoot, DEFAULT_RUNTIME_ROOT, KERNEL_RUNTIME_EVENTS_DIR_NAME, KERNEL_RUNTIME_EVENTS_FILE_NAME));
660
+ const eventLockFilePath = path.resolve(options.eventLockFilePath ??
661
+ path.join(workspaceRoot, DEFAULT_RUNTIME_ROOT, KERNEL_RUNTIME_EVENTS_DIR_NAME, KERNEL_RUNTIME_EVENTS_LOCK_FILE_NAME));
662
+ const evidenceRoot = path.resolve(options.evidenceRoot ??
663
+ path.join(workspaceRoot, DEFAULT_RUNTIME_ROOT, KERNEL_RUNTIME_EVIDENCE_DIR_NAME));
664
+ const packLoader = new PackLoader({ packsRoot });
665
+ const loadedPacks = [];
666
+ for (const pin of workspaceSpec.packs) {
667
+ let loadedPack;
668
+ try {
669
+ loadedPack = await packLoader.load({
670
+ workspaceSpec,
671
+ packId: pin.id,
672
+ });
673
+ }
674
+ catch (error) {
675
+ throw new Error(formatRuntimeLoadStageError(pin.id), { cause: error });
676
+ }
677
+ loadedPacks.push(loadedPack);
678
+ }
679
+ const registry = new ToolRegistry();
680
+ if (options.includeBuiltinTools !== false) {
681
+ registerBuiltinToolCapabilities(registry, {
682
+ declaredScopes: workspaceSpec.security.allowed_scopes,
683
+ });
684
+ }
685
+ const toolCapabilityResolver = options.toolCapabilityResolver ?? defaultRuntimeToolCapabilityResolver;
686
+ for (const loadedPack of loadedPacks) {
687
+ for (const tool of loadedPack.manifest.tools) {
688
+ let capability;
689
+ try {
690
+ capability = await toolCapabilityResolver({
691
+ workspaceSpec,
692
+ loadedPack,
693
+ tool,
694
+ });
695
+ }
696
+ catch (error) {
697
+ throw new Error(formatRuntimeRegistrationStageError(tool.name, loadedPack.pin.id), {
698
+ cause: error,
699
+ });
700
+ }
701
+ if (capability) {
702
+ registry.register(capability);
703
+ }
704
+ }
705
+ }
706
+ const policyEngine = new PolicyEngine({
707
+ layers: options.policyLayers ?? buildDefaultPolicyLayers(loadedPacks),
708
+ });
709
+ const evidenceStore = new EvidenceStore({ evidenceRoot });
710
+ const eventStoreOptions = {
711
+ eventsFilePath,
712
+ lockFilePath: eventLockFilePath,
713
+ taskSpecLoader: async (taskId) => readTaskSpecFromDisk(taskSpecRoot, taskId),
714
+ };
715
+ const eventStore = new EventStore(eventStoreOptions);
716
+ const workspaceUpdatedEvent = {
717
+ schema_version: 1,
718
+ kind: KERNEL_EVENT_KINDS.WORKSPACE_UPDATED,
719
+ timestamp: normalizeTimestamp(now),
720
+ config_hash: resolvedWorkspace.workspace_config_hash,
721
+ changes_summary: WORKSPACE_UPDATED_INIT_SUMMARY,
722
+ };
723
+ await eventStore.append(workspaceUpdatedEvent);
724
+ const toolHost = new ToolHost({
725
+ registry,
726
+ evidenceStore,
727
+ subprocessDispatcher: options.subprocessDispatcher ||
728
+ new SandboxSubprocessDispatcher({
729
+ workspaceRoot,
730
+ ...options.sandboxSubprocessDispatcherOptions,
731
+ }),
732
+ policyHook: createRuntimePolicyHook(policyEngine),
733
+ runtimeVersion: options.runtimeVersion,
734
+ });
735
+ await toolHost.onStartup();
736
+ return new DefaultKernelRuntime({
737
+ workspace_spec: workspaceSpec,
738
+ workspace_file_path: resolvedWorkspace.workspace_file_path,
739
+ workspace_config_hash: resolvedWorkspace.workspace_config_hash,
740
+ loaded_packs: loadedPacks,
741
+ task_spec_root: taskSpecRoot,
742
+ event_store: eventStore,
743
+ evidence_store: evidenceStore,
744
+ tool_host: toolHost,
745
+ policy_engine: policyEngine,
746
+ state_aliases: mergeStateAliases(loadedPacks),
747
+ now,
748
+ run_id_factory: options.runIdFactory,
749
+ });
750
+ }
751
+ //# sourceMappingURL=kernel-runtime.js.map