cohorte 2.10.1 → 3.0.0-dev.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 (211) hide show
  1. package/README.md +7 -0
  2. package/assets/manifest.json +193 -0
  3. package/assets/migrations/config/README.md +12 -0
  4. package/assets/migrations/state/0001_init.sql +154 -0
  5. package/assets/prompts/README.md +11 -0
  6. package/assets/prompts/agents/fixer.md +7 -0
  7. package/assets/prompts/agents/implementer.md +12 -0
  8. package/assets/prompts/agents/reviewer.md +8 -0
  9. package/assets/prompts/agents/security-reviewer.md +7 -0
  10. package/assets/prompts/phases/build.md +5 -0
  11. package/assets/prompts/phases/review.md +5 -0
  12. package/assets/prompts/phases/test.md +5 -0
  13. package/assets/prompts/system.md +11 -0
  14. package/assets/schemas/agent-output.schema.json +225 -0
  15. package/assets/schemas/auth-status.schema.json +87 -0
  16. package/assets/schemas/command-result.schema.json +192 -0
  17. package/assets/schemas/commands.schema.json +952 -0
  18. package/assets/schemas/config.schema.json +2259 -0
  19. package/assets/schemas/doctor-report.schema.json +104 -0
  20. package/assets/schemas/events.schema.json +8514 -0
  21. package/assets/schemas/fake-script.schema.json +549 -0
  22. package/assets/schemas/inspect.schema.json +3133 -0
  23. package/assets/schemas/manifest.schema.json +79 -0
  24. package/assets/schemas/ownership.schema.json +52 -0
  25. package/assets/schemas/policy-verdict.schema.json +282 -0
  26. package/assets/schemas/project-model.schema.json +783 -0
  27. package/assets/schemas/project-status.schema.json +217 -0
  28. package/assets/schemas/reconcile-plan.schema.json +155 -0
  29. package/assets/schemas/run-diff.schema.json +177 -0
  30. package/assets/schemas/run-snapshot-manifest.schema.json +2104 -0
  31. package/assets/schemas/run-state.schema.json +1755 -0
  32. package/assets/schemas/runtime-capabilities.schema.json +1545 -0
  33. package/assets/schemas/sandbox-capabilities.schema.json +116 -0
  34. package/assets/schemas/skill.schema.json +87 -0
  35. package/assets/schemas/spec.schema.json +202 -0
  36. package/assets/schemas/tool-catalogue.schema.json +655 -0
  37. package/assets/schemas/trust-record.schema.json +38 -0
  38. package/assets/skills/README.md +9 -0
  39. package/assets/skills/inspect-surface.md +4 -0
  40. package/dist/agent-host.mjs +50 -0
  41. package/dist/agent-host.mjs.map +1 -0
  42. package/dist/bundle-manifest.json +102 -0
  43. package/dist/chunks/__host-DgGPyco9.mjs +18 -0
  44. package/dist/chunks/__host-DgGPyco9.mjs.map +1 -0
  45. package/dist/chunks/approve-Cv135UqI.mjs +19 -0
  46. package/dist/chunks/approve-Cv135UqI.mjs.map +1 -0
  47. package/dist/chunks/auth-BT5oVp76.mjs +132 -0
  48. package/dist/chunks/auth-BT5oVp76.mjs.map +1 -0
  49. package/dist/chunks/auth-D8rCVY33.mjs +84 -0
  50. package/dist/chunks/auth-D8rCVY33.mjs.map +1 -0
  51. package/dist/chunks/brainstorm-CU10T1tm.mjs +7 -0
  52. package/dist/chunks/brainstorm-CU10T1tm.mjs.map +1 -0
  53. package/dist/chunks/cancel-KV_cpsqS.mjs +15 -0
  54. package/dist/chunks/cancel-KV_cpsqS.mjs.map +1 -0
  55. package/dist/chunks/canonical-C8UxeEmB.mjs +57 -0
  56. package/dist/chunks/canonical-C8UxeEmB.mjs.map +1 -0
  57. package/dist/chunks/catalogue-BvwSrM_j.mjs +1549 -0
  58. package/dist/chunks/catalogue-BvwSrM_j.mjs.map +1 -0
  59. package/dist/chunks/compose-Cr0hP0qm.mjs +7303 -0
  60. package/dist/chunks/compose-Cr0hP0qm.mjs.map +1 -0
  61. package/dist/chunks/config-DefHA2Lw.mjs +96 -0
  62. package/dist/chunks/config-DefHA2Lw.mjs.map +1 -0
  63. package/dist/chunks/contract-B0ra2-bm.mjs +295 -0
  64. package/dist/chunks/contract-B0ra2-bm.mjs.map +1 -0
  65. package/dist/chunks/contract-vYbwYr0Y.mjs +123 -0
  66. package/dist/chunks/contract-vYbwYr0Y.mjs.map +1 -0
  67. package/dist/chunks/deny-ClTxSvHJ.mjs +15 -0
  68. package/dist/chunks/deny-ClTxSvHJ.mjs.map +1 -0
  69. package/dist/chunks/diff-Qw0II0sj.mjs +101 -0
  70. package/dist/chunks/diff-Qw0II0sj.mjs.map +1 -0
  71. package/dist/chunks/discover-30Ciya_0.mjs +20 -0
  72. package/dist/chunks/discover-30Ciya_0.mjs.map +1 -0
  73. package/dist/chunks/doctor-DfCrKpfT.mjs +222 -0
  74. package/dist/chunks/doctor-DfCrKpfT.mjs.map +1 -0
  75. package/dist/chunks/fix-D4Tj8RVA.mjs +18 -0
  76. package/dist/chunks/fix-D4Tj8RVA.mjs.map +1 -0
  77. package/dist/chunks/gc-BBXlBF5w.mjs +161 -0
  78. package/dist/chunks/gc-BBXlBF5w.mjs.map +1 -0
  79. package/dist/chunks/init-Okz4mlhW.mjs +30 -0
  80. package/dist/chunks/init-Okz4mlhW.mjs.map +1 -0
  81. package/dist/chunks/inspect-Buby4cQD.mjs +22 -0
  82. package/dist/chunks/inspect-Buby4cQD.mjs.map +1 -0
  83. package/dist/chunks/logs-Giq5sUau.mjs +25 -0
  84. package/dist/chunks/logs-Giq5sUau.mjs.map +1 -0
  85. package/dist/chunks/migrate-DRoFUg53.mjs +19 -0
  86. package/dist/chunks/migrate-DRoFUg53.mjs.map +1 -0
  87. package/dist/chunks/models-Btp6X2YZ.mjs +24 -0
  88. package/dist/chunks/models-Btp6X2YZ.mjs.map +1 -0
  89. package/dist/chunks/pause-BASBfKLO.mjs +16 -0
  90. package/dist/chunks/pause-BASBfKLO.mjs.map +1 -0
  91. package/dist/chunks/policy-DUIgzF3W.mjs +139 -0
  92. package/dist/chunks/policy-DUIgzF3W.mjs.map +1 -0
  93. package/dist/chunks/providers-Dx78imKj.mjs +19 -0
  94. package/dist/chunks/providers-Dx78imKj.mjs.map +1 -0
  95. package/dist/chunks/reconcile-BfD785gd.mjs +26 -0
  96. package/dist/chunks/reconcile-BfD785gd.mjs.map +1 -0
  97. package/dist/chunks/render-DyZr5Lgi.mjs +53 -0
  98. package/dist/chunks/render-DyZr5Lgi.mjs.map +1 -0
  99. package/dist/chunks/resume-GJQRb2Eb.mjs +15 -0
  100. package/dist/chunks/resume-GJQRb2Eb.mjs.map +1 -0
  101. package/dist/chunks/retry-CLrUem1H.mjs +15 -0
  102. package/dist/chunks/retry-CLrUem1H.mjs.map +1 -0
  103. package/dist/chunks/review-BDdjb6kB.mjs +17 -0
  104. package/dist/chunks/review-BDdjb6kB.mjs.map +1 -0
  105. package/dist/chunks/run-CgTWYlcd.mjs +82 -0
  106. package/dist/chunks/run-CgTWYlcd.mjs.map +1 -0
  107. package/dist/chunks/run-tool-C3rAv4XF.mjs +26 -0
  108. package/dist/chunks/run-tool-C3rAv4XF.mjs.map +1 -0
  109. package/dist/chunks/schema-C9QbUW4H.mjs +578 -0
  110. package/dist/chunks/schema-C9QbUW4H.mjs.map +1 -0
  111. package/dist/chunks/send-BdnJ85Nz.mjs +19 -0
  112. package/dist/chunks/send-BdnJ85Nz.mjs.map +1 -0
  113. package/dist/chunks/ship-DTuLGoaJ.mjs +19 -0
  114. package/dist/chunks/ship-DTuLGoaJ.mjs.map +1 -0
  115. package/dist/chunks/shutdown-BM92Iyv2.mjs +15 -0
  116. package/dist/chunks/shutdown-BM92Iyv2.mjs.map +1 -0
  117. package/dist/chunks/skip-DZG5kpcH.mjs +18 -0
  118. package/dist/chunks/skip-DZG5kpcH.mjs.map +1 -0
  119. package/dist/chunks/spec-Bq0P3jVh.mjs +20 -0
  120. package/dist/chunks/spec-Bq0P3jVh.mjs.map +1 -0
  121. package/dist/chunks/src-CId6kMsY.mjs +1689 -0
  122. package/dist/chunks/src-CId6kMsY.mjs.map +1 -0
  123. package/dist/chunks/src-CfAOde7C.mjs +5403 -0
  124. package/dist/chunks/src-CfAOde7C.mjs.map +1 -0
  125. package/dist/chunks/src-Cu3TxNob.mjs +783 -0
  126. package/dist/chunks/src-Cu3TxNob.mjs.map +1 -0
  127. package/dist/chunks/src-DcU4IXMe.mjs +117 -0
  128. package/dist/chunks/src-DcU4IXMe.mjs.map +1 -0
  129. package/dist/chunks/src-JxawBQBn.mjs +446 -0
  130. package/dist/chunks/src-JxawBQBn.mjs.map +1 -0
  131. package/dist/chunks/src-s7YRvk0t.mjs +2116 -0
  132. package/dist/chunks/src-s7YRvk0t.mjs.map +1 -0
  133. package/dist/chunks/status-SMjiOMYy.mjs +29 -0
  134. package/dist/chunks/status-SMjiOMYy.mjs.map +1 -0
  135. package/dist/chunks/tail-BuhT-2PH.mjs +25 -0
  136. package/dist/chunks/tail-BuhT-2PH.mjs.map +1 -0
  137. package/dist/chunks/update-BXIppsbi.mjs +16 -0
  138. package/dist/chunks/update-BXIppsbi.mjs.map +1 -0
  139. package/dist/cli.mjs +175 -0
  140. package/dist/cli.mjs.map +1 -0
  141. package/package.json +17 -32
  142. package/CHANGELOG.md +0 -1461
  143. package/bin/cli.js +0 -817
  144. package/bin/report.js +0 -141
  145. package/core/adapter/render.js +0 -415
  146. package/core/agents/implementer.template.md +0 -142
  147. package/core/agents/profile-reader.md +0 -50
  148. package/core/agents/release.md +0 -58
  149. package/core/agents/review.md +0 -179
  150. package/core/commands/cohorte-align-ds.md +0 -33
  151. package/core/commands/cohorte-audit.md +0 -58
  152. package/core/commands/cohorte-brainstorm.md +0 -70
  153. package/core/commands/cohorte-build.md +0 -219
  154. package/core/commands/cohorte-doctor.md +0 -194
  155. package/core/commands/cohorte-fix.md +0 -80
  156. package/core/commands/cohorte-fleet.md +0 -103
  157. package/core/commands/cohorte-init-pipeline.md +0 -31
  158. package/core/commands/cohorte-intake.md +0 -92
  159. package/core/commands/cohorte-patch.md +0 -118
  160. package/core/commands/cohorte-refactor.md +0 -59
  161. package/core/commands/cohorte-retro.md +0 -85
  162. package/core/commands/cohorte-review.md +0 -270
  163. package/core/commands/cohorte-ship.md +0 -122
  164. package/core/commands/cohorte-spec.md +0 -101
  165. package/core/commands/cohorte-update-pipeline.md +0 -165
  166. package/core/hooks/gate.py +0 -588
  167. package/core/runtimes/claude.json +0 -74
  168. package/core/runtimes/codex.json +0 -85
  169. package/core/runtimes/cursor.json +0 -76
  170. package/core/runtimes/gemini.json +0 -76
  171. package/core/runtimes/opencode.json +0 -73
  172. package/core/templates/agent-handoff.md +0 -34
  173. package/core/templates/brainstorm-return.md +0 -36
  174. package/core/templates/decisions.template.md +0 -42
  175. package/core/templates/design-brief.md +0 -44
  176. package/core/templates/patch.template.md +0 -86
  177. package/core/templates/pr-body.md +0 -25
  178. package/core/templates/review-feedback.md +0 -38
  179. package/core/templates/spec.template.md +0 -87
  180. package/core/templates/steps/init-pipeline/01-detect-stack.md +0 -44
  181. package/core/templates/steps/init-pipeline/02-interview-gaps.md +0 -55
  182. package/core/templates/steps/init-pipeline/03-draft-profile.md +0 -10
  183. package/core/templates/steps/init-pipeline/04-write-render.md +0 -145
  184. package/core/templates/steps/init-pipeline/05-report.md +0 -12
  185. package/core/workflows/audit.js +0 -240
  186. package/core/workflows/loop.js +0 -641
  187. package/core/workflows/refactor.js +0 -276
  188. package/core/workflows/review.js +0 -456
  189. package/install.ps1 +0 -103
  190. package/install.sh +0 -114
  191. package/lib/doctor.js +0 -534
  192. package/lib/runtime.js +0 -139
  193. package/lib/versions.js +0 -149
  194. package/lib/yaml.js +0 -129
  195. package/profile/PIPELINE.template.md +0 -259
  196. package/profile/SCHEMA.md +0 -735
  197. package/profile/cohorte.config.template.yaml +0 -43
  198. package/scripts/assert-gate-hook.mjs +0 -75
  199. package/scripts/kanban-move.sh +0 -313
  200. package/scripts/metrics/collect.mjs +0 -531
  201. package/scripts/metrics/prices.json +0 -42
  202. package/scripts/new-feature.sh.template +0 -98
  203. package/scripts/preflight.sh +0 -124
  204. package/scripts/remove-feature.sh.template +0 -56
  205. package/scripts/test-adapter.mjs +0 -437
  206. package/scripts/test-gate.mjs +0 -443
  207. package/scripts/test-kanban.mjs +0 -304
  208. package/scripts/test-lib.mjs +0 -356
  209. package/scripts/test-metrics.mjs +0 -156
  210. package/scripts/test-workflows.mjs +0 -758
  211. package/scripts/validate-core.mjs +0 -325
@@ -0,0 +1,1549 @@
1
+ import { n as sha256Hex, t as canonicalJson } from "./canonical-C8UxeEmB.mjs";
2
+ import { M as NotImplemented, o as err, s as ok } from "./src-Cu3TxNob.mjs";
3
+ import { c as AgentOutput, g as ClosedEnum, o as toStrictSchema } from "./src-s7YRvk0t.mjs";
4
+ import { l as DEFAULT_NETWORK_POLICY } from "./schema-C9QbUW4H.mjs";
5
+ import { c as toolGrantProblems, f as AGENT_GIT_DENIED_SUBCOMMANDS, h as isTrampoline, m as PROTECTED_REPO_GLOBS, s as ToolGrant } from "./src-CId6kMsY.mjs";
6
+ import { createHash } from "node:crypto";
7
+ import { Type } from "typebox";
8
+ import { Compile } from "typebox/compile";
9
+ import { readFile, readdir, rename, writeFile } from "node:fs/promises";
10
+ import { dirname, join, relative, resolve } from "node:path";
11
+ import { accessSync, constants, lstatSync, readdirSync, realpathSync } from "node:fs";
12
+ import picomatch from "picomatch";
13
+ //#region ../../packages/security/src/decide/commands/profiles.ts
14
+ /** True for a token that starts a run of leading options, e.g. `-C`, `--dir=x`, `-e"code"`. */
15
+ const isFlagToken = (token) => token.startsWith("-") && token !== "-";
16
+ /** True when `token` invokes `flag`, whether joined by `=` (long options) or directly attached (short options). */
17
+ function matchesFlag(token, flag) {
18
+ if (token === flag) return true;
19
+ if (flag.startsWith("--")) return token.startsWith(`${flag}=`);
20
+ return flag.length === 2 && token.length > flag.length && token.startsWith(flag);
21
+ }
22
+ /**
23
+ * One shared structural parser for every built-in profile: walk leading flag tokens (denying a re-targeting one on
24
+ * sight), the first non-flag token is the subcommand, everything after it splits into `flags` / `positionals` by
25
+ * whether it starts with `-`. This is a STRUCTURAL split, not a full CLI grammar: it does not know which flags of
26
+ * `git commit -m <msg>` consume a value, because the callers that need that (CommandRule matching) work in terms of
27
+ * flag NAMES and positional VALUES declared by policy data, never by re-deriving one CLI's full option grammar here.
28
+ */
29
+ function createProfile(spec) {
30
+ return {
31
+ program: spec.program,
32
+ aliases: spec.aliases,
33
+ parse(args) {
34
+ const retargeted = (token) => {
35
+ const denied = spec.retargeting.find((flag) => matchesFlag(token, flag));
36
+ if (denied === void 0) return void 0;
37
+ return {
38
+ kind: "denied",
39
+ code: "security/command-global-option",
40
+ token,
41
+ reason: `${spec.program}: \`${denied}\` re-targets the command onto a different directory or repository`
42
+ };
43
+ };
44
+ const globals = [];
45
+ let index = 0;
46
+ while (index < args.length) {
47
+ const token = args[index];
48
+ if (token === void 0 || !isFlagToken(token)) break;
49
+ const denial = retargeted(token);
50
+ if (denial !== void 0) return denial;
51
+ globals.push(token);
52
+ index += 1;
53
+ }
54
+ const subcommandToken = args[index];
55
+ const subcommand = subcommandToken === void 0 ? [] : [subcommandToken];
56
+ const rest = subcommandToken === void 0 ? [] : args.slice(index + 1);
57
+ const flags = [];
58
+ const positionals = [];
59
+ for (const token of rest) (isFlagToken(token) ? flags : positionals).push(token);
60
+ for (const token of flags) {
61
+ const denial = retargeted(token);
62
+ if (denial !== void 0) return denial;
63
+ }
64
+ return {
65
+ kind: "parsed",
66
+ globals,
67
+ subcommand,
68
+ flags,
69
+ positionals
70
+ };
71
+ }
72
+ };
73
+ }
74
+ const GIT_RETARGETING = [
75
+ "-C",
76
+ "-c",
77
+ "--config-env",
78
+ "--git-dir",
79
+ "--work-tree",
80
+ "--exec-path",
81
+ "--namespace",
82
+ "--super-prefix",
83
+ "--attr-source"
84
+ ];
85
+ const PNPM_RETARGETING = [
86
+ "-C",
87
+ "--dir",
88
+ "--prefix"
89
+ ];
90
+ const NPM_RETARGETING = ["--prefix"];
91
+ const NODE_RETARGETING = [
92
+ "-e",
93
+ "-p",
94
+ "-r",
95
+ "--require",
96
+ "--import",
97
+ "--loader",
98
+ "--experimental-loader",
99
+ "--eval",
100
+ "--env-file",
101
+ "--env-file-if-exists"
102
+ ];
103
+ /** `yarn --cwd <dir>` runs the whole command in another directory: `git -C` under another name. */
104
+ const YARN_RETARGETING = ["--cwd"];
105
+ /** `docker -H`/`--host`/`--context` re-target the DAEMON (a remote one included); `--config` the credential store. */
106
+ const DOCKER_RETARGETING = [
107
+ "-H",
108
+ "--host",
109
+ "--context",
110
+ "--config"
111
+ ];
112
+ const GIT_PROFILE = createProfile({
113
+ program: "git",
114
+ aliases: [],
115
+ retargeting: GIT_RETARGETING
116
+ });
117
+ const PNPM_PROFILE = createProfile({
118
+ program: "pnpm",
119
+ aliases: [],
120
+ retargeting: PNPM_RETARGETING
121
+ });
122
+ const NPM_PROFILE = createProfile({
123
+ program: "npm",
124
+ aliases: [],
125
+ retargeting: NPM_RETARGETING
126
+ });
127
+ const YARN_PROFILE = createProfile({
128
+ program: "yarn",
129
+ aliases: [],
130
+ retargeting: YARN_RETARGETING
131
+ });
132
+ const NODE_PROFILE = createProfile({
133
+ program: "node",
134
+ aliases: [],
135
+ retargeting: NODE_RETARGETING
136
+ });
137
+ /** `docker-compose up` normalises to `docker compose up`: EV-10. */
138
+ const DOCKER_PROFILE = createProfile({
139
+ program: "docker",
140
+ aliases: ["docker-compose"],
141
+ retargeting: DOCKER_RETARGETING
142
+ });
143
+ const BUILTIN_PROFILES = Object.freeze([
144
+ GIT_PROFILE,
145
+ PNPM_PROFILE,
146
+ NPM_PROFILE,
147
+ YARN_PROFILE,
148
+ NODE_PROFILE,
149
+ DOCKER_PROFILE
150
+ ]);
151
+ /**
152
+ * Subcommand tokens an alias implies, prepended to argv[1..] before the matched profile parses it: `docker-compose`
153
+ * behaves as `docker compose`. No other built-in alias carries an implied subcommand.
154
+ */
155
+ const ALIAS_SUBCOMMAND_PREFIX = Object.freeze({ "docker-compose": Object.freeze(["compose"]) });
156
+ /** `bareName` is argv[0] as typed (already checked to be a bare name with no path separator). */
157
+ function findProfile(bareName, profiles) {
158
+ for (const profile of profiles) {
159
+ if (profile.program === bareName) return {
160
+ profile,
161
+ prefix: []
162
+ };
163
+ if (profile.aliases.includes(bareName)) return {
164
+ profile,
165
+ prefix: ALIAS_SUBCOMMAND_PREFIX[bareName] ?? []
166
+ };
167
+ }
168
+ }
169
+ //#endregion
170
+ //#region ../../packages/security/src/decide/commands/resolver.ts
171
+ const NODE_FS = {
172
+ accessSync,
173
+ realpathSync
174
+ };
175
+ /**
176
+ * `bareName` SHOULD already be a bare name (no path separator): that is checked one layer up, as the very first
177
+ * thing `evaluate()` does (DESIGN 2.6.4 step 1). This resolver enforces it too, defensively: `node:path.join`
178
+ * normalises a segment like `./node` or `../x` away, so without this guard a relative-looking name would silently
179
+ * resolve through directory-list normalisation rather than being rejected outright, for any OTHER caller of this
180
+ * port that does not repeat the step-1 check.
181
+ */
182
+ function createProgramResolver(options) {
183
+ const dirs = [...options.pathDirs];
184
+ const fs = options.fs ?? NODE_FS;
185
+ const cache = /* @__PURE__ */ new Map();
186
+ return { resolve(bareName) {
187
+ if (bareName === "" || bareName.includes("/") || bareName.includes("\\")) return void 0;
188
+ const cached = cache.get(bareName);
189
+ if (cached !== void 0 || cache.has(bareName)) return cached;
190
+ let found;
191
+ for (const dir of dirs) {
192
+ const candidate = join(dir, bareName);
193
+ try {
194
+ fs.accessSync(candidate, constants.X_OK);
195
+ found = fs.realpathSync(candidate);
196
+ break;
197
+ } catch {}
198
+ }
199
+ cache.set(bareName, found);
200
+ return found;
201
+ } };
202
+ }
203
+ //#endregion
204
+ //#region ../../packages/security/src/decide/commands/index.ts
205
+ const tokensEqual = (a, b) => a.length === b.length && a.every((value, index) => value === b[index]);
206
+ /** `path` inside `root`, by path SEGMENTS (never `startsWith` alone: `/w/api` must not contain `/w/api-gateway`). */
207
+ const isContainedIn = (path, root) => path === root || path.startsWith(`${root}/`);
208
+ /**
209
+ * "Anything not allowed is denied" (DESIGN 2.6.4) is a rule about what may be PERMITTED, so the allowlist only ever
210
+ * narrows an `allow` rule — where narrowing fails closed. Applied to a `deny` or an `ask` rule it narrows in the
211
+ * other direction: the restrictive rule stops matching as soon as a flag it did not declare is present, while a
212
+ * broader `allow` rule that DOES list that flag keeps matching, and `deny > ask > allow` (step 5) is defeated by one
213
+ * token (`pnpm --silent run build`). A restrictive rule therefore matches on program + subcommand + positionals
214
+ * alone. Recorded in docs/v3/requests/U1.03.md R4 item 3.
215
+ */
216
+ function flagsMatch(flags, rule) {
217
+ if (flags.length === 0 || rule.decision !== "allow") return true;
218
+ const spec = rule.flags;
219
+ if (spec === void 0) return false;
220
+ return flags.every((flag) => !spec.deny?.includes(flag) && spec.allow.includes(flag));
221
+ }
222
+ /** Structural, not filesystem-aware: real containment/symlink checks of path-typed arguments are stage 3's job. */
223
+ const looksLikeWorktreeRelativePath = (value) => !value.startsWith("-") && !value.startsWith("/") && !value.startsWith("~") && !value.split(/[/\\]/).includes("..");
224
+ function positionalsMatch(positionals, spec) {
225
+ if (spec === void 0) return positionals.length === 0;
226
+ switch (spec.kind) {
227
+ case "none": return positionals.length === 0;
228
+ case "exact": return tokensEqual(positionals, spec.values);
229
+ case "enum": return positionals.length > 0 && positionals.length <= spec.max && positionals.every((value) => spec.values.includes(value));
230
+ case "paths-in-worktree": return positionals.length > 0 && positionals.length <= spec.max && positionals.every(looksLikeWorktreeRelativePath);
231
+ }
232
+ }
233
+ /** A rule with no `subcommand`, no `flags` and `positionals.kind: 'exact'` matches the WHOLE rest of argv, whether
234
+ * or not the program has a profile: this is how `dangerousCommands` and `checksToCommandRules` rules match. */
235
+ function matchesExactArgv(rule, rest) {
236
+ if (rule.subcommand !== void 0 || rule.flags !== void 0) return false;
237
+ const positionals = rule.positionals;
238
+ return positionals !== void 0 && positionals.kind === "exact" && tokensEqual(rest, positionals.values);
239
+ }
240
+ /**
241
+ * Only reachable for a profiled program: `(subcommand, flags, positionals)`, per DESIGN 2.6.4 step 4.
242
+ *
243
+ * The profile's structural split is deliberately shallow — the FIRST non-flag token is `subcommand`, every later
244
+ * non-flag token is a positional — so a rule's `subcommand` is matched as a PREFIX of the whole non-flag token
245
+ * stream. That is what makes DESIGN 2.6.4's own example (`subcommand: ['run','test']`) and `['compose','up']`
246
+ * reachable, and it keeps `docker-compose up` ≡ `docker compose up`. The tokens the prefix consumes are removed
247
+ * before the positional check; an absent or empty `subcommand` is "no subcommand constraint", not "never matches".
248
+ *
249
+ * Deviation (recorded): a rule's `flags` allowlist is matched against `globals` TOO, not only against the flags
250
+ * that follow the subcommand. DESIGN 2.6.4 says of `flags` "anything not allowed is denied"; leaving the leading
251
+ * globals unchecked would let `pnpm --silent run test` satisfy a rule written for `pnpm run test`, which fails
252
+ * OPEN. Benign globals are still parsed as globals (EV-08/09: `git --no-pager push` still has subcommand `push`);
253
+ * a policy that wants to permit one lists it in `flags.allow`.
254
+ */
255
+ function matchesStructurally(rule, parsed) {
256
+ const tokens = [...parsed.subcommand, ...parsed.positionals];
257
+ const required = rule.subcommand ?? [];
258
+ if (required.length > tokens.length) return false;
259
+ if (!required.every((token, index) => token === tokens[index])) return false;
260
+ return flagsMatch([...parsed.globals, ...parsed.flags], rule) && positionalsMatch(tokens.slice(required.length), rule.positionals);
261
+ }
262
+ /**
263
+ * The D9 subcommand of a `git` call, if it is one of the denied ones (ADR-0007 item 2).
264
+ *
265
+ * With a structural parse in hand the subcommand is exact, and the profile has already denied every value-taking
266
+ * re-targeting global. Without one — a caller passed `profiles` without git — nothing can tell a subcommand from a
267
+ * global option's VALUE (`git --super-prefix x commit`), so every token is held against the set: conservative in
268
+ * the only safe direction for an invariant that must not be switchable off.
269
+ */
270
+ function deniedGitSubcommand(parsed, rest) {
271
+ const denied = AGENT_GIT_DENIED_SUBCOMMANDS;
272
+ if (parsed === void 0) return rest.find((token) => denied.includes(token));
273
+ const subcommand = parsed.subcommand[0];
274
+ return subcommand !== void 0 && denied.includes(subcommand) ? subcommand : void 0;
275
+ }
276
+ function branchAllows(rule, branch) {
277
+ return (rule.when?.branch ?? "any") === "any" || branch.kind === "branch" && !branch.protected;
278
+ }
279
+ function whenMatches(rule, context, branch) {
280
+ if (!branchAllows(rule, branch)) return false;
281
+ if (rule.when?.roles && !rule.when.roles.includes(context.role)) return false;
282
+ if (rule.when?.phases && !rule.when.phases.includes(context.phase)) return false;
283
+ return true;
284
+ }
285
+ const DECISION_RANK = {
286
+ deny: 0,
287
+ ask: 1,
288
+ allow: 2
289
+ };
290
+ /** deny beats ask beats allow (DESIGN 2.6.4 step 5, 2.6.2 precedence). `matches` must be non-empty. */
291
+ function pickWinner(matches) {
292
+ return matches.reduce((best, rule) => DECISION_RANK[rule.decision] < DECISION_RANK[best.decision] ? rule : best);
293
+ }
294
+ /** Every rule of `program`, structurally or exactly matching, whose `when` is satisfied. Records every rule id it
295
+ * looked at in `evaluatedRules`, matched or not (`PolicyVerdict.evaluatedRules`, "for `cohorte policy explain`"). */
296
+ function matchRules(rules, program, rest, parsed, context, branch, evaluatedRules) {
297
+ const matches = [];
298
+ for (const rule of rules) {
299
+ if (rule.program !== program) continue;
300
+ evaluatedRules.push(rule.id);
301
+ const structurallyOk = parsed !== void 0 && matchesStructurally(rule, parsed);
302
+ const exactlyOk = matchesExactArgv(rule, rest);
303
+ if ((structurallyOk || exactlyOk) && whenMatches(rule, context, branch)) matches.push(rule);
304
+ }
305
+ return matches;
306
+ }
307
+ function denyEval(args) {
308
+ const { ruleId, code, reason, overridable, securityViolation, evaluatedRules } = args;
309
+ return {
310
+ decision: "deny",
311
+ ruleId,
312
+ reason,
313
+ overridable,
314
+ securityViolation,
315
+ evaluatedRules,
316
+ ...code === void 0 ? {} : { code }
317
+ };
318
+ }
319
+ function applyRule(rule, args) {
320
+ const { resolvedFile, rest, context, timeoutMs, evaluatedRules } = args;
321
+ const overridable = rule.origin !== "builtin";
322
+ if (rule.decision === "deny") return denyEval({
323
+ ruleId: rule.id,
324
+ code: "permission/command-not-allowed",
325
+ reason: `denied by rule ${rule.id}`,
326
+ overridable,
327
+ securityViolation: false,
328
+ evaluatedRules
329
+ });
330
+ if (rule.network && context.sandboxLevel === "L0-process") return denyEval({
331
+ ruleId: rule.id,
332
+ code: "permission/network-denied",
333
+ reason: `rule ${rule.id} needs network access, which the L0 process sandbox cannot enforce a deny for`,
334
+ overridable: false,
335
+ securityViolation: false,
336
+ evaluatedRules
337
+ });
338
+ const command = {
339
+ file: resolvedFile,
340
+ args: [...rest],
341
+ cwd: context.cwd,
342
+ ruleId: rule.id,
343
+ replay: rule.replay,
344
+ network: rule.network,
345
+ timeoutMs: timeoutMs ?? context.defaultTimeoutMs
346
+ };
347
+ return {
348
+ decision: rule.decision === "ask" ? "ask" : "allow",
349
+ ruleId: rule.id,
350
+ reason: rule.decision === "ask" ? `rule ${rule.id} requires confirmation` : `allowed by rule ${rule.id}`,
351
+ overridable: true,
352
+ securityViolation: false,
353
+ evaluatedRules,
354
+ command
355
+ };
356
+ }
357
+ function createCommandPolicy(options) {
358
+ const profiles = options.profiles ?? BUILTIN_PROFILES;
359
+ return { evaluate(request, policy, context) {
360
+ const evaluatedRules = [];
361
+ const argv = request.argv;
362
+ const bareName = argv[0];
363
+ if (bareName === void 0 || bareName === "") return denyEval({
364
+ ruleId: "builtin/argv-empty",
365
+ code: "permission/command-not-allowed",
366
+ reason: "argv is empty: there is no program to run",
367
+ overridable: false,
368
+ securityViolation: true,
369
+ evaluatedRules
370
+ });
371
+ if (bareName.includes("/") || bareName.includes("\\")) return denyEval({
372
+ ruleId: "builtin/bare-name",
373
+ code: "permission/command-not-allowed",
374
+ reason: `argv[0] "${bareName}" is not a bare program name (DESIGN 2.6.4 step 1)`,
375
+ overridable: false,
376
+ securityViolation: true,
377
+ evaluatedRules
378
+ });
379
+ evaluatedRules.push("builtin/resolve");
380
+ const resolvedFile = options.programs.resolve(bareName);
381
+ if (resolvedFile === void 0) return denyEval({
382
+ ruleId: "builtin/resolve",
383
+ code: "permission/command-not-allowed",
384
+ reason: `"${bareName}" does not resolve to a program on the PATH pinned at run start`,
385
+ overridable: true,
386
+ securityViolation: false,
387
+ evaluatedRules
388
+ });
389
+ evaluatedRules.push("builtin/cwd-containment");
390
+ if (!isContainedIn(context.cwd, context.worktree)) return denyEval({
391
+ ruleId: "builtin/cwd-containment",
392
+ code: "permission/command-not-allowed",
393
+ reason: `cwd "${context.cwd}" is outside the agent's own worktree "${context.worktree}"`,
394
+ overridable: false,
395
+ securityViolation: true,
396
+ evaluatedRules
397
+ });
398
+ const branch = options.branches.branchOf(context.cwd);
399
+ const match = findProfile(bareName, profiles);
400
+ const canonicalName = match?.profile.program ?? bareName;
401
+ const rest = argv.slice(1);
402
+ evaluatedRules.push("builtin/trampolines");
403
+ if (isTrampoline(bareName) || isTrampoline(canonicalName)) {
404
+ const matches = matchRules(policy.rules, canonicalName, rest, void 0, context, branch, evaluatedRules);
405
+ const winner = matches.length > 0 ? pickWinner(matches) : void 0;
406
+ if (winner !== void 0) {
407
+ const evaluation = applyRule(winner.decision === "deny" ? winner : {
408
+ ...winner,
409
+ decision: "ask"
410
+ }, {
411
+ resolvedFile,
412
+ rest,
413
+ context,
414
+ timeoutMs: request.timeoutMs,
415
+ evaluatedRules
416
+ });
417
+ if (evaluation.decision !== "ask") return evaluation;
418
+ return {
419
+ ...evaluation,
420
+ reason: `"${bareName}" launches another program on behalf of the agent: rule ${winner.id} matches, but every use of a trampoline is \`ask\` (ADR-0024)`
421
+ };
422
+ }
423
+ return denyEval({
424
+ ruleId: "builtin/trampolines",
425
+ code: "security/command-trampoline",
426
+ reason: `"${bareName}" would launch another program on behalf of the agent`,
427
+ overridable: false,
428
+ securityViolation: true,
429
+ evaluatedRules
430
+ });
431
+ }
432
+ let parsed;
433
+ if (match !== void 0) {
434
+ evaluatedRules.push("builtin/profile-parse");
435
+ const outcome = match.profile.parse([...match.prefix, ...rest]);
436
+ if (outcome.kind === "denied") return denyEval({
437
+ ruleId: "builtin/global-option",
438
+ code: outcome.code,
439
+ reason: outcome.reason,
440
+ overridable: false,
441
+ securityViolation: true,
442
+ evaluatedRules
443
+ });
444
+ parsed = outcome;
445
+ }
446
+ if (canonicalName === "git") {
447
+ const subcommand = deniedGitSubcommand(parsed, rest);
448
+ if (subcommand !== void 0) {
449
+ evaluatedRules.push("builtin/agent-git-deny");
450
+ return denyEval({
451
+ ruleId: "builtin/agent-git-deny",
452
+ code: "permission/command-not-allowed",
453
+ reason: `agents may not run \`git ${subcommand}\`: Cohorte creates every commit (ADR-0007)`,
454
+ overridable: false,
455
+ securityViolation: true,
456
+ evaluatedRules
457
+ });
458
+ }
459
+ }
460
+ const matches = matchRules(policy.rules, canonicalName, rest, parsed, context, branch, evaluatedRules);
461
+ if (matches.length === 0) return denyEval({
462
+ ruleId: "builtin/no-rule",
463
+ code: "permission/command-not-allowed",
464
+ reason: `no command rule matches "${[bareName, ...rest].join(" ")}"; the default is deny`,
465
+ overridable: true,
466
+ securityViolation: false,
467
+ evaluatedRules
468
+ });
469
+ return applyRule(pickWinner(matches), {
470
+ resolvedFile,
471
+ rest,
472
+ context,
473
+ timeoutMs: request.timeoutMs,
474
+ evaluatedRules
475
+ });
476
+ } };
477
+ }
478
+ const closed = { additionalProperties: false };
479
+ Type.Object({
480
+ decision: Type.Union([
481
+ Type.Literal("allow"),
482
+ Type.Literal("ask"),
483
+ Type.Literal("deny")
484
+ ]),
485
+ ruleId: Type.String({ minLength: 1 }),
486
+ code: Type.Optional(Type.String({ minLength: 1 })),
487
+ reason: Type.String(),
488
+ overridable: Type.Boolean(),
489
+ securityViolation: Type.Boolean(),
490
+ evaluatedRules: Type.Array(Type.String()),
491
+ command: Type.Optional(Type.Object({
492
+ file: Type.String({ minLength: 1 }),
493
+ args: Type.Array(Type.String()),
494
+ cwd: Type.String({ minLength: 1 }),
495
+ ruleId: Type.String({ minLength: 1 }),
496
+ replay: Type.Union([Type.Literal("idempotent"), Type.Literal("at-most-once")]),
497
+ network: Type.Boolean(),
498
+ timeoutMs: Type.Integer({ minimum: 0 })
499
+ }, closed))
500
+ }, closed);
501
+ //#endregion
502
+ //#region ../../packages/security/src/decide/gate/index.ts
503
+ const deny = (stage, ruleId, reason, code = "permission/tool-not-granted", securityViolation = false) => ({
504
+ decision: "deny",
505
+ stage,
506
+ ruleId,
507
+ reason: `${code}: ${reason}`,
508
+ modelFacingReason: `${reason}. Do not retry.`,
509
+ overridable: !securityViolation,
510
+ securityViolation,
511
+ asks: [],
512
+ evaluatedRules: [ruleId],
513
+ normalized: null
514
+ });
515
+ const invalidPolicy = (message) => {
516
+ throw new Error(`configuration/policy-invalid: ${message}`);
517
+ };
518
+ const cloneFreeze = (value) => {
519
+ if (value && typeof value === "object") {
520
+ Object.freeze(value);
521
+ for (const child of Object.values(value)) cloneFreeze(child);
522
+ }
523
+ return value;
524
+ };
525
+ function createPolicyEngine(options) {
526
+ if (!options?.tools || !options.globs || !options.commands) invalidPolicy("policy engine ports are incomplete");
527
+ return { evaluate(call, grant, policy, ports) {
528
+ try {
529
+ const schema = options.tools.schemaOf(call.tool);
530
+ if (schema === void 0) return deny("schema", "builtin/unknown-tool", `unknown tool "${call.tool}"`, "validation/tool-input", true);
531
+ if (!Compile(schema).Check(call.input)) return deny("schema", "builtin/tool-schema", `input for "${call.tool}" does not match its strict schema`, "validation/tool-input");
532
+ const evaluatedRules = ["builtin/tool-schema"];
533
+ if (!grant.tools.includes(call.tool)) return {
534
+ ...deny("capability", "grant/tool-not-granted", `tool "${call.tool}" is not granted to this agent`),
535
+ evaluatedRules
536
+ };
537
+ const normalizedPaths = [];
538
+ const base = grant.roots.workspace ?? grant.roots.readOnly[0];
539
+ if (base === void 0) return deny("path", "grant/no-root", "the agent has no workspace or read-only root", "permission/path-outside-grant");
540
+ for (const arg of options.tools.pathArgsOf(call.tool, call.input)) {
541
+ const resolved = ports.paths.resolve(arg.value, base, arg.intent);
542
+ if (!resolved.ok) return deny("path", `path/${resolved.error.code}`, `path argument "${arg.arg}" was refused: ${resolved.error.detail}`, resolved.error.security ? `security/${resolved.error.code}` : "permission/path-outside-grant", resolved.error.security);
543
+ if (options.globs.isDenied(resolved.value.relative, grant, arg.intent === "read" || arg.intent === "list" ? "read" : "write")) return deny("path", "grant/deny-glob", `path argument "${arg.arg}" is denied by the grant`);
544
+ normalizedPaths.push({
545
+ arg: arg.arg,
546
+ resolved: resolved.value,
547
+ intent: arg.intent
548
+ });
549
+ }
550
+ let command;
551
+ if (call.tool === "network_request") return deny("network", "builtin/network-denied", "network requests are disabled in V3.0", "permission/network-denied");
552
+ if (call.tool === "run_command") {
553
+ const input = call.input;
554
+ const argv = Array.isArray(input.argv) && input.argv.every((v) => typeof v === "string") ? input.argv : [];
555
+ const cwd = typeof input.cwd === "string" ? input.cwd : "";
556
+ const cwdResult = ports.paths.resolve(cwd, base, "exec-cwd");
557
+ if (!cwdResult.ok) return deny("command", "command/cwd", "command cwd is outside the grant", "permission/command-not-allowed", true);
558
+ const evaluation = options.commands.evaluate({
559
+ argv,
560
+ cwd
561
+ }, grant.commands, {
562
+ worktree: grant.roots.workspace ?? cwdResult.value.root,
563
+ cwd: cwdResult.value.canonical,
564
+ role: grant.role,
565
+ phase: call.phase,
566
+ sandboxLevel: policy.sandboxLevel,
567
+ defaultTimeoutMs: grant.limits.perTool.run_command?.timeoutMs ?? 6e4
568
+ });
569
+ evaluatedRules.push(...evaluation.evaluatedRules);
570
+ if (evaluation.decision === "deny" || evaluation.command === void 0) return deny("command", evaluation.ruleId, evaluation.reason, evaluation.code, evaluation.securityViolation);
571
+ command = evaluation.command;
572
+ }
573
+ const limit = grant.limits.perTool[call.tool];
574
+ const levels = [
575
+ ["run", call.runId],
576
+ ["phase", call.phase],
577
+ ["agent", call.agentId],
578
+ ["tool", call.tool]
579
+ ];
580
+ for (const [level, id] of levels) {
581
+ const remaining = ports.budgets.remaining(level, id);
582
+ if ((remaining.toolCalls ?? 1) <= 0 || (remaining.wallClockMs ?? 1) <= 0) return deny("budget", `budget/${level}`, `${level} budget is exhausted`, "budget/exhausted");
583
+ }
584
+ if (ports.budgets.callsInLastMinute(call.agentId, call.tool) >= grant.limits.maxCallsPerMinute) return deny("budget", "budget/rate-limit", "tool rate limit is exhausted", "budget/rate-limit");
585
+ const timeoutMs = Math.max(1, Math.min(limit?.timeoutMs ?? 6e4, command?.timeoutMs ?? Number.MAX_SAFE_INTEGER, ports.budgets.remaining("agent", call.agentId).wallClockMs ?? Number.MAX_SAFE_INTEGER));
586
+ return {
587
+ decision: "allow",
588
+ stage: "budget",
589
+ ruleId: command?.ruleId ?? "grant/tool",
590
+ reason: "allowed by the policy",
591
+ modelFacingReason: "",
592
+ overridable: true,
593
+ securityViolation: false,
594
+ asks: [],
595
+ evaluatedRules,
596
+ normalized: {
597
+ tool: call.tool,
598
+ paths: normalizedPaths,
599
+ ...command ? { command: {
600
+ ...command,
601
+ timeoutMs
602
+ } } : {},
603
+ input: call.input,
604
+ grantKeyMaterial: {
605
+ tool: call.tool,
606
+ input: call.input
607
+ }
608
+ }
609
+ };
610
+ } catch (error) {
611
+ return deny("schema", "security/gate-internal-error", error instanceof Error ? error.message : "gate failed closed", "security/gate-internal-error", true);
612
+ }
613
+ } };
614
+ }
615
+ /** Missing, unparseable or empty policy => `configuration/policy-invalid`: no tool ever executes. */
616
+ function buildPolicySnapshot(config, ownership, options) {
617
+ if (!config || config.policy === void 0 || !ownership || options?.sandboxLevel === void 0) invalidPolicy("missing policy or ownership");
618
+ const commands = [
619
+ ...config.policy.commands.deny,
620
+ ...config.policy.commands.ask,
621
+ ...config.policy.commands.allow,
622
+ ...config.policy.dangerousCommands
623
+ ];
624
+ if (commands.some((rule) => rule.origin !== "project-config" && rule.origin !== "project-checks")) invalidPolicy("policy command origin is invalid");
625
+ const snapshot = {
626
+ commands: Object.freeze({
627
+ default: "deny",
628
+ rules: commands.map((rule) => ({ ...rule }))
629
+ }),
630
+ symlinks: { ...config.policy.symlinks },
631
+ network: { ...DEFAULT_NETWORK_POLICY },
632
+ protectedBranches: [...config.project.protectedBranches],
633
+ ownership: cloneFreeze(structuredClone(ownership)),
634
+ sandboxLevel: options.sandboxLevel
635
+ };
636
+ return Object.freeze({
637
+ ...snapshot,
638
+ digest: sha256Hex(canonicalJson(snapshot))
639
+ });
640
+ }
641
+ /** `cohorte policy explain -- <argv…>`: stages 1-5, offline, nothing executes. */
642
+ function explainPolicy(engine, call, grant, policy, ports) {
643
+ if (!engine || !call || !grant || !policy || !ports) invalidPolicy("policy explain requires a complete call, grant, snapshot and ports");
644
+ return engine.evaluate(call, grant, policy, ports);
645
+ }
646
+ //#endregion
647
+ //#region ../../packages/security/src/decide/paths/glob.ts
648
+ /** Locked once, here: deny sets and allow sets alike are matched with the SAME options. */
649
+ const PICOMATCH_OPTIONS = {
650
+ dot: true,
651
+ nocase: false
652
+ };
653
+ const NEVER = () => false;
654
+ function normalizePattern(pattern) {
655
+ return pattern.includes("/") ? pattern : `**/${pattern}`;
656
+ }
657
+ const PATTERN_CACHE_LIMIT = 512;
658
+ const patternCache = /* @__PURE__ */ new Map();
659
+ const globSetCache = /* @__PURE__ */ new WeakMap();
660
+ function cacheKey(patterns) {
661
+ return patterns.join("\0");
662
+ }
663
+ function memoize(key, compile) {
664
+ const cached = patternCache.get(key);
665
+ if (cached !== void 0) return cached;
666
+ const compiled = compile();
667
+ if (patternCache.size >= PATTERN_CACHE_LIMIT) patternCache.clear();
668
+ patternCache.set(key, compiled);
669
+ return compiled;
670
+ }
671
+ /**
672
+ * Compiled once per pattern list and memoized, so every consumer of this module shares the exact same semantics
673
+ * at the same cost. An empty pattern list matches nothing (picomatch(`[]`) would otherwise match everything).
674
+ */
675
+ function compilePatterns(patterns) {
676
+ if (patterns.length === 0) return NEVER;
677
+ return memoize(`p${cacheKey(patterns)}`, () => picomatch(patterns.map(normalizePattern), PICOMATCH_OPTIONS));
678
+ }
679
+ function compileGlobSet(set) {
680
+ const cached = globSetCache.get(set);
681
+ if (cached !== void 0) return cached;
682
+ const compiled = memoize(`s${cacheKey(set.include)}${cacheKey(set.exclude)}`, () => {
683
+ const included = compilePatterns(set.include);
684
+ if (set.exclude.length === 0) return included;
685
+ const excluded = compilePatterns(set.exclude);
686
+ return (relativePosixPath) => included(relativePosixPath) && !excluded(relativePosixPath);
687
+ });
688
+ globSetCache.set(set, compiled);
689
+ return compiled;
690
+ }
691
+ /**
692
+ * Neither dialect can express a RE-INCLUDE, so `toExcludeArgs` renders the exclusions alone:
693
+ *
694
+ * - `rg --glob` reads its list in order, later entries overriding earlier matches — but a POSITIVE glob turns
695
+ * ripgrep's override set into a whitelist (the `ignore` crate ignores every file that matches no positive
696
+ * glob), which would silently empty the listing. Unprovable on this machine anyway (PLAN F-1: no `rg`).
697
+ * - git pathspec is NOT order-dependent: `:(exclude,…)` magic is subtractive and applied AFTER the union of
698
+ * every positive pathspec. A rendered `:(glob)<re-include>` therefore cannot bring back what the exclusion
699
+ * removed, and when the caller passes no positive pathspec of its own it narrows the enumeration to the
700
+ * re-include, which the exclusion then empties: `git ls-files`/`git grep` return NOTHING, exit 0 (measured).
701
+ *
702
+ * The consequence is deliberate and fail-closed (I2): for a deny set with a non-empty `exclude` the external
703
+ * enumerator drops MORE than the matcher denies — never less. `matches()` / `isDenied` stay the authority, and
704
+ * a caller that needs the re-included paths in its output must enumerate without the args and post-filter.
705
+ */
706
+ function toExcludePatterns(set) {
707
+ return set.include.map(normalizePattern);
708
+ }
709
+ function createGlobMatcher() {
710
+ function deniedBySet(relativePosixPath, grant, intent) {
711
+ return compileGlobSet(intent === "read" ? grant.denyRead : grant.denyWrite)(relativePosixPath);
712
+ }
713
+ return {
714
+ matches(relativePosixPath, set) {
715
+ return compileGlobSet(set)(relativePosixPath);
716
+ },
717
+ /**
718
+ * "Not permitted for this intent" — the whole of step 7 in one call, which is why the contract takes an
719
+ * `AgentGrant` and not a `GlobSet`: the deny set wins over the allow set, and a path that no allow pattern
720
+ * matches is denied too (fail closed, I2). That second half is what keeps the I11 ownership boundary in tool
721
+ * OUTPUT filtering (`search`, `list_files`, `git_diff`), where the gate's own allow stage never runs.
722
+ *
723
+ * The empty relative path is the WORKSPACE ROOT itself — what `resolve('.', root, 'list')` returns — and no
724
+ * picomatch pattern matches the empty string, `**` and `**​/*` included. The root is not "outside the
725
+ * surface": it IS the surface, so an allow set that has any pattern at all covers it, and only a deny
726
+ * pattern (or a grant that permits nothing for this intent) can forbid it.
727
+ */
728
+ isDenied(relativePosixPath, grant, intent) {
729
+ if (deniedBySet(relativePosixPath, grant, intent)) return true;
730
+ const allowSet = intent === "read" ? grant.read : grant.write;
731
+ if (relativePosixPath === "") return allowSet.include.length === 0;
732
+ return !compileGlobSet(allowSet)(relativePosixPath);
733
+ },
734
+ toExcludeArgs(set, dialect) {
735
+ const patterns = toExcludePatterns(set);
736
+ return dialect === "rg-glob" ? patterns.map((pattern) => `!${pattern}`) : patterns.map((pattern) => `:(exclude,glob)${pattern}`);
737
+ }
738
+ };
739
+ }
740
+ //#endregion
741
+ //#region ../../packages/security/src/decide/paths/resolve.ts
742
+ const MAX_INPUT_LENGTH = 4096;
743
+ const CONTROL_OR_NUL = /[\x00-\x1f\x7f]/;
744
+ const ENV_SYNTAX = /\$\{[^}]*\}|\$[A-Za-z_][A-Za-z0-9_]*|%[A-Za-z_][A-Za-z0-9_]*%/;
745
+ const HOME_SHORTHAND_SEGMENT = /^~([A-Za-z0-9_][A-Za-z0-9_-]*)?$/;
746
+ const WINDOWS_DRIVE = /^[A-Za-z]:[\\/]/;
747
+ const UNC_PREFIX = /^\\\\/;
748
+ const WRITE_LIKE = /* @__PURE__ */ new Set(["write", "create"]);
749
+ function describeError(error) {
750
+ return error instanceof Error ? error.message : String(error);
751
+ }
752
+ function violation(code, security, detail) {
753
+ return err({
754
+ code,
755
+ security,
756
+ detail
757
+ });
758
+ }
759
+ function segmentsOf(absolutePosixPath) {
760
+ return absolutePosixPath.split("/").filter((part) => part.length > 0);
761
+ }
762
+ /** Containment by path SEGMENTS, never `startsWith`: `apps/api` must not contain `apps/api-gateway`. */
763
+ function segmentsContain(longer, shorter) {
764
+ if (longer.length < shorter.length) return false;
765
+ for (let index = 0; index < shorter.length; index += 1) if (longer[index] !== shorter[index]) return false;
766
+ return true;
767
+ }
768
+ function findRoot(absolutePosixPath, roots) {
769
+ const target = segmentsOf(absolutePosixPath);
770
+ return roots.find((root) => segmentsContain(target, segmentsOf(root)));
771
+ }
772
+ function isWithinAny(absolutePosixPath, candidates) {
773
+ const target = segmentsOf(absolutePosixPath);
774
+ return candidates.some((candidate) => segmentsContain(target, segmentsOf(candidate)));
775
+ }
776
+ /**
777
+ * POSIX-relative form of `absolutePosixPath` to `root`, which always CONTAINS it: the caller picks `root` with
778
+ * `findRoot` on the CANONICAL path. It can therefore never climb with `..` — and it must not, because the deny
779
+ * globs and the protected-repository patterns are matched against this string, and a `..` segment matches no
780
+ * double-star pattern (a relative form with `..` silently disabled both layers).
781
+ */
782
+ function relativeTo(root, absolutePosixPath) {
783
+ return segmentsOf(absolutePosixPath).slice(segmentsOf(root).length).join("/");
784
+ }
785
+ function isErrnoException(error) {
786
+ return error instanceof Error && "code" in error;
787
+ }
788
+ /** `null` for "does not exist" (ENOENT — or ENOTDIR, an ancestor turned out not to be a directory). */
789
+ function lstatOrNull(absolutePath) {
790
+ try {
791
+ return lstatSync(absolutePath);
792
+ } catch (error) {
793
+ if (isErrnoException(error) && (error.code === "ENOENT" || error.code === "ENOTDIR")) return null;
794
+ throw error;
795
+ }
796
+ }
797
+ function isSpecialFile(stat) {
798
+ return stat.isFIFO() || stat.isSocket() || stat.isBlockDevice() || stat.isCharacterDevice();
799
+ }
800
+ /**
801
+ * The one point a blind create could alias an existing entry that `realpath` cannot fix for us, because the
802
+ * requested name does not exist yet: a case-insensitive volume already holds a differently-cased sibling.
803
+ */
804
+ function caseInsensitiveSiblingExists(dir, requestedName) {
805
+ let entries;
806
+ try {
807
+ entries = readdirSync(dir);
808
+ } catch {
809
+ return false;
810
+ }
811
+ const lower = requestedName.toLowerCase();
812
+ return entries.some((entry) => entry !== requestedName && entry.toLowerCase() === lower);
813
+ }
814
+ function createPathResolver(options) {
815
+ const roots = options.roots.map((root) => root);
816
+ const protectedRoots = options.protectedRoots.map((root) => root);
817
+ const protectedRepoPath = compilePatterns(PROTECTED_REPO_GLOBS);
818
+ const { mode: symlinkMode, hardlinksOnWrite } = options.symlinks;
819
+ /** DESIGN 2.6.3 steps 3-4: one component at a time, from `root` to the leaf. */
820
+ function walk(root, target, intent) {
821
+ const remaining = segmentsOf(target).slice(segmentsOf(root).length);
822
+ let current = root;
823
+ let viaSymlink = false;
824
+ for (let index = 0; index < remaining.length; index += 1) {
825
+ const name = remaining[index];
826
+ if (name === void 0) break;
827
+ const candidate = `${current}/${name}`;
828
+ const isFinal = index === remaining.length - 1;
829
+ const stat = lstatOrNull(candidate);
830
+ if (stat === null) {
831
+ const canonicalDir = realpathSync.native(current);
832
+ if (WRITE_LIKE.has(intent) && caseInsensitiveSiblingExists(canonicalDir, name)) return violation("case-collision", true, `${candidate} collides, case-insensitively, with an existing entry of ${canonicalDir}`);
833
+ return ok({
834
+ canonicalDir,
835
+ tailSegments: remaining.slice(index),
836
+ exists: false,
837
+ viaSymlink
838
+ });
839
+ }
840
+ if (stat.isSymbolicLink()) {
841
+ if (isFinal && WRITE_LIKE.has(intent)) return violation("symlink-final-write", true, `${candidate} is a symlink; a write never follows the final path component`);
842
+ if (symlinkMode === "deny-all") return violation("symlink-denied", true, `${candidate} is a symlink; policy is deny-all`);
843
+ let linkTarget;
844
+ try {
845
+ linkTarget = realpathSync.native(candidate);
846
+ } catch (error) {
847
+ return violation("symlink-escape", true, `${candidate} could not be resolved: ${describeError(error)}`);
848
+ }
849
+ if (symlinkMode === "deny-outgoing" && findRoot(linkTarget, roots) === void 0) return violation("symlink-escape", true, `${candidate} -> ${linkTarget} leaves every root`);
850
+ current = linkTarget;
851
+ viaSymlink = true;
852
+ continue;
853
+ }
854
+ if (isSpecialFile(stat) && !isFinal) return violation("special-file", true, `${candidate} is a FIFO, socket or device: cannot descend into it`);
855
+ current = candidate;
856
+ }
857
+ return ok({
858
+ canonicalDir: realpathSync.native(current),
859
+ tailSegments: [],
860
+ exists: true,
861
+ viaSymlink
862
+ });
863
+ }
864
+ function resolveOnce(input, base, intent) {
865
+ if (CONTROL_OR_NUL.test(input)) return violation("nul-byte", true, "control character or NUL byte in path");
866
+ if (Buffer.byteLength(input, "utf8") > MAX_INPUT_LENGTH) return violation("too-long", true, `path exceeds ${MAX_INPUT_LENGTH} bytes`);
867
+ if (input.split("/").some((segment) => HOME_SHORTHAND_SEGMENT.test(segment))) return violation("outside-roots", true, "home-directory shorthand (~) is never expanded");
868
+ if (ENV_SYNTAX.test(input)) return violation("outside-roots", true, "environment-variable syntax is never expanded");
869
+ if (WINDOWS_DRIVE.test(input) || UNC_PREFIX.test(input)) return violation("outside-roots", true, "Windows drive/UNC path forms are not supported");
870
+ const target = resolve(base, input.normalize("NFC"));
871
+ const lexicalRoot = findRoot(target, roots);
872
+ if (lexicalRoot === void 0) return violation("outside-roots", true, `${target} is outside every root`);
873
+ const walked = walk(lexicalRoot, target, intent);
874
+ if (!walked.ok) return walked;
875
+ const { canonicalDir, tailSegments, exists, viaSymlink } = walked.value;
876
+ const canonical = tailSegments.length === 0 ? canonicalDir : `${canonicalDir}/${tailSegments.join("/")}`;
877
+ const root = findRoot(canonical, roots);
878
+ if (root === void 0) return violation("outside-roots", true, `${canonical} resolves outside every root`);
879
+ if (isWithinAny(canonical, protectedRoots)) return violation("protected-root", true, `${canonical} is a protected root`);
880
+ const relative = relativeTo(root, canonical);
881
+ if (protectedRepoPath(relative)) return violation("protected-root", true, `${relative} is a protected repository path`);
882
+ let identity;
883
+ if (exists) {
884
+ const stat = lstatSync(canonical);
885
+ if (isSpecialFile(stat)) return violation("special-file", true, `${canonical} is a FIFO, socket or device`);
886
+ if (WRITE_LIKE.has(intent) && stat.isFile() && stat.nlink > 1 && hardlinksOnWrite === "deny") return violation("hardlink-multiply-linked", true, `${canonical} has ${stat.nlink} hard links`);
887
+ identity = {
888
+ dev: stat.dev,
889
+ ino: stat.ino,
890
+ nlink: stat.nlink
891
+ };
892
+ }
893
+ return ok({
894
+ canonical,
895
+ relative,
896
+ root,
897
+ exists,
898
+ viaSymlink,
899
+ ...identity ? { identity } : {}
900
+ });
901
+ }
902
+ return { resolve(input, base, intent) {
903
+ try {
904
+ return resolveOnce(input, base, intent);
905
+ } catch (error) {
906
+ return violation("outside-roots", true, `unexpected error resolving ${JSON.stringify(input)}: ${describeError(error)}`);
907
+ }
908
+ } };
909
+ }
910
+ //#endregion
911
+ //#region ../../packages/tools/src/catalogue/schemas.ts
912
+ const ReadFileInput = Type.Object({
913
+ path: Type.String(),
914
+ offset: Type.Optional(Type.Integer({ minimum: 0 })),
915
+ limit: Type.Optional(Type.Integer({ minimum: 1 }))
916
+ });
917
+ const ListFilesInput = Type.Object({
918
+ path: Type.Optional(Type.String()),
919
+ glob: Type.Optional(Type.String()),
920
+ maxEntries: Type.Optional(Type.Integer({ minimum: 1 }))
921
+ });
922
+ const SearchInput = Type.Object({
923
+ pattern: Type.String(),
924
+ path: Type.Optional(Type.String()),
925
+ glob: Type.Optional(Type.String()),
926
+ caseInsensitive: Type.Optional(Type.Boolean()),
927
+ maxMatches: Type.Optional(Type.Integer({ minimum: 1 }))
928
+ });
929
+ const WriteFileInput = Type.Object({
930
+ path: Type.String(),
931
+ content: Type.String()
932
+ });
933
+ const PatchFileInput = Type.Object({
934
+ path: Type.String(),
935
+ edits: Type.Array(Type.Object({
936
+ oldText: Type.String(),
937
+ newText: Type.String()
938
+ }), { minItems: 1 })
939
+ });
940
+ const RunCommandInput = Type.Object({
941
+ argv: Type.Array(Type.String(), { minItems: 1 }),
942
+ cwd: Type.Optional(Type.String()),
943
+ timeoutMs: Type.Optional(Type.Integer({ minimum: 1 }))
944
+ });
945
+ const GitDiffInput = Type.Object({
946
+ base: Type.Optional(ClosedEnum([
947
+ "run-base",
948
+ "integration",
949
+ "checkpoint"
950
+ ])),
951
+ paths: Type.Optional(Type.Array(Type.String())),
952
+ stat: Type.Optional(Type.Boolean())
953
+ });
954
+ const ApprovalRequestInput = Type.Object({
955
+ question: Type.String(),
956
+ options: Type.Optional(Type.Array(Type.String()))
957
+ });
958
+ const GitCommitInput = Type.Object({ message: Type.String() });
959
+ const NetworkRequestInput = Type.Object({
960
+ url: Type.String(),
961
+ method: Type.Optional(Type.String())
962
+ });
963
+ const SecretReadInput = Type.Object({ id: Type.String() });
964
+ const noPaths = () => [];
965
+ const stringField = (input, key) => {
966
+ if (typeof input !== "object" || input === null || Array.isArray(input)) return void 0;
967
+ const value = input[key];
968
+ return typeof value === "string" ? value : void 0;
969
+ };
970
+ const arrayField = (input, key) => {
971
+ if (typeof input !== "object" || input === null || Array.isArray(input)) return void 0;
972
+ const value = input[key];
973
+ return Array.isArray(value) ? value : void 0;
974
+ };
975
+ /** DESIGN 2.7, in table order. `submit_result.inputSchema` is `AgentOutput` itself — closed by the same
976
+ * `toStrictSchema` every other row goes through in `./index.ts`, so the two are deep-equal by construction. */
977
+ const CATALOGUE_ROWS = [
978
+ {
979
+ name: "read_file",
980
+ description: "Read a file as sealed text with line numbers. offset/limit page through it; binary files error; capped at 256 KiB per call.",
981
+ inputSchema: ReadFileInput,
982
+ effect: "read",
983
+ terminal: false,
984
+ pathArgsOf: (input) => {
985
+ const path = stringField(input, "path");
986
+ return path === void 0 ? [] : [{
987
+ arg: "path",
988
+ value: path,
989
+ intent: "read"
990
+ }];
991
+ }
992
+ },
993
+ {
994
+ name: "list_files",
995
+ description: "List files under a path (default the workspace root), filtered by glob. Deny sets are filtered out; outgoing symlinks are never followed.",
996
+ inputSchema: ListFilesInput,
997
+ effect: "read",
998
+ terminal: false,
999
+ pathArgsOf: (input) => {
1000
+ const path = stringField(input, "path");
1001
+ return path === void 0 ? [] : [{
1002
+ arg: "path",
1003
+ value: path,
1004
+ intent: "list"
1005
+ }];
1006
+ }
1007
+ },
1008
+ {
1009
+ name: "search",
1010
+ description: "Search file contents for a pattern (a literal argv element, never a shell string). Every hit is filtered against the deny sets and the grant before it is returned.",
1011
+ inputSchema: SearchInput,
1012
+ effect: "read",
1013
+ terminal: false,
1014
+ pathArgsOf: (input) => {
1015
+ const path = stringField(input, "path");
1016
+ return path === void 0 ? [] : [{
1017
+ arg: "path",
1018
+ value: path,
1019
+ intent: "list"
1020
+ }];
1021
+ }
1022
+ },
1023
+ {
1024
+ name: "write_file",
1025
+ description: "Write the complete content of a file, creating it if it does not exist. The whole file is replaced.",
1026
+ inputSchema: WriteFileInput,
1027
+ effect: "write",
1028
+ terminal: false,
1029
+ pathArgsOf: (input) => {
1030
+ const path = stringField(input, "path");
1031
+ return path === void 0 ? [] : [{
1032
+ arg: "path",
1033
+ value: path,
1034
+ intent: "write"
1035
+ }];
1036
+ }
1037
+ },
1038
+ {
1039
+ name: "patch_file",
1040
+ description: "Apply exact-match text edits to a file: each oldText must occur exactly once in the current content, or the call fails without writing anything.",
1041
+ inputSchema: PatchFileInput,
1042
+ effect: "write",
1043
+ terminal: false,
1044
+ pathArgsOf: (input) => {
1045
+ const path = stringField(input, "path");
1046
+ return path === void 0 ? [] : [{
1047
+ arg: "path",
1048
+ value: path,
1049
+ intent: "write"
1050
+ }];
1051
+ }
1052
+ },
1053
+ {
1054
+ name: "run_command",
1055
+ description: "Run one allow-listed command (argv only, never a shell line) and return its output.",
1056
+ inputSchema: RunCommandInput,
1057
+ effect: "execute",
1058
+ terminal: false,
1059
+ pathArgsOf: (input) => {
1060
+ const cwd = stringField(input, "cwd");
1061
+ return cwd === void 0 ? [] : [{
1062
+ arg: "cwd",
1063
+ value: cwd,
1064
+ intent: "exec-cwd"
1065
+ }];
1066
+ }
1067
+ },
1068
+ {
1069
+ name: "git_diff",
1070
+ description: "Show a diff of the worktree against 'run-base' (the run's pinned base, the default on a read-only review ref), 'integration' (the default in a slot) or 'checkpoint'.",
1071
+ inputSchema: GitDiffInput,
1072
+ effect: "read",
1073
+ terminal: false,
1074
+ pathArgsOf: (input) => {
1075
+ const paths = arrayField(input, "paths");
1076
+ if (!paths) return [];
1077
+ const out = [];
1078
+ paths.forEach((value, index) => {
1079
+ if (typeof value === "string") out.push({
1080
+ arg: `paths[${index}]`,
1081
+ value,
1082
+ intent: "read"
1083
+ });
1084
+ });
1085
+ return out;
1086
+ }
1087
+ },
1088
+ {
1089
+ name: "approval_request",
1090
+ description: "Ask a human a question before proceeding, optionally offering a fixed set of answers. Waits for a decision.",
1091
+ inputSchema: ApprovalRequestInput,
1092
+ effect: "control",
1093
+ terminal: false,
1094
+ pathArgsOf: noPaths
1095
+ },
1096
+ {
1097
+ name: "submit_result",
1098
+ description: "Submit the final structured result for this task. Call submit_result alone, last: no other tool call may follow it in the same batch.",
1099
+ inputSchema: AgentOutput,
1100
+ effect: "control",
1101
+ terminal: true,
1102
+ pathArgsOf: (input) => {
1103
+ const artifacts = arrayField(input, "artifacts");
1104
+ if (!artifacts) return [];
1105
+ const out = [];
1106
+ artifacts.forEach((artifact, index) => {
1107
+ const path = stringField(artifact, "path");
1108
+ if (path !== void 0) out.push({
1109
+ arg: `artifacts[${index}].path`,
1110
+ value: path,
1111
+ intent: "read"
1112
+ });
1113
+ });
1114
+ return out;
1115
+ }
1116
+ },
1117
+ {
1118
+ name: "git_commit",
1119
+ description: "Reserved: granted to nobody in V3.0. Cohorte alone commits agent work.",
1120
+ inputSchema: GitCommitInput,
1121
+ effect: "write",
1122
+ terminal: false,
1123
+ pathArgsOf: noPaths
1124
+ },
1125
+ {
1126
+ name: "network_request",
1127
+ description: "Reserved: granted to nobody in V3.0. Agents have no network access.",
1128
+ inputSchema: NetworkRequestInput,
1129
+ effect: "network",
1130
+ terminal: false,
1131
+ pathArgsOf: noPaths
1132
+ },
1133
+ {
1134
+ name: "secret_read",
1135
+ description: "Reserved: granted to nobody in V3.0.",
1136
+ inputSchema: SecretReadInput,
1137
+ effect: "read",
1138
+ terminal: false,
1139
+ pathArgsOf: noPaths
1140
+ }
1141
+ ];
1142
+ CATALOGUE_ROWS.map((row) => row.name);
1143
+ //#endregion
1144
+ //#region ../../packages/tools/src/impl/exec/index.ts
1145
+ const metadata$1 = CATALOGUE_ROWS.find((row) => row.name === "run_command");
1146
+ if (!metadata$1) throw new Error("missing catalogue row: run_command");
1147
+ const fail$2 = (code, message) => {
1148
+ throw new Error(`${code}: ${message}`);
1149
+ };
1150
+ const runCommand = {
1151
+ ...metadata$1,
1152
+ plan(input, n) {
1153
+ return {
1154
+ kind: "tool.run_command",
1155
+ replayClass: n.command?.replay ?? "at-most-once",
1156
+ verify: {
1157
+ tool: "run_command",
1158
+ input,
1159
+ normalized: JSON.parse(JSON.stringify(n))
1160
+ }
1161
+ };
1162
+ },
1163
+ async execute(input, n, ctx, signal) {
1164
+ if (signal.aborted) fail$2("tool-transient/cancelled", "command was cancelled");
1165
+ const command = n.command ?? fail$2("permission/command-not-allowed", "run_command has no normalized command");
1166
+ const value = input;
1167
+ const result = await ctx.executor.run({
1168
+ file: command.file,
1169
+ args: command.args,
1170
+ cwd: command.cwd,
1171
+ env: {},
1172
+ fs: {
1173
+ readWrite: [ctx.workspaceRoot],
1174
+ readOnly: [],
1175
+ denyRead: []
1176
+ },
1177
+ network: "none",
1178
+ timeoutMs: value.timeoutMs ?? command.timeoutMs,
1179
+ maxOutputBytes: 1048576,
1180
+ stdin: "ignore",
1181
+ limits: {},
1182
+ require: "best-effort"
1183
+ }, signal);
1184
+ const text = String(result.tail);
1185
+ return {
1186
+ output: {
1187
+ argv: value.argv,
1188
+ exitCode: result.exitCode,
1189
+ ...result.signal === void 0 ? {} : { signal: result.signal },
1190
+ outcome: result.outcome,
1191
+ text,
1192
+ outputSha256: result.outputSha256,
1193
+ outputBytes: result.outputBytes,
1194
+ truncated: result.truncated
1195
+ },
1196
+ modelText: text,
1197
+ filesTouched: []
1198
+ };
1199
+ },
1200
+ verifyAfterCrash: async (_record) => "in-doubt",
1201
+ describeForNote: (record) => `run_command ${record.idempotencyKey}`
1202
+ };
1203
+ const EXEC_TOOLS = Object.freeze({ run_command: runCommand });
1204
+ //#endregion
1205
+ //#region ../../packages/tools/src/impl/read/index.ts
1206
+ const row$1 = (name) => {
1207
+ const entry = CATALOGUE_ROWS.find((candidate) => candidate.name === name);
1208
+ if (entry === void 0) throw new Error(`missing catalogue row: ${name}`);
1209
+ return entry;
1210
+ };
1211
+ const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
1212
+ const result$1 = (output, modelText) => ({
1213
+ output,
1214
+ modelText,
1215
+ filesTouched: []
1216
+ });
1217
+ const fail$1 = (code, message) => {
1218
+ throw new Error(`${code}: ${message}`);
1219
+ };
1220
+ const requiredPath$1 = (value, message) => value ?? fail$1("permission/path-outside-grant", message);
1221
+ const make$1 = (name, execute) => {
1222
+ return {
1223
+ ...row$1(name),
1224
+ plan: (input, n) => ({
1225
+ kind: "tool.read",
1226
+ replayClass: "idempotent",
1227
+ verify: {
1228
+ tool: name,
1229
+ input,
1230
+ normalized: JSON.parse(JSON.stringify(n))
1231
+ }
1232
+ }),
1233
+ execute,
1234
+ verifyAfterCrash: async (_record) => "done",
1235
+ describeForNote: (record) => `${name} ${record.idempotencyKey}`
1236
+ };
1237
+ };
1238
+ const readFileTool = make$1("read_file", async (input, n, _ctx, signal) => {
1239
+ if (signal.aborted) fail$1("tool-transient/cancelled", "read was cancelled");
1240
+ const target = requiredPath$1(n.paths[0]?.resolved.canonical, "read_file has no resolved path");
1241
+ const bytes = await readFile(target);
1242
+ if (bytes.byteLength > 262144) fail$1("tool-terminal/output-too-large", "file exceeds the 256 KiB read limit");
1243
+ if (bytes.includes(0)) fail$1("tool-terminal/binary-file", "binary files are not returned as text");
1244
+ const value = input;
1245
+ const offset = value.offset ?? 0;
1246
+ const selected = bytes.toString("utf8").split(/\r?\n/).slice(offset, value.limit === void 0 ? void 0 : offset + value.limit);
1247
+ const text = selected.map((line, index) => `${offset + index + 1}: ${line}`).join("\n");
1248
+ return result$1({
1249
+ path: n.paths[0]?.resolved.relative ?? value.path,
1250
+ text,
1251
+ sha256: sha256(bytes),
1252
+ offset,
1253
+ lines: selected.length
1254
+ }, text);
1255
+ });
1256
+ const walk = async (root, current, output, max) => {
1257
+ if (output.length >= max) return;
1258
+ for (const entry of await readdir(current, { withFileTypes: true })) {
1259
+ if (entry.name === ".git" || entry.name === ".cohorte" || entry.name === ".pi" || entry.isSymbolicLink()) continue;
1260
+ const absolute = join(current, entry.name);
1261
+ if (entry.isDirectory()) await walk(root, absolute, output, max);
1262
+ else output.push(relative(root, absolute).split("\\").join("/"));
1263
+ if (output.length >= max) return;
1264
+ }
1265
+ };
1266
+ const globMatch = (value, pattern) => {
1267
+ if (!pattern || pattern === "**") return true;
1268
+ const escaped = pattern.replace(/[.*+?^()|[\]\\]/g, "\\$&").replaceAll("\\*\\*", ".*").replaceAll("\\*", "[^/]*").replaceAll("\\?", "[^/]");
1269
+ return new RegExp(`^${escaped}$`).test(value);
1270
+ };
1271
+ const listFilesTool = make$1("list_files", async (input, n, _ctx, signal) => {
1272
+ if (signal.aborted) fail$1("tool-transient/cancelled", "listing was cancelled");
1273
+ const root = requiredPath$1(n.paths[0]?.resolved.canonical, "list_files has no resolved root");
1274
+ const value = input;
1275
+ const files = [];
1276
+ await walk(root, root, files, value.maxEntries ?? 1e3);
1277
+ const filtered = files.filter((file) => globMatch(file, value.glob));
1278
+ return result$1({
1279
+ files: filtered,
1280
+ truncated: files.length >= (value.maxEntries ?? 1e3)
1281
+ }, filtered.join("\n"));
1282
+ });
1283
+ const searchTool = make$1("search", async (input, n, _ctx, signal) => {
1284
+ if (signal.aborted) fail$1("tool-transient/cancelled", "search was cancelled");
1285
+ const root = requiredPath$1(n.paths[0]?.resolved.canonical, "search has no resolved root");
1286
+ const value = input;
1287
+ const expression = new RegExp(value.pattern, value.caseInsensitive ? "i" : "");
1288
+ const files = [];
1289
+ await walk(root, root, files, 1e4);
1290
+ const matches = [];
1291
+ for (const file of files.filter((candidate) => globMatch(candidate, value.glob))) {
1292
+ if (matches.length >= (value.maxMatches ?? 100)) break;
1293
+ const text = await readFile(join(root, file), "utf8").catch(() => void 0);
1294
+ if (text === void 0) continue;
1295
+ text.split(/\r?\n/).forEach((line, index) => {
1296
+ if (matches.length < (value.maxMatches ?? 100) && expression.test(line)) matches.push({
1297
+ path: file,
1298
+ line: index + 1,
1299
+ text: line
1300
+ });
1301
+ });
1302
+ }
1303
+ return result$1({
1304
+ matches,
1305
+ filteredPaths: 0
1306
+ }, matches.map((match) => `${match.path}:${match.line}:${match.text}`).join("\n"));
1307
+ });
1308
+ const gitDiffTool = make$1("git_diff", async (input, _n, ctx, signal) => {
1309
+ if (signal.aborted) fail$1("tool-transient/cancelled", "diff was cancelled");
1310
+ const value = input;
1311
+ const output = await ctx.git.diffBySurface({
1312
+ repo: ctx.workspaceRoot,
1313
+ base: value.base ?? "integration",
1314
+ head: "HEAD",
1315
+ surfaces: { surfaceOf: () => "shared" }
1316
+ });
1317
+ const text = output.map((item) => new TextDecoder().decode(item.patch.bytes)).join("\\n");
1318
+ return result$1({
1319
+ base: value.base ?? "integration",
1320
+ stat: value.stat ?? false,
1321
+ files: output.flatMap((item) => item.files),
1322
+ diff: text
1323
+ }, text);
1324
+ });
1325
+ const READ_TOOLS = Object.freeze({
1326
+ read_file: readFileTool,
1327
+ list_files: listFilesTool,
1328
+ search: searchTool,
1329
+ git_diff: gitDiffTool
1330
+ });
1331
+ //#endregion
1332
+ //#region ../../packages/tools/src/catalogue/stub.ts
1333
+ const rowByName$1 = new Map(CATALOGUE_ROWS.map((row) => [row.name, row]));
1334
+ /**
1335
+ * Every deliverable of THIS unit (U0.08) ends here: a `ToolImplementation` whose `plan`/`execute`/`verifyAfterCrash`
1336
+ * /`describeForNote` throw. The metadata (`name`, `inputSchema`, `description`, `effect`, `terminal`) is real —
1337
+ * `impl/read`, `impl/write`, `impl/exec` and `impl/state` (Wave 2: `U2.03`, `U2.04`) call this for every tool they
1338
+ * do not yet implement, and REPLACE the body, never the metadata, one tool at a time.
1339
+ */
1340
+ function stubImplementation(name) {
1341
+ const row = rowByName$1.get(name);
1342
+ if (!row) throw new RangeError(`stubImplementation: unknown tool ${JSON.stringify(name)}`);
1343
+ const notImplemented = (member) => {
1344
+ throw new NotImplemented(`tools/${name}.${member} (Wave 2 fills packages/tools/src/impl/**)`);
1345
+ };
1346
+ return {
1347
+ name: row.name,
1348
+ inputSchema: row.inputSchema,
1349
+ description: row.description,
1350
+ effect: row.effect,
1351
+ terminal: row.terminal,
1352
+ plan() {
1353
+ return notImplemented("plan");
1354
+ },
1355
+ execute() {
1356
+ return notImplemented("execute");
1357
+ },
1358
+ verifyAfterCrash() {
1359
+ return notImplemented("verifyAfterCrash");
1360
+ },
1361
+ describeForNote() {
1362
+ return notImplemented("describeForNote");
1363
+ }
1364
+ };
1365
+ }
1366
+ //#endregion
1367
+ //#region ../../packages/tools/src/impl/state/index.ts
1368
+ const metadata = (name) => {
1369
+ const row = CATALOGUE_ROWS.find((candidate) => candidate.name === name);
1370
+ if (row === void 0) throw new Error(`missing catalogue row: ${name}`);
1371
+ return row;
1372
+ };
1373
+ const approvalRequest = {
1374
+ ...metadata("approval_request"),
1375
+ plan: () => null,
1376
+ execute: async (input, _normalized, ctx) => {
1377
+ const value = input;
1378
+ const decision = await ctx.requestApproval(value.question, value.options);
1379
+ return {
1380
+ output: decision,
1381
+ modelText: decision.answer ?? decision.decision,
1382
+ filesTouched: []
1383
+ };
1384
+ },
1385
+ verifyAfterCrash: async () => "done",
1386
+ describeForNote: () => "approval request"
1387
+ };
1388
+ const submitResult = {
1389
+ ...metadata("submit_result"),
1390
+ plan: () => null,
1391
+ execute: async (input, _normalized, ctx) => {
1392
+ const accepted = await ctx.acceptResult(input);
1393
+ if (!accepted.accepted) throw new Error(`validation/agent-output: ${accepted.reason ?? "result was rejected"}`);
1394
+ return {
1395
+ output: { accepted: true },
1396
+ modelText: "result submitted",
1397
+ filesTouched: []
1398
+ };
1399
+ },
1400
+ verifyAfterCrash: async () => "done",
1401
+ describeForNote: () => "submit result"
1402
+ };
1403
+ const STATE_TOOLS = Object.freeze({
1404
+ approval_request: approvalRequest,
1405
+ submit_result: submitResult,
1406
+ git_commit: stubImplementation("git_commit"),
1407
+ network_request: stubImplementation("network_request"),
1408
+ secret_read: stubImplementation("secret_read")
1409
+ });
1410
+ //#endregion
1411
+ //#region ../../packages/tools/src/impl/write/index.ts
1412
+ const row = (name) => {
1413
+ const entry = CATALOGUE_ROWS.find((candidate) => candidate.name === name);
1414
+ if (entry === void 0) throw new Error(`missing catalogue row: ${name}`);
1415
+ return entry;
1416
+ };
1417
+ const digest = (value) => sha256Hex(createHash("sha256").update(value).digest());
1418
+ const fail = (code, message) => {
1419
+ throw new Error(`${code}: ${message}`);
1420
+ };
1421
+ const requiredPath = (n) => n.paths[0]?.resolved.canonical ?? fail("permission/path-outside-grant", "write tool has no resolved path");
1422
+ const result = (output, modelText, filesTouched) => ({
1423
+ output,
1424
+ modelText,
1425
+ filesTouched
1426
+ });
1427
+ const make = (name, execute) => {
1428
+ return {
1429
+ ...row(name),
1430
+ plan: (input, _n) => ({
1431
+ kind: name === "write_file" ? "tool.write_file" : "tool.patch_file",
1432
+ replayClass: "verifiable",
1433
+ verify: {
1434
+ tool: name,
1435
+ input
1436
+ }
1437
+ }),
1438
+ execute,
1439
+ verifyAfterCrash: async (record, ctx) => {
1440
+ const request = record.request;
1441
+ if (!request.path) return "in-doubt";
1442
+ try {
1443
+ const path = ctx.paths.resolve(request.path, ctx.workspaceRoot, "read");
1444
+ if (!path.ok) return "in-doubt";
1445
+ const bytes = await readFile(path.value.canonical);
1446
+ return request.content !== void 0 && digest(bytes) === digest(request.content) ? "done" : "not-done";
1447
+ } catch {
1448
+ return "not-done";
1449
+ }
1450
+ },
1451
+ describeForNote: (record) => `${name} ${record.idempotencyKey}`
1452
+ };
1453
+ };
1454
+ const atomicWrite = async (target, content) => {
1455
+ const temporary = join(dirname(target), `.cohorte-write-${process.pid}-${Date.now()}`);
1456
+ await writeFile(temporary, content, {
1457
+ encoding: "utf8",
1458
+ flag: "wx"
1459
+ });
1460
+ await rename(temporary, target);
1461
+ };
1462
+ const writeFileTool = make("write_file", async (input, n, _ctx, signal) => {
1463
+ if (signal.aborted) fail("tool-transient/cancelled", "write was cancelled");
1464
+ const target = requiredPath(n);
1465
+ const value = input;
1466
+ let before;
1467
+ try {
1468
+ before = await readFile(target);
1469
+ } catch {}
1470
+ await atomicWrite(target, value.content);
1471
+ const after = Buffer.from(value.content);
1472
+ const path = n.paths[0]?.resolved.relative ?? value.path;
1473
+ return result({
1474
+ path,
1475
+ bytes: after.byteLength,
1476
+ sha256: digest(after)
1477
+ }, `wrote ${after.byteLength} bytes`, [{
1478
+ path,
1479
+ op: before === void 0 ? "create" : "modify",
1480
+ ...before === void 0 ? {} : { beforeSha256: digest(before) },
1481
+ afterSha256: digest(after),
1482
+ bytes: after.byteLength
1483
+ }]);
1484
+ });
1485
+ const patchFileTool = make("patch_file", async (input, n, _ctx, signal) => {
1486
+ if (signal.aborted) fail("tool-transient/cancelled", "patch was cancelled");
1487
+ const target = requiredPath(n);
1488
+ const value = input;
1489
+ const before = await readFile(target, "utf8");
1490
+ let next = before;
1491
+ for (const edit of value.edits) {
1492
+ const count = next.split(edit.oldText).length - 1;
1493
+ if (count !== 1) fail("tool-terminal/patch-mismatch", `expected one occurrence, found ${count}`);
1494
+ next = next.replace(edit.oldText, edit.newText);
1495
+ }
1496
+ await atomicWrite(target, next);
1497
+ const after = Buffer.from(next);
1498
+ const path = n.paths[0]?.resolved.relative ?? value.path;
1499
+ return result({
1500
+ path,
1501
+ bytes: after.byteLength,
1502
+ sha256: digest(after),
1503
+ edits: value.edits.length
1504
+ }, `patched ${path}`, [{
1505
+ path,
1506
+ op: "modify",
1507
+ beforeSha256: digest(Buffer.from(before)),
1508
+ afterSha256: digest(after),
1509
+ bytes: after.byteLength
1510
+ }]);
1511
+ });
1512
+ const WRITE_TOOLS = Object.freeze({
1513
+ write_file: writeFileTool,
1514
+ patch_file: patchFileTool
1515
+ });
1516
+ //#endregion
1517
+ //#region ../../packages/tools/src/catalogue/index.ts
1518
+ const rowByName = new Map(CATALOGUE_ROWS.map((row) => [row.name, row]));
1519
+ /** `ToolIntrospection` is on the per-tool-call path (PLAN PC-4: the gate validates a call's shape without importing
1520
+ * `tools`), so the strict schema of every row is built ONCE, here: `schemaOf` returns the same object identity on
1521
+ * every call and a validator cache keyed by schema identity in the gate (U2.01/U2.02) keeps working. */
1522
+ const strictSchemaByName = new Map(CATALOGUE_ROWS.map((row) => [row.name, toStrictSchema(row.inputSchema)]));
1523
+ Compile(ToolGrant);
1524
+ const namingProblems = toolGrantProblems(CATALOGUE_ROWS.map((row) => ({ tool: row.name })));
1525
+ if (namingProblems.length > 0) throw new TypeError(`tool catalogue: ${namingProblems.join("; ")}`);
1526
+ /** Catalogue-derived: never a second, hand-written copy of what a tool's input schema says (PLAN PC-4: `security`
1527
+ * validates a call's shape and path arguments without importing `tools`). */
1528
+ const toolIntrospection = {
1529
+ schemaOf(tool) {
1530
+ return strictSchemaByName.get(tool);
1531
+ },
1532
+ pathArgsOf(tool, input) {
1533
+ const row = rowByName.get(tool);
1534
+ if (!row) return [];
1535
+ return row.pathArgsOf(input);
1536
+ }
1537
+ };
1538
+ /** The 9 V3.0 tools + the 3 seams, by name (DESIGN 2.7). Every value is `stubImplementation(name)` until Wave 2
1539
+ * replaces the bodies in `impl/**`; the metadata is already the catalogue's own. */
1540
+ const TOOL_CATALOGUE = Object.freeze({
1541
+ ...READ_TOOLS,
1542
+ ...WRITE_TOOLS,
1543
+ ...EXEC_TOOLS,
1544
+ ...STATE_TOOLS
1545
+ });
1546
+ //#endregion
1547
+ export { buildPolicySnapshot as a, createCommandPolicy as c, createGlobMatcher as i, createProgramResolver as l, toolIntrospection as n, createPolicyEngine as o, createPathResolver as r, explainPolicy as s, TOOL_CATALOGUE as t };
1548
+
1549
+ //# sourceMappingURL=catalogue-BvwSrM_j.mjs.map