@fabric-harness/sdk 2.4.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/session.js CHANGED
@@ -1,29 +1,30 @@
1
- import { createHash } from 'node:crypto';
2
- import { CostBudgetTracker } from './cost-budget.js';
3
- import { inMemorySessionMemory } from './session-memory.js';
4
- import { FabricError } from './error.js';
5
- import { SessionHistory, buildModelMessagesFromHistory, findTrailingDanglingToolCalls, findTrailingUnfinishedTasks } from './history.js';
6
- import { getLogger } from './logger.js';
7
- import { defaultLoopRuntime } from './loop.js';
8
- import { defaultModelProvider, generateWithRuntime } from './model.js';
9
- import { clampCommandTimeout, evaluateCommandPolicy, evaluateToolCallPolicy } from './policy.js';
10
- import { buildResultRetryPrompt, validateResult } from './result.js';
11
- import { estimateSessionEntryTokens, evaluateContextBudget, isContextOverflowError } from './context-budget.js';
12
- import { policiedSandboxEnv, runWithSandboxPolicyApproval } from './policied-sandbox.js';
13
- import { currentSubmissionContext } from './submission-context.js';
14
- import { createSandboxEnv, createScopedSandboxEnv } from './sandbox.js';
15
- import { createFabricFs } from './filesystem.js';
16
- import { registerSandbox, serializeSandboxRef, unregisterSandbox } from './sandbox-ref.js';
17
- import path from 'node:path';
18
- import { defaultSessionStore } from './store.js';
19
- import { createActivateSkillTool, createBuiltinTools, createCommandTools, shellQuote } from './tools.js';
20
- import { formatPackagedSkillResourceList, readPackagedSkillFile } from './skills.js';
21
- import { MAX_TASK_DEPTH } from './types.js';
1
+ import { createHash } from "node:crypto";
2
+ import { CostBudgetTracker } from "./cost-budget.js";
3
+ import { inMemorySessionMemory } from "./session-memory.js";
4
+ import { FabricError } from "./error.js";
5
+ import { approvalGrantFromState, approvalInputDigest, createApprovalGrant } from "./approval.js";
6
+ import { SessionHistory, buildModelMessagesFromHistory, findTrailingDanglingToolCalls, findTrailingUnfinishedTasks, } from "./history.js";
7
+ import { getLogger } from "./logger.js";
8
+ import { defaultLoopRuntime } from "./loop.js";
9
+ import { defaultModelProvider, generateWithRuntime } from "./model.js";
10
+ import { clampCommandTimeout, evaluateCommandPolicy, evaluateToolCallPolicy } from "./policy.js";
11
+ import { buildResultRetryPrompt, validateResult } from "./result.js";
12
+ import { estimateSessionEntryTokens, evaluateContextBudget, isContextOverflowError, } from "./context-budget.js";
13
+ import { policiedSandboxEnv, runWithSandboxPolicyApproval } from "./policied-sandbox.js";
14
+ import { currentSubmissionContext } from "./submission-context.js";
15
+ import { createSandboxEnv, createScopedSandboxEnv } from "./sandbox.js";
16
+ import { createFabricFs } from "./filesystem.js";
17
+ import { registerSandbox, serializeSandboxRef, unregisterSandbox } from "./sandbox-ref.js";
18
+ import path from "node:path";
19
+ import { defaultSessionStore } from "./store.js";
20
+ import { createActivateSkillTool, createBuiltinTools, createCommandTools, shellQuote, } from "./tools.js";
21
+ import { formatPackagedSkillResourceList, readPackagedSkillFile } from "./skills.js";
22
+ import { MAX_TASK_DEPTH } from "./types.js";
22
23
  let nextTaskId = 0;
23
- const CROCKFORD_ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
24
+ const CROCKFORD_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
24
25
  const AFFINITY_KEY_REGEX = /^aff_[0-7][0-9A-HJKMNP-TV-Z]{25}$/;
25
26
  function encodeBase32(buf, length) {
26
- let result = '';
27
+ let result = "";
27
28
  let bitIndex = 0; // 0 = MSB of buf[0]
28
29
  for (let i = 0; i < length; i++) {
29
30
  let value = 0;
@@ -46,7 +47,7 @@ function encodeBase32(buf, length) {
46
47
  * with overwhelming probability.
47
48
  */
48
49
  export function generateAffinityKey(agentId, sessionId) {
49
- const hash = createHash('sha256').update(agentId).update('\0').update(sessionId).digest();
50
+ const hash = createHash("sha256").update(agentId).update("\0").update(sessionId).digest();
50
51
  // Build a 128-bit buffer from the hash, clearing the top 5 bits of the
51
52
  // first byte so the first Crockford base32 char is in [0-7].
52
53
  const combined = Buffer.alloc(16);
@@ -57,7 +58,7 @@ export function generateAffinityKey(agentId, sessionId) {
57
58
  export function isValidAffinityKey(key) {
58
59
  return AFFINITY_KEY_REGEX.test(key);
59
60
  }
60
- const COMPACTION_SUMMARIZATION_SYSTEM_PROMPT = 'Summarize durable agent session history for future context. Preserve user goals, decisions, tool/command effects, files touched, errors, policy/approval events, and pending work. Do not include secrets.';
61
+ const COMPACTION_SUMMARIZATION_SYSTEM_PROMPT = "Summarize durable agent session history for future context. Preserve user goals, decisions, tool/command effects, files touched, errors, policy/approval events, and pending work. Do not include secrets.";
61
62
  const TURN_PREFIX_SUMMARIZATION_PROMPT = `This is the PREFIX of a turn that was too large to keep. The SUFFIX (recent work) is retained.
62
63
 
63
64
  Summarize the prefix to provide context for the retained suffix:
@@ -126,20 +127,22 @@ export class StubFabricSession {
126
127
  }
127
128
  async prompt(text, promptOptions = {}) {
128
129
  const ambientSignal = promptOptions.signal === undefined ? currentSubmissionContext()?.signal : undefined;
129
- const options = ambientSignal ? { ...promptOptions, signal: ambientSignal } : promptOptions;
130
+ const options = ambientSignal
131
+ ? { ...promptOptions, signal: ambientSignal }
132
+ : promptOptions;
130
133
  const durable = await this.getDurableRuntime();
131
134
  if (durable) {
132
135
  // Delegate to the durable runtime; the runtime is responsible for
133
136
  // recording entries / events on its own side. We still emit a single
134
137
  // local prompt_start/end pair so local subscribers see activity.
135
- await this.emit('prompt_start', { text });
138
+ await this.emit("prompt_start", { text });
136
139
  try {
137
140
  const result = await durable.prompt({ text, ...(options ? { options } : {}) });
138
- await this.emit('prompt_end', { text });
141
+ await this.emit("prompt_end", { text });
139
142
  return result;
140
143
  }
141
144
  catch (error) {
142
- await this.emit('error', { error: error instanceof Error ? error.message : String(error) });
145
+ await this.emit("error", { error: error instanceof Error ? error.message : String(error) });
143
146
  throw error;
144
147
  }
145
148
  }
@@ -149,7 +152,7 @@ export class StubFabricSession {
149
152
  this.eventSubscribers.add(promptSubscriber);
150
153
  try {
151
154
  await this.repairDanglingToolCalls();
152
- await this.emit('prompt_start', { text, ...(role ? { role: role.name } : {}) });
155
+ await this.emit("prompt_start", { text, ...(role ? { role: role.name } : {}) });
153
156
  const sandbox = createScopedSandboxEnv(await this.sandbox, options.cwd);
154
157
  const tools = this.resolveTools(sandbox, options.tools, options.commands);
155
158
  // Submission-correlated prompts are idempotent by submission id: a
@@ -159,10 +162,11 @@ export class StubFabricSession {
159
162
  if (options.submission) {
160
163
  const prior = await this.history();
161
164
  const submissionId = options.submission.submissionId;
162
- inputAlreadyApplied = (prior.entries ?? []).some((entry) => entry.type === 'user_prompt' && entry.data?.submissionId === submissionId);
165
+ inputAlreadyApplied = (prior.entries ?? []).some((entry) => entry.type === "user_prompt" &&
166
+ entry.data?.submissionId === submissionId);
163
167
  }
164
168
  if (!inputAlreadyApplied) {
165
- await this.recordEntry('user_prompt', {
169
+ await this.recordEntry("user_prompt", {
166
170
  text,
167
171
  ...(role ? { role: role.name } : {}),
168
172
  ...(options.submission ? { submissionId: options.submission.submissionId } : {}),
@@ -175,13 +179,13 @@ export class StubFabricSession {
175
179
  await options.submission?.onInputApplied?.();
176
180
  const sessionData = await this.history();
177
181
  let messages = buildModelMessagesFromHistory(sessionData, role);
178
- if (!messages.some((message) => message.role === 'user' && message.content === text))
179
- messages.push({ role: 'user', content: text });
182
+ if (!messages.some((message) => message.role === "user" && message.content === text))
183
+ messages.push({ role: "user", content: text });
180
184
  messages = await this.autoCompactIfNeeded(messages, role, options);
181
185
  if (options.toolCalls?.length) {
182
186
  const results = await this.executeToolCalls(options.toolCalls, tools, sandbox, options.policy, options.approvalTimeoutMs ?? this.options.approvalTimeoutMs, options.onApproval, undefined, options.signal);
183
- await this.recordEntry('result', { result: results });
184
- await this.emit('prompt_end', { text, result: results });
187
+ await this.recordEntry("result", { result: results });
188
+ await this.emit("prompt_end", { text, result: results });
185
189
  return validateResult(results, options.result, options.resultExtraction);
186
190
  }
187
191
  // Use tool-based result extraction when a validator is provided and the caller
@@ -190,25 +194,44 @@ export class StubFabricSession {
190
194
  const runtime = options.loopRuntime ?? this.options.loopRuntime ?? defaultLoopRuntime;
191
195
  const runRuntime = () => runtime.runPrompt({
192
196
  sessionId: this.id,
193
- ...(sessionData.affinityKey !== undefined ? { affinityKey: sessionData.affinityKey } : {}),
197
+ ...(sessionData.affinityKey !== undefined
198
+ ? { affinityKey: sessionData.affinityKey }
199
+ : {}),
194
200
  text,
195
201
  messages,
196
202
  tools,
197
203
  sandbox,
198
- ...(typeof (options.model ?? this.options.model ?? this.agent.model) === 'string' ? { model: (options.model ?? this.options.model ?? this.agent.model) } : {}),
199
- ...((options.thinkingLevel ?? this.options.thinkingLevel ?? this.agent.thinkingLevel) !== undefined ? { thinkingLevel: options.thinkingLevel ?? this.options.thinkingLevel ?? this.agent.thinkingLevel } : {}),
200
- ...((options.modelProvider ?? this.options.modelProvider) !== undefined ? { modelProvider: options.modelProvider ?? this.options.modelProvider } : {}),
204
+ ...(typeof (options.model ?? this.options.model ?? this.agent.model) === "string"
205
+ ? { model: (options.model ?? this.options.model ?? this.agent.model) }
206
+ : {}),
207
+ ...((options.thinkingLevel ?? this.options.thinkingLevel ?? this.agent.thinkingLevel) !==
208
+ undefined
209
+ ? {
210
+ thinkingLevel: options.thinkingLevel ?? this.options.thinkingLevel ?? this.agent.thinkingLevel,
211
+ }
212
+ : {}),
213
+ ...((options.modelProvider ?? this.options.modelProvider) !== undefined
214
+ ? { modelProvider: options.modelProvider ?? this.options.modelProvider }
215
+ : {}),
201
216
  ...(options.maxIterations !== undefined ? { maxIterations: options.maxIterations } : {}),
202
- ...(options.modelTimeoutMs !== undefined ? { modelTimeoutMs: options.modelTimeoutMs } : {}),
217
+ ...(options.modelTimeoutMs !== undefined
218
+ ? { modelTimeoutMs: options.modelTimeoutMs }
219
+ : {}),
203
220
  ...(options.modelRetries !== undefined ? { modelRetries: options.modelRetries } : {}),
204
- ...(options.modelRetryDelayMs !== undefined ? { modelRetryDelayMs: options.modelRetryDelayMs } : {}),
221
+ ...(options.modelRetryDelayMs !== undefined
222
+ ? { modelRetryDelayMs: options.modelRetryDelayMs }
223
+ : {}),
205
224
  ...(options.signal !== undefined ? { signal: options.signal } : {}),
206
225
  ...(options.policy !== undefined ? { policy: options.policy } : {}),
207
- ...((options.approvalTimeoutMs ?? this.options.approvalTimeoutMs) !== undefined ? { approvalTimeoutMs: options.approvalTimeoutMs ?? this.options.approvalTimeoutMs } : {}),
208
- ...(this.costBudget.enabled() ? {
209
- costBudget: this.costBudget,
210
- requestCostLimitApproval: (ctx) => this.requestCostLimitApproval(ctx, options.approvalTimeoutMs ?? this.options.approvalTimeoutMs, options.onApproval),
211
- } : {}),
226
+ ...((options.approvalTimeoutMs ?? this.options.approvalTimeoutMs) !== undefined
227
+ ? { approvalTimeoutMs: options.approvalTimeoutMs ?? this.options.approvalTimeoutMs }
228
+ : {}),
229
+ ...(this.costBudget.enabled()
230
+ ? {
231
+ costBudget: this.costBudget,
232
+ requestCostLimitApproval: (ctx) => this.requestCostLimitApproval(ctx, options.approvalTimeoutMs ?? this.options.approvalTimeoutMs, options.onApproval),
233
+ }
234
+ : {}),
212
235
  ...(useResultTools ? { resultValidator: options.result } : {}),
213
236
  agentId: this.agent.id,
214
237
  ...(this.options.tenantId !== undefined ? { tenantId: this.options.tenantId } : {}),
@@ -225,11 +248,13 @@ export class StubFabricSession {
225
248
  const recoverFromOverflow = options.recoverContextOverflow ?? compactionSettings?.recoverFromOverflow ?? true;
226
249
  if (!isContextOverflowError(error) || !recoverFromOverflow)
227
250
  throw error;
228
- await this.emit('error', { error: 'Context overflow detected; compacting and retrying once.' });
251
+ await this.emit("error", {
252
+ error: "Context overflow detected; compacting and retrying once.",
253
+ });
229
254
  await this.compact({
230
255
  keepRecentEntries: options.compactionKeepRecentEntries ?? compactionSettings?.keepRecentEntries ?? 20,
231
256
  generateSummary: true,
232
- reason: 'overflow',
257
+ reason: "overflow",
233
258
  });
234
259
  messages = buildModelMessagesFromHistory(await this.history(), role);
235
260
  result = await runRuntime();
@@ -247,13 +272,28 @@ export class StubFabricSession {
247
272
  if (attempt >= resultRetries)
248
273
  throw error;
249
274
  const message = error instanceof Error ? error.message : String(error);
250
- await this.recordEntry('result_retry', { error: message, retryAttempt: attempt + 1, maxRetries: resultRetries });
251
- await this.emit('result_retry', { error: message, retryAttempt: attempt + 1, maxRetries: resultRetries });
252
- messages.push({ role: 'user', content: buildResultRetryPrompt(error, options.resultExtraction) });
275
+ await this.recordEntry("result_retry", {
276
+ error: message,
277
+ retryAttempt: attempt + 1,
278
+ maxRetries: resultRetries,
279
+ });
280
+ await this.emit("result_retry", {
281
+ error: message,
282
+ retryAttempt: attempt + 1,
283
+ maxRetries: resultRetries,
284
+ });
285
+ messages.push({
286
+ role: "user",
287
+ content: buildResultRetryPrompt(error, options.resultExtraction),
288
+ });
253
289
  result = await runRuntime();
254
290
  }
255
291
  }
256
- throw new FabricError({ code: 'RESULT_VALIDATION_FAILED', message: 'Typed result validation failed.', retryable: true });
292
+ throw new FabricError({
293
+ code: "RESULT_VALIDATION_FAILED",
294
+ message: "Typed result validation failed.",
295
+ retryable: true,
296
+ });
257
297
  }
258
298
  finally {
259
299
  if (promptSubscriber)
@@ -304,34 +344,44 @@ export class StubFabricSession {
304
344
  }
305
345
  async skill(name, options = {}) {
306
346
  const skill = this.resolveSkill(name);
307
- await this.recordEntry('skill_start', { skill: name });
308
- await this.emit('skill_start', { skill: name });
347
+ await this.recordEntry("skill_start", { skill: name });
348
+ await this.emit("skill_start", { skill: name });
309
349
  if (!skill)
310
- throw new FabricError({ code: 'SKILL_NOT_FOUND', message: `Skill not found: ${name}`, details: { skill: name } });
311
- const argsText = options.args ? `\n\nArguments:\n${JSON.stringify(options.args, null, 2)}` : '';
312
- const packagedText = skill.packaged ? formatPackagedSkillResourceList(skill.packaged) : '';
313
- const result = await this.prompt(`${skill.content ?? ''}${argsText}${packagedText}`, options);
314
- await this.recordEntry('skill_end', { skill: name });
315
- await this.emit('skill_end', { skill: name });
350
+ throw new FabricError({
351
+ code: "SKILL_NOT_FOUND",
352
+ message: `Skill not found: ${name}`,
353
+ details: { skill: name },
354
+ });
355
+ const argsText = options.args ? `\n\nArguments:\n${JSON.stringify(options.args, null, 2)}` : "";
356
+ const packagedText = skill.packaged ? formatPackagedSkillResourceList(skill.packaged) : "";
357
+ const result = await this.prompt(`${skill.content ?? ""}${argsText}${packagedText}`, options);
358
+ await this.recordEntry("skill_end", { skill: name });
359
+ await this.emit("skill_end", { skill: name });
316
360
  return result;
317
361
  }
318
362
  async task(text, options = {}) {
319
363
  const durable = await this.getDurableRuntime();
320
364
  if (durable) {
321
365
  const taskId = options.id ?? `${this.id}:task-${++nextTaskId}`;
322
- await this.emit('task_start', { taskId, depth: 1, parentSessionId: this.id });
366
+ await this.emit("task_start", { taskId, depth: 1, parentSessionId: this.id });
323
367
  try {
324
368
  const { id: _id, ...taskOptions } = options;
325
369
  const result = await durable.task({
326
370
  text,
327
371
  taskId,
328
- ...(Object.keys(taskOptions).length ? { options: taskOptions } : {}),
372
+ ...(Object.keys(taskOptions).length
373
+ ? { options: taskOptions }
374
+ : {}),
329
375
  });
330
- await this.emit('task_end', { taskId, depth: 1, parentSessionId: this.id });
376
+ await this.emit("task_end", { taskId, depth: 1, parentSessionId: this.id });
331
377
  return result;
332
378
  }
333
379
  catch (error) {
334
- await this.emit('task_failed', { taskId, status: 'failed', error: error instanceof Error ? error.message : String(error) });
380
+ await this.emit("task_failed", {
381
+ taskId,
382
+ status: "failed",
383
+ error: error instanceof Error ? error.message : String(error),
384
+ });
335
385
  throw error;
336
386
  }
337
387
  }
@@ -341,22 +391,42 @@ export class StubFabricSession {
341
391
  throw new Error(`Task nesting exceeds MAX_TASK_DEPTH (${MAX_TASK_DEPTH}). Refusing to spawn deeper child task at depth ${depth}.`);
342
392
  }
343
393
  const startedAt = new Date().toISOString();
344
- await this.recordEntry('task_start', { taskId, text, status: 'running', startedAt, depth, ...(options.agent ? { agent: options.agent } : {}), ...(options.metadata ? { metadata: options.metadata } : {}) });
345
- await this.emit('task_start', { taskId, depth, parentSessionId: this.id });
346
- const checkpointLabel = typeof options.checkpoint === 'string' ? options.checkpoint : `task:${taskId}`;
394
+ await this.recordEntry("task_start", {
395
+ taskId,
396
+ text,
397
+ status: "running",
398
+ startedAt,
399
+ depth,
400
+ ...(options.agent ? { agent: options.agent } : {}),
401
+ ...(options.metadata ? { metadata: options.metadata } : {}),
402
+ });
403
+ await this.emit("task_start", { taskId, depth, parentSessionId: this.id });
404
+ const checkpointLabel = typeof options.checkpoint === "string" ? options.checkpoint : `task:${taskId}`;
347
405
  try {
348
406
  if (options.signal?.aborted)
349
407
  throw abortError();
350
408
  if (options.checkpoint) {
351
- const checkpoint = await this.createCheckpoint({ label: `${checkpointLabel}:before`, metadata: { taskId, phase: 'before' } });
352
- await this.recordEntry('task_checkpoint', { taskId, phase: 'before', checkpoint: checkpoint, ...(checkpoint.snapshotId ? { checkpointId: checkpoint.snapshotId } : {}) });
353
- await this.emit('task_checkpoint', { taskId, phase: 'before', ...(checkpoint.snapshotId ? { checkpointId: checkpoint.snapshotId } : {}) });
409
+ const checkpoint = await this.createCheckpoint({
410
+ label: `${checkpointLabel}:before`,
411
+ metadata: { taskId, phase: "before" },
412
+ });
413
+ await this.recordEntry("task_checkpoint", {
414
+ taskId,
415
+ phase: "before",
416
+ checkpoint: checkpoint,
417
+ ...(checkpoint.snapshotId ? { checkpointId: checkpoint.snapshotId } : {}),
418
+ });
419
+ await this.emit("task_checkpoint", {
420
+ taskId,
421
+ phase: "before",
422
+ ...(checkpoint.snapshotId ? { checkpointId: checkpoint.snapshotId } : {}),
423
+ });
354
424
  }
355
425
  const taskAbort = new AbortController();
356
426
  const stopCancellationWatcher = this.watchTaskCancellation(taskId, taskAbort);
357
427
  const forwardAbort = () => taskAbort.abort(options.signal?.reason);
358
428
  if (options.signal)
359
- options.signal.addEventListener('abort', forwardAbort, { once: true });
429
+ options.signal.addEventListener("abort", forwardAbort, { once: true });
360
430
  try {
361
431
  const child = new StubFabricSession(this.agent, taskId, {
362
432
  ...this.options,
@@ -364,39 +434,65 @@ export class StubFabricSession {
364
434
  store: this.store,
365
435
  sandbox: await this.sandbox,
366
436
  // Propagate depth so nested tasks can enforce MAX_TASK_DEPTH.
367
- metadata: { ...(this.options.metadata ?? {}), ...(options.metadata ?? {}), taskDepth: depth },
437
+ metadata: {
438
+ ...(this.options.metadata ?? {}),
439
+ ...(options.metadata ?? {}),
440
+ taskDepth: depth,
441
+ },
368
442
  });
369
443
  const result = await child.prompt(text, { ...options, signal: taskAbort.signal });
370
444
  if (taskAbort.signal.aborted)
371
445
  throw abortError();
372
446
  if (options.checkpoint) {
373
- const checkpoint = await this.createCheckpoint({ label: `${checkpointLabel}:after`, metadata: { taskId, phase: 'after' } });
374
- await this.recordEntry('task_checkpoint', { taskId, phase: 'after', checkpoint: checkpoint, ...(checkpoint.snapshotId ? { checkpointId: checkpoint.snapshotId } : {}) });
375
- await this.emit('task_checkpoint', { taskId, phase: 'after', ...(checkpoint.snapshotId ? { checkpointId: checkpoint.snapshotId } : {}) });
447
+ const checkpoint = await this.createCheckpoint({
448
+ label: `${checkpointLabel}:after`,
449
+ metadata: { taskId, phase: "after" },
450
+ });
451
+ await this.recordEntry("task_checkpoint", {
452
+ taskId,
453
+ phase: "after",
454
+ checkpoint: checkpoint,
455
+ ...(checkpoint.snapshotId ? { checkpointId: checkpoint.snapshotId } : {}),
456
+ });
457
+ await this.emit("task_checkpoint", {
458
+ taskId,
459
+ phase: "after",
460
+ ...(checkpoint.snapshotId ? { checkpointId: checkpoint.snapshotId } : {}),
461
+ });
376
462
  }
377
463
  const completedAt = new Date().toISOString();
378
- await this.recordEntry('task_end', { taskId, status: 'completed', completedAt, depth });
379
- await this.emit('task_end', { taskId, depth, parentSessionId: this.id });
464
+ await this.recordEntry("task_end", { taskId, status: "completed", completedAt, depth });
465
+ await this.emit("task_end", { taskId, depth, parentSessionId: this.id });
380
466
  return result;
381
467
  }
382
468
  finally {
383
469
  stopCancellationWatcher();
384
470
  if (options.signal)
385
- options.signal.removeEventListener('abort', forwardAbort);
471
+ options.signal.removeEventListener("abort", forwardAbort);
386
472
  }
387
473
  }
388
474
  catch (error) {
389
475
  if (isAbortError(error) || options.signal?.aborted) {
390
476
  const cancelledAt = new Date().toISOString();
391
- const message = error instanceof Error ? error.message : 'Task cancelled';
392
- await this.recordEntry('task_cancelled', { taskId, status: 'cancelled', cancelledAt, reason: message });
393
- await this.emit('task_cancelled', { taskId, status: 'cancelled', cancelledAt, reason: message });
477
+ const message = error instanceof Error ? error.message : "Task cancelled";
478
+ await this.recordEntry("task_cancelled", {
479
+ taskId,
480
+ status: "cancelled",
481
+ cancelledAt,
482
+ reason: message,
483
+ });
484
+ await this.emit("task_cancelled", {
485
+ taskId,
486
+ status: "cancelled",
487
+ cancelledAt,
488
+ reason: message,
489
+ });
394
490
  throw error;
395
491
  }
396
492
  const failedAt = new Date().toISOString();
397
493
  const message = error instanceof Error ? error.message : String(error);
398
- await this.recordEntry('task_failed', { taskId, status: 'failed', failedAt, error: message });
399
- await this.emit('task_failed', { taskId, status: 'failed', failedAt, error: message });
494
+ await this.recordEntry("task_failed", { taskId, status: "failed", failedAt, error: message });
495
+ await this.emit("task_failed", { taskId, status: "failed", failedAt, error: message });
400
496
  throw error;
401
497
  }
402
498
  }
@@ -419,11 +515,15 @@ export class StubFabricSession {
419
515
  return;
420
516
  try {
421
517
  const entries = (await this.store.load(this.id))?.entries ?? [];
422
- if (entries.some((entry) => entry.type === 'task_cancelled' && entry.data?.taskId === taskId))
518
+ if (entries.some((entry) => entry.type === "task_cancelled" && entry.data?.taskId === taskId))
423
519
  controller.abort(abortError());
424
520
  }
425
521
  catch (error) {
426
- logger.warn('Task-cancellation poller failed to load session; will retry next interval.', { error: error instanceof Error ? error.message : String(error), taskId, sessionId: this.id });
522
+ logger.warn("Task-cancellation poller failed to load session; will retry next interval.", {
523
+ error: error instanceof Error ? error.message : String(error),
524
+ taskId,
525
+ sessionId: this.id,
526
+ });
427
527
  }
428
528
  })();
429
529
  }, 500);
@@ -432,9 +532,9 @@ export class StubFabricSession {
432
532
  async shell(command, options = {}) {
433
533
  const durable = await this.getDurableRuntime();
434
534
  if (durable) {
435
- await this.emit('command_start', { command });
535
+ await this.emit("command_start", { command });
436
536
  const result = await durable.shell({ command, ...(options ? { options } : {}) });
437
- await this.emit('command_end', { command, exitCode: result.exitCode });
537
+ await this.emit("command_end", { command, exitCode: result.exitCode });
438
538
  return result;
439
539
  }
440
540
  const sandbox = await this.sandbox;
@@ -445,24 +545,33 @@ export class StubFabricSession {
445
545
  if (!decision.allowed) {
446
546
  if (decision.approvalRequired) {
447
547
  const approved = await this.requestApproval({
448
- kind: 'command',
548
+ kind: "command",
449
549
  command,
450
- input: { command, ...((options.cwd ?? this.options.cwd) ? { cwd: options.cwd ?? this.options.cwd } : {}) },
550
+ input: {
551
+ command,
552
+ ...((options.cwd ?? this.options.cwd)
553
+ ? { cwd: options.cwd ?? this.options.cwd }
554
+ : {}),
555
+ },
451
556
  reason: decision.reason ?? `Approval required for command: ${command}`,
452
557
  ...(decision.risk ? { risk: decision.risk } : {}),
453
558
  ...(decision.matchedPattern ? { matchedPattern: decision.matchedPattern } : {}),
454
559
  }, policy, options.approvalTimeoutMs ?? this.options.approvalTimeoutMs, options.onApproval);
455
560
  if (!approved) {
456
- const message = decision.reason ?? 'Command approval denied or unavailable';
457
- await this.emit('error', { reason: message, command, approvalRequired: true });
458
- throw new FabricError({ code: 'COMMAND_DENIED', message, details: { command, approvalRequired: true } });
561
+ const message = decision.reason ?? "Command approval denied or unavailable";
562
+ await this.emit("error", { reason: message, command, approvalRequired: true });
563
+ throw new FabricError({
564
+ code: "COMMAND_DENIED",
565
+ message,
566
+ details: { command, approvalRequired: true },
567
+ });
459
568
  }
460
569
  policyApproved = true;
461
570
  }
462
571
  else {
463
- const message = decision.reason ?? 'Command denied by policy';
464
- await this.emit('error', { reason: message, command });
465
- throw new FabricError({ code: 'COMMAND_DENIED', message, details: { command } });
572
+ const message = decision.reason ?? "Command denied by policy";
573
+ await this.emit("error", { reason: message, command });
574
+ throw new FabricError({ code: "COMMAND_DENIED", message, details: { command } });
466
575
  }
467
576
  }
468
577
  if (options.cwd !== undefined)
@@ -474,17 +583,22 @@ export class StubFabricSession {
474
583
  const timeout = clampCommandTimeout(options.timeout, policy);
475
584
  if (timeout !== undefined)
476
585
  execOptions.timeout = timeout;
477
- await this.emit('command_start', { command });
586
+ await this.emit("command_start", { command });
478
587
  const execute = () => sandbox.exec(command, execOptions);
479
588
  const result = policyApproved
480
- ? await runWithSandboxPolicyApproval(sandbox, { tool: 'bash', input: { command } }, execute)
589
+ ? await runWithSandboxPolicyApproval(sandbox, { tool: "bash", input: { command } }, execute)
481
590
  : await execute();
482
591
  const redactedCommand = redactEnvValues(command, options.env);
483
592
  const toolCallId = `shell-${Date.now()}`;
484
- await this.recordEntry('user_prompt', { text: `Run shell command: ${redactedCommand}` });
485
- await this.recordEntry('tool_call', { tool: 'bash', input: { command: redactedCommand }, toolCallId, synthetic: true });
486
- await this.recordEntry('tool_result', {
487
- tool: 'bash',
593
+ await this.recordEntry("user_prompt", { text: `Run shell command: ${redactedCommand}` });
594
+ await this.recordEntry("tool_call", {
595
+ tool: "bash",
596
+ input: { command: redactedCommand },
597
+ toolCallId,
598
+ synthetic: true,
599
+ });
600
+ await this.recordEntry("tool_result", {
601
+ tool: "bash",
488
602
  output: {
489
603
  stdout: redactEnvValues(result.stdout, options.env),
490
604
  stderr: redactEnvValues(result.stderr, options.env),
@@ -493,47 +607,75 @@ export class StubFabricSession {
493
607
  toolCallId,
494
608
  synthetic: true,
495
609
  });
496
- await this.emit('command_end', { command, exitCode: result.exitCode, ...(result.durationMs !== undefined ? { durationMs: result.durationMs } : {}) });
610
+ await this.emit("command_end", {
611
+ command,
612
+ exitCode: result.exitCode,
613
+ ...(result.durationMs !== undefined ? { durationMs: result.durationMs } : {}),
614
+ });
497
615
  return result;
498
616
  }
499
617
  async mount(mountAt, source, options = {}) {
500
- if (!mountAt || typeof mountAt !== 'string') {
501
- throw new FabricError({ code: 'MOUNT_INVALID_PATH', message: 'mount() requires a non-empty mountAt path.' });
618
+ if (!mountAt || typeof mountAt !== "string") {
619
+ throw new FabricError({
620
+ code: "MOUNT_INVALID_PATH",
621
+ message: "mount() requires a non-empty mountAt path.",
622
+ });
502
623
  }
503
624
  const sandbox = await this.sandbox;
504
625
  const root = resolveMountPath(sandbox.cwd, mountAt);
505
- if (!await sandbox.exists(root))
626
+ if (!(await sandbox.exists(root)))
506
627
  await sandbox.mkdir(root, { recursive: true });
507
- const mode = options.mode ?? 'read';
628
+ const mode = options.mode ?? "read";
508
629
  let files = 0;
509
630
  let bytes = 0;
510
631
  for await (const entry of source.entries()) {
511
- const relative = entry.path.replace(/^\/+/, '');
632
+ const relative = entry.path.replace(/^\/+/, "");
512
633
  const target = path.posix.join(root, relative);
513
634
  const parent = path.posix.dirname(target);
514
- if (parent && parent !== '.' && parent !== '/') {
515
- if (!await sandbox.exists(parent))
635
+ if (parent && parent !== "." && parent !== "/") {
636
+ if (!(await sandbox.exists(parent)))
516
637
  await sandbox.mkdir(parent, { recursive: true });
517
638
  }
518
639
  await sandbox.writeFile(target, entry.content);
519
640
  files += 1;
520
- bytes += typeof entry.content === 'string' ? Buffer.byteLength(entry.content, 'utf8') : entry.content.byteLength;
641
+ bytes +=
642
+ typeof entry.content === "string"
643
+ ? Buffer.byteLength(entry.content, "utf8")
644
+ : entry.content.byteLength;
521
645
  }
522
646
  this.mounts.push({ mountAt: root, mode });
523
- const result = { mountAt: root, files, bytes, mode, ...(source.name ? { source: source.name } : {}) };
524
- await this.recordEntry('mount', { mountAt: root, files, bytes, mode, ...(source.name ? { source: source.name } : {}) });
525
- await this.emit('mount', { mountAt: root, files, bytes, mode, ...(source.name ? { source: source.name } : {}) });
647
+ const result = {
648
+ mountAt: root,
649
+ files,
650
+ bytes,
651
+ mode,
652
+ ...(source.name ? { source: source.name } : {}),
653
+ };
654
+ await this.recordEntry("mount", {
655
+ mountAt: root,
656
+ files,
657
+ bytes,
658
+ mode,
659
+ ...(source.name ? { source: source.name } : {}),
660
+ });
661
+ await this.emit("mount", {
662
+ mountAt: root,
663
+ files,
664
+ bytes,
665
+ mode,
666
+ ...(source.name ? { source: source.name } : {}),
667
+ });
526
668
  return result;
527
669
  }
528
670
  checkMountWriteAccess(call) {
529
- const writeTools = new Set(['write', 'edit', 'mkdir', 'rm']);
671
+ const writeTools = new Set(["write", "edit", "mkdir", "rm"]);
530
672
  if (!writeTools.has(call.tool) || this.mounts.length === 0)
531
673
  return undefined;
532
- const targetPath = readStringPropertyFromInput(call.input, 'path');
674
+ const targetPath = readStringPropertyFromInput(call.input, "path");
533
675
  if (!targetPath)
534
676
  return undefined;
535
677
  for (const mount of this.mounts) {
536
- if (mount.mode !== 'read')
678
+ if (mount.mode !== "read")
537
679
  continue;
538
680
  if (targetPath === mount.mountAt || targetPath.startsWith(`${mount.mountAt}/`)) {
539
681
  return {
@@ -549,7 +691,7 @@ export class StubFabricSession {
549
691
  return this.ensureData();
550
692
  }
551
693
  async createSignalEntry(data) {
552
- return this.recordEntry('signal', data);
694
+ return this.recordEntry("signal", data);
553
695
  }
554
696
  async sandboxRef(options) {
555
697
  const sandbox = await this.sandbox;
@@ -566,7 +708,11 @@ export class StubFabricSession {
566
708
  await originalCleanup();
567
709
  };
568
710
  }
569
- await this.recordEntry('metric', { event: 'sandbox_ref_created', refId: ref.id, ...(options?.portable ? { portable: true } : {}) });
711
+ await this.recordEntry("metric", {
712
+ event: "sandbox_ref_created",
713
+ refId: ref.id,
714
+ ...(options?.portable ? { portable: true } : {}),
715
+ });
570
716
  if (options?.portable) {
571
717
  return serializeSandboxRef(ref, this.id, this.options.tenantId);
572
718
  }
@@ -576,27 +722,31 @@ export class StubFabricSession {
576
722
  const sandbox = await this.sandbox;
577
723
  if (!sandbox.snapshot || !sandbox.fork) {
578
724
  throw new FabricError({
579
- code: 'SANDBOX_UNAVAILABLE',
580
- message: 'Sandbox does not support fork. Required capabilities: snapshot + fork.',
725
+ code: "SANDBOX_UNAVAILABLE",
726
+ message: "Sandbox does not support fork. Required capabilities: snapshot + fork.",
581
727
  });
582
728
  }
583
729
  const snapshot = await sandbox.snapshot();
584
730
  const sandboxFork = sandbox.fork;
585
731
  const agent = this.agent;
586
732
  const baseOptions = this.options;
587
- await this.recordEntry('checkpoint_created', {
733
+ await this.recordEntry("checkpoint_created", {
588
734
  label: label ?? `fork:${snapshot.id}`,
589
735
  ...(label ? { metadata: { fork: true } } : { metadata: { fork: true } }),
590
736
  snapshotId: snapshot.id,
591
737
  snapshot: snapshot,
592
738
  });
593
- await this.emit('checkpoint_created', { label: label ?? `fork:${snapshot.id}`, snapshotId: snapshot.id });
739
+ await this.emit("checkpoint_created", {
740
+ label: label ?? `fork:${snapshot.id}`,
741
+ snapshotId: snapshot.id,
742
+ });
594
743
  return {
595
744
  snapshotId: snapshot.id,
596
745
  ...(label !== undefined ? { label } : {}),
597
746
  async attach(attachOptions = {}) {
598
747
  const newSandbox = await sandboxFork.call(sandbox, snapshot);
599
- const newId = attachOptions.id ?? `${baseOptions.metadata?.parentSessionId ?? 'fork'}-${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
748
+ const newId = attachOptions.id ??
749
+ `${baseOptions.metadata?.parentSessionId ?? "fork"}-${Date.now().toString(36)}-${Math.random().toString(16).slice(2, 8)}`;
600
750
  const merged = {
601
751
  ...baseOptions,
602
752
  ...attachOptions,
@@ -613,21 +763,23 @@ export class StubFabricSession {
613
763
  };
614
764
  }
615
765
  async artifact(name, content, options = {}) {
616
- const bytes = typeof content === 'string' ? new TextEncoder().encode(content) : content;
766
+ const bytes = typeof content === "string" ? new TextEncoder().encode(content) : content;
617
767
  const fallback = {
618
- id: `${Date.now()}-${name.replace(/[^a-zA-Z0-9._-]/g, '_')}`,
768
+ id: `${Date.now()}-${name.replace(/[^a-zA-Z0-9._-]/g, "_")}`,
619
769
  sessionId: this.id,
620
770
  name,
621
771
  path: name,
622
772
  size: bytes.byteLength,
623
- sha256: createHash('sha256').update(bytes).digest('hex'),
773
+ sha256: createHash("sha256").update(bytes).digest("hex"),
624
774
  createdAt: new Date().toISOString(),
625
775
  ...(options.contentType ? { contentType: options.contentType } : {}),
626
776
  ...(options.metadata ? { metadata: options.metadata } : {}),
627
777
  };
628
- const ref = this.store.putArtifact ? await this.store.putArtifact(this.id, name, bytes, options) : fallback;
629
- await this.recordEntry('artifact_created', { artifact: ref });
630
- await this.emit('artifact_created', { artifact: ref });
778
+ const ref = this.store.putArtifact
779
+ ? await this.store.putArtifact(this.id, name, bytes, options)
780
+ : fallback;
781
+ await this.recordEntry("artifact_created", { artifact: ref });
782
+ await this.emit("artifact_created", { artifact: ref });
631
783
  return ref;
632
784
  }
633
785
  async compact(options = {}) {
@@ -661,12 +813,13 @@ export class StubFabricSession {
661
813
  isSplitTurn: false,
662
814
  fileRefs,
663
815
  };
664
- const baseSummary = options.summary ?? (options.generateSummary
665
- ? await this.generateCompactionSummary(summaryInput)
666
- : summarizeEntries(compacted));
816
+ const baseSummary = options.summary ??
817
+ (options.generateSummary
818
+ ? await this.generateCompactionSummary(summaryInput)
819
+ : summarizeEntries(compacted));
667
820
  const summary = appendFileReferenceTags(baseSummary, fileRefs);
668
821
  const compactedEntries = compacted.length;
669
- const entry = await this.recordEntry('compaction', {
822
+ const entry = await this.recordEntry("compaction", {
670
823
  summary,
671
824
  compactedEntries,
672
825
  ...(fileRefs.readFiles.length > 0 ? { readFiles: fileRefs.readFiles } : {}),
@@ -674,12 +827,12 @@ export class StubFabricSession {
674
827
  ...(firstKeptEntry ? { firstKeptEntryId: firstKeptEntry.id } : {}),
675
828
  });
676
829
  const messagesAfter = Math.max(0, entries.length - compactedEntries) + (compactedEntries > 0 ? 1 : 0);
677
- await this.emit('compaction', {
830
+ await this.emit("compaction", {
678
831
  summary,
679
832
  compactedEntries,
680
833
  messagesBefore: options.messagesBefore ?? entries.length,
681
834
  messagesAfter,
682
- ...(options.reason ? { reason: options.reason } : { reason: 'manual' }),
835
+ ...(options.reason ? { reason: options.reason } : { reason: "manual" }),
683
836
  ...(options.tokensBefore !== undefined ? { tokensBefore: options.tokensBefore } : {}),
684
837
  });
685
838
  return {
@@ -704,13 +857,20 @@ export class StubFabricSession {
704
857
  ...(contextWindowTokens !== undefined ? { contextWindowTokens } : {}),
705
858
  ...(compactAtTokens !== undefined ? { compactAtTokens } : {}),
706
859
  ...(reserveTokens !== undefined ? { reserveTokens } : {}),
707
- ...(settings?.keepRecentTokens !== undefined ? { keepRecentTokens: settings.keepRecentTokens } : {}),
860
+ ...(settings?.keepRecentTokens !== undefined
861
+ ? { keepRecentTokens: settings.keepRecentTokens }
862
+ : {}),
708
863
  });
709
864
  if (!budget.shouldCompact)
710
865
  return messages;
711
866
  const messagesBefore = messages.length;
712
867
  const tokensBefore = budget.estimatedTokens;
713
- const compactOpts = { generateSummary: true, reason: 'threshold', tokensBefore, messagesBefore };
868
+ const compactOpts = {
869
+ generateSummary: true,
870
+ reason: "threshold",
871
+ tokensBefore,
872
+ messagesBefore,
873
+ };
714
874
  if (keepRecentEntries !== undefined) {
715
875
  compactOpts.keepRecentEntries = keepRecentEntries;
716
876
  }
@@ -725,7 +885,7 @@ export class StubFabricSession {
725
885
  const provider = options.modelProvider ?? this.options.modelProvider;
726
886
  if (!provider)
727
887
  return undefined;
728
- const model = typeof (options.model ?? this.options.model ?? this.agent.model) === 'string'
888
+ const model = typeof (options.model ?? this.options.model ?? this.agent.model) === "string"
729
889
  ? (options.model ?? this.options.model ?? this.agent.model)
730
890
  : undefined;
731
891
  return this.readProviderModelMetadata(provider, model)?.contextWindowTokens;
@@ -754,13 +914,19 @@ export class StubFabricSession {
754
914
  const deterministic = summarizeEntries(entries);
755
915
  const provider = this.options.modelProvider ?? defaultModelProvider;
756
916
  try {
757
- const serialized = JSON.stringify(entries.map((entry) => ({ type: entry.type, timestamp: entry.timestamp, data: entry.data ?? {} })).slice(0, 200));
917
+ const serialized = JSON.stringify(entries
918
+ .map((entry) => ({
919
+ type: entry.type,
920
+ timestamp: entry.timestamp,
921
+ data: entry.data ?? {},
922
+ }))
923
+ .slice(0, 200));
758
924
  const userContent = prefixPrompt ? `${serialized}\n\n${prefixPrompt}` : serialized;
759
925
  const response = await generateWithRuntime(provider, {
760
926
  ...(this.agent.model !== undefined ? { model: this.agent.model } : {}),
761
927
  messages: [
762
- { role: 'system', content: COMPACTION_SUMMARIZATION_SYSTEM_PROMPT },
763
- { role: 'user', content: userContent },
928
+ { role: "system", content: COMPACTION_SUMMARIZATION_SYSTEM_PROMPT },
929
+ { role: "user", content: userContent },
764
930
  ],
765
931
  }, { retries: 1, retryDelayMs: 250 });
766
932
  return response.message?.content?.trim() || deterministic;
@@ -772,25 +938,25 @@ export class StubFabricSession {
772
938
  async requestCustomApproval(input) {
773
939
  const timeoutMs = input.timeoutMs ?? this.options.approvalTimeoutMs;
774
940
  const approvalPromise = this.requestApproval({
775
- kind: 'custom',
941
+ kind: "custom",
776
942
  reason: input.reason,
777
- subject: input.subject ?? 'custom',
943
+ subject: input.subject ?? "custom",
778
944
  input: undefined,
779
945
  ...(input.risk ? { risk: input.risk } : {}),
780
946
  ...(input.idempotencyKey !== undefined ? { idempotencyKey: input.idempotencyKey } : {}),
781
947
  }, this.options.policy, timeoutMs, input.onApproval);
782
- let approved;
948
+ let grant;
783
949
  if (timeoutMs !== undefined) {
784
950
  let timer;
785
951
  const timeoutPromise = new Promise((_, reject) => {
786
952
  timer = setTimeout(() => reject(new FabricError({
787
- code: 'APPROVAL_REQUIRED',
953
+ code: "APPROVAL_REQUIRED",
788
954
  message: `Approval timed out after ${timeoutMs}ms.`,
789
955
  retryable: false,
790
956
  })), timeoutMs);
791
957
  });
792
958
  try {
793
- approved = await Promise.race([approvalPromise, timeoutPromise]);
959
+ grant = await Promise.race([approvalPromise, timeoutPromise]);
794
960
  }
795
961
  finally {
796
962
  if (timer)
@@ -798,11 +964,11 @@ export class StubFabricSession {
798
964
  }
799
965
  }
800
966
  else {
801
- approved = await approvalPromise;
967
+ grant = await approvalPromise;
802
968
  }
803
- if (!approved) {
969
+ if (!grant) {
804
970
  throw new FabricError({
805
- code: 'APPROVAL_DENIED',
971
+ code: "APPROVAL_DENIED",
806
972
  message: `Custom approval denied: ${input.reason}`,
807
973
  retryable: false,
808
974
  });
@@ -813,38 +979,52 @@ export class StubFabricSession {
813
979
  const durable = await this.getDurableRuntime();
814
980
  if (durable)
815
981
  return durable.checkpointCreate({ options });
816
- const input = typeof options === 'string' ? { label: options } : options;
982
+ const input = typeof options === "string" ? { label: options } : options;
817
983
  const sandbox = await this.sandbox;
818
984
  const snapshot = sandbox.snapshot ? await sandbox.snapshot() : undefined;
819
- const entry = await this.recordEntry('checkpoint_created', {
985
+ const entry = await this.recordEntry("checkpoint_created", {
820
986
  label: input.label,
821
987
  ...(input.metadata ? { metadata: input.metadata } : {}),
822
988
  ...(snapshot ? { snapshotId: snapshot.id, snapshot: snapshot } : {}),
823
989
  });
824
- await this.emit('checkpoint_created', { label: input.label, ...(snapshot ? { snapshotId: snapshot.id } : {}) });
825
- return { label: input.label, entryId: entry.id, ...(snapshot ? { snapshotId: snapshot.id } : {}) };
990
+ await this.emit("checkpoint_created", {
991
+ label: input.label,
992
+ ...(snapshot ? { snapshotId: snapshot.id } : {}),
993
+ });
994
+ return {
995
+ label: input.label,
996
+ entryId: entry.id,
997
+ ...(snapshot ? { snapshotId: snapshot.id } : {}),
998
+ };
826
999
  }
827
1000
  async restoreCheckpoint(options) {
828
1001
  const durable = await this.getDurableRuntime();
829
1002
  if (durable)
830
1003
  return durable.checkpointRestore({ options });
831
- const input = typeof options === 'string' ? { label: options } : options;
1004
+ const input = typeof options === "string" ? { label: options } : options;
832
1005
  const history = await this.history();
833
1006
  const checkpoint = [...(history.entries ?? [])]
834
1007
  .reverse()
835
- .find((entry) => entry.type === 'checkpoint_created' && entry.data?.label === input.label);
1008
+ .find((entry) => entry.type === "checkpoint_created" && entry.data?.label === input.label);
836
1009
  if (!checkpoint)
837
- throw new FabricError({ code: 'CHECKPOINT_NOT_FOUND', message: `Checkpoint not found: ${input.label}`, details: { label: input.label } });
1010
+ throw new FabricError({
1011
+ code: "CHECKPOINT_NOT_FOUND",
1012
+ message: `Checkpoint not found: ${input.label}`,
1013
+ details: { label: input.label },
1014
+ });
838
1015
  const sandbox = await this.sandbox;
839
1016
  const snapshot = checkpoint.data?.snapshot;
840
1017
  if (sandbox.restore && isSnapshot(snapshot))
841
1018
  await sandbox.restore(snapshot);
842
- const entry = await this.recordEntry('checkpoint_restored', {
1019
+ const entry = await this.recordEntry("checkpoint_restored", {
843
1020
  label: input.label,
844
1021
  checkpointEntryId: checkpoint.id,
845
1022
  ...(isSnapshot(snapshot) ? { snapshotId: snapshot.id } : {}),
846
1023
  });
847
- await this.emit('checkpoint_restored', { label: input.label, checkpointEntryId: checkpoint.id });
1024
+ await this.emit("checkpoint_restored", {
1025
+ label: input.label,
1026
+ checkpointEntryId: checkpoint.id,
1027
+ });
848
1028
  return {
849
1029
  label: input.label,
850
1030
  entryId: entry.id,
@@ -854,12 +1034,19 @@ export class StubFabricSession {
854
1034
  async createSandbox() {
855
1035
  const sandbox = this.options.sandbox;
856
1036
  let env;
857
- if (typeof sandbox === 'object' && sandbox !== null && 'exec' in sandbox)
1037
+ if (typeof sandbox === "object" && sandbox !== null && "exec" in sandbox)
858
1038
  env = sandbox;
859
- else if (typeof sandbox === 'function')
860
- env = await sandbox({ sessionId: this.id, ...(this.options.cwd !== undefined ? { cwd: this.options.cwd } : {}) });
1039
+ else if (typeof sandbox === "function")
1040
+ env = await sandbox({
1041
+ sessionId: this.id,
1042
+ ...(this.options.cwd !== undefined ? { cwd: this.options.cwd } : {}),
1043
+ });
861
1044
  else
862
- env = await createSandboxEnv({ backend: sandbox ?? 'empty', sessionId: this.id, ...(this.options.cwd !== undefined ? { cwd: this.options.cwd } : {}) });
1045
+ env = await createSandboxEnv({
1046
+ backend: sandbox ?? "empty",
1047
+ sessionId: this.id,
1048
+ ...(this.options.cwd !== undefined ? { cwd: this.options.cwd } : {}),
1049
+ });
863
1050
  const scoped = createScopedSandboxEnv(env, this.options.cwd);
864
1051
  return wrapWithPolicy(scoped, this.options.policy, this.options.bypassPolicyEnforcement);
865
1052
  }
@@ -868,39 +1055,56 @@ export class StubFabricSession {
868
1055
  const policy = callPolicy ?? this.options.policy;
869
1056
  for (const call of calls) {
870
1057
  const approvedSandboxCalls = [];
1058
+ let approvalGrant;
871
1059
  const tool = tools.get(call.tool);
1060
+ const executingPrincipal = toolExecutingPrincipal(tool);
872
1061
  const mountDecision = this.checkMountWriteAccess(call);
873
1062
  const policyCall = tool?.policyAlias ? { ...call, tool: tool.policyAlias } : call;
874
1063
  const decision = mountDecision ?? evaluateToolCallPolicy(policyCall, policy);
875
1064
  if (!decision.allowed) {
876
1065
  if (decision.approvalRequired) {
877
- const approved = await this.requestApproval({
878
- kind: 'tool',
1066
+ const grant = await this.requestApproval({
1067
+ kind: "tool",
879
1068
  tool: call.tool,
1069
+ toolCallId: call.id ?? `${this.id}:${call.tool}`,
1070
+ ...(executingPrincipal ? { executingPrincipal } : {}),
880
1071
  input: call.input,
881
1072
  reason: decision.reason ?? `Approval required for tool: ${call.tool}`,
882
1073
  ...(decision.risk ? { risk: decision.risk } : {}),
883
1074
  ...(decision.matchedPattern ? { matchedPattern: decision.matchedPattern } : {}),
884
1075
  ...(decision.approvalAudience ? { audience: decision.approvalAudience } : {}),
885
- ...(decision.approvalTtlSeconds !== undefined ? { ttlSeconds: decision.approvalTtlSeconds } : {}),
1076
+ ...(decision.approvalTtlSeconds !== undefined
1077
+ ? { ttlSeconds: decision.approvalTtlSeconds }
1078
+ : {}),
886
1079
  affectedPaths: affectedPathsFromInput(call.input),
887
1080
  }, policy, approvalTimeoutMs, approvalHandler);
888
- if (approved) {
1081
+ if (grant) {
1082
+ approvalGrant = grant;
889
1083
  approvedSandboxCalls.push(policyCall);
890
1084
  }
891
1085
  else {
892
1086
  const error = decision.reason ?? `Approval required for tool call: ${call.tool}`;
893
- results.push({ tool: call.tool, input: call.input, error, ...(call.id ? { id: call.id } : {}) });
894
- await this.recordEntry('error', { tool: call.tool, error, approvalRequired: true });
895
- await this.emit('error', { tool: call.tool, error, approvalRequired: true });
1087
+ results.push({
1088
+ tool: call.tool,
1089
+ input: call.input,
1090
+ error,
1091
+ ...(call.id ? { id: call.id } : {}),
1092
+ });
1093
+ await this.recordEntry("error", { tool: call.tool, error, approvalRequired: true });
1094
+ await this.emit("error", { tool: call.tool, error, approvalRequired: true });
896
1095
  continue;
897
1096
  }
898
1097
  }
899
1098
  else {
900
1099
  const error = decision.reason ?? `Tool call denied by policy: ${call.tool}`;
901
- results.push({ tool: call.tool, input: call.input, error, ...(call.id ? { id: call.id } : {}) });
902
- await this.recordEntry('error', { tool: call.tool, error, approvalRequired: false });
903
- await this.emit('error', { tool: call.tool, error, approvalRequired: false });
1100
+ results.push({
1101
+ tool: call.tool,
1102
+ input: call.input,
1103
+ error,
1104
+ ...(call.id ? { id: call.id } : {}),
1105
+ });
1106
+ await this.recordEntry("error", { tool: call.tool, error, approvalRequired: false });
1107
+ await this.emit("error", { tool: call.tool, error, approvalRequired: false });
904
1108
  continue;
905
1109
  }
906
1110
  }
@@ -908,87 +1112,160 @@ export class StubFabricSession {
908
1112
  if (!commandDecision.allowed) {
909
1113
  if (commandDecision.approvalRequired) {
910
1114
  const command = this.commandTextForApproval(tool, call.input);
911
- const approved = await this.requestApproval({
912
- kind: 'command',
1115
+ const grant = await this.requestApproval({
1116
+ kind: "command",
913
1117
  tool: call.tool,
1118
+ toolCallId: call.id ?? `${this.id}:${call.tool}`,
1119
+ ...(executingPrincipal ? { executingPrincipal } : {}),
914
1120
  ...(command ? { command } : {}),
915
1121
  input: call.input,
916
1122
  reason: commandDecision.reason ?? `Approval required for command: ${call.tool}`,
917
1123
  ...(commandDecision.risk ? { risk: commandDecision.risk } : {}),
918
- ...(commandDecision.matchedPattern ? { matchedPattern: commandDecision.matchedPattern } : {}),
919
- ...(commandDecision.approvalAudience ? { audience: commandDecision.approvalAudience } : {}),
920
- ...(commandDecision.approvalTtlSeconds !== undefined ? { ttlSeconds: commandDecision.approvalTtlSeconds } : {}),
1124
+ ...(commandDecision.matchedPattern
1125
+ ? { matchedPattern: commandDecision.matchedPattern }
1126
+ : {}),
1127
+ ...(commandDecision.approvalAudience
1128
+ ? { audience: commandDecision.approvalAudience }
1129
+ : {}),
1130
+ ...(commandDecision.approvalTtlSeconds !== undefined
1131
+ ? { ttlSeconds: commandDecision.approvalTtlSeconds }
1132
+ : {}),
921
1133
  envKeys: commandEnvKeys(tool),
922
1134
  affectedPaths: affectedPathsFromInput(call.input),
923
1135
  }, policy, approvalTimeoutMs, approvalHandler);
924
- if (!approved) {
1136
+ if (!grant) {
925
1137
  const error = commandDecision.reason ?? `Approval required for command: ${call.tool}`;
926
- results.push({ tool: call.tool, input: call.input, error, ...(call.id ? { id: call.id } : {}) });
927
- await this.recordEntry('error', { tool: call.tool, error, approvalRequired: true });
928
- await this.emit('error', { tool: call.tool, error, approvalRequired: true });
1138
+ results.push({
1139
+ tool: call.tool,
1140
+ input: call.input,
1141
+ error,
1142
+ ...(call.id ? { id: call.id } : {}),
1143
+ });
1144
+ await this.recordEntry("error", { tool: call.tool, error, approvalRequired: true });
1145
+ await this.emit("error", { tool: call.tool, error, approvalRequired: true });
929
1146
  continue;
930
1147
  }
1148
+ approvalGrant = grant;
931
1149
  const approvedCommand = this.commandTextForApproval(tool, call.input);
932
1150
  if (approvedCommand)
933
- approvedSandboxCalls.push({ tool: 'bash', input: { command: approvedCommand } });
1151
+ approvedSandboxCalls.push({ tool: "bash", input: { command: approvedCommand } });
934
1152
  }
935
1153
  else {
936
1154
  const error = commandDecision.reason ?? `Command denied by policy: ${call.tool}`;
937
- results.push({ tool: call.tool, input: call.input, error, ...(call.id ? { id: call.id } : {}) });
938
- await this.recordEntry('error', { tool: call.tool, error, approvalRequired: false });
939
- await this.emit('error', { tool: call.tool, error, approvalRequired: false });
1155
+ results.push({
1156
+ tool: call.tool,
1157
+ input: call.input,
1158
+ error,
1159
+ ...(call.id ? { id: call.id } : {}),
1160
+ });
1161
+ await this.recordEntry("error", { tool: call.tool, error, approvalRequired: false });
1162
+ await this.emit("error", { tool: call.tool, error, approvalRequired: false });
940
1163
  continue;
941
1164
  }
942
1165
  }
943
- await this.recordEntry('tool_call', { tool: call.tool, input: call.input, ...(tool?.metadata?.effect ? { effect: tool.metadata.effect } : {}), ...(call.id ? { toolCallId: call.id } : {}), ...(attribution ? { attribution: attribution } : {}) });
944
- await this.emit('tool_start', { tool: call.tool, ...(attribution ? { attribution: attribution } : {}) });
1166
+ await this.recordEntry("tool_call", {
1167
+ tool: call.tool,
1168
+ input: call.input,
1169
+ ...(tool?.metadata?.effect ? { effect: tool.metadata.effect } : {}),
1170
+ ...(call.id ? { toolCallId: call.id } : {}),
1171
+ ...(attribution ? { attribution: attribution } : {}),
1172
+ });
1173
+ await this.emit("tool_start", {
1174
+ tool: call.tool,
1175
+ ...(attribution ? { attribution: attribution } : {}),
1176
+ });
945
1177
  if (!tool?.execute) {
946
1178
  const error = `Tool is not available: ${call.tool}`;
947
- results.push({ tool: call.tool, input: call.input, error, ...(call.id ? { id: call.id } : {}) });
948
- await this.recordEntry('error', { tool: call.tool, error, approvalRequired: false });
949
- await this.emit('tool_end', { tool: call.tool, error, ...(attribution ? { attribution: attribution } : {}) });
1179
+ results.push({
1180
+ tool: call.tool,
1181
+ input: call.input,
1182
+ error,
1183
+ ...(call.id ? { id: call.id } : {}),
1184
+ });
1185
+ await this.recordEntry("error", { tool: call.tool, error, approvalRequired: false });
1186
+ await this.emit("tool_end", {
1187
+ tool: call.tool,
1188
+ error,
1189
+ ...(attribution ? { attribution: attribution } : {}),
1190
+ });
950
1191
  continue;
951
1192
  }
952
1193
  try {
953
- const input = call.tool === 'bash' ? this.clampBashToolInput(call.input, policy) : call.input;
1194
+ const input = call.tool === "bash" ? this.clampBashToolInput(call.input, policy) : call.input;
954
1195
  const startedAt = Date.now();
955
- const execute = () => Promise.resolve(tool.execute?.(input, { sandbox, ...(signal ? { signal } : {}) }));
1196
+ const execute = () => Promise.resolve(tool.execute?.(input, {
1197
+ sandbox,
1198
+ toolCallId: call.id ?? `${this.id}:${call.tool}`,
1199
+ ...(signal ? { signal } : {}),
1200
+ ...(approvalGrant ? { approval: approvalGrant } : {}),
1201
+ }));
956
1202
  const output = await approvedSandboxCalls.reduceRight((next, approvedCall) => () => runWithSandboxPolicyApproval(sandbox, approvedCall, next), execute)();
957
1203
  const durationMs = Date.now() - startedAt;
958
1204
  results.push({ tool: call.tool, input, output, ...(call.id ? { id: call.id } : {}) });
959
- await this.recordEntry('tool_result', { tool: call.tool, output: output, durationMs, ...(call.id ? { toolCallId: call.id } : {}), ...(attribution ? { attribution: attribution } : {}) });
960
- await this.emit('tool_end', { tool: call.tool, durationMs, ...(attribution ? { attribution: attribution } : {}) });
1205
+ await this.recordEntry("tool_result", {
1206
+ tool: call.tool,
1207
+ output: output,
1208
+ durationMs,
1209
+ ...(call.id ? { toolCallId: call.id } : {}),
1210
+ ...(attribution ? { attribution: attribution } : {}),
1211
+ });
1212
+ await this.emit("tool_end", {
1213
+ tool: call.tool,
1214
+ durationMs,
1215
+ ...(attribution ? { attribution: attribution } : {}),
1216
+ });
961
1217
  }
962
1218
  catch (error) {
963
1219
  const message = error instanceof Error ? error.message : String(error);
964
- results.push({ tool: call.tool, input: call.input, error: message, ...(call.id ? { id: call.id } : {}) });
965
- await this.recordEntry('error', { tool: call.tool, error: message });
966
- await this.emit('tool_end', { tool: call.tool, error: message, ...(attribution ? { attribution: attribution } : {}) });
1220
+ results.push({
1221
+ tool: call.tool,
1222
+ input: call.input,
1223
+ error: message,
1224
+ ...(call.id ? { id: call.id } : {}),
1225
+ });
1226
+ await this.recordEntry("error", { tool: call.tool, error: message });
1227
+ await this.emit("tool_end", {
1228
+ tool: call.tool,
1229
+ error: message,
1230
+ ...(attribution ? { attribution: attribution } : {}),
1231
+ });
967
1232
  }
968
1233
  }
969
1234
  return results;
970
1235
  }
971
1236
  async requestCostLimitApproval(ctx, approvalTimeoutMs, approvalHandler) {
972
- return this.requestApproval({
973
- kind: 'cost-limit',
1237
+ return Boolean(await this.requestApproval({
1238
+ kind: "cost-limit",
974
1239
  input: ctx,
975
1240
  reason: `Cost limit (${ctx.scope}) exceeded: $${ctx.observedUsd.toFixed(6)} > $${ctx.limitUsd.toFixed(6)}`,
976
1241
  subject: `cost-limit/${ctx.scope}`,
977
- }, this.options.policy, approvalTimeoutMs, approvalHandler);
1242
+ }, this.options.policy, approvalTimeoutMs, approvalHandler));
978
1243
  }
979
1244
  async requestApproval(input, callPolicy, approvalTimeoutMs, approvalHandler) {
980
- const id = input.idempotencyKey !== undefined
981
- ? `${this.id}-approval-${createHash('sha256').update(`${input.kind}\0${input.idempotencyKey}`).digest('hex').slice(0, 32)}`
1245
+ const executingPrincipal = input.executingPrincipal ?? this.approvalPrincipal();
1246
+ let id = input.idempotencyKey !== undefined
1247
+ ? `${this.id}-approval-${createHash("sha256").update(`${input.kind}\0${input.idempotencyKey}`).digest("hex").slice(0, 32)}`
982
1248
  : `${this.id}-approval-${Date.now()}-${Math.random().toString(16).slice(2)}`;
983
1249
  if (input.idempotencyKey !== undefined && this.store.getApprovalState) {
984
1250
  const existing = await this.store.getApprovalState(this.id, id);
985
- if (existing?.status === 'approved')
986
- return true;
987
- if (existing?.status === 'denied' || existing?.status === 'expired')
988
- return false;
989
- if (existing?.status === 'pending' && this.store.waitForApproval) {
1251
+ if (existing?.status === "approved") {
1252
+ const grant = approvalGrantFromState(existing, input.input, executingPrincipal, input.toolCallId ?? id);
1253
+ if (grant)
1254
+ return grant;
1255
+ // Pre-provenance approvals cannot authorize a model-facing operation.
1256
+ // Ask again under a new id so upgraded durable sessions recover safely.
1257
+ id = `${id}-provenance-${Date.now()}`;
1258
+ }
1259
+ if (existing?.status === "denied" || existing?.status === "expired")
1260
+ return undefined;
1261
+ if (existing?.status === "pending" && this.store.waitForApproval) {
990
1262
  const response = await this.waitForStoredApproval(existing.request, approvalTimeoutMs ?? callPolicy?.approvals?.defaultTimeoutMs);
991
- return response?.decision === 'approved';
1263
+ if (response?.decision !== "approved")
1264
+ return undefined;
1265
+ const resolved = await this.store.getApprovalState(this.id, id);
1266
+ return resolved
1267
+ ? approvalGrantFromState(resolved, input.input, executingPrincipal, input.toolCallId ?? id)
1268
+ : undefined;
992
1269
  }
993
1270
  }
994
1271
  const request = {
@@ -999,27 +1276,34 @@ export class StubFabricSession {
999
1276
  kind: input.kind,
1000
1277
  createdAt: new Date().toISOString(),
1001
1278
  ...(input.risk ? { risk: input.risk } : {}),
1002
- ...(callPolicy?.approvals?.requiredApprovals ? { requiredApprovals: callPolicy.approvals.requiredApprovals } : {}),
1279
+ ...(callPolicy?.approvals?.requiredApprovals
1280
+ ? { requiredApprovals: callPolicy.approvals.requiredApprovals }
1281
+ : {}),
1003
1282
  ...(callPolicy?.approvals?.escalation ? { escalation: callPolicy.approvals.escalation } : {}),
1004
1283
  ...(input.matchedPattern ? { matchedPattern: input.matchedPattern } : {}),
1005
1284
  ...(input.envKeys && input.envKeys.length > 0 ? { envKeys: input.envKeys } : {}),
1006
- ...(input.affectedPaths && input.affectedPaths.length > 0 ? { affectedPaths: input.affectedPaths } : {}),
1285
+ ...(input.affectedPaths && input.affectedPaths.length > 0
1286
+ ? { affectedPaths: input.affectedPaths }
1287
+ : {}),
1007
1288
  ...(input.tool ? { tool: input.tool } : {}),
1008
1289
  ...(input.command ? { command: input.command } : {}),
1009
1290
  ...(isJsonValue(input.input) ? { input: input.input } : {}),
1291
+ toolCallId: input.toolCallId ?? id,
1292
+ inputDigest: approvalInputDigest(input.input),
1293
+ executingPrincipal,
1010
1294
  ...(this.options.actor ? { actor: this.options.actor } : {}),
1011
1295
  ...(input.audience ? { audience: input.audience } : {}),
1012
1296
  ...(input.ttlSeconds !== undefined ? { ttlSeconds: input.ttlSeconds } : {}),
1013
1297
  ...(this.options.tenantId ? { tenantId: this.options.tenantId } : {}),
1014
1298
  };
1015
- await this.recordEntry('approval_requested', request);
1016
- await this.emit('approval_requested', request);
1299
+ await this.recordEntry("approval_requested", request);
1300
+ await this.emit("approval_requested", request);
1017
1301
  const handler = approvalHandler ?? this.options.onApproval;
1018
1302
  const autonomy = this.effectiveAutonomy();
1019
1303
  const waitTimeout = approvalTimeoutMs ?? callPolicy?.approvals?.defaultTimeoutMs;
1020
1304
  const durable = await this.getDurableRuntime();
1021
- const hasDurableApprovalChannel = typeof durable?.approvalRequest === 'function';
1022
- const hasStoredApprovalBackend = typeof this.store.waitForApproval === 'function';
1305
+ const hasDurableApprovalChannel = typeof durable?.approvalRequest === "function";
1306
+ const hasStoredApprovalBackend = typeof this.store.waitForApproval === "function";
1023
1307
  let response;
1024
1308
  if (handler) {
1025
1309
  response = normalizeApprovalResponse(await handler(request));
@@ -1042,25 +1326,66 @@ export class StubFabricSession {
1042
1326
  ? `Approval unavailable in ${autonomy.mode} mode: no approval handler, durable runtime channel, or store waiter is configured (${autonomy.onApprovalUnavailable}).`
1043
1327
  : waitTimeout !== undefined
1044
1328
  ? `Approval timed out after ${waitTimeout}ms`
1045
- : 'No approval response was received';
1046
- if (!handler && !hasStoredApprovalBackend && !hasDurableApprovalChannel && autonomy.onApprovalUnavailable === 'deny') {
1047
- await this.recordEntry('approval_denied', { approvalId: id, reason: unavailableReason });
1048
- await this.emit('approval_denied', { approvalId: id, reason: unavailableReason });
1329
+ : "No approval response was received";
1330
+ if (!handler &&
1331
+ !hasStoredApprovalBackend &&
1332
+ !hasDurableApprovalChannel &&
1333
+ autonomy.onApprovalUnavailable === "deny") {
1334
+ await this.recordEntry("approval_denied", { approvalId: id, reason: unavailableReason });
1335
+ await this.emit("approval_denied", { approvalId: id, reason: unavailableReason });
1049
1336
  }
1050
1337
  else {
1051
- await this.recordEntry('approval_expired', { approvalId: id, reason: unavailableReason });
1052
- await this.emit('approval_expired', { approvalId: id, reason: unavailableReason });
1338
+ await this.recordEntry("approval_expired", { approvalId: id, reason: unavailableReason });
1339
+ await this.emit("approval_expired", { approvalId: id, reason: unavailableReason });
1053
1340
  }
1054
- return false;
1341
+ return undefined;
1055
1342
  }
1056
- if (response.decision === 'approved') {
1057
- await this.recordEntry('approval_granted', { approvalId: id, reason: response.reason ?? 'Approved by handler' });
1058
- await this.emit('approval_granted', { approvalId: id, reason: response.reason ?? 'Approved by handler' });
1059
- return true;
1343
+ if (response.decision === "approved") {
1344
+ if (this.store.getApprovalState) {
1345
+ const resolved = await this.store.getApprovalState(this.id, id);
1346
+ const persisted = resolved
1347
+ ? approvalGrantFromState(resolved, input.input, executingPrincipal, input.toolCallId ?? id)
1348
+ : undefined;
1349
+ if (persisted)
1350
+ return persisted;
1351
+ }
1352
+ const grant = createApprovalGrant({
1353
+ approvalId: id,
1354
+ toolCallId: input.toolCallId ?? id,
1355
+ toolInput: input.input,
1356
+ principal: executingPrincipal,
1357
+ response,
1358
+ createdAt: request.createdAt,
1359
+ ...(request.ttlSeconds !== undefined ? { ttlSeconds: request.ttlSeconds } : {}),
1360
+ });
1361
+ const grantData = grant;
1362
+ await this.recordEntry("approval_granted", {
1363
+ approvalId: id,
1364
+ reason: response.reason ?? "Approved by handler",
1365
+ grant: grantData,
1366
+ });
1367
+ await this.emit("approval_granted", {
1368
+ approvalId: id,
1369
+ reason: response.reason ?? "Approved by handler",
1370
+ grant: grantData,
1371
+ });
1372
+ return grant;
1060
1373
  }
1061
- await this.recordEntry('approval_denied', { approvalId: id, reason: response.reason ?? 'Denied by handler' });
1062
- await this.emit('approval_denied', { approvalId: id, reason: response.reason ?? 'Denied by handler' });
1063
- return false;
1374
+ await this.recordEntry("approval_denied", {
1375
+ approvalId: id,
1376
+ reason: response.reason ?? "Denied by handler",
1377
+ });
1378
+ await this.emit("approval_denied", {
1379
+ approvalId: id,
1380
+ reason: response.reason ?? "Denied by handler",
1381
+ });
1382
+ return undefined;
1383
+ }
1384
+ approvalPrincipal() {
1385
+ return (this.options.actor?.principal ?? {
1386
+ kind: "app-principal",
1387
+ id: `fabric-session:${this.id}`,
1388
+ });
1064
1389
  }
1065
1390
  async waitForStoredApproval(request, timeoutMs) {
1066
1391
  if (!this.store.waitForApproval)
@@ -1069,39 +1394,45 @@ export class StubFabricSession {
1069
1394
  }
1070
1395
  effectiveAutonomy() {
1071
1396
  return {
1072
- mode: this.options.autonomy?.mode ?? 'background',
1073
- onMissingInput: this.options.autonomy?.onMissingInput ?? 'assume',
1074
- onApprovalUnavailable: this.options.autonomy?.onApprovalUnavailable ?? 'fail',
1075
- onCredentialMissing: this.options.autonomy?.onCredentialMissing ?? 'fail',
1397
+ mode: this.options.autonomy?.mode ?? "background",
1398
+ onMissingInput: this.options.autonomy?.onMissingInput ?? "assume",
1399
+ onApprovalUnavailable: this.options.autonomy?.onApprovalUnavailable ?? "fail",
1400
+ onCredentialMissing: this.options.autonomy?.onCredentialMissing ?? "fail",
1076
1401
  };
1077
1402
  }
1078
1403
  evaluateScopedCommandToolCall(tool, input, policy) {
1079
- if (tool?.metadata?.kind !== 'command' || typeof tool.metadata.command !== 'string')
1404
+ if (tool?.metadata?.kind !== "command" || typeof tool.metadata.command !== "string")
1080
1405
  return { allowed: true };
1081
- const args = typeof input === 'object' && input !== null && Array.isArray(input.args)
1082
- ? input.args.filter((value) => typeof value === 'string')
1406
+ const args = typeof input === "object" &&
1407
+ input !== null &&
1408
+ Array.isArray(input.args)
1409
+ ? input.args.filter((value) => typeof value === "string")
1083
1410
  : [];
1084
- const command = [tool.metadata.command, ...args.map(shellQuote)].join(' ');
1411
+ const command = [tool.metadata.command, ...args.map(shellQuote)].join(" ");
1085
1412
  return evaluateCommandPolicy(command, policy);
1086
1413
  }
1087
1414
  commandTextForApproval(tool, input) {
1088
- if (tool?.metadata?.kind !== 'command' || typeof tool.metadata.command !== 'string')
1415
+ if (tool?.metadata?.kind !== "command" || typeof tool.metadata.command !== "string")
1089
1416
  return undefined;
1090
- const args = typeof input === 'object' && input !== null && Array.isArray(input.args)
1091
- ? input.args.filter((value) => typeof value === 'string')
1417
+ const args = typeof input === "object" &&
1418
+ input !== null &&
1419
+ Array.isArray(input.args)
1420
+ ? input.args.filter((value) => typeof value === "string")
1092
1421
  : [];
1093
- return [tool.metadata.command, ...args].join(' ');
1422
+ return [tool.metadata.command, ...args].join(" ");
1094
1423
  }
1095
1424
  clampBashToolInput(input, policy) {
1096
- if (!policy?.maxCommandTimeoutMs || typeof input !== 'object' || input === null)
1425
+ if (!policy?.maxCommandTimeoutMs || typeof input !== "object" || input === null)
1097
1426
  return input;
1098
1427
  const record = input;
1099
- const timeout = typeof record.timeout === 'number' ? record.timeout : undefined;
1428
+ const timeout = typeof record.timeout === "number" ? record.timeout : undefined;
1100
1429
  return { ...record, timeout: clampCommandTimeout(timeout, policy) };
1101
1430
  }
1102
1431
  resolveTools(sandbox, promptTools = [], promptCommands = []) {
1103
1432
  const tools = new Map();
1104
- const commandToolOptions = this.options.resolveSecret ? { resolveSecret: this.options.resolveSecret } : {};
1433
+ const commandToolOptions = this.options.resolveSecret
1434
+ ? { resolveSecret: this.options.resolveSecret }
1435
+ : {};
1105
1436
  for (const tool of createBuiltinTools(sandbox, this.options.packagedSkills))
1106
1437
  tools.set(tool.name, tool);
1107
1438
  for (const tool of createCommandTools(this.options.commands ?? [], commandToolOptions))
@@ -1153,18 +1484,20 @@ export class StubFabricSession {
1153
1484
  skill.name,
1154
1485
  skill.relativePath,
1155
1486
  skill.relativePath ? stripMarkdownExtension(skill.relativePath) : undefined,
1156
- skill.relativePath?.endsWith('/SKILL.md') ? skill.relativePath.slice(0, -'/SKILL.md'.length) : undefined,
1487
+ skill.relativePath?.endsWith("/SKILL.md")
1488
+ ? skill.relativePath.slice(0, -"/SKILL.md".length)
1489
+ : undefined,
1157
1490
  skill.path,
1158
1491
  skill.path ? stripMarkdownExtension(skill.path) : undefined,
1159
- skill.path?.endsWith('/SKILL.md') ? skill.path.slice(0, -'/SKILL.md'.length) : undefined,
1160
- ].filter((value) => typeof value === 'string');
1492
+ skill.path?.endsWith("/SKILL.md") ? skill.path.slice(0, -"/SKILL.md".length) : undefined,
1493
+ ].filter((value) => typeof value === "string");
1161
1494
  return candidates.some((candidate) => normalizeSkillReference(candidate) === normalized);
1162
1495
  });
1163
1496
  }
1164
1497
  resolveRole(role) {
1165
1498
  if (!role)
1166
1499
  return undefined;
1167
- if (typeof role !== 'string')
1500
+ if (typeof role !== "string")
1168
1501
  return role;
1169
1502
  return this.options.roles?.find((candidate) => candidate.name === role);
1170
1503
  }
@@ -1184,9 +1517,11 @@ export class StubFabricSession {
1184
1517
  if (existing) {
1185
1518
  // Cross-tenant access guard: a session that already belongs to a
1186
1519
  // different tenant should not be silently appended to.
1187
- if (this.options.tenantId !== undefined && existing.tenantId !== undefined && existing.tenantId !== this.options.tenantId) {
1520
+ if (this.options.tenantId !== undefined &&
1521
+ existing.tenantId !== undefined &&
1522
+ existing.tenantId !== this.options.tenantId) {
1188
1523
  throw new FabricError({
1189
- code: 'POLICY_DENIED',
1524
+ code: "POLICY_DENIED",
1190
1525
  message: `Session ${this.id} belongs to tenant ${existing.tenantId}; refusing to append from tenant ${this.options.tenantId}.`,
1191
1526
  retryable: false,
1192
1527
  });
@@ -1234,37 +1569,37 @@ export class StubFabricSession {
1234
1569
  if (dangling.length === 0 && unfinishedTasks.length === 0)
1235
1570
  return;
1236
1571
  for (const call of dangling) {
1237
- const tool = typeof call.data?.tool === 'string' ? call.data.tool : 'unknown';
1238
- const toolCallId = typeof call.data?.toolCallId === 'string' ? call.data.toolCallId : undefined;
1239
- await this.recordEntry('tool_result', {
1572
+ const tool = typeof call.data?.tool === "string" ? call.data.tool : "unknown";
1573
+ const toolCallId = typeof call.data?.toolCallId === "string" ? call.data.toolCallId : undefined;
1574
+ await this.recordEntry("tool_result", {
1240
1575
  tool,
1241
1576
  ...(toolCallId ? { toolCallId } : {}),
1242
1577
  output: {
1243
- type: 'interrupted',
1244
- message: 'Tool execution was interrupted before completion. The outcome is unknown.',
1578
+ type: "interrupted",
1579
+ message: "Tool execution was interrupted before completion. The outcome is unknown.",
1245
1580
  },
1246
1581
  isError: true,
1247
1582
  interrupted: true,
1248
1583
  synthetic: true,
1249
1584
  });
1250
- await this.emit('tool_end', { tool, error: 'interrupted', synthetic: true });
1585
+ await this.emit("tool_end", { tool, error: "interrupted", synthetic: true });
1251
1586
  }
1252
1587
  for (const task of unfinishedTasks) {
1253
- const taskId = typeof task.data?.taskId === 'string' ? task.data.taskId : 'unknown';
1254
- await this.recordEntry('task_end', {
1588
+ const taskId = typeof task.data?.taskId === "string" ? task.data.taskId : "unknown";
1589
+ await this.recordEntry("task_end", {
1255
1590
  taskId,
1256
- status: 'interrupted',
1591
+ status: "interrupted",
1257
1592
  synthetic: true,
1258
1593
  // Keep the child transcript locatable from the settled record.
1259
- ...(typeof task.data?.agent === 'string' ? { agent: task.data.agent } : {}),
1594
+ ...(typeof task.data?.agent === "string" ? { agent: task.data.agent } : {}),
1260
1595
  });
1261
1596
  }
1262
1597
  if (dangling.length > 0) {
1263
1598
  const tools = dangling
1264
- .map((call) => (typeof call.data?.tool === 'string' ? call.data.tool : 'unknown'))
1265
- .join(', ');
1266
- await this.recordEntry('signal', {
1267
- signalType: 'interrupted',
1599
+ .map((call) => (typeof call.data?.tool === "string" ? call.data.tool : "unknown"))
1600
+ .join(", ");
1601
+ await this.recordEntry("signal", {
1602
+ signalType: "interrupted",
1268
1603
  content: `The previous turn was interrupted; ${dangling.length} tool call(s) did not complete and may or may not have taken effect: ${tools}. Verify their effects before repeating side-effecting operations.`,
1269
1604
  });
1270
1605
  }
@@ -1301,43 +1636,50 @@ export class StubFabricSession {
1301
1636
  }
1302
1637
  }
1303
1638
  function normalizeSkillReference(value) {
1304
- return stripMarkdownExtension(value.replace(/\\/g, '/').replace(/^\.\//, '').replace(/^skills\//, '').replace(/^\.fabricharness\/skills\//, '').replace(/^\/+/, ''));
1639
+ return stripMarkdownExtension(value
1640
+ .replace(/\\/g, "/")
1641
+ .replace(/^\.\//, "")
1642
+ .replace(/^skills\//, "")
1643
+ .replace(/^\.fabricharness\/skills\//, "")
1644
+ .replace(/^\/+/, ""));
1305
1645
  }
1306
1646
  function stripMarkdownExtension(value) {
1307
- return value.endsWith('.md') ? value.slice(0, -3) : value;
1647
+ return value.endsWith(".md") ? value.slice(0, -3) : value;
1308
1648
  }
1309
1649
  function affectedPathsFromInput(input) {
1310
- if (typeof input !== 'object' || input === null)
1650
+ if (typeof input !== "object" || input === null)
1311
1651
  return [];
1312
1652
  const record = input;
1313
- const paths = [record.path, record.cwd].filter((value) => typeof value === 'string');
1653
+ const paths = [record.path, record.cwd].filter((value) => typeof value === "string");
1314
1654
  return [...new Set(paths)];
1315
1655
  }
1316
1656
  function commandEnvKeys(tool) {
1317
1657
  const value = tool?.metadata?.envKeys;
1318
- return Array.isArray(value) ? value.filter((entry) => typeof entry === 'string') : [];
1658
+ return Array.isArray(value)
1659
+ ? value.filter((entry) => typeof entry === "string")
1660
+ : [];
1319
1661
  }
1320
1662
  function normalizeApprovalResponse(value) {
1321
- if (typeof value === 'boolean')
1322
- return { decision: value ? 'approved' : 'denied' };
1323
- if (typeof value === 'string')
1663
+ if (typeof value === "boolean")
1664
+ return { decision: value ? "approved" : "denied" };
1665
+ if (typeof value === "string")
1324
1666
  return { decision: value };
1325
1667
  return value;
1326
1668
  }
1327
1669
  function readStringPropertyFromInput(input, key) {
1328
- if (typeof input !== 'object' || input === null || !(key in input))
1670
+ if (typeof input !== "object" || input === null || !(key in input))
1329
1671
  return undefined;
1330
1672
  const value = input[key];
1331
- return typeof value === 'string' ? value : undefined;
1673
+ return typeof value === "string" ? value : undefined;
1332
1674
  }
1333
1675
  function isJsonValue(value) {
1334
1676
  if (value === null)
1335
1677
  return true;
1336
- if (['string', 'number', 'boolean'].includes(typeof value))
1678
+ if (["string", "number", "boolean"].includes(typeof value))
1337
1679
  return true;
1338
1680
  if (Array.isArray(value))
1339
1681
  return value.every(isJsonValue);
1340
- if (typeof value !== 'object')
1682
+ if (typeof value !== "object")
1341
1683
  return false;
1342
1684
  return Object.values(value).every(isJsonValue);
1343
1685
  }
@@ -1347,41 +1689,41 @@ function redactEnvValues(text, env) {
1347
1689
  let result = text;
1348
1690
  for (const [key, value] of Object.entries(env)) {
1349
1691
  if (key.length > 0) {
1350
- result = result.replaceAll(key, '[REDACTED]');
1692
+ result = result.replaceAll(key, "[REDACTED]");
1351
1693
  }
1352
1694
  if (value.length > 0) {
1353
- result = result.replaceAll(value, '[REDACTED]');
1695
+ result = result.replaceAll(value, "[REDACTED]");
1354
1696
  }
1355
1697
  }
1356
1698
  return result;
1357
1699
  }
1358
1700
  function summarizeEntries(entries) {
1359
1701
  if (entries.length === 0)
1360
- return 'No entries to compact.';
1702
+ return "No entries to compact.";
1361
1703
  const counts = new Map();
1362
1704
  for (const entry of entries)
1363
1705
  counts.set(entry.type, (counts.get(entry.type) ?? 0) + 1);
1364
- const summary = [...counts.entries()].map(([type, count]) => `${type}: ${count}`).join(', ');
1706
+ const summary = [...counts.entries()].map(([type, count]) => `${type}: ${count}`).join(", ");
1365
1707
  const first = entries[0];
1366
1708
  const last = entries.at(-1);
1367
- return `Compacted ${entries.length} entries (${summary}). Range: ${first?.timestamp ?? 'unknown'} to ${last?.timestamp ?? 'unknown'}.`;
1709
+ return `Compacted ${entries.length} entries (${summary}). Range: ${first?.timestamp ?? "unknown"} to ${last?.timestamp ?? "unknown"}.`;
1368
1710
  }
1369
1711
  function collectFileReferences(entries) {
1370
1712
  const readFiles = new Set();
1371
1713
  const modifiedFiles = new Set();
1372
1714
  for (const entry of entries) {
1373
- if (entry.type === 'tool_call') {
1374
- const tool = typeof entry.data?.tool === 'string' ? entry.data.tool : undefined;
1375
- const effect = typeof entry.data?.effect === 'string' ? entry.data.effect : undefined;
1715
+ if (entry.type === "tool_call") {
1716
+ const tool = typeof entry.data?.tool === "string" ? entry.data.tool : undefined;
1717
+ const effect = typeof entry.data?.effect === "string" ? entry.data.effect : undefined;
1376
1718
  const input = entry.data?.input;
1377
1719
  for (const path of filePathsFromToolInput(input)) {
1378
- if (effect === 'read' || (!effect && isReadOnlyFileTool(tool)))
1720
+ if (effect === "read" || (!effect && isReadOnlyFileTool(tool)))
1379
1721
  readFiles.add(path);
1380
- if (effect === 'write' || (!effect && isMutatingFileTool(tool)))
1722
+ if (effect === "write" || (!effect && isMutatingFileTool(tool)))
1381
1723
  modifiedFiles.add(path);
1382
1724
  }
1383
1725
  }
1384
- if (entry.type === 'shell_command' && typeof entry.data?.cwd === 'string')
1726
+ if (entry.type === "shell_command" && typeof entry.data?.cwd === "string")
1385
1727
  readFiles.add(entry.data.cwd);
1386
1728
  }
1387
1729
  return { readFiles: [...readFiles].sort(), modifiedFiles: [...modifiedFiles].sort() };
@@ -1389,17 +1731,17 @@ function collectFileReferences(entries) {
1389
1731
  function appendFileReferenceTags(summary, refs) {
1390
1732
  const sections = [summary];
1391
1733
  if (refs.readFiles.length > 0)
1392
- sections.push(`<read-files>\n${refs.readFiles.join('\n')}\n</read-files>`);
1734
+ sections.push(`<read-files>\n${refs.readFiles.join("\n")}\n</read-files>`);
1393
1735
  if (refs.modifiedFiles.length > 0)
1394
- sections.push(`<modified-files>\n${refs.modifiedFiles.join('\n')}\n</modified-files>`);
1395
- return sections.join('\n\n');
1736
+ sections.push(`<modified-files>\n${refs.modifiedFiles.join("\n")}\n</modified-files>`);
1737
+ return sections.join("\n\n");
1396
1738
  }
1397
1739
  /** Valid cut points: user prompts and assistant messages. Never cut at tool_result. */
1398
1740
  function findValidCutPoints(entries, start, end) {
1399
1741
  const cutPoints = [];
1400
1742
  for (let i = start; i < end; i++) {
1401
1743
  const type = entries[i]?.type;
1402
- if (type === 'user_prompt' || type === 'assistant_message') {
1744
+ if (type === "user_prompt" || type === "assistant_message") {
1403
1745
  cutPoints.push(i);
1404
1746
  }
1405
1747
  }
@@ -1407,7 +1749,7 @@ function findValidCutPoints(entries, start, end) {
1407
1749
  }
1408
1750
  function findTurnStartIndex(entries, index, start) {
1409
1751
  for (let i = index; i >= start; i--) {
1410
- if (entries[i]?.type === 'user_prompt')
1752
+ if (entries[i]?.type === "user_prompt")
1411
1753
  return i;
1412
1754
  }
1413
1755
  return -1;
@@ -1434,7 +1776,7 @@ function findCutPoint(entries, start, end, keepRecentTokens) {
1434
1776
  break;
1435
1777
  }
1436
1778
  }
1437
- const isUserMessage = entries[firstKeptIndex]?.type === 'user_prompt';
1779
+ const isUserMessage = entries[firstKeptIndex]?.type === "user_prompt";
1438
1780
  const turnStartIndex = isUserMessage ? -1 : findTurnStartIndex(entries, firstKeptIndex, start);
1439
1781
  return {
1440
1782
  firstKeptIndex,
@@ -1466,30 +1808,49 @@ export function prepareCompaction(entries, keepRecentTokens, previousCompaction)
1466
1808
  };
1467
1809
  }
1468
1810
  function isReadOnlyFileTool(tool) {
1469
- return tool !== undefined && ['read', 'read_buffer', 'stat', 'readdir', 'exists', 'grep', 'glob'].includes(tool);
1811
+ return (tool !== undefined &&
1812
+ ["read", "read_buffer", "stat", "readdir", "exists", "grep", "glob"].includes(tool));
1470
1813
  }
1471
1814
  function isMutatingFileTool(tool) {
1472
- return tool !== undefined && ['write', 'edit', 'mkdir', 'rm'].includes(tool);
1815
+ return tool !== undefined && ["write", "edit", "mkdir", "rm"].includes(tool);
1816
+ }
1817
+ function toolExecutingPrincipal(tool) {
1818
+ const value = tool?.metadata?.executingPrincipal;
1819
+ if (!value || typeof value !== "object" || Array.isArray(value))
1820
+ return undefined;
1821
+ const principal = value;
1822
+ if ((principal.kind !== "user" &&
1823
+ principal.kind !== "service-principal" &&
1824
+ principal.kind !== "app-principal") ||
1825
+ typeof principal.id !== "string")
1826
+ return undefined;
1827
+ return value;
1473
1828
  }
1474
1829
  function filePathsFromToolInput(input) {
1475
- if (typeof input !== 'object' || input === null)
1830
+ if (typeof input !== "object" || input === null)
1476
1831
  return [];
1477
1832
  const record = input;
1478
1833
  const values = [record.path, record.cwd, record.pattern];
1479
1834
  if (Array.isArray(record.paths))
1480
1835
  values.push(...record.paths);
1481
- return [...new Set(values.filter((value) => typeof value === 'string' && value.length > 0))];
1836
+ return [
1837
+ ...new Set(values.filter((value) => typeof value === "string" && value.length > 0)),
1838
+ ];
1482
1839
  }
1483
1840
  function isSnapshot(value) {
1484
- return typeof value === 'object' && value !== null && 'id' in value && typeof value.id === 'string';
1841
+ return (typeof value === "object" &&
1842
+ value !== null &&
1843
+ "id" in value &&
1844
+ typeof value.id === "string");
1485
1845
  }
1486
1846
  function abortError() {
1487
- const error = new Error('Task cancelled');
1488
- error.name = 'AbortError';
1847
+ const error = new Error("Task cancelled");
1848
+ error.name = "AbortError";
1489
1849
  return error;
1490
1850
  }
1491
1851
  function isAbortError(error) {
1492
- return error instanceof Error && (error.name === 'AbortError' || /aborted|cancelled|canceled/i.test(error.message));
1852
+ return (error instanceof Error &&
1853
+ (error.name === "AbortError" || /aborted|cancelled|canceled/i.test(error.message)));
1493
1854
  }
1494
1855
  function wrapWithPolicy(env, policy, bypass) {
1495
1856
  if (bypass === true)
@@ -1511,10 +1872,10 @@ function wrapWithPolicy(env, policy, bypass) {
1511
1872
  */
1512
1873
  function resolveMountPath(cwd, mountAt) {
1513
1874
  const normalized = path.posix.normalize(mountAt);
1514
- if (!normalized.startsWith('/'))
1875
+ if (!normalized.startsWith("/"))
1515
1876
  return path.posix.join(cwd, normalized);
1516
1877
  if (normalized === cwd || normalized.startsWith(`${cwd}/`))
1517
1878
  return normalized;
1518
- return path.posix.join(cwd, normalized.replace(/^\/+/, ''));
1879
+ return path.posix.join(cwd, normalized.replace(/^\/+/, ""));
1519
1880
  }
1520
1881
  //# sourceMappingURL=session.js.map