@garuda-agent/garuda 0.16.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/CHANGELOG.md +319 -0
  2. package/README.md +56 -1074
  3. package/SECURITY.md +9 -2
  4. package/dist/{acpCommand-OTAYW7BS.js → acpCommand-IUZPLPLI.js} +25 -23
  5. package/dist/{anthropic-NQDBCHJU.js → anthropic-NQIZLD64.js} +12 -6
  6. package/dist/{chunk-NP6RIC25.js → chunk-2FIV2VZP.js} +11 -7
  7. package/dist/{chunk-YQTGMCU6.js → chunk-3MRQJKUO.js} +29 -5
  8. package/dist/{chunk-HE2K657U.js → chunk-4BVCYCQT.js} +47 -16
  9. package/dist/{chunk-WMOLXTBU.js → chunk-4LFJFDCF.js} +2 -1
  10. package/dist/{chunk-44QN5SWZ.js → chunk-55CFEEIN.js} +22 -2
  11. package/dist/{chunk-USCUXCAU.js → chunk-5B5HDTUR.js} +3 -3
  12. package/dist/chunk-65NUEOI7.js +143 -0
  13. package/dist/{chunk-TUUEBHG2.js → chunk-6XHON3Q6.js} +2 -2
  14. package/dist/chunk-7KBOIPUT.js +157 -0
  15. package/dist/{chunk-B6FA4I7Q.js → chunk-7TH3NHCQ.js} +2 -2
  16. package/dist/{chunk-OUCV3FCT.js → chunk-AOGFYUKG.js} +3 -3
  17. package/dist/{chunk-GXZACJUI.js → chunk-B4SJY7KT.js} +322 -39
  18. package/dist/{chunk-3EQU2GUX.js → chunk-BHZ6G65V.js} +5 -1
  19. package/dist/chunk-C57GHLKP.js +9 -0
  20. package/dist/chunk-DOCDCQVM.js +484 -0
  21. package/dist/chunk-FOPBOB7W.js +332 -0
  22. package/dist/{chunk-TILFPYWO.js → chunk-NHWHG3SW.js} +1930 -992
  23. package/dist/{chunk-BQTUOO4G.js → chunk-OQ26W5L4.js} +81 -32
  24. package/dist/{chunk-HUIK3WXO.js → chunk-P6XWWIEK.js} +20 -3
  25. package/dist/chunk-Q4P3W657.js +6372 -0
  26. package/dist/{chunk-ZAE25JM2.js → chunk-QANRZW7W.js} +8 -1
  27. package/dist/chunk-UIRJOQQS.js +118 -0
  28. package/dist/chunk-UKIE4AAM.js +472 -0
  29. package/dist/{chunk-KKB5W47S.js → chunk-VTMD5IHD.js} +26 -2
  30. package/dist/cli/index.js +148 -57
  31. package/dist/config-WV2H64TF.js +23 -0
  32. package/dist/{create-VXIH2GUZ.js → create-W6BV75QX.js} +18 -12
  33. package/dist/{deadline-F5RJJ2QW.js → deadline-TP7ZGLT2.js} +4 -2
  34. package/dist/evalCommand-SKJQ6CW2.js +686 -0
  35. package/dist/evalTune-CZRCPSZ3.js +295 -0
  36. package/dist/{export-HN3CT7AV.js → export-SJ3DUP6W.js} +2 -2
  37. package/dist/{inkChat-WEDGQMCY.js → inkChat-WYYAFZXD.js} +20 -19
  38. package/dist/install-TKWN5AYH.js +19 -0
  39. package/dist/{job-3NYDMP2M.js → job-7JOI4YIQ.js} +6 -2
  40. package/dist/jobCommand-2ODQLHB3.js +33 -0
  41. package/dist/{launchd-SSX362VG.js → launchd-3A3ONEW5.js} +9 -8
  42. package/dist/{lspCommand-JRXKRSL7.js → lspCommand-ZZ6R4PA2.js} +7 -6
  43. package/dist/{manager-773PUDFE.js → manager-TCYTA5XU.js} +15 -14
  44. package/dist/{nightCommand-65MUEV32.js → nightCommand-WJU6VZA6.js} +26 -20
  45. package/dist/{openaiCompatible-CJWVJP5M.js → openaiCompatible-6TLDDW5U.js} +29 -15
  46. package/dist/{policy-EJPLEEXP.js → policy-ZJ7RMOFN.js} +10 -4
  47. package/dist/{remove-6GDSTAIO.js → remove-42WQNVPX.js} +8 -7
  48. package/dist/{run-ML6GSPN4.js → run-PTMRQCIU.js} +5 -5
  49. package/dist/{server-NGKVIQEV.js → server-VEYIJHEU.js} +2 -2
  50. package/dist/{setupCommand-VA7D5MZK.js → setupCommand-YMF6I3MD.js} +45 -13
  51. package/dist/text-LS35WSRL.js +17 -0
  52. package/dist/tune-YVI2KWQF.js +31 -0
  53. package/native/bin/garuda-reap +0 -0
  54. package/native/garuda-reap.c +113 -0
  55. package/package.json +39 -13
  56. package/dist/chunk-EIXUSVFX.js +0 -116
  57. package/dist/chunk-R7I2NBPX.js +0 -71
  58. package/dist/chunk-ULAJPMY7.js +0 -107
  59. package/dist/chunk-VBZCNMUD.js +0 -9
  60. package/dist/evalCommand-RGMZRYDR.js +0 -2812
  61. package/dist/install-EWDSPFFZ.js +0 -18
  62. package/dist/jobCommand-ACSGSBQ4.js +0 -32
  63. package/dist/text-J2GD647J.js +0 -11
package/SECURITY.md CHANGED
@@ -6,9 +6,15 @@ Garuda runs commands and edits files for a model, so we take security reports se
6
6
 
7
7
  | Version | Supported |
8
8
  | --- | --- |
9
- | 0.14.x | Yes |
9
+ | 0.18.x | Yes |
10
10
  | Older | No |
11
11
 
12
+ ## Node and dependencies
13
+
14
+ Run Garuda on Node 22.23.4, 24.22.1 or 26.12.1, or a later release of the same line. Garuda warns
15
+ at start on an older Node. CI, the release workflow and the dependency audit refuse a Node or a
16
+ package with a known security fix missing. See [docs/security-deps.md](docs/security-deps.md).
17
+
12
18
  ## Report a vulnerability
13
19
 
14
20
  Do not open a public issue. Use GitHub's private vulnerability reporting: in this repository, open
@@ -28,5 +34,6 @@ In scope: the permission engine and the team policy, the OS sandbox (Seatbelt, b
28
34
  network allowlist and its proxy, secret redaction, the audit log, and the consent for project
29
35
  content (hooks, MCP servers, settings, commands, skills, agents).
30
36
 
31
- Known limits are listed in the README under the status table. A report about one of them is still
37
+ Known limits are listed in [docs/guide/status.md](docs/guide/status.md) and the
38
+ [issue register](docs/quality-baseline/issue-register.md). A report about one of them is still
32
39
  welcome when it shows a new way to reach the same effect.
@@ -1,37 +1,39 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- FileSessionStore,
4
- Runtime,
5
- auditDirFor
6
- } from "./chunk-TILFPYWO.js";
7
- import "./chunk-G2AX7MGS.js";
8
- import "./chunk-A2QAIRQL.js";
9
- import "./chunk-ULAJPMY7.js";
10
2
  import {
11
3
  chooseModel,
12
4
  keepProviderKeys,
13
5
  loadModelsConfig,
14
6
  providerKeySource,
15
7
  resolveModel
16
- } from "./chunk-NP6RIC25.js";
17
- import "./chunk-EIXUSVFX.js";
18
- import "./chunk-R7I2NBPX.js";
8
+ } from "./chunk-2FIV2VZP.js";
9
+ import {
10
+ Runtime,
11
+ auditDirFor
12
+ } from "./chunk-NHWHG3SW.js";
13
+ import "./chunk-65NUEOI7.js";
14
+ import "./chunk-G2AX7MGS.js";
15
+ import "./chunk-A2QAIRQL.js";
16
+ import {
17
+ FileSessionStore
18
+ } from "./chunk-UKIE4AAM.js";
19
+ import "./chunk-7KBOIPUT.js";
19
20
  import {
20
21
  loadSearchConfig
21
22
  } from "./chunk-B25VL4RU.js";
22
23
  import "./chunk-23FXBNBB.js";
23
24
  import {
24
25
  VERSION
25
- } from "./chunk-VBZCNMUD.js";
26
+ } from "./chunk-C57GHLKP.js";
26
27
  import "./chunk-5DEWP3YX.js";
27
- import "./chunk-ZAE25JM2.js";
28
- import "./chunk-ATW7BNIG.js";
29
28
  import "./chunk-PZS3V6P3.js";
30
- import "./chunk-OUCV3FCT.js";
31
- import "./chunk-GXZACJUI.js";
32
- import "./chunk-WMOLXTBU.js";
33
- import "./chunk-3EQU2GUX.js";
34
- import "./chunk-HUIK3WXO.js";
29
+ import "./chunk-QANRZW7W.js";
30
+ import "./chunk-ATW7BNIG.js";
31
+ import "./chunk-AOGFYUKG.js";
32
+ import "./chunk-B4SJY7KT.js";
33
+ import "./chunk-BHZ6G65V.js";
34
+ import "./chunk-P6XWWIEK.js";
35
+ import "./chunk-UIRJOQQS.js";
36
+ import "./chunk-4LFJFDCF.js";
35
37
  import "./chunk-3NJNVZTK.js";
36
38
 
37
39
  // src/cli/acpCommand.ts
@@ -45,12 +47,12 @@ async function acpCommand(options) {
45
47
  `);
46
48
  return 1;
47
49
  }
48
- const { setupCommand, terminalIO } = await import("./setupCommand-VA7D5MZK.js");
50
+ const { setupCommand, terminalIO } = await import("./setupCommand-YMF6I3MD.js");
49
51
  return setupCommand({}, { io: await terminalIO() });
50
52
  }
51
53
  const output = protocolOutput(process.stdout, process.stderr);
52
54
  const input = Readable.toWeb(process.stdin);
53
- const { acpServer, setupNeeded } = await import("./server-NGKVIQEV.js");
55
+ const { acpServer, setupNeeded } = await import("./server-VEYIJHEU.js");
54
56
  const server = acpServer({
55
57
  version: VERSION,
56
58
  // Not on native Windows: the setup there could not keep a key private (no 0600).
@@ -114,7 +116,7 @@ async function prepare(options, from = {}) {
114
116
  const why = credentialsWarning === void 0 ? "" : ` ${credentialsWarning}`;
115
117
  return fail(`Garuda has no key for ${resolved.spec} (${keyName}).${why} ${SETUP_HINT}`, true);
116
118
  }
117
- const { loadTeamPolicy } = await import("./policy-EJPLEEXP.js");
119
+ const { loadTeamPolicy } = await import("./policy-ZJ7RMOFN.js");
118
120
  let team;
119
121
  try {
120
122
  team = await loadTeamPolicy("managed" in from ? { home, managed: from.managed } : { home });
@@ -188,4 +190,4 @@ export {
188
190
  prepare,
189
191
  protocolOutput
190
192
  };
191
- //# sourceMappingURL=acpCommand-OTAYW7BS.js.map
193
+ //# sourceMappingURL=acpCommand-IUZPLPLI.js.map
@@ -14,10 +14,12 @@ var AnthropicClient = class {
14
14
  this.client = options.client ?? new Anthropic(clientOptions(options.apiKey));
15
15
  }
16
16
  async *stream(request, options) {
17
- const stream = this.client.messages.stream(
18
- toWireParams(this.model, request),
19
- options?.signal === void 0 ? {} : { signal: options.signal }
20
- );
17
+ const stream = this.client.messages.stream(toWireParams(this.model, request), {
18
+ ...options?.signal === void 0 ? {} : { signal: options.signal },
19
+ // Under the token budget (0.17) the SDK must not send the request again on its own: the loop
20
+ // retries, and each of its attempts reserves first.
21
+ ...options?.meter === void 0 ? {} : { maxRetries: 0 }
22
+ });
21
23
  for await (const event of stream) {
22
24
  if (event.type === "content_block_delta" && event.delta.type === "text_delta") {
23
25
  yield { type: "text_delta", text: event.delta.text };
@@ -238,7 +240,11 @@ var AnthropicBatchClient = class {
238
240
  );
239
241
  const batch = await this.client.messages.batches.create(
240
242
  { requests: [{ custom_id: "garuda", params }] },
241
- signal === void 0 ? {} : { signal }
243
+ {
244
+ ...signal === void 0 ? {} : { signal },
245
+ // Batch submission is a generation attempt too. The loop, not the SDK, owns retries.
246
+ ...options?.meter === void 0 ? {} : { maxRetries: 0 }
247
+ }
242
248
  );
243
249
  const started = Date.now();
244
250
  this.onWait?.({ batchId: batch.id, waitedMs: 0, status: "created" });
@@ -324,4 +330,4 @@ export {
324
330
  toWireTools,
325
331
  withHiddenThinking
326
332
  };
327
- //# sourceMappingURL=anthropic-NQDBCHJU.js.map
333
+ //# sourceMappingURL=anthropic-NQIZLD64.js.map
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- keepSecretForRedaction,
4
3
  writeFileAtomic
5
- } from "./chunk-EIXUSVFX.js";
4
+ } from "./chunk-65NUEOI7.js";
6
5
  import {
6
+ keepSecretForRedaction,
7
7
  lookupModel
8
- } from "./chunk-R7I2NBPX.js";
8
+ } from "./chunk-7KBOIPUT.js";
9
9
  import {
10
10
  isLoopbackHost
11
11
  } from "./chunk-ATW7BNIG.js";
@@ -228,6 +228,9 @@ async function keepProviderKeys(names, home) {
228
228
  }
229
229
  return stored.warning;
230
230
  }
231
+ function providerKeyValue(name) {
232
+ return providerKey(process.env, name);
233
+ }
231
234
  function providerKey(env, name) {
232
235
  if (name === void 0) return void 0;
233
236
  return keptKeys.get(name)?.value ?? env[name];
@@ -269,7 +272,7 @@ function resolveModel(spec, config = { providers: {}, models: {} }) {
269
272
  ...def.type === "anthropic" ? {
270
273
  async createBatch(env = process.env, options = {}) {
271
274
  const apiKey = providerKey(env, def.apiKeyEnv ?? "ANTHROPIC_API_KEY");
272
- const { AnthropicBatchClient } = await import("./anthropic-NQDBCHJU.js");
275
+ const { AnthropicBatchClient } = await import("./anthropic-NQIZLD64.js");
273
276
  return new AnthropicBatchClient({ model, ...apiKey ? { apiKey } : {}, ...options });
274
277
  }
275
278
  } : {},
@@ -282,10 +285,10 @@ function resolveModel(spec, config = { providers: {}, models: {} }) {
282
285
  }
283
286
  if (def.type === "anthropic") {
284
287
  const key = providerKey(env, def.apiKeyEnv ?? "ANTHROPIC_API_KEY");
285
- const { AnthropicClient } = await import("./anthropic-NQDBCHJU.js");
288
+ const { AnthropicClient } = await import("./anthropic-NQIZLD64.js");
286
289
  return new AnthropicClient({ model, ...key ? { apiKey: key } : {} });
287
290
  }
288
- const { OpenAICompatibleClient } = await import("./openaiCompatible-CJWVJP5M.js");
291
+ const { OpenAICompatibleClient } = await import("./openaiCompatible-6TLDDW5U.js");
289
292
  return new OpenAICompatibleClient({
290
293
  provider,
291
294
  baseUrl: def.baseUrl,
@@ -326,6 +329,7 @@ export {
326
329
  setDefaultModel,
327
330
  providerKeySource,
328
331
  keepProviderKeys,
332
+ providerKeyValue,
329
333
  resolveModel
330
334
  };
331
- //# sourceMappingURL=chunk-NP6RIC25.js.map
335
+ //# sourceMappingURL=chunk-2FIV2VZP.js.map
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  passed,
4
4
  testSummary
5
- } from "./chunk-OUCV3FCT.js";
5
+ } from "./chunk-AOGFYUKG.js";
6
6
 
7
7
  // src/jobs/text.ts
8
8
  function jobPrompt(request, plan) {
@@ -22,7 +22,28 @@ function jobPrompt(request, plan) {
22
22
  "</plan>"
23
23
  ].join("\n");
24
24
  }
25
- var cost = (usd) => usd === void 0 ? "unknown" : `$${usd.toFixed(4)}`;
25
+ var cost = (usd, incomplete = false) => formatCost({ usd, incomplete });
26
+ function formatCost(c) {
27
+ if (c.usd === void 0) return "unknown";
28
+ return c.incomplete ? `\u2265 $${c.usd.toFixed(4)} (incomplete)` : `$${c.usd.toFixed(4)}`;
29
+ }
30
+ function sumCosts(costs) {
31
+ let usd = 0;
32
+ let incomplete = false;
33
+ for (const c of costs) {
34
+ usd = usd === void 0 || c.usd === void 0 ? void 0 : usd + c.usd;
35
+ incomplete ||= c.incomplete;
36
+ }
37
+ return { usd, incomplete };
38
+ }
39
+ function jobCost(r) {
40
+ const run = { usd: r.costUsd, incomplete: r.costIncomplete === true };
41
+ const proof = r.proof;
42
+ const review = proof?.review;
43
+ const reviewIncomplete = proof?.reviewCostIncomplete === true;
44
+ if (review === void 0) return sumCosts([run, { usd: 0, incomplete: reviewIncomplete }]);
45
+ return sumCosts([run, { usd: review.costUsd, incomplete: reviewIncomplete }]);
46
+ }
26
47
  function jobReport(job) {
27
48
  const r = job.result;
28
49
  const lines = [`# Garuda job ${job.id}: ${job.title}`, ""];
@@ -40,7 +61,7 @@ function jobReport(job) {
40
61
  if (job.startedAt !== void 0) lines.push(`- Started: ${job.startedAt}`);
41
62
  if (r === void 0) return lines.join("\n");
42
63
  lines.push(
43
- `- Steps: ${r.steps} \xB7 tokens: ${Math.round(r.tokens / 100) / 10}k \xB7 cost: ${cost(r.costUsd)} \xB7 time: ${Math.round(r.durationMs / 1e3)} s`
64
+ `- Steps: ${r.steps} \xB7 tokens: ${Math.round(r.tokens / 100) / 10}k \xB7 cost: ${cost(r.costUsd, r.costIncomplete === true)} \xB7 time: ${Math.round(r.durationMs / 1e3)} s`
44
65
  );
45
66
  if (r.sessionId !== void 0) lines.push(`- Session: ${r.sessionId}`);
46
67
  if (r.modelCalls !== void 0) {
@@ -96,7 +117,7 @@ function proofLines(job, proof) {
96
117
  if (proof.flags.length === 0) out.push("None.");
97
118
  for (const f of proof.flags) out.push(`- ${f.level === "stop" ? "STOP" : "look"}: ${f.text}`);
98
119
  if (proof.review !== void 0) {
99
- const cost2 = proof.review.costUsd === void 0 ? "" : ` (${`$${proof.review.costUsd.toFixed(4)}`})`;
120
+ const cost2 = proof.review.costUsd === void 0 ? "" : ` (${formatCost({ usd: proof.review.costUsd, incomplete: proof.reviewCostIncomplete === true })})`;
100
121
  out.push("", `## Review by a principal engineer${cost2}`, "", proof.review.text);
101
122
  } else if (proof.reviewError !== void 0) {
102
123
  out.push("", "## Review", "", `The review failed: ${proof.reviewError}`);
@@ -106,6 +127,9 @@ function proofLines(job, proof) {
106
127
 
107
128
  export {
108
129
  jobPrompt,
130
+ formatCost,
131
+ sumCosts,
132
+ jobCost,
109
133
  jobReport
110
134
  };
111
- //# sourceMappingURL=chunk-YQTGMCU6.js.map
135
+ //# sourceMappingURL=chunk-3MRQJKUO.js.map
@@ -4,11 +4,12 @@ import {
4
4
  pickChannel
5
5
  } from "./chunk-N4I6YS3E.js";
6
6
  import {
7
- gateProjectSettings
8
- } from "./chunk-ULAJPMY7.js";
7
+ gateProjectSettings,
8
+ isCostIncomplete
9
+ } from "./chunk-UKIE4AAM.js";
9
10
  import {
10
11
  totalTokens
11
- } from "./chunk-R7I2NBPX.js";
12
+ } from "./chunk-7KBOIPUT.js";
12
13
  import {
13
14
  TrustStore
14
15
  } from "./chunk-5DEWP3YX.js";
@@ -16,15 +17,16 @@ import {
16
17
  cleanArgs,
17
18
  commitJob,
18
19
  jobChanges,
20
+ outsideScope,
19
21
  prepareWorktree
20
- } from "./chunk-44QN5SWZ.js";
22
+ } from "./chunk-55CFEEIN.js";
21
23
  import {
22
24
  defaultAgentEnv,
23
25
  removeAgent
24
- } from "./chunk-USCUXCAU.js";
26
+ } from "./chunk-5B5HDTUR.js";
25
27
  import {
26
28
  jobReport
27
- } from "./chunk-YQTGMCU6.js";
29
+ } from "./chunk-3MRQJKUO.js";
28
30
  import {
29
31
  jobVerdict,
30
32
  parseReview,
@@ -33,21 +35,24 @@ import {
33
35
  riskFlags,
34
36
  stackOf,
35
37
  testSummary
36
- } from "./chunk-OUCV3FCT.js";
38
+ } from "./chunk-AOGFYUKG.js";
37
39
  import {
38
40
  JobGitError,
39
41
  git,
40
42
  hostCommand
41
- } from "./chunk-TUUEBHG2.js";
43
+ } from "./chunk-6XHON3Q6.js";
42
44
  import {
43
45
  createExecutor,
44
46
  loadSettings
45
- } from "./chunk-GXZACJUI.js";
47
+ } from "./chunk-B4SJY7KT.js";
46
48
  import {
47
49
  JOBS_DIR,
48
50
  loadJob,
49
51
  saveJob
50
- } from "./chunk-KKB5W47S.js";
52
+ } from "./chunk-VTMD5IHD.js";
53
+ import {
54
+ prepareWriteScope
55
+ } from "./chunk-UIRJOQQS.js";
51
56
  import {
52
57
  parseRule
53
58
  } from "./chunk-3NJNVZTK.js";
@@ -93,6 +98,7 @@ async function proveJob(prepared, result, runtime, renderer, git2) {
93
98
  };
94
99
  if (job.review !== false && result.commit !== void 0) {
95
100
  renderer.info("Reviewing the diff (a principal engineer's review)\u2026");
101
+ const unreportedBefore = runtime.session?.unreportedTokens ?? 0;
96
102
  try {
97
103
  const diff = (await git(git2, job.root, ["diff", "--no-renames", job.base, job.branch], {
98
104
  check: false
@@ -118,8 +124,10 @@ async function proveJob(prepared, result, runtime, renderer, git2) {
118
124
  } catch (error) {
119
125
  proof.reviewError = error.message;
120
126
  }
127
+ if ((runtime.session?.unreportedTokens ?? 0) > unreportedBefore)
128
+ proof.reviewCostIncomplete = true;
121
129
  }
122
- proof.verdict = jobVerdict(flags, proof.review?.verdict);
130
+ proof.verdict = jobVerdict(flags, proof.review?.verdict, proof.reviewError !== void 0);
123
131
  return proof;
124
132
  }
125
133
  async function prepareJob(mainRoot, id, at, renderer, git2 = createExecutor("host").executor, launchd = {}, trustHome) {
@@ -159,6 +167,15 @@ async function prepare(mainRoot, id, at, renderer, git2, trustHome) {
159
167
  renderer.error(`The job's worktree failed: ${error.message}`);
160
168
  return 1;
161
169
  }
170
+ if (job.writeScope !== void 0) {
171
+ const problem = prepareWriteScope(job.worktree, job.writeScope);
172
+ if (problem !== void 0) {
173
+ renderer.error(
174
+ `The job's write scope cannot be used: ${problem}. Fix "writeScope" in ${JOBS_DIR}/${id}.json.`
175
+ );
176
+ return 1;
177
+ }
178
+ }
162
179
  const gate = await gateProjectSettings({
163
180
  root: job.root,
164
181
  settings: await loadSettings(job.worktree),
@@ -172,6 +189,7 @@ async function prepare(mainRoot, id, at, renderer, git2, trustHome) {
172
189
  ...projectRest,
173
190
  ...job.network === void 0 ? {} : { network: { allow: job.network } },
174
191
  allow: [...project.allow, ...job.allow.map((rule) => parseRule(rule))],
192
+ ...job.writeScope === void 0 ? {} : { writeScope: job.writeScope },
175
193
  maxSteps: job.maxSteps,
176
194
  sandbox: {
177
195
  ...project.sandbox,
@@ -185,7 +203,8 @@ async function prepare(mainRoot, id, at, renderer, git2, trustHome) {
185
203
  renderer.info(
186
204
  `Job ${job.id}: ${job.title}
187
205
  Branch ${job.branch} \xB7 worktree ${job.worktree}
188
- Approved: ${job.allow.length === 0 ? "nothing beyond the sandbox" : job.allow.join(", ")}`
206
+ Approved: ${job.allow.length === 0 ? "nothing beyond the sandbox" : job.allow.join(", ")}
207
+ ${job.writeScope === void 0 ? "Write scope: none. Commands may write anywhere in the worktree." : `Write scope: ${job.writeScope.length === 0 ? "nothing in the worktree" : job.writeScope.join(", ")} (the OS sandbox enforces it for commands).`}`
189
208
  );
190
209
  return { job, root: job.worktree, settings, denied: [], startedAt: Date.now() };
191
210
  }
@@ -224,8 +243,11 @@ function msUntil(hour, minute, now = /* @__PURE__ */ new Date()) {
224
243
  if (next.getTime() <= now.getTime()) next.setDate(next.getDate() + 1);
225
244
  return next.getTime() - now.getTime();
226
245
  }
246
+ var ScopeViolation = class extends Error {
247
+ };
227
248
  async function finishJob(prepared, outcome, runtime, renderer, git2 = createExecutor("host").executor, launchd = {}) {
228
249
  const { job } = prepared;
250
+ let scopeFailure;
229
251
  const hadAgent = job.launchd !== void 0 || launchd.fromLaunchd === true;
230
252
  delete job.launchd;
231
253
  const result = {
@@ -233,6 +255,7 @@ async function finishJob(prepared, outcome, runtime, renderer, git2 = createExec
233
255
  steps: outcome.kind === "done" ? outcome.result.steps : 0,
234
256
  tokens: runtime.session === void 0 ? 0 : totalTokens(runtime.session.usage),
235
257
  ...runtime.session?.costUsd === void 0 ? {} : { costUsd: runtime.session.costUsd },
258
+ ...runtime.session !== void 0 && isCostIncomplete(runtime.session) ? { costIncomplete: true } : {},
236
259
  durationMs: Date.now() - prepared.startedAt,
237
260
  ...runtime.session === void 0 ? {} : { sessionId: runtime.session.id },
238
261
  files: [],
@@ -249,6 +272,12 @@ async function finishJob(prepared, outcome, runtime, renderer, git2 = createExec
249
272
  ...outcome.kind === "error" ? { error: outcome.message } : {}
250
273
  };
251
274
  try {
275
+ const outside = job.writeScope === void 0 ? [] : await outsideScope(git2, job);
276
+ if (outside.length > 0) {
277
+ scopeFailure = `changes outside the approved write scope, so nothing was committed: ${outside.slice(0, 20).join(", ")}${outside.length > 20 ? ", \u2026" : ""}`;
278
+ result.error = [result.error, scopeFailure].filter(Boolean).join("; ");
279
+ throw new ScopeViolation();
280
+ }
252
281
  const commit = await commitJob(
253
282
  git2,
254
283
  job,
@@ -257,13 +286,15 @@ async function finishJob(prepared, outcome, runtime, renderer, git2 = createExec
257
286
  if (commit !== void 0) result.commit = commit;
258
287
  result.files = await jobChanges(git2, job);
259
288
  } catch (error) {
260
- if (!(error instanceof JobGitError)) throw error;
261
- result.error = [result.error, `commit: ${error.message}`].filter(Boolean).join("; ");
289
+ if (!(error instanceof ScopeViolation)) {
290
+ if (!(error instanceof JobGitError)) throw error;
291
+ result.error = [result.error, `commit: ${error.message}`].filter(Boolean).join("; ");
292
+ }
262
293
  }
263
294
  if (outcome.kind !== "interrupted") {
264
295
  result.proof = await proveJob(prepared, result, runtime, renderer, git2);
265
296
  }
266
- job.status = outcome.kind === "error" ? "failed" : outcome.kind === "interrupted" ? "stopped" : "done";
297
+ job.status = outcome.kind === "error" || scopeFailure !== void 0 ? "failed" : outcome.kind === "interrupted" ? "stopped" : "done";
267
298
  job.endedAt = (/* @__PURE__ */ new Date()).toISOString();
268
299
  job.result = result;
269
300
  await saveJob(job);
@@ -319,4 +350,4 @@ export {
319
350
  msUntil,
320
351
  finishJob
321
352
  };
322
- //# sourceMappingURL=chunk-HE2K657U.js.map
353
+ //# sourceMappingURL=chunk-4BVCYCQT.js.map
@@ -56,6 +56,7 @@ export {
56
56
  isInside,
57
57
  resolveInRoot,
58
58
  realRelative,
59
+ realpathOfNearestExisting,
59
60
  displayPath
60
61
  };
61
- //# sourceMappingURL=chunk-WMOLXTBU.js.map
62
+ //# sourceMappingURL=chunk-4LFJFDCF.js.map
@@ -2,7 +2,10 @@
2
2
  import {
3
3
  JobGitError,
4
4
  git
5
- } from "./chunk-TUUEBHG2.js";
5
+ } from "./chunk-6XHON3Q6.js";
6
+ import {
7
+ inWriteScope
8
+ } from "./chunk-UIRJOQQS.js";
6
9
 
7
10
  // src/jobs/worktree.ts
8
11
  import { createHash } from "crypto";
@@ -86,6 +89,22 @@ async function commitJob(executor, job, message) {
86
89
  await git(executor, job.worktree, ["commit", "--no-verify", "-q", "-m", message]);
87
90
  return (await git(executor, job.worktree, ["rev-parse", "HEAD"])).stdout.trim();
88
91
  }
92
+ async function outsideScope(executor, job) {
93
+ if (job.writeScope === void 0) return [];
94
+ const exclude = [...NEVER_COMMIT, ...job.links].map((p) => `:(exclude)${p}`);
95
+ const status = await git(executor, job.worktree, [
96
+ "status",
97
+ "--porcelain=v1",
98
+ "-z",
99
+ "--untracked-files=all",
100
+ "--no-renames",
101
+ "--",
102
+ ".",
103
+ ...exclude
104
+ ]);
105
+ const scope = job.writeScope;
106
+ return status.stdout.split("\0").filter((entry) => entry.length > 3).map((entry) => entry.slice(3)).filter((path) => !inWriteScope(path, scope));
107
+ }
89
108
  async function jobChanges(executor, job) {
90
109
  const range = [job.base, job.branch];
91
110
  const names = await git(executor, job.root, [
@@ -133,6 +152,7 @@ export {
133
152
  linkCandidates,
134
153
  prepareWorktree,
135
154
  commitJob,
155
+ outsideScope,
136
156
  jobChanges
137
157
  };
138
- //# sourceMappingURL=chunk-44QN5SWZ.js.map
158
+ //# sourceMappingURL=chunk-55CFEEIN.js.map
@@ -2,10 +2,10 @@
2
2
  import {
3
3
  hostCommand,
4
4
  shellWord
5
- } from "./chunk-TUUEBHG2.js";
5
+ } from "./chunk-6XHON3Q6.js";
6
6
  import {
7
7
  JOBS_DIR
8
- } from "./chunk-KKB5W47S.js";
8
+ } from "./chunk-VTMD5IHD.js";
9
9
 
10
10
  // src/jobs/launchd.ts
11
11
  import { createHash } from "crypto";
@@ -154,4 +154,4 @@ export {
154
154
  nightAgentTime,
155
155
  removeAgent
156
156
  };
157
- //# sourceMappingURL=chunk-USCUXCAU.js.map
157
+ //# sourceMappingURL=chunk-5B5HDTUR.js.map
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ knownModels
4
+ } from "./chunk-7KBOIPUT.js";
5
+
6
+ // src/model/catalog.ts
7
+ import { mkdir, readFile, writeFile } from "fs/promises";
8
+ import { homedir } from "os";
9
+ import { dirname, join } from "path";
10
+ var CATALOG_MAX_AGE_MS = 24 * 60 * 60 * 1e3;
11
+ var FETCH_TIMEOUT_MS = 1e4;
12
+ var MAX_PAGES = 10;
13
+ function catalogPath(home = homedir()) {
14
+ return join(home, ".garuda", "cache", "anthropic-models.json");
15
+ }
16
+ async function fetchCatalog(options) {
17
+ const fetcher = options.fetcher ?? fetch;
18
+ const base = (options.baseUrl ?? "https://api.anthropic.com").replace(/\/+$/, "");
19
+ const headers = { "anthropic-version": "2023-06-01" };
20
+ if (options.key !== void 0) headers["x-api-key"] = options.key;
21
+ const models = [];
22
+ let after;
23
+ try {
24
+ for (let page = 0; page < MAX_PAGES; page++) {
25
+ const url = `${base}/v1/models?limit=1000${after === void 0 ? "" : `&after_id=${encodeURIComponent(after)}`}`;
26
+ const response = await fetcher(url, {
27
+ headers,
28
+ redirect: "error",
29
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
30
+ });
31
+ if (!response.ok) {
32
+ await response.body?.cancel();
33
+ return { problem: `the Models API answered HTTP ${response.status}` };
34
+ }
35
+ const body = await response.json();
36
+ for (const m of body.data ?? []) {
37
+ if (typeof m.id !== "string" || !/^[A-Za-z0-9._:-]+$/.test(m.id)) continue;
38
+ models.push({
39
+ id: m.id,
40
+ ...typeof m.display_name === "string" ? { displayName: m.display_name } : {},
41
+ ...typeof m.max_input_tokens === "number" && m.max_input_tokens > 0 ? { contextWindow: m.max_input_tokens } : {}
42
+ });
43
+ }
44
+ if (body.has_more !== true || typeof body.last_id !== "string") break;
45
+ after = body.last_id;
46
+ }
47
+ } catch (error) {
48
+ return { problem: `Garuda could not read the Models API (${error.message})` };
49
+ }
50
+ return { catalog: { fetchedAt: (options.now ?? /* @__PURE__ */ new Date()).toISOString(), models } };
51
+ }
52
+ async function saveCatalog(catalog, home = homedir()) {
53
+ const path = catalogPath(home);
54
+ await mkdir(dirname(path), { recursive: true, mode: 448 });
55
+ await writeFile(path, `${JSON.stringify(catalog, null, 2)}
56
+ `, { mode: 384 });
57
+ }
58
+ async function loadCatalog(home = homedir()) {
59
+ try {
60
+ const data = JSON.parse(await readFile(catalogPath(home), "utf8"));
61
+ if (typeof data.fetchedAt !== "string" || !Array.isArray(data.models)) return void 0;
62
+ return {
63
+ fetchedAt: data.fetchedAt,
64
+ models: data.models.filter((m) => typeof m?.id === "string")
65
+ };
66
+ } catch {
67
+ return void 0;
68
+ }
69
+ }
70
+ function mergedModels(catalog) {
71
+ const table = knownModels();
72
+ const exact = new Map(table.map((m) => [m.id, m.info]));
73
+ const out = [];
74
+ for (const m of catalog?.models ?? []) {
75
+ const info = exact.get(m.id) ?? prefixInfo(m.id);
76
+ if (info !== void 0) out.push({ id: m.id, info, known: true });
77
+ else out.push({ id: m.id, info: { contextWindow: m.contextWindow ?? 2e5 }, known: false });
78
+ }
79
+ for (const m of table) {
80
+ if (!out.some((o) => o.id === m.id)) out.push({ id: m.id, info: m.info, known: true });
81
+ }
82
+ return out;
83
+ }
84
+ function prefixInfo(id) {
85
+ let best;
86
+ for (const m of knownModels()) {
87
+ if (id.startsWith(`${m.id}-`) && /^\d{8}$/.test(id.slice(m.id.length + 1))) {
88
+ if (m.id.length > (best?.id.length ?? 0)) best = m;
89
+ }
90
+ }
91
+ return best?.info;
92
+ }
93
+
94
+ // src/tools/atomicWrite.ts
95
+ import { randomBytes } from "crypto";
96
+ import { chmod, link, realpath, rename, stat, unlink, writeFile as writeFile2 } from "fs/promises";
97
+ import { basename, dirname as dirname2, join as join2 } from "path";
98
+ async function writeFileAtomic(path, content, { createOnly }) {
99
+ const target = createOnly ? path : await realpath(path);
100
+ const temp = join2(
101
+ dirname2(target),
102
+ `.${basename(target)}.garuda-${randomBytes(4).toString("hex")}.tmp`
103
+ );
104
+ const mode = createOnly ? void 0 : (await stat(target)).mode & 4095;
105
+ await writeFile2(temp, content, { flag: "wx", ...mode === void 0 ? {} : { mode } });
106
+ try {
107
+ if (createOnly) {
108
+ try {
109
+ await link(temp, target);
110
+ } catch (error) {
111
+ const code = error.code;
112
+ if (code !== "EPERM" && code !== "ENOTSUP" && code !== "EXDEV") throw error;
113
+ if (await exists(target)) throw Object.assign(new Error("EEXIST"), { code: "EEXIST" });
114
+ await rename(temp, target);
115
+ return;
116
+ }
117
+ } else {
118
+ await chmod(temp, mode ?? 420);
119
+ await rename(temp, target);
120
+ return;
121
+ }
122
+ } finally {
123
+ await unlink(temp).catch(() => {
124
+ });
125
+ }
126
+ }
127
+ async function exists(path) {
128
+ try {
129
+ await stat(path);
130
+ return true;
131
+ } catch {
132
+ return false;
133
+ }
134
+ }
135
+
136
+ export {
137
+ writeFileAtomic,
138
+ fetchCatalog,
139
+ saveCatalog,
140
+ loadCatalog,
141
+ mergedModels
142
+ };
143
+ //# sourceMappingURL=chunk-65NUEOI7.js.map
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  DEFAULT_ENV_ALLOWLIST
4
- } from "./chunk-GXZACJUI.js";
4
+ } from "./chunk-B4SJY7KT.js";
5
5
 
6
6
  // src/jobs/git.ts
7
7
  var GIT_TIMEOUT_MS = 12e4;
@@ -46,4 +46,4 @@ export {
46
46
  git,
47
47
  hostCommand
48
48
  };
49
- //# sourceMappingURL=chunk-TUUEBHG2.js.map
49
+ //# sourceMappingURL=chunk-6XHON3Q6.js.map