Skip to content

Add budget tracking and enforcement for sandbox sessions (v1.1) - #52

Merged
frankbria merged 7 commits into
mainfrom
claude/add-budget-limits-eMu6m
Jan 30, 2026
Merged

Add budget tracking and enforcement for sandbox sessions (v1.1)#52
frankbria merged 7 commits into
mainfrom
claude/add-budget-limits-eMu6m

refactor: use 'budget status' subcommand per naming convention

c3a8ddf
Select commit
Loading
Failed to load commit list.
MacroscopeApp / Macroscope - Correctness Check succeeded Jan 30, 2026 in 4m 13s

No issues identified (54 code objects reviewed).

• Merge Base: 5270a6b
• Head: c3a8ddf

Details

File Path Comments Posted
CLAUDE.md 0
src/cli.ts 0
src/config.ts 0
src/db.ts 0
src/e2b/claude-runner.ts 0
src/e2b/sandbox-manager.ts 0
src/types.ts 0

Filtered Issues Details

src/cli.ts
  • line 1931: The budget enforcement logic is implemented in SandboxManager.checkBudgetLimit but is never invoked, rendering the --budget feature non-functional. In cli.ts, the --budget option is parsed and passed to sandboxManager.setBudgetLimit, but the execution flow subsequently waits on executeClaudeInSandbox. Neither cli.ts nor claude-runner.ts (which runs sandbox.commands.run) establishes a monitoring loop or interval to periodically call checkBudgetLimit. As a result, the BudgetExceededError logic in SandboxManager.ts is unreachable code, and sandboxes will continue running and accruing costs beyond the specified limit until the hard timeout is reached. [ Out of scope ]
  • line 3048: The logic for identifying budget-related keys relies on a loose substring check normalizedKey.includes('budget'). This implicitly traps any configuration key containing the word "budget" (e.g., budget_tracker.url, my_budget.enabled) into the numeric validation block. Consequently, users cannot set string or boolean values for these keys; the CLI will incorrectly attempt to parse them as numbers and throw an Invalid number error. [ Already posted ]
  • line 3189: The budgetStatusAction function calls db.migrateToLatest(), but the SessionDB class definition in src/db.ts does not define this method (it defines runMigration(version) instead). This will result in a TypeError: db.migrateToLatest is not a function at runtime. [ Already posted ]
  • line 3207: The budgetStatusAction function calls tracker.generateBudgetStatus(period), which internally calls this.db.getOrCreateBudgetTrackingRecord (in src/budget-tracker.ts). However, the SessionDB class in src/db.ts only implements createBudgetTrackingRecord and getBudgetTrackingRecord, not the combined getOrCreate... method. This mismatch will cause a TypeError at runtime. [ Already posted ]
  • line 3262: The budgetStatusAction function calls db.close() in the finally block. The SessionDB class in src/db.ts (which wraps better-sqlite3) does not expose a public close() method in the provided references. Unless inherited from a superclass not shown, this will cause a TypeError on every execution cleanup. [ Already posted ]
src/config.ts
  • line 68: The ConfigManager constructor implicitly schedules an uncatchable asynchronous file write when initializing a missing configuration file. If load() (called in the constructor) detects a missing file, it calls save(), which schedules flushSync() via setTimeout. If flushSync() fails (e.g., due to a read-only filesystem or permission errors on the directory), it explicitly re-throws the error. Since this error occurs inside a timer callback outside the constructor's execution stack, it cannot be caught by wrapping new ConfigManager() in a try/catch block, resulting in an unhandled exception that crashes the process. [ Already posted ]
  • line 141: The debounced save mechanism causes the application to crash on file system errors (e.g., read-only file system, disk full, permissions) or serialization errors. The save method schedules flushSync using setTimeout. Inside flushSync, errors from writeFileSync or JSON.stringify (e.g., if a circular reference was introduced via set) are caught, but any error other than ENOENT is re-thrown. Because this re-throw happens inside a setTimeout callback, it results in an unhandled exception that terminates the Node.js process, transforming a recoverable I/O error into a denial of service. [ Already posted ]
  • line 175: The flushSync method creates a risk of permanent configuration data loss due to non-atomic file writes. Line 175 calls writeFileSync directly on this.configPath. If the process crashes or loses power during this write operation, the configuration file will be corrupted (partially written or empty). Upon restart, the load method (line 101) will fail to parse the corrupted JSON. The catch block (lines 114-123) then backs up the 'corrupted' file and returns default values. This recovery mechanism combined with the non-atomic write means a single write failure results in the permanent loss of the user's valid configuration settings. [ Already posted ]
  • line 179: The flushSync method contains a high-severity runtime crash vector when handling non-serializable data. The set method allows insertion of any value (typed as unknown) into this.config, including objects with circular references or BigInts. The save method schedules an asynchronous write via setTimeout. When the timer fires, flushSync attempts to serialize the configuration using JSON.stringify (line 175). If this.config contains a circular reference, JSON.stringify throws a TypeError. This error is caught but explicitly re-thrown (line 179) because it is not an 'ENOENT' error. Since this occurs inside a setTimeout callback, the thrown error becomes an uncaught exception in the Node.js event loop, causing the entire process to crash. This allows a local denial-of-service via configuration updates. [ Already posted ]
  • line 286: The delete method permits the removal of the root budget configuration key, which violates the internal Config interface and corrupts the object state. The validateKeyPart method only checks for reserved words and format, not for required schema keys. If a caller invokes delete('budget'), this.config.budget becomes undefined. Subsequently, calling getBudgetConfig() executes structuredClone(this.config.budget). Since structuredClone throws a DataCloneError on undefined (in many environments) or returns undefined (violating the BudgetConfig return type contract), this leads to a runtime crash or TypeError when accessing properties on the result. [ Already posted ]
src/e2b/sandbox-manager.ts
  • line 432: In checkBudgetLimit, the budget warning thresholds array from the configuration (this.config.budgetWarningThresholds) is sorted in-place using .sort((a, b) => b - a). Since arrays are passed by reference in JavaScript/TypeScript and this.config.budgetWarningThresholds is a reference to the array in the persistent configuration object (which might be the shared DEFAULT_CONFIG or a user-provided array), this operation mutates the shared configuration state. This means the order of thresholds is permanently changed for subsequent calls or other sandboxes sharing the same config object. While sorting descending is intended for the logic inside this function, mutating a shared config object is a dangerous side effect that can lead to unexpected behavior if other parts of the system rely on the original order. [ Already posted ]
  • line 440: In checkBudgetLimit, the code calls this.config.budgetWarningThresholds.sort(...). The sort method mutates the array in-place. Because this.config.budgetWarningThresholds refers to the array from DEFAULT_CONFIG (if not overridden) or a shared configuration object passed into the constructor, this mutation affects all instances of SandboxManager or any other code sharing that configuration object. Over time, or with concurrent usage patterns, unexpected re-ordering of a shared configuration array can lead to race conditions or confusing side-effects in other parts of the application relying on a specific order (though here it sorts descending every time, repeated sorting of a shared mutable reference is bad practice and can lead to issues if the config is frozen or used elsewhere). [ Already posted ]