voratiq 0.1.0-beta.0 → 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Voratiq
2
2
 
3
- Run multiple AI coding agents in parallel, compare their results, and apply the best solution.
3
+ Run agents in parallel, compare results, and apply the best solution.
4
4
 
5
5
  ![`voratiq run --spec specs/p1/agent-workspace-guardrails.md`](https://raw.githubusercontent.com/voratiq/voratiq/main/assets/run-demo.png)
6
6
 
@@ -3,7 +3,7 @@ import { assertSandboxDestination } from "../staging.js";
3
3
  import { buildAuthFailedMessage } from "./messages.js";
4
4
  import { disposeHandles, registerSandboxSecrets, stageSecretFile, } from "./secret-staging.js";
5
5
  import { teardownAuthProvider } from "./teardown.js";
6
- import { assertReadableFileOrThrow, composeSandboxEnvResult, copyOptionalDirectoryWithPermissions, copyOptionalFileWithPermissions, createSandboxPaths, ensureDirectories, resolveChildPath, } from "./utils.js";
6
+ import { assertReadableFileOrThrow, composeSandboxEnvResult, copyOptionalFileWithPermissions, createSandboxPaths, ensureDirectories, resolveChildPath, } from "./utils.js";
7
7
  const GEMINI_PROVIDER_ID = "gemini";
8
8
  const GEMINI_LOGIN_HINT = buildAuthFailedMessage("Gemini");
9
9
  const GEMINI_REQUIRED_FILES = [
@@ -124,7 +124,8 @@ async function stageOptionalFiles(geminiHome, sandboxGeminiDir, sandboxHome) {
124
124
  return new GeminiAuthProviderError(GEMINI_LOGIN_HINT, { cause });
125
125
  });
126
126
  }
127
- const tmpSource = resolveChildPath(geminiHome, "tmp");
127
+ // Create empty tmp directory (don't copy contents to avoid polluting
128
+ // chat transcripts with historical sessions from previous runs)
128
129
  const sandboxTmpDestination = resolveChildPath(sandboxGeminiDir, "tmp");
129
130
  assertSandboxDestination({
130
131
  sandboxHome,
@@ -132,7 +133,7 @@ async function stageOptionalFiles(geminiHome, sandboxGeminiDir, sandboxHome) {
132
133
  providerId: GEMINI_PROVIDER_ID,
133
134
  fileLabel: "tmp",
134
135
  });
135
- await copyOptionalDirectoryWithPermissions(tmpSource, sandboxTmpDestination, (cause) => new GeminiAuthProviderError(GEMINI_LOGIN_HINT, { cause }));
136
+ await ensureDirectories([sandboxTmpDestination]);
136
137
  }
137
138
  async function validateSettingsFile(settingsPath) {
138
139
  let raw;
package/dist/bin.js CHANGED
@@ -82,7 +82,8 @@ export async function runCli(argv = process.argv) {
82
82
  .description("Voratiq CLI")
83
83
  .version(getVoratiqVersion(), "-v, --version", "print the Voratiq version")
84
84
  .exitOverride()
85
- .showHelpAfterError();
85
+ .showHelpAfterError()
86
+ .helpCommand(false);
86
87
  program.addCommand(createInitCommand());
87
88
  program.addCommand(createListCommand());
88
89
  program.addCommand(createRunCommand());
@@ -1,6 +1,8 @@
1
+ import { loadSandboxProviderConfig } from "../../../configs/sandbox/loader.js";
1
2
  import { toErrorMessage } from "../../../utils/errors.js";
2
3
  import { GIT_AUTHOR_EMAIL, GIT_AUTHOR_NAME } from "../../../utils/git.js";
3
4
  import { AgentProcessError, GitOperationError, RunCommandError, } from "../errors.js";
5
+ import { DEFAULT_DENIAL_BACKOFF, } from "../sandbox.js";
4
6
  import { teardownRegisteredSandboxContext } from "../sandbox-registry.js";
5
7
  import { captureAgentChatTranscripts } from "./chat-preserver.js";
6
8
  import { runPostProcessingAndEvaluations } from "./eval-runner.js";
@@ -29,18 +31,33 @@ export async function executeAgentLifecycle(execution) {
29
31
  await execution.progress.onRunning(buildRunningAgentRecord(execution, agentContext));
30
32
  }
31
33
  // Create watchdog trigger callback for immediate UI surfacing
32
- const onWatchdogTrigger = (trigger, reason) => {
34
+ const onWatchdogTrigger = (trigger, reason, failFast) => {
33
35
  // Update watchdog metadata with trigger
34
36
  agentContext.setWatchdogMetadata({
35
37
  ...initialWatchdog,
36
38
  trigger,
37
39
  });
40
+ if (failFast) {
41
+ agentContext.setFailFastTriggered(failFast);
42
+ }
38
43
  // Fire early failure callback for immediate UI update
39
44
  if (execution.progress?.onEarlyFailure) {
40
45
  const earlyRecord = agentContext.buildEarlyFailureRecord(reason);
41
46
  void execution.progress.onEarlyFailure(earlyRecord);
42
47
  }
43
48
  };
49
+ let denialBackoff = DEFAULT_DENIAL_BACKOFF;
50
+ try {
51
+ const sandboxProviderConfig = loadSandboxProviderConfig({
52
+ root,
53
+ providerId: agent.provider,
54
+ });
55
+ denialBackoff = sandboxProviderConfig.denialBackoff;
56
+ }
57
+ catch {
58
+ // If sandbox.yaml is missing or invalid, fall back to defaults rather than
59
+ // failing the entire agent lifecycle.
60
+ }
44
61
  const processResult = await runAgentProcess({
45
62
  runtimeManifestPath,
46
63
  agentRoot: workspacePaths.agentRoot,
@@ -49,11 +66,15 @@ export async function executeAgentLifecycle(execution) {
49
66
  sandboxSettingsPath: workspacePaths.sandboxSettingsPath,
50
67
  providerId: agent.provider,
51
68
  onWatchdogTrigger,
69
+ denialBackoff,
52
70
  });
53
71
  // Update watchdog metadata from process result (in case trigger came via watchdog)
54
72
  if (processResult.watchdog) {
55
73
  agentContext.setWatchdogMetadata(processResult.watchdog);
56
74
  }
75
+ if (processResult.failFast) {
76
+ agentContext.setFailFastTriggered(processResult.failFast);
77
+ }
57
78
  if (processResult.exitCode !== 0 || processResult.errorMessage) {
58
79
  // Use watchdog error message if available, otherwise detect from logs
59
80
  const failureDetail = processResult.watchdog?.trigger && processResult.errorMessage
@@ -5,6 +5,7 @@ import type { ArtifactCollectionResult } from "../../../workspace/agents.js";
5
5
  import type { ChatArtifactFormat } from "../../../workspace/chat/types.js";
6
6
  import type { RunCommandError } from "../errors.js";
7
7
  import { type AgentExecutionResult, type AgentExecutionState } from "../reports.js";
8
+ import type { SandboxFailFastInfo } from "../sandbox.js";
8
9
  export declare class AgentRunContext {
9
10
  readonly state: AgentExecutionState;
10
11
  status: AgentStatus;
@@ -12,6 +13,7 @@ export declare class AgentRunContext {
12
13
  evalResults: AgentEvalResult[];
13
14
  errorMessage: string | undefined;
14
15
  watchdogMetadata: WatchdogMetadata | undefined;
16
+ private failFast;
15
17
  private completedAt;
16
18
  private startedAt;
17
19
  private readonly evalPlan;
@@ -36,6 +38,7 @@ export declare class AgentRunContext {
36
38
  markChatArtifact(format: ChatArtifactFormat): void;
37
39
  recordEvalWarnings(warnings: readonly string[]): void;
38
40
  setWatchdogMetadata(metadata: WatchdogMetadata): void;
41
+ setFailFastTriggered(info: SandboxFailFastInfo): void;
39
42
  finalize(): AgentExecutionResult;
40
43
  /**
41
44
  * Build an early failure record for immediate UI surfacing when watchdog triggers.
@@ -11,6 +11,7 @@ export class AgentRunContext {
11
11
  evalResults;
12
12
  errorMessage;
13
13
  watchdogMetadata;
14
+ failFast;
14
15
  completedAt;
15
16
  startedAt;
16
17
  evalPlan;
@@ -113,6 +114,9 @@ export class AgentRunContext {
113
114
  setWatchdogMetadata(metadata) {
114
115
  this.watchdogMetadata = metadata;
115
116
  }
117
+ setFailFastTriggered(info) {
118
+ this.failFast = info;
119
+ }
116
120
  finalize() {
117
121
  this.setCompleted();
118
122
  const record = buildAgentRecord({
@@ -127,6 +131,7 @@ export class AgentRunContext {
127
131
  warnings: this.evalWarnings,
128
132
  diffStatistics: this.state.diffStatistics,
129
133
  watchdog: this.watchdogMetadata,
134
+ failFast: this.failFast,
130
135
  });
131
136
  return finalizeAgentResult(this.runId, record, this.state);
132
137
  }
@@ -147,6 +152,7 @@ export class AgentRunContext {
147
152
  warnings: this.evalWarnings,
148
153
  diffStatistics: undefined,
149
154
  watchdog: this.watchdogMetadata,
155
+ failFast: this.failFast,
150
156
  });
151
157
  }
152
158
  }
@@ -158,7 +164,7 @@ export function buildDefaultEvalResults(definitions) {
158
164
  }));
159
165
  }
160
166
  function buildAgentRecord(options) {
161
- const { agent, commitSha, completedAt, errorMessage, startedAt, status, artifacts, evalResults, warnings, diffStatistics, watchdog, } = options;
167
+ const { agent, commitSha, completedAt, errorMessage, startedAt, status, artifacts, evalResults, warnings, diffStatistics, watchdog, failFast, } = options;
162
168
  const snapshots = toEvalSnapshots(evalResults);
163
169
  const artifactState = Object.keys(artifacts).length > 0 ? artifacts : undefined;
164
170
  const normalizedWarnings = warnings.length > 0 ? Array.from(new Set(warnings)) : undefined;
@@ -175,6 +181,13 @@ function buildAgentRecord(options) {
175
181
  error: errorMessage,
176
182
  warnings: normalizedWarnings,
177
183
  watchdog,
184
+ ...(failFast
185
+ ? {
186
+ failFastTriggered: true,
187
+ failFastTarget: failFast.target,
188
+ failFastOperation: failFast.operation,
189
+ }
190
+ : {}),
178
191
  };
179
192
  if (normalizedDiffStatistics) {
180
193
  record.diffStatistics = normalizedDiffStatistics;
@@ -1,5 +1,7 @@
1
+ import type { DenialBackoffConfig } from "../../../configs/sandbox/types.js";
1
2
  import type { WatchdogMetadata } from "../../../records/types.js";
2
3
  import type { AgentWorkspacePaths } from "../../../workspace/layout.js";
4
+ import { type SandboxFailFastInfo } from "../sandbox.js";
3
5
  import { type WatchdogTrigger } from "./watchdog.js";
4
6
  export interface AgentProcessOptions {
5
7
  runtimeManifestPath: string;
@@ -7,11 +9,12 @@ export interface AgentProcessOptions {
7
9
  stdoutPath: string;
8
10
  stderrPath: string;
9
11
  sandboxSettingsPath: string;
12
+ denialBackoff?: DenialBackoffConfig;
10
13
  resolveRunInvocation?: RunInvocationResolver;
11
14
  /** Provider ID for watchdog fatal pattern matching. */
12
15
  providerId?: string;
13
16
  /** Callback fired immediately when watchdog triggers, before process exits. */
14
- onWatchdogTrigger?: (trigger: WatchdogTrigger, reason: string) => void;
17
+ onWatchdogTrigger?: (trigger: WatchdogTrigger, reason: string, failFast?: SandboxFailFastInfo) => void;
15
18
  }
16
19
  export interface AgentProcessResult {
17
20
  exitCode: number;
@@ -19,6 +22,8 @@ export interface AgentProcessResult {
19
22
  signal?: NodeJS.Signals | null;
20
23
  /** Watchdog metadata showing enforced limits and trigger reason. */
21
24
  watchdog?: WatchdogMetadata;
25
+ /** Sandbox fail-fast metadata when repeated denials trigger an abort. */
26
+ failFast?: SandboxFailFastInfo;
22
27
  }
23
28
  export interface RunInvocationContext {
24
29
  agentRoot: string;
@@ -61,7 +61,7 @@ async function defaultResolveRunInvocation(context) {
61
61
  return { command, args };
62
62
  }
63
63
  export async function runAgentProcess(options) {
64
- const { runtimeManifestPath, agentRoot, stdoutPath, stderrPath, sandboxSettingsPath, resolveRunInvocation, providerId = "", onWatchdogTrigger, } = options;
64
+ const { runtimeManifestPath, agentRoot, stdoutPath, stderrPath, sandboxSettingsPath, denialBackoff, resolveRunInvocation, providerId = "", onWatchdogTrigger, } = options;
65
65
  const stdoutStream = createWriteStream(stdoutPath, { flags: "w" });
66
66
  const stderrStream = createWriteStream(stderrPath, { flags: "w" });
67
67
  const shimEntryPath = resolveShimEntryPath();
@@ -105,6 +105,7 @@ export async function runAgentProcess(options) {
105
105
  watchdogController = createWatchdog(child, stderrStream, {
106
106
  providerId,
107
107
  onWatchdogTrigger,
108
+ denialBackoff,
108
109
  });
109
110
  // Bridge watchdog's abort signal to our shared abort controller
110
111
  abortSignalHandler = () => forceAbortController.abort();
@@ -135,6 +136,7 @@ export async function runAgentProcess(options) {
135
136
  }
136
137
  const watchdogState = watchdogController?.getState();
137
138
  const watchdogTrigger = watchdogState?.triggered ?? undefined;
139
+ const failFast = watchdogState?.sandboxFailFast;
138
140
  let errorMessage;
139
141
  if (watchdogTrigger && watchdogState?.triggeredReason) {
140
142
  errorMessage = aborted
@@ -152,7 +154,7 @@ export async function runAgentProcess(options) {
152
154
  wallClockCapMs: WATCHDOG_DEFAULTS.wallClockCapMs,
153
155
  trigger: watchdogTrigger,
154
156
  };
155
- return { exitCode, errorMessage, signal, watchdog };
157
+ return { exitCode, errorMessage, signal, watchdog, failFast };
156
158
  }
157
159
  export async function stageManifestForSandbox(options) {
158
160
  const { runtimeManifestPath } = options;
@@ -1,12 +1,14 @@
1
1
  import type { ChildProcess } from "node:child_process";
2
2
  import type { Writable } from "node:stream";
3
+ import type { DenialBackoffConfig } from "../../../configs/sandbox/types.js";
4
+ import { type SandboxFailFastInfo } from "../sandbox.js";
3
5
  /**
4
6
  * Watchdog types and constants for enforcing per-agent process timeouts.
5
7
  *
6
8
  * Watchdog enforcement prevents hung agent binaries from blocking the entire
7
9
  * voratiq run pipeline by enforcing silence, wall-clock, and fatal pattern limits.
8
10
  */
9
- export type WatchdogTrigger = "silence" | "wall-clock" | "fatal-pattern";
11
+ export type WatchdogTrigger = "silence" | "wall-clock" | "fatal-pattern" | "sandbox-denial";
10
12
  export declare const WATCHDOG_DEFAULTS: {
11
13
  readonly silenceTimeoutMs: number;
12
14
  readonly wallClockCapMs: number;
@@ -24,7 +26,8 @@ export interface WatchdogResult {
24
26
  }
25
27
  export interface WatchdogOptions {
26
28
  providerId: string;
27
- onWatchdogTrigger?: (trigger: WatchdogTrigger, reason: string) => void;
29
+ denialBackoff?: DenialBackoffConfig;
30
+ onWatchdogTrigger?: (trigger: WatchdogTrigger, reason: string, failFast?: SandboxFailFastInfo) => void;
28
31
  }
29
32
  export declare function createWatchdog(child: ChildProcess, stderrStream: Writable, options: WatchdogOptions): WatchdogController;
30
33
  export interface WatchdogController {
@@ -33,6 +36,7 @@ export interface WatchdogController {
33
36
  getState: () => {
34
37
  triggered: WatchdogTrigger | null;
35
38
  triggeredReason: string | null;
39
+ sandboxFailFast?: SandboxFailFastInfo;
36
40
  };
37
41
  /** AbortSignal that fires after watchdog triggers and hard abort timeout passes. */
38
42
  abortSignal: AbortSignal;
@@ -1,3 +1,4 @@
1
+ import { DenialBackoffTracker, parseSandboxDenialLine, resolveDenialBackoffConfig, } from "../sandbox.js";
1
2
  export const WATCHDOG_DEFAULTS = {
2
3
  silenceTimeoutMs: 15 * 60 * 1000,
3
4
  wallClockCapMs: 120 * 60 * 1000,
@@ -12,6 +13,7 @@ export const FATAL_PATTERNS = new Map([
12
13
  ]);
13
14
  export function createWatchdog(child, stderrStream, options) {
14
15
  const { silenceTimeoutMs, wallClockCapMs, killGraceMs, hardAbortMs } = WATCHDOG_DEFAULTS;
16
+ const denialBackoff = resolveDenialBackoffConfig(options.denialBackoff);
15
17
  const state = {
16
18
  silenceTimer: null,
17
19
  wallClockTimer: null,
@@ -20,6 +22,10 @@ export function createWatchdog(child, stderrStream, options) {
20
22
  fatalPatternFirstSeen: null,
21
23
  triggered: null,
22
24
  triggeredReason: null,
25
+ sandboxFailFast: null,
26
+ denialBackoff: new DenialBackoffTracker(denialBackoff),
27
+ lineBuffer: "",
28
+ delayInProgress: false,
23
29
  abortController: new AbortController(),
24
30
  };
25
31
  const fatalPatterns = FATAL_PATTERNS.get(options.providerId) ?? [];
@@ -54,17 +60,20 @@ export function createWatchdog(child, stderrStream, options) {
54
60
  state.hardAbortTimer = null;
55
61
  }
56
62
  };
57
- const triggerWatchdog = (trigger, reason) => {
63
+ const triggerWatchdog = (trigger, reason, failFast) => {
58
64
  if (state.triggered) {
59
65
  return;
60
66
  }
61
67
  state.triggered = trigger;
62
68
  state.triggeredReason = reason;
69
+ if (failFast) {
70
+ state.sandboxFailFast = failFast;
71
+ }
63
72
  clearAllTimers();
64
73
  const banner = formatWatchdogBanner(trigger, reason);
65
74
  stderrStream.write(banner);
66
75
  if (options.onWatchdogTrigger) {
67
- options.onWatchdogTrigger(trigger, reason);
76
+ options.onWatchdogTrigger(trigger, reason, failFast);
68
77
  }
69
78
  terminateProcess(child, state, { killGraceMs, hardAbortMs });
70
79
  };
@@ -91,6 +100,60 @@ export function createWatchdog(child, stderrStream, options) {
91
100
  resetSilenceTimer();
92
101
  const text = typeof chunk === "string" ? chunk : chunk.toString("utf8");
93
102
  checkFatalPattern(text);
103
+ handleSandboxDenialText(text);
104
+ };
105
+ const handleSandboxDenialText = (text) => {
106
+ if (state.triggered) {
107
+ return;
108
+ }
109
+ state.lineBuffer += text;
110
+ const lines = state.lineBuffer.split("\n");
111
+ state.lineBuffer = lines.pop() ?? "";
112
+ for (const line of lines) {
113
+ if (state.triggered) {
114
+ return;
115
+ }
116
+ if (line.startsWith("Running: ")) {
117
+ state.denialBackoff.resetAll();
118
+ continue;
119
+ }
120
+ const denial = parseSandboxDenialLine(line);
121
+ if (!denial) {
122
+ continue;
123
+ }
124
+ const decision = state.denialBackoff.register(denial);
125
+ if (decision.action === "warn") {
126
+ stderrStream.write(`\n[SandboxBackoff: WARN] Repeated denial to ${denial.target} (count=${decision.count}).\n`);
127
+ }
128
+ else if (decision.action === "delay") {
129
+ stderrStream.write(`\n[SandboxBackoff: ERROR] Repeated denial to ${denial.target} (count=${decision.count}); delaying ${denialBackoff.delayMs}ms.\n`);
130
+ void applyBackoffDelay(child, state, denialBackoff);
131
+ }
132
+ else if (decision.action === "fail-fast") {
133
+ triggerWatchdog("sandbox-denial", `Sandbox: repeated denial to ${denial.target}, aborting to prevent resource exhaustion`, denial);
134
+ return;
135
+ }
136
+ }
137
+ };
138
+ const applyBackoffDelay = async (child, state, config) => {
139
+ if (state.delayInProgress || state.triggered) {
140
+ return;
141
+ }
142
+ const pid = child.pid;
143
+ if (pid === undefined) {
144
+ return;
145
+ }
146
+ state.delayInProgress = true;
147
+ const delayMs = config.delayMs;
148
+ killProcessGroup(pid, "SIGSTOP");
149
+ await new Promise((resolve) => {
150
+ const timer = setTimeout(resolve, delayMs);
151
+ timer.unref();
152
+ });
153
+ if (!state.triggered) {
154
+ killProcessGroup(pid, "SIGCONT");
155
+ }
156
+ state.delayInProgress = false;
94
157
  };
95
158
  resetSilenceTimer();
96
159
  const wallClockMinutes = Math.round(wallClockCapMs / 60000);
@@ -115,6 +178,7 @@ export function createWatchdog(child, stderrStream, options) {
115
178
  getState: () => ({
116
179
  triggered: state.triggered,
117
180
  triggeredReason: state.triggeredReason,
181
+ sandboxFailFast: state.sandboxFailFast ?? undefined,
118
182
  }),
119
183
  /** AbortSignal that fires after watchdog triggers and hard abort timeout passes. */
120
184
  abortSignal: state.abortController.signal,
@@ -1,5 +1,12 @@
1
1
  import type { SandboxRuntimeConfig } from "@voratiq/sandbox-runtime";
2
+ import type { DenialBackoffConfig } from "../../configs/sandbox/types.js";
2
3
  export type SandboxSettings = SandboxRuntimeConfig;
4
+ export type DenialOperationType = "network-connect" | "file-read" | "file-write";
5
+ export interface SandboxFailFastInfo {
6
+ operation: DenialOperationType;
7
+ target: string;
8
+ }
9
+ export declare const DEFAULT_DENIAL_BACKOFF: DenialBackoffConfig;
3
10
  export interface SandboxSettingsOptions {
4
11
  sandboxHomePath: string;
5
12
  workspacePath: string;
@@ -11,6 +18,21 @@ export interface SandboxSettingsOptions {
11
18
  evalsPath: string;
12
19
  }
13
20
  export declare function generateSandboxSettings(options: SandboxSettingsOptions): SandboxSettings;
21
+ export declare function resolveDenialBackoffConfig(config: DenialBackoffConfig | undefined): DenialBackoffConfig;
22
+ export declare function parseSandboxDenialLine(line: string): SandboxFailFastInfo | undefined;
23
+ export type DenialBackoffAction = "none" | "warn" | "delay" | "fail-fast";
24
+ export interface DenialBackoffDecision {
25
+ action: DenialBackoffAction;
26
+ count: number;
27
+ info: SandboxFailFastInfo;
28
+ }
29
+ export declare class DenialBackoffTracker {
30
+ private readonly config;
31
+ private readonly byTarget;
32
+ constructor(config: DenialBackoffConfig);
33
+ resetAll(): void;
34
+ register(info: SandboxFailFastInfo, now?: number): DenialBackoffDecision;
35
+ }
14
36
  export declare function writeSandboxSettings(sandboxSettingsPath: string, settings: SandboxSettings): Promise<void>;
15
37
  export declare function resolveSrtBinary(cliRoot: string): string;
16
38
  export declare function checkPlatformSupport(): void;
@@ -2,6 +2,14 @@ import { mkdir, writeFile } from "node:fs/promises";
2
2
  import { dirname, isAbsolute } from "node:path";
3
3
  import { loadSandboxProviderConfig } from "../../configs/sandbox/loader.js";
4
4
  import { resolvePath } from "../../utils/path.js";
5
+ export const DEFAULT_DENIAL_BACKOFF = {
6
+ enabled: true,
7
+ warningThreshold: 2,
8
+ delayThreshold: 3,
9
+ delayMs: 5000,
10
+ failFastThreshold: 4,
11
+ windowMs: 120000,
12
+ };
5
13
  export function generateSandboxSettings(options) {
6
14
  const { sandboxHomePath, workspacePath, provider, root, sandboxSettingsPath, runtimePath, artifactsPath, evalsPath, } = options;
7
15
  const providerConfig = loadSandboxProviderConfig({
@@ -41,6 +49,107 @@ export function generateSandboxSettings(options) {
41
49
  },
42
50
  };
43
51
  }
52
+ export function resolveDenialBackoffConfig(config) {
53
+ if (!config) {
54
+ return { ...DEFAULT_DENIAL_BACKOFF };
55
+ }
56
+ return {
57
+ enabled: typeof config.enabled === "boolean"
58
+ ? config.enabled
59
+ : DEFAULT_DENIAL_BACKOFF.enabled,
60
+ warningThreshold: typeof config.warningThreshold === "number"
61
+ ? config.warningThreshold
62
+ : DEFAULT_DENIAL_BACKOFF.warningThreshold,
63
+ delayThreshold: typeof config.delayThreshold === "number"
64
+ ? config.delayThreshold
65
+ : DEFAULT_DENIAL_BACKOFF.delayThreshold,
66
+ delayMs: typeof config.delayMs === "number"
67
+ ? config.delayMs
68
+ : DEFAULT_DENIAL_BACKOFF.delayMs,
69
+ failFastThreshold: typeof config.failFastThreshold === "number"
70
+ ? config.failFastThreshold
71
+ : DEFAULT_DENIAL_BACKOFF.failFastThreshold,
72
+ windowMs: typeof config.windowMs === "number"
73
+ ? config.windowMs
74
+ : DEFAULT_DENIAL_BACKOFF.windowMs,
75
+ };
76
+ }
77
+ export function parseSandboxDenialLine(line) {
78
+ const trimmed = line.trim();
79
+ if (trimmed.length === 0) {
80
+ return undefined;
81
+ }
82
+ const debugNetworkDeny = trimmed.match(/^\[SandboxDebug\]\s+(?:Denied by config rule|No matching config rule, denying|User denied):\s+([^\s]+)$/u);
83
+ if (debugNetworkDeny?.[1]) {
84
+ return { operation: "network-connect", target: debugNetworkDeny[1] };
85
+ }
86
+ const macosKernelDeny = trimmed.match(/\bSandbox:\s+deny(?:\(\d+\))?\s+([^\s]+)\s+(.+)$/u);
87
+ if (macosKernelDeny?.[1] && macosKernelDeny[2]) {
88
+ const op = macosKernelDeny[1].toLowerCase();
89
+ const target = macosKernelDeny[2].trim();
90
+ if (op.includes("network")) {
91
+ return { operation: "network-connect", target };
92
+ }
93
+ if (op.includes("file-read")) {
94
+ return { operation: "file-read", target };
95
+ }
96
+ if (op.includes("file-write")) {
97
+ return { operation: "file-write", target };
98
+ }
99
+ }
100
+ return undefined;
101
+ }
102
+ export class DenialBackoffTracker {
103
+ config;
104
+ byTarget = new Map();
105
+ constructor(config) {
106
+ this.config = resolveDenialBackoffConfig(config);
107
+ }
108
+ resetAll() {
109
+ this.byTarget.clear();
110
+ }
111
+ register(info, now = Date.now()) {
112
+ const key = `${info.operation}:${info.target}`;
113
+ const windowMs = this.config.windowMs;
114
+ const existing = this.byTarget.get(key) ?? [];
115
+ const last = existing.length > 0 ? existing[existing.length - 1] : undefined;
116
+ const timestamps = last !== undefined && now - last > windowMs ? [] : [...existing];
117
+ timestamps.push(now);
118
+ const maxKept = Math.max(1, this.config.failFastThreshold);
119
+ while (timestamps.length > maxKept) {
120
+ timestamps.shift();
121
+ }
122
+ this.byTarget.set(key, timestamps);
123
+ const countInWindow = countWithinMs(timestamps, now, windowMs);
124
+ const countIn60 = countWithinMs(timestamps, now, 60_000);
125
+ const countIn30 = countWithinMs(timestamps, now, 30_000);
126
+ let action = "none";
127
+ if (this.config.enabled && countInWindow >= this.config.failFastThreshold) {
128
+ action = "fail-fast";
129
+ }
130
+ else if (this.config.enabled &&
131
+ countIn60 === this.config.delayThreshold) {
132
+ action = "delay";
133
+ }
134
+ else if (this.config.enabled &&
135
+ countIn30 === this.config.warningThreshold) {
136
+ action = "warn";
137
+ }
138
+ return { action, count: countInWindow, info };
139
+ }
140
+ }
141
+ function countWithinMs(timestamps, now, windowMs) {
142
+ let count = 0;
143
+ for (let i = timestamps.length - 1; i >= 0; i -= 1) {
144
+ if (now - timestamps[i] <= windowMs) {
145
+ count += 1;
146
+ }
147
+ else {
148
+ break;
149
+ }
150
+ }
151
+ return count;
152
+ }
44
153
  function getDefaultSandboxWritePaths() {
45
154
  return [];
46
155
  }
@@ -1,5 +1,5 @@
1
1
  import type { LoadSandboxConfigurationOptions, LoadSandboxProviderConfigOptions, SandboxConfig, SandboxProviderConfig } from "./types.js";
2
- export type { LoadSandboxConfigurationOptions, LoadSandboxNetworkConfigOptions, LoadSandboxProviderConfigOptions, SandboxConfig, SandboxFilesystemConfig, SandboxNetworkConfig, SandboxProviderConfig, } from "./types.js";
2
+ export type { DenialBackoffConfig, LoadSandboxConfigurationOptions, LoadSandboxNetworkConfigOptions, LoadSandboxProviderConfigOptions, SandboxConfig, SandboxFilesystemConfig, SandboxNetworkConfig, SandboxProviderConfig, } from "./types.js";
3
3
  export declare function loadSandboxConfiguration(options?: LoadSandboxConfigurationOptions): SandboxConfig;
4
4
  export declare function loadSandboxProviderConfig(options: LoadSandboxProviderConfigOptions, providerIdOverride?: string): SandboxProviderConfig;
5
5
  export declare function enableSandboxLoaderTestHooks(): void;
@@ -15,6 +15,14 @@ const DEFAULT_FILESYSTEM_CONFIG = {
15
15
  denyRead: [],
16
16
  denyWrite: [],
17
17
  };
18
+ const DEFAULT_DENIAL_BACKOFF = {
19
+ enabled: true,
20
+ warningThreshold: 2,
21
+ delayThreshold: 3,
22
+ delayMs: 5000,
23
+ failFastThreshold: 4,
24
+ windowMs: 120000,
25
+ };
18
26
  const sandboxConfigLoader = createConfigLoader({
19
27
  resolveFilePath: (root, options) => resolveSandboxFilePath(root, options),
20
28
  selectReadFile: (options) => options.readFile,
@@ -48,6 +56,7 @@ const sandboxConfigLoader = createConfigLoader({
48
56
  providerId: canonical.id,
49
57
  network: providerNetwork,
50
58
  filesystem: providerFilesystem,
59
+ denialBackoff: mergeDenialBackoffConfig(DEFAULT_DENIAL_BACKOFF, override?.denialBackoff),
51
60
  };
52
61
  }
53
62
  return {
@@ -57,6 +66,25 @@ const sandboxConfigLoader = createConfigLoader({
57
66
  };
58
67
  },
59
68
  });
69
+ function mergeDenialBackoffConfig(base, override) {
70
+ if (!override) {
71
+ return { ...base };
72
+ }
73
+ return {
74
+ enabled: typeof override.enabled === "boolean" ? override.enabled : base.enabled,
75
+ warningThreshold: typeof override.warningThreshold === "number"
76
+ ? override.warningThreshold
77
+ : base.warningThreshold,
78
+ delayThreshold: typeof override.delayThreshold === "number"
79
+ ? override.delayThreshold
80
+ : base.delayThreshold,
81
+ delayMs: typeof override.delayMs === "number" ? override.delayMs : base.delayMs,
82
+ failFastThreshold: typeof override.failFastThreshold === "number"
83
+ ? override.failFastThreshold
84
+ : base.failFastThreshold,
85
+ windowMs: typeof override.windowMs === "number" ? override.windowMs : base.windowMs,
86
+ };
87
+ }
60
88
  function clearSandboxConfigurationCache() {
61
89
  configCache.clear();
62
90
  }
@@ -101,6 +129,7 @@ export function loadSandboxProviderConfig(options, providerIdOverride) {
101
129
  providerId: providerConfig.providerId,
102
130
  network: cloneNetworkConfig(providerConfig.network),
103
131
  filesystem: cloneFilesystemConfig(providerConfig.filesystem),
132
+ denialBackoff: { ...providerConfig.denialBackoff },
104
133
  };
105
134
  }
106
135
  const SANDBOX_LOADER_TEST_HOOKS = Symbol.for("voratiq.configs.sandbox.loader.testHooks");
@@ -162,6 +191,7 @@ function cloneSandboxConfig(config) {
162
191
  providerId,
163
192
  network: cloneNetworkConfig(providerConfig.network),
164
193
  filesystem: cloneFilesystemConfig(providerConfig.filesystem),
194
+ denialBackoff: { ...providerConfig.denialBackoff },
165
195
  };
166
196
  }
167
197
  return {
@@ -6,6 +6,14 @@ export declare const networkOverrideSchema: z.ZodObject<{
6
6
  allowUnixSockets: z.ZodOptional<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
7
7
  allowAllUnixSockets: z.ZodOptional<z.ZodBoolean>;
8
8
  }, z.core.$strict>;
9
+ export declare const denialBackoffOverrideSchema: z.ZodObject<{
10
+ enabled: z.ZodOptional<z.ZodBoolean>;
11
+ warningThreshold: z.ZodOptional<z.ZodNumber>;
12
+ delayThreshold: z.ZodOptional<z.ZodNumber>;
13
+ delayMs: z.ZodOptional<z.ZodNumber>;
14
+ failFastThreshold: z.ZodOptional<z.ZodNumber>;
15
+ windowMs: z.ZodOptional<z.ZodNumber>;
16
+ }, z.core.$strict>;
9
17
  export declare const filesystemOverrideSchema: z.ZodObject<{
10
18
  allowWrite: z.ZodOptional<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
11
19
  denyRead: z.ZodOptional<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
@@ -24,6 +32,14 @@ export declare const providerOverrideSchema: z.ZodObject<{
24
32
  denyRead: z.ZodOptional<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
25
33
  denyWrite: z.ZodOptional<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
26
34
  }, z.core.$strict>>;
35
+ denialBackoff: z.ZodOptional<z.ZodObject<{
36
+ enabled: z.ZodOptional<z.ZodBoolean>;
37
+ warningThreshold: z.ZodOptional<z.ZodNumber>;
38
+ delayThreshold: z.ZodOptional<z.ZodNumber>;
39
+ delayMs: z.ZodOptional<z.ZodNumber>;
40
+ failFastThreshold: z.ZodOptional<z.ZodNumber>;
41
+ windowMs: z.ZodOptional<z.ZodNumber>;
42
+ }, z.core.$strict>>;
27
43
  allowedDomains: z.ZodOptional<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
28
44
  deniedDomains: z.ZodOptional<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
29
45
  allowLocalBinding: z.ZodOptional<z.ZodBoolean>;
@@ -44,6 +60,14 @@ export declare const sandboxConfigSchema: z.ZodObject<{
44
60
  denyRead: z.ZodOptional<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
45
61
  denyWrite: z.ZodOptional<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
46
62
  }, z.core.$strict>>;
63
+ denialBackoff: z.ZodOptional<z.ZodObject<{
64
+ enabled: z.ZodOptional<z.ZodBoolean>;
65
+ warningThreshold: z.ZodOptional<z.ZodNumber>;
66
+ delayThreshold: z.ZodOptional<z.ZodNumber>;
67
+ delayMs: z.ZodOptional<z.ZodNumber>;
68
+ failFastThreshold: z.ZodOptional<z.ZodNumber>;
69
+ windowMs: z.ZodOptional<z.ZodNumber>;
70
+ }, z.core.$strict>>;
47
71
  allowedDomains: z.ZodOptional<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
48
72
  deniedDomains: z.ZodOptional<z.ZodArray<z.ZodPipe<z.ZodString, z.ZodTransform<string, string>>>>;
49
73
  allowLocalBinding: z.ZodOptional<z.ZodBoolean>;
@@ -52,6 +76,7 @@ export declare const sandboxConfigSchema: z.ZodObject<{
52
76
  }, z.core.$strict>>;
53
77
  }, z.core.$strip>;
54
78
  export type NetworkOverride = z.infer<typeof networkOverrideSchema>;
79
+ export type DenialBackoffOverride = z.infer<typeof denialBackoffOverrideSchema>;
55
80
  export type FilesystemOverride = z.infer<typeof filesystemOverrideSchema>;
56
81
  export type ProviderOverride = z.infer<typeof providerOverrideSchema>;
57
82
  export type SandboxOverrideDocument = z.infer<typeof sandboxConfigSchema>;
@@ -31,6 +31,16 @@ const networkOverrideShape = {
31
31
  allowAllUnixSockets: z.boolean().optional(),
32
32
  };
33
33
  export const networkOverrideSchema = z.object(networkOverrideShape).strict();
34
+ export const denialBackoffOverrideSchema = z
35
+ .object({
36
+ enabled: z.boolean().optional(),
37
+ warningThreshold: z.number().int().positive().optional(),
38
+ delayThreshold: z.number().int().positive().optional(),
39
+ delayMs: z.number().int().nonnegative().optional(),
40
+ failFastThreshold: z.number().int().positive().optional(),
41
+ windowMs: z.number().int().positive().optional(),
42
+ })
43
+ .strict();
34
44
  export const filesystemOverrideSchema = z
35
45
  .object({
36
46
  allowWrite: z
@@ -48,6 +58,7 @@ export const providerOverrideSchema = z
48
58
  ...networkOverrideShape,
49
59
  network: networkOverrideSchema.optional(),
50
60
  filesystem: filesystemOverrideSchema.optional(),
61
+ denialBackoff: denialBackoffOverrideSchema.optional(),
51
62
  })
52
63
  .strict();
53
64
  export const sandboxConfigSchema = z.object({
@@ -5,6 +5,14 @@ export interface SandboxNetworkConfig {
5
5
  allowUnixSockets?: string[];
6
6
  allowAllUnixSockets?: boolean;
7
7
  }
8
+ export interface DenialBackoffConfig {
9
+ enabled: boolean;
10
+ warningThreshold: number;
11
+ delayThreshold: number;
12
+ delayMs: number;
13
+ failFastThreshold: number;
14
+ windowMs: number;
15
+ }
8
16
  export interface SandboxFilesystemConfig {
9
17
  allowWrite: string[];
10
18
  denyRead: string[];
@@ -14,6 +22,7 @@ export interface SandboxProviderConfig {
14
22
  providerId: string;
15
23
  network: SandboxNetworkConfig;
16
24
  filesystem: SandboxFilesystemConfig;
25
+ denialBackoff: DenialBackoffConfig;
17
26
  }
18
27
  export interface SandboxConfig {
19
28
  filePath: string;
@@ -24,6 +24,7 @@ export declare const watchdogMetadataSchema: z.ZodObject<{
24
24
  silence: "silence";
25
25
  "wall-clock": "wall-clock";
26
26
  "fatal-pattern": "fatal-pattern";
27
+ "sandbox-denial": "sandbox-denial";
27
28
  }>>;
28
29
  }, z.core.$strip>;
29
30
  export type WatchdogMetadata = z.infer<typeof watchdogMetadataSchema>;
@@ -107,8 +108,16 @@ export declare const agentInvocationRecordSchema: z.ZodObject<{
107
108
  silence: "silence";
108
109
  "wall-clock": "wall-clock";
109
110
  "fatal-pattern": "fatal-pattern";
111
+ "sandbox-denial": "sandbox-denial";
110
112
  }>>;
111
113
  }, z.core.$strip>>;
114
+ failFastTriggered: z.ZodOptional<z.ZodBoolean>;
115
+ failFastTarget: z.ZodOptional<z.ZodString>;
116
+ failFastOperation: z.ZodOptional<z.ZodEnum<{
117
+ "network-connect": "network-connect";
118
+ "file-read": "file-read";
119
+ "file-write": "file-write";
120
+ }>>;
112
121
  }, z.core.$strip>;
113
122
  export type AgentInvocationRecord = z.infer<typeof agentInvocationRecordSchema>;
114
123
  export declare const applyStatusSchema: z.ZodObject<{
@@ -189,8 +198,16 @@ export declare const runRecordSchema: z.ZodObject<{
189
198
  silence: "silence";
190
199
  "wall-clock": "wall-clock";
191
200
  "fatal-pattern": "fatal-pattern";
201
+ "sandbox-denial": "sandbox-denial";
192
202
  }>>;
193
203
  }, z.core.$strip>>;
204
+ failFastTriggered: z.ZodOptional<z.ZodBoolean>;
205
+ failFastTarget: z.ZodOptional<z.ZodString>;
206
+ failFastOperation: z.ZodOptional<z.ZodEnum<{
207
+ "network-connect": "network-connect";
208
+ "file-read": "file-read";
209
+ "file-write": "file-write";
210
+ }>>;
194
211
  }, z.core.$strip>>;
195
212
  applyStatus: z.ZodOptional<z.ZodObject<{
196
213
  agentId: z.ZodString;
@@ -42,7 +42,17 @@ const CHAT_ARTIFACT_FORMATS = [
42
42
  "json",
43
43
  "jsonl",
44
44
  ];
45
- const WATCHDOG_TRIGGERS = ["silence", "wall-clock", "fatal-pattern"];
45
+ const WATCHDOG_TRIGGERS = [
46
+ "silence",
47
+ "wall-clock",
48
+ "fatal-pattern",
49
+ "sandbox-denial",
50
+ ];
51
+ const FAIL_FAST_OPERATIONS = [
52
+ "network-connect",
53
+ "file-read",
54
+ "file-write",
55
+ ];
46
56
  export const watchdogMetadataSchema = z.object({
47
57
  /** Silence timeout in milliseconds that was enforced. */
48
58
  silenceTimeoutMs: z.number(),
@@ -82,6 +92,9 @@ export const agentInvocationRecordSchema = z
82
92
  warnings: z.array(z.string()).optional(),
83
93
  diffStatistics: z.string().optional(),
84
94
  watchdog: watchdogMetadataSchema.optional(),
95
+ failFastTriggered: z.boolean().optional(),
96
+ failFastTarget: z.string().optional(),
97
+ failFastOperation: z.enum(FAIL_FAST_OPERATIONS).optional(),
85
98
  })
86
99
  .superRefine((data, ctx) => {
87
100
  if (IN_PROGRESS_AGENT_STATUSES.includes(data.status)) {
@@ -110,6 +123,22 @@ export const agentInvocationRecordSchema = z
110
123
  });
111
124
  }
112
125
  }
126
+ if (data.failFastTriggered) {
127
+ if (!data.failFastTarget) {
128
+ ctx.addIssue({
129
+ code: z.ZodIssueCode.custom,
130
+ path: ["failFastTarget"],
131
+ message: "failFastTarget is required when failFastTriggered is true",
132
+ });
133
+ }
134
+ if (!data.failFastOperation) {
135
+ ctx.addIssue({
136
+ code: z.ZodIssueCode.custom,
137
+ path: ["failFastOperation"],
138
+ message: "failFastOperation is required when failFastTriggered is true",
139
+ });
140
+ }
141
+ }
113
142
  });
114
143
  export const applyStatusSchema = z.object({
115
144
  agentId: agentIdSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "voratiq",
3
- "version": "0.1.0-beta.0",
3
+ "version": "0.1.0-beta.1",
4
4
  "description": "Run multiple AI coding agents in parallel, compare their results, and apply the best solution.",
5
5
  "keywords": [
6
6
  "ai",