@garuda-agent/garuda 0.16.0 → 0.17.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 (54) hide show
  1. package/CHANGELOG.md +188 -0
  2. package/README.md +56 -1074
  3. package/SECURITY.md +9 -2
  4. package/dist/{acpCommand-AJJBKAE5.js → acpCommand-7EY3NHPG.js} +18 -16
  5. package/dist/{anthropic-NQDBCHJU.js → anthropic-NQIZLD64.js} +12 -6
  6. package/dist/{chunk-WIJL72ZE.js → chunk-2DYMJMRQ.js} +1880 -982
  7. package/dist/{chunk-YQTGMCU6.js → chunk-3MRQJKUO.js} +29 -5
  8. package/dist/{chunk-WMOLXTBU.js → chunk-4LFJFDCF.js} +2 -1
  9. package/dist/{chunk-44QN5SWZ.js → chunk-55CFEEIN.js} +22 -2
  10. package/dist/{chunk-USCUXCAU.js → chunk-5B5HDTUR.js} +3 -3
  11. package/dist/{chunk-TUUEBHG2.js → chunk-6XHON3Q6.js} +2 -2
  12. package/dist/chunk-735BKABA.js +472 -0
  13. package/dist/{chunk-B6FA4I7Q.js → chunk-7TH3NHCQ.js} +2 -2
  14. package/dist/{chunk-OUCV3FCT.js → chunk-AOGFYUKG.js} +3 -3
  15. package/dist/{chunk-GXZACJUI.js → chunk-B4SJY7KT.js} +322 -39
  16. package/dist/{chunk-3EQU2GUX.js → chunk-BHZ6G65V.js} +5 -1
  17. package/dist/chunk-BSK5T2ZG.js +9 -0
  18. package/dist/chunk-J4346IDH.js +156 -0
  19. package/dist/{chunk-NP6RIC25.js → chunk-JRVWH27M.js} +7 -7
  20. package/dist/{chunk-HUIK3WXO.js → chunk-P6XWWIEK.js} +20 -3
  21. package/dist/{chunk-HE2K657U.js → chunk-TJIWD7R7.js} +47 -16
  22. package/dist/chunk-U65DQZDB.js +143 -0
  23. package/dist/chunk-UIRJOQQS.js +118 -0
  24. package/dist/{chunk-KKB5W47S.js → chunk-VTMD5IHD.js} +26 -2
  25. package/dist/{chunk-ENLDPN4S.js → chunk-Z66BWTMD.js} +81 -32
  26. package/dist/cli/index.js +99 -41
  27. package/dist/{create-VXIH2GUZ.js → create-W6BV75QX.js} +18 -12
  28. package/dist/{deadline-F5RJJ2QW.js → deadline-TP7ZGLT2.js} +4 -2
  29. package/dist/{evalCommand-OX2LQYEK.js → evalCommand-YVQAWUZE.js} +50 -29
  30. package/dist/{export-HN3CT7AV.js → export-SJ3DUP6W.js} +2 -2
  31. package/dist/{inkChat-YMMMGU7Y.js → inkChat-FNUBK43U.js} +14 -13
  32. package/dist/install-TKWN5AYH.js +19 -0
  33. package/dist/{job-3NYDMP2M.js → job-7JOI4YIQ.js} +6 -2
  34. package/dist/jobCommand-FE6XN3YD.js +33 -0
  35. package/dist/{launchd-SSX362VG.js → launchd-3A3ONEW5.js} +9 -8
  36. package/dist/{lspCommand-JRXKRSL7.js → lspCommand-ZZ6R4PA2.js} +7 -6
  37. package/dist/{manager-AWU4NHHK.js → manager-73MPYFTP.js} +7 -6
  38. package/dist/{nightCommand-65MUEV32.js → nightCommand-OODJNWDT.js} +26 -20
  39. package/dist/{openaiCompatible-CJWVJP5M.js → openaiCompatible-6TLDDW5U.js} +29 -15
  40. package/dist/{policy-EJPLEEXP.js → policy-ZJ7RMOFN.js} +10 -4
  41. package/dist/{remove-6GDSTAIO.js → remove-42WQNVPX.js} +8 -7
  42. package/dist/{server-KHHNHUYV.js → server-VEYIJHEU.js} +7 -3
  43. package/dist/{setupCommand-VA7D5MZK.js → setupCommand-GBQIVYJC.js} +45 -13
  44. package/dist/text-LS35WSRL.js +17 -0
  45. package/native/bin/garuda-reap +0 -0
  46. package/native/garuda-reap.c +113 -0
  47. package/package.json +39 -13
  48. package/dist/chunk-EIXUSVFX.js +0 -116
  49. package/dist/chunk-R7I2NBPX.js +0 -71
  50. package/dist/chunk-ULAJPMY7.js +0 -107
  51. package/dist/chunk-XOK5OZOC.js +0 -9
  52. package/dist/install-EWDSPFFZ.js +0 -18
  53. package/dist/jobCommand-ACSGSBQ4.js +0 -32
  54. package/dist/text-J2GD647J.js +0 -11
@@ -0,0 +1,156 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ EFFORTS
4
+ } from "./chunk-BHZ6G65V.js";
5
+
6
+ // src/model/pricing.ts
7
+ var DEFAULT_CONTEXT_WINDOW = 2e5;
8
+ var M = 1e6;
9
+ var ALWAYS = { mode: "always", efforts: EFFORTS };
10
+ var OPTIONAL = { mode: "optional", efforts: EFFORTS };
11
+ var OPTIONAL_46 = {
12
+ mode: "optional",
13
+ efforts: EFFORTS.filter((e) => e !== "xhigh")
14
+ };
15
+ var KNOWN = [
16
+ // October 2026. Sonnet 5.5 turns thinking "off" with `between_tools`, Haiku 5.5 only at high
17
+ // effort or below: Garuda does not offer off for them yet, so they are "always" here.
18
+ ["claude-sonnet-5-5", { price: p(2, 10, 0.1, 2.5), contextWindow: M, thinking: ALWAYS }],
19
+ [
20
+ "claude-haiku-5-5",
21
+ {
22
+ price: {
23
+ ...p(0.1, 0.5, 0.01, 0.125),
24
+ over: { tokens: 1e5, price: p(0.5, 2.5, 0.05, 0.625) }
25
+ },
26
+ contextWindow: M,
27
+ thinking: ALWAYS
28
+ }
29
+ ],
30
+ ["claude-fable-5-1", { price: p(10, 50, 0.25, 12.5), contextWindow: M, thinking: ALWAYS }],
31
+ ["claude-fable-5", { price: p(10, 50, 1, 12.5), contextWindow: M, thinking: ALWAYS }],
32
+ ["claude-opus-5-5", { price: p(4, 20, 0.2, 5), contextWindow: M, thinking: ALWAYS }],
33
+ ["claude-opus-5", { price: p(5, 25, 0.5, 6.25), contextWindow: M, thinking: ALWAYS }],
34
+ ["claude-opus-4-8", { price: p(5, 25, 0.5, 6.25), contextWindow: M, thinking: OPTIONAL }],
35
+ ["claude-opus-4-7", { price: p(5, 25, 0.5, 6.25), contextWindow: M, thinking: OPTIONAL }],
36
+ ["claude-opus-4-6", { price: p(5, 25, 0.5, 6.25), contextWindow: M, thinking: OPTIONAL_46 }],
37
+ ["claude-opus-4-5", { price: p(5, 25, 0.5, 6.25), contextWindow: 2e5 }],
38
+ ["claude-sonnet-5", { price: p(2, 10, 0.2, 2.5), contextWindow: M, thinking: ALWAYS }],
39
+ ["claude-sonnet-4-6", { price: p(3, 15, 0.3, 3.75), contextWindow: M, thinking: OPTIONAL_46 }],
40
+ ["claude-sonnet-4-5", { price: p(3, 15, 0.3, 3.75), contextWindow: 2e5 }],
41
+ ["claude-haiku-4-5", { price: p(1, 5, 0.1, 1.25), contextWindow: 2e5 }]
42
+ ];
43
+ function p(input, output, cacheRead, cacheWrite) {
44
+ return { input, output, cacheRead, cacheWrite };
45
+ }
46
+ function aliasModel(alias) {
47
+ if (!/^(haiku|sonnet|opus|fable)$/.test(alias)) return void 0;
48
+ return KNOWN.find(([prefix]) => prefix.startsWith(`claude-${alias}-`))?.[0];
49
+ }
50
+ function knownModels() {
51
+ return KNOWN.map(([id, info]) => ({ id, info }));
52
+ }
53
+ function lookupModel(modelId) {
54
+ let best;
55
+ for (const [prefix, info] of KNOWN) {
56
+ if (modelId.startsWith(prefix) && prefix.length > (best?.prefix.length ?? 0)) {
57
+ best = { prefix, info };
58
+ }
59
+ }
60
+ return best?.info ?? { contextWindow: DEFAULT_CONTEXT_WINDOW };
61
+ }
62
+ var WEB_SEARCH_USD = 0.01;
63
+ function costOf(usage, price, factor = 1) {
64
+ const prompt = usage.inputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
65
+ const at = price.over !== void 0 && prompt > price.over.tokens ? price.over.price : price;
66
+ return (usage.inputTokens * at.input + usage.outputTokens * at.output + usage.cacheReadTokens * at.cacheRead + usage.cacheWriteTokens * at.cacheWrite) / M * factor + (usage.webSearches ?? 0) * WEB_SEARCH_USD;
67
+ }
68
+ function responseCost(response, price) {
69
+ if (response.usageReported === false) return void 0;
70
+ return costOf(response.usage, price, response.priceFactor ?? 1);
71
+ }
72
+ function totalTokens(usage) {
73
+ return usage.inputTokens + usage.outputTokens + usage.cacheReadTokens + usage.cacheWriteTokens;
74
+ }
75
+ function contextSize(usage) {
76
+ return totalTokens(usage);
77
+ }
78
+
79
+ // src/session/redact.ts
80
+ var REDACTED = "[REDACTED]";
81
+ var PATTERNS = [
82
+ /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z0-9 ]*PRIVATE KEY-----/g,
83
+ /sk-ant-[A-Za-z0-9_-]{16,}/g,
84
+ /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}/g,
85
+ /\bAKIA[0-9A-Z]{16}\b/g,
86
+ /\bgh[pousr]_[A-Za-z0-9]{30,}\b/g,
87
+ /\bgithub_pat_[A-Za-z0-9_]{30,}\b/g,
88
+ /\bxox[abprs]-[A-Za-z0-9-]{10,}/g,
89
+ /\bAIza[0-9A-Za-z_-]{35}\b/g,
90
+ /\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g
91
+ ];
92
+ var ASSIGNMENT = /\b([A-Za-z0-9_]*(?:api[_-]?key|secret|token|password|passwd)[A-Za-z0-9_]*)(["']?\s*[:=]\s*["']?)([^\s"'`,;]{8,})/gi;
93
+ var OPAQUE_KEYS = /* @__PURE__ */ new Set(["encrypted_content", "encrypted_index", "signature"]);
94
+ function looksLikeCode(name, value, next) {
95
+ if (/^[\d_.]+$/.test(value)) return !/pass|secret/i.test(name);
96
+ if (value.includes("(") || next === "(") return true;
97
+ return /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+$/.test(value) && !/\d{4,}/.test(value);
98
+ }
99
+ var SECRET_ENV_NAME = /(KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL)/i;
100
+ var movedSecrets = /* @__PURE__ */ new Map();
101
+ function keepSecretForRedaction(name, value) {
102
+ movedSecrets.set(name, value);
103
+ }
104
+ var Redactor = class {
105
+ literals;
106
+ /**
107
+ * `env`: variables whose names look secret have their values removed wherever they appear.
108
+ * Default: this process's environment and the secrets moved out of it.
109
+ */
110
+ constructor(env = { ...process.env, ...Object.fromEntries(movedSecrets) }) {
111
+ this.literals = Object.entries(env).filter(
112
+ ([name, value]) => (SECRET_ENV_NAME.test(name) || value !== void 0 && movedSecrets.get(name) === value) && (value?.length ?? 0) >= 8
113
+ ).map(([, value]) => value).sort((a, b) => b.length - a.length);
114
+ }
115
+ text(input) {
116
+ let out = input;
117
+ for (const literal of this.literals) out = out.split(literal).join(REDACTED);
118
+ for (const pattern of PATTERNS) out = out.replace(pattern, REDACTED);
119
+ return out.replace(
120
+ ASSIGNMENT,
121
+ (all, name, sep, value, at) => looksLikeCode(name, value, out[at + all.length]) ? all : `${name}${sep}${REDACTED}`
122
+ );
123
+ }
124
+ /** A copy of `value` with every string redacted. */
125
+ value(value) {
126
+ return this.walk(value);
127
+ }
128
+ walk(value) {
129
+ if (typeof value === "string") return this.text(value);
130
+ if (Array.isArray(value)) return value.map((item) => this.walk(item));
131
+ if (value !== null && typeof value === "object") {
132
+ const redactedThinking = value.type === "redacted_thinking";
133
+ return Object.fromEntries(
134
+ Object.entries(value).map(([k, v]) => [
135
+ k,
136
+ (OPAQUE_KEYS.has(k) || redactedThinking && k === "data") && typeof v === "string" ? v : this.walk(v)
137
+ ])
138
+ );
139
+ }
140
+ return value;
141
+ }
142
+ };
143
+
144
+ export {
145
+ aliasModel,
146
+ knownModels,
147
+ lookupModel,
148
+ WEB_SEARCH_USD,
149
+ responseCost,
150
+ totalTokens,
151
+ contextSize,
152
+ REDACTED,
153
+ keepSecretForRedaction,
154
+ Redactor
155
+ };
156
+ //# sourceMappingURL=chunk-J4346IDH.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-U65DQZDB.js";
6
5
  import {
6
+ keepSecretForRedaction,
7
7
  lookupModel
8
- } from "./chunk-R7I2NBPX.js";
8
+ } from "./chunk-J4346IDH.js";
9
9
  import {
10
10
  isLoopbackHost
11
11
  } from "./chunk-ATW7BNIG.js";
@@ -269,7 +269,7 @@ function resolveModel(spec, config = { providers: {}, models: {} }) {
269
269
  ...def.type === "anthropic" ? {
270
270
  async createBatch(env = process.env, options = {}) {
271
271
  const apiKey = providerKey(env, def.apiKeyEnv ?? "ANTHROPIC_API_KEY");
272
- const { AnthropicBatchClient } = await import("./anthropic-NQDBCHJU.js");
272
+ const { AnthropicBatchClient } = await import("./anthropic-NQIZLD64.js");
273
273
  return new AnthropicBatchClient({ model, ...apiKey ? { apiKey } : {}, ...options });
274
274
  }
275
275
  } : {},
@@ -282,10 +282,10 @@ function resolveModel(spec, config = { providers: {}, models: {} }) {
282
282
  }
283
283
  if (def.type === "anthropic") {
284
284
  const key = providerKey(env, def.apiKeyEnv ?? "ANTHROPIC_API_KEY");
285
- const { AnthropicClient } = await import("./anthropic-NQDBCHJU.js");
285
+ const { AnthropicClient } = await import("./anthropic-NQIZLD64.js");
286
286
  return new AnthropicClient({ model, ...key ? { apiKey: key } : {} });
287
287
  }
288
- const { OpenAICompatibleClient } = await import("./openaiCompatible-CJWVJP5M.js");
288
+ const { OpenAICompatibleClient } = await import("./openaiCompatible-6TLDDW5U.js");
289
289
  return new OpenAICompatibleClient({
290
290
  provider,
291
291
  baseUrl: def.baseUrl,
@@ -328,4 +328,4 @@ export {
328
328
  keepProviderKeys,
329
329
  resolveModel
330
330
  };
331
- //# sourceMappingURL=chunk-NP6RIC25.js.map
331
+ //# sourceMappingURL=chunk-JRVWH27M.js.map
@@ -22,7 +22,9 @@ function ignoredProjectPolicy(root) {
22
22
  const file = join(root, POLICY_FILE);
23
23
  return existsSync(file) ? file : void 0;
24
24
  }
25
+ var PROFILES = ["default", "strict"];
25
26
  var policySchema = z.strictObject({
27
+ profile: z.enum(PROFILES).optional(),
26
28
  disallowedCommands: z.array(z.string()).optional(),
27
29
  requireSandbox: z.boolean().optional(),
28
30
  denyPaths: z.array(z.string()).optional(),
@@ -37,7 +39,8 @@ var policySchema = z.strictObject({
37
39
  }).optional(),
38
40
  audit: z.strictObject({
39
41
  enabled: z.boolean().optional(),
40
- level: z.enum(["all", "mutations", "denials"]).optional()
42
+ level: z.enum(["all", "mutations", "denials"]).optional(),
43
+ checkpoint: z.boolean().optional()
41
44
  }).optional()
42
45
  });
43
46
  function parsePolicy(json) {
@@ -47,6 +50,7 @@ function parsePolicy(json) {
47
50
  }
48
51
  const data = result.data;
49
52
  return {
53
+ ...data.profile === void 0 ? {} : { profile: data.profile },
50
54
  ...data.disallowedCommands === void 0 ? {} : { disallowedCommands: data.disallowedCommands },
51
55
  ...data.requireSandbox === void 0 ? {} : { requireSandbox: data.requireSandbox },
52
56
  ...data.denyPaths === void 0 ? {} : { denyPaths: data.denyPaths },
@@ -66,7 +70,8 @@ function parsePolicy(json) {
66
70
  ...data.audit === void 0 ? {} : {
67
71
  audit: {
68
72
  ...data.audit.enabled === void 0 ? {} : { enabled: data.audit.enabled },
69
- ...data.audit.level === void 0 ? {} : { level: data.audit.level }
73
+ ...data.audit.level === void 0 ? {} : { level: data.audit.level },
74
+ ...data.audit.checkpoint === void 0 ? {} : { checkpoint: data.audit.checkpoint }
70
75
  }
71
76
  }
72
77
  };
@@ -106,7 +111,9 @@ function mergePolicies(user, managed) {
106
111
  const maxSteps = lower(user.limits?.maxSteps, managed.limits?.maxSteps);
107
112
  const tokenBudget = lower(user.limits?.tokenBudget, managed.limits?.tokenBudget);
108
113
  const audit = managed.audit ?? user.audit;
114
+ const profile = user.profile === void 0 && managed.profile === void 0 ? void 0 : user.profile === "strict" || managed.profile === "strict" ? "strict" : "default";
109
115
  return {
116
+ ...profile === void 0 ? {} : { profile },
110
117
  ...disallowedCommands === void 0 ? {} : { disallowedCommands },
111
118
  ...requireSandbox === void 0 ? {} : { requireSandbox },
112
119
  ...denyPaths === void 0 ? {} : { denyPaths },
@@ -156,6 +163,13 @@ function isCommandDisallowedByPolicy(policy, command) {
156
163
  }
157
164
  return { disallowed: false };
158
165
  }
166
+ function isStrict(policy) {
167
+ return policy?.profile === "strict";
168
+ }
169
+ function withProfile(policy, projectProfile) {
170
+ if (!isStrict(policy) && projectProfile !== "strict") return policy;
171
+ return { ...policy, profile: "strict", requireSandbox: true };
172
+ }
159
173
  function isSandboxRequiredByPolicy(policy, outsideSandbox) {
160
174
  if (policy.requireSandbox === true && outsideSandbox) {
161
175
  return {
@@ -221,14 +235,17 @@ export {
221
235
  POLICY_FILE,
222
236
  managedPolicyPath,
223
237
  ignoredProjectPolicy,
238
+ PROFILES,
224
239
  parsePolicy,
225
240
  mergePolicies,
226
241
  loadTeamPolicy,
227
242
  isCommandDisallowedByPolicy,
243
+ isStrict,
244
+ withProfile,
228
245
  isSandboxRequiredByPolicy,
229
246
  isPathDeniedByPolicy,
230
247
  isModelAllowedByPolicy,
231
248
  assertModelAllowedByPolicy,
232
249
  isHostBlockedByPolicy
233
250
  };
234
- //# sourceMappingURL=chunk-HUIK3WXO.js.map
251
+ //# sourceMappingURL=chunk-P6XWWIEK.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-735BKABA.js";
9
10
  import {
10
11
  totalTokens
11
- } from "./chunk-R7I2NBPX.js";
12
+ } from "./chunk-J4346IDH.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-TJIWD7R7.js.map
@@ -0,0 +1,143 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ knownModels
4
+ } from "./chunk-J4346IDH.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-U65DQZDB.js.map