@midhunxavier/code-flow 0.0.0-stage → 0.1.0-preview.3

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 (319) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +141 -2
  3. package/bin/codeflow.mjs +8 -0
  4. package/bin/launcher.mjs +127 -0
  5. package/dist/remote-host.json +1 -0
  6. package/licenses/inter.txt +92 -0
  7. package/licenses/preact.txt +21 -0
  8. package/licenses/signals-core.txt +21 -0
  9. package/licenses/signals.txt +21 -0
  10. package/licenses/typescript-notices.txt +193 -0
  11. package/licenses/typescript.txt +55 -0
  12. package/licenses/uqr.txt +22 -0
  13. package/licenses/xterm-fit.txt +19 -0
  14. package/licenses/xterm.txt +21 -0
  15. package/package.json +34 -4
  16. package/public/assets/changes-62ZIEQSN.js +1 -0
  17. package/public/assets/chunk-2PZVOKID.js +1 -0
  18. package/public/assets/chunk-4D5IENEB.js +1 -0
  19. package/public/assets/chunk-5SARP5MU.js +1 -0
  20. package/public/assets/chunk-63YLZ3PR.js +1 -0
  21. package/public/assets/chunk-7FUHKF2M.js +1 -0
  22. package/public/assets/chunk-APQEVJ2K.js +1 -0
  23. package/public/assets/chunk-CGOZQMEZ.js +2 -0
  24. package/public/assets/chunk-J4VYS3KV.js +2 -0
  25. package/public/assets/chunk-L7TUCIYF.js +1 -0
  26. package/public/assets/chunk-SIHDWM2Y.js +154 -0
  27. package/public/assets/chunk-TEJVYIKC.js +1 -0
  28. package/public/assets/code-browser-VB2HGZSG.js +4 -0
  29. package/public/assets/filtered-tasks-ES5CP4KV.js +1 -0
  30. package/public/assets/interactive-browser-7F5ZXRUV.js +7 -0
  31. package/public/assets/join-project-6RTFHNCA.js +1 -0
  32. package/public/assets/main-65EW73SB.js +1 -0
  33. package/public/assets/project-invitations-view-GMPWGWYM.js +1 -0
  34. package/public/assets/settings-UB5Q4AQM.js +9 -0
  35. package/public/assets/styles-18792BB4.css +32 -0
  36. package/public/assets/team-server-wizard-2GLDWS6D.js +8 -0
  37. package/public/assets/terminal-IWAIM4DE.js +62 -0
  38. package/public/assets.json +1 -0
  39. package/public/bundle-meta.json +1 -0
  40. package/public/docs/.nojekyll +0 -0
  41. package/public/docs/accessibility.html +19 -0
  42. package/public/docs/acp-agents.html +117 -0
  43. package/public/docs/agent-containers.html +79 -0
  44. package/public/docs/api.html +1130 -0
  45. package/public/docs/app.js +342 -0
  46. package/public/docs/application-improvements.html +275 -0
  47. package/public/docs/architecture-explorer.html +19 -0
  48. package/public/docs/architecture.html +149 -0
  49. package/public/docs/auto-team.html +148 -0
  50. package/public/docs/automations.html +109 -0
  51. package/public/docs/checkpoint-review.html +37 -0
  52. package/public/docs/code-hosts.html +35 -0
  53. package/public/docs/code-intelligence.html +35 -0
  54. package/public/docs/configuration.html +346 -0
  55. package/public/docs/context-inspection.html +23 -0
  56. package/public/docs/coordinated-changes.html +28 -0
  57. package/public/docs/deferred-runs.html +25 -0
  58. package/public/docs/dependency-discovery.html +19 -0
  59. package/public/docs/documentation.html +84 -0
  60. package/public/docs/dynamic-teams-evaluation.html +174 -0
  61. package/public/docs/dynamic-teams-implementation-plan.html +882 -0
  62. package/public/docs/dynamic-teams-implementation-review-1.html +141 -0
  63. package/public/docs/dynamic-teams-plan-review-0.html +204 -0
  64. package/public/docs/extensions.html +42 -0
  65. package/public/docs/external-sources.html +96 -0
  66. package/public/docs/filesystem-policy.html +26 -0
  67. package/public/docs/filtered-tasks.html +44 -0
  68. package/public/docs/font-license.txt +92 -0
  69. package/public/docs/getting-started.html +143 -0
  70. package/public/docs/github-review-threads.html +26 -0
  71. package/public/docs/github.html +157 -0
  72. package/public/docs/help.html +69 -0
  73. package/public/docs/index.html +88 -0
  74. package/public/docs/installation.html +80 -0
  75. package/public/docs/launch-profiles.html +48 -0
  76. package/public/docs/maintenance.html +60 -0
  77. package/public/docs/memory-claims.html +31 -0
  78. package/public/docs/memory-storage.html +58 -0
  79. package/public/docs/notifications.html +39 -0
  80. package/public/docs/patterns.html +195 -0
  81. package/public/docs/performance-results.html +210 -0
  82. package/public/docs/performance.html +166 -0
  83. package/public/docs/project-conventions.html +22 -0
  84. package/public/docs/project-first-navigation-plan.html +587 -0
  85. package/public/docs/project-teams-implementation-plan.html +772 -0
  86. package/public/docs/project-teams.html +129 -0
  87. package/public/docs/provider-compatibility.html +20 -0
  88. package/public/docs/releasing.html +65 -0
  89. package/public/docs/remote-workers.html +64 -0
  90. package/public/docs/review-inbox.html +26 -0
  91. package/public/docs/run-storage.html +23 -0
  92. package/public/docs/runs.html +141 -0
  93. package/public/docs/search.json +1 -0
  94. package/public/docs/security.html +133 -0
  95. package/public/docs/shared-projects.html +31 -0
  96. package/public/docs/ssh-projects.html +60 -0
  97. package/public/docs/style.css +768 -0
  98. package/public/docs/task-reruns.html +30 -0
  99. package/public/docs/team-collaboration.html +412 -0
  100. package/public/docs/verification.html +22 -0
  101. package/public/docs/vscode.html +53 -0
  102. package/public/docs/worker-permission-verification.html +131 -0
  103. package/public/index.html +17 -0
  104. package/scripts/check-node.mjs +7 -0
  105. package/scripts/codeflow.mjs +55 -0
  106. package/scripts/computer-use.swift +64 -0
  107. package/scripts/pty.py +273 -0
  108. package/src/acp-connections.js +110 -0
  109. package/src/adapters/acp-presets.js +37 -0
  110. package/src/adapters/acp-types.js +0 -0
  111. package/src/adapters/acp.js +328 -0
  112. package/src/adapters/agent-tools.js +28 -0
  113. package/src/adapters/agy-auth.js +67 -0
  114. package/src/adapters/agy-isolation.js +242 -0
  115. package/src/adapters/agy-tools.js +45 -0
  116. package/src/adapters/agy.js +326 -0
  117. package/src/adapters/claude-stream.js +58 -0
  118. package/src/adapters/claude.js +282 -0
  119. package/src/adapters/codex-app-server.js +555 -0
  120. package/src/adapters/codex.js +311 -0
  121. package/src/adapters/errors.js +57 -0
  122. package/src/adapters/index.js +91 -0
  123. package/src/adapters/managed-permissions.js +37 -0
  124. package/src/adapters/model-discovery.js +100 -0
  125. package/src/adapters/process.js +433 -0
  126. package/src/adapters/team-tools.js +103 -0
  127. package/src/adapters/types.js +0 -0
  128. package/src/agent-browser.js +688 -0
  129. package/src/agent-computer.js +88 -0
  130. package/src/agent-container.js +155 -0
  131. package/src/agent-team-definitions.js +131 -0
  132. package/src/agent-tool-client.mjs +79 -0
  133. package/src/agent-tools.js +296 -0
  134. package/src/approvals.js +134 -0
  135. package/src/architecture.js +456 -0
  136. package/src/assets.js +44 -0
  137. package/src/automation.js +901 -0
  138. package/src/browser-files.js +293 -0
  139. package/src/browser-ownership.js +150 -0
  140. package/src/certificate-renewal.js +75 -0
  141. package/src/chat-family.js +25 -0
  142. package/src/chat-handoff.js +87 -0
  143. package/src/checkpoint-review.js +72 -0
  144. package/src/checks.js +303 -0
  145. package/src/code-host-git.js +102 -0
  146. package/src/code-hosts-api.js +99 -0
  147. package/src/code-hosts.js +482 -0
  148. package/src/code-intelligence-worker.mjs +452 -0
  149. package/src/code-intelligence.js +141 -0
  150. package/src/collab/api.js +365 -0
  151. package/src/collab/checkpoints.js +198 -0
  152. package/src/collab/connections.js +139 -0
  153. package/src/collab/host-execution.js +347 -0
  154. package/src/collab/hub-client.js +210 -0
  155. package/src/collab/link-api.js +229 -0
  156. package/src/collab/link.js +595 -0
  157. package/src/collab/model.js +496 -0
  158. package/src/collab/runner.js +1014 -0
  159. package/src/collab/store.js +1452 -0
  160. package/src/collab/terminals.js +361 -0
  161. package/src/collaboration-reviews.js +116 -0
  162. package/src/config.js +48 -0
  163. package/src/context-budget.js +14 -0
  164. package/src/context-inspection.js +204 -0
  165. package/src/context.js +154 -0
  166. package/src/conventions.js +283 -0
  167. package/src/coordinated-api.js +247 -0
  168. package/src/deferred-launch.js +82 -0
  169. package/src/deferred-runs.js +515 -0
  170. package/src/dependency-health.js +35 -0
  171. package/src/diff.js +43 -0
  172. package/src/docs-help.js +316 -0
  173. package/src/doctor.js +232 -0
  174. package/src/documentation-changes.js +20 -0
  175. package/src/engine.js +5258 -0
  176. package/src/env.js +44 -0
  177. package/src/execution-budget.js +8 -0
  178. package/src/experience.js +50 -0
  179. package/src/extensions.js +144 -0
  180. package/src/filesystem-access.js +107 -0
  181. package/src/filesystem-policy.js +238 -0
  182. package/src/filtered-task-limits.js +93 -0
  183. package/src/filtered-tasks-api.js +53 -0
  184. package/src/filtered-tasks.js +576 -0
  185. package/src/frame-policy.js +66 -0
  186. package/src/fs.js +57 -0
  187. package/src/git.js +626 -0
  188. package/src/github-review-threads.js +181 -0
  189. package/src/github.js +538 -0
  190. package/src/http.js +69 -0
  191. package/src/improve-settings.js +38 -0
  192. package/src/improve.js +187 -0
  193. package/src/integrations.js +373 -0
  194. package/src/integrity.js +259 -0
  195. package/src/journey-api.js +77 -0
  196. package/src/journey-replay.js +65 -0
  197. package/src/journey-store.js +97 -0
  198. package/src/journeys.js +60 -0
  199. package/src/json.js +81 -0
  200. package/src/launch-profiles.js +292 -0
  201. package/src/learnings.js +80 -0
  202. package/src/local-app-url.js +19 -0
  203. package/src/log.js +39 -0
  204. package/src/maintenance.js +335 -0
  205. package/src/memory-analysis.js +182 -0
  206. package/src/memory-claims.js +153 -0
  207. package/src/memory-sqlite.js +236 -0
  208. package/src/memory.js +552 -0
  209. package/src/merge-validation.js +58 -0
  210. package/src/model-catalog.js +239 -0
  211. package/src/model-research.js +91 -0
  212. package/src/notes.js +176 -0
  213. package/src/notifications.js +413 -0
  214. package/src/patterns/auto-team-model.js +230 -0
  215. package/src/patterns/auto-team.js +816 -0
  216. package/src/patterns/council.js +199 -0
  217. package/src/patterns/debate.js +188 -0
  218. package/src/patterns/findings.js +141 -0
  219. package/src/patterns/index.js +21 -0
  220. package/src/patterns/loop.js +158 -0
  221. package/src/patterns/pipeline.js +125 -0
  222. package/src/patterns/prompts.js +181 -0
  223. package/src/patterns/race.js +147 -0
  224. package/src/patterns/schemas.js +40 -0
  225. package/src/patterns/solo.js +46 -0
  226. package/src/patterns/supervisor.js +235 -0
  227. package/src/patterns/task-draft.js +29 -0
  228. package/src/patterns/task-review.js +45 -0
  229. package/src/patterns/tdd.js +231 -0
  230. package/src/patterns/team-strategies.js +67 -0
  231. package/src/performance.js +43 -0
  232. package/src/persistence-status.js +35 -0
  233. package/src/phone-sessions.js +96 -0
  234. package/src/preview-overlay.js +392 -0
  235. package/src/preview.js +230 -0
  236. package/src/privacy.js +23 -0
  237. package/src/project-report.js +22 -0
  238. package/src/project-team-budget.js +22 -0
  239. package/src/project-team-model.js +225 -0
  240. package/src/project-team-policy.js +42 -0
  241. package/src/project-team-store.js +234 -0
  242. package/src/project-team-usage.js +24 -0
  243. package/src/project-teams-api.js +43 -0
  244. package/src/project-teams.js +644 -0
  245. package/src/projects.js +290 -0
  246. package/src/provider-eligibility.js +9 -0
  247. package/src/readiness.js +98 -0
  248. package/src/remote-tls.js +66 -0
  249. package/src/remote-workers/bundles.js +198 -0
  250. package/src/remote-workers/client.js +332 -0
  251. package/src/remote-workers/durability.js +37 -0
  252. package/src/remote-workers/protocol.js +68 -0
  253. package/src/remote-workers/stdio.js +138 -0
  254. package/src/remote-workers/worker.js +304 -0
  255. package/src/remote.js +204 -0
  256. package/src/repo-config.js +201 -0
  257. package/src/repo-coordination.js +725 -0
  258. package/src/repo-tree.js +106 -0
  259. package/src/repository-identity.js +13 -0
  260. package/src/retention-policy.js +466 -0
  261. package/src/review-inbox.js +38 -0
  262. package/src/routing.js +49 -0
  263. package/src/run-analysis.js +111 -0
  264. package/src/run-cold-store.js +231 -0
  265. package/src/run-environment.js +15 -0
  266. package/src/run-history.js +91 -0
  267. package/src/run-log.js +160 -0
  268. package/src/run-permissions.js +111 -0
  269. package/src/run-projection.js +77 -0
  270. package/src/run-recovery.js +53 -0
  271. package/src/run-status.js +56 -0
  272. package/src/serve.js +138 -0
  273. package/src/server.js +2871 -0
  274. package/src/shared-access.js +103 -0
  275. package/src/shared-accounts.js +381 -0
  276. package/src/shared-api.js +239 -0
  277. package/src/shell.js +346 -0
  278. package/src/ship.js +344 -0
  279. package/src/slots.js +74 -0
  280. package/src/snapshot.js +388 -0
  281. package/src/source-context.js +17 -0
  282. package/src/sse-writer.js +70 -0
  283. package/src/ssh-projects/bootstrap.mjs +95 -0
  284. package/src/ssh-projects/config.js +45 -0
  285. package/src/ssh-projects/gateway.js +143 -0
  286. package/src/ssh-projects/manager.js +320 -0
  287. package/src/ssh-projects/team-autostart.mjs +93 -0
  288. package/src/ssh-projects/team-remove.mjs +69 -0
  289. package/src/ssh-projects/team-status.mjs +39 -0
  290. package/src/ssh-projects/transport.js +133 -0
  291. package/src/stats.js +139 -0
  292. package/src/store.js +544 -0
  293. package/src/stream.js +91 -0
  294. package/src/sweeps.js +241 -0
  295. package/src/tailscale.js +101 -0
  296. package/src/task-draft.js +23 -0
  297. package/src/task-team.js +164 -0
  298. package/src/team-evaluation.js +243 -0
  299. package/src/team-mailbox.js +160 -0
  300. package/src/team-memory.js +84 -0
  301. package/src/team-rerun-graph.js +15 -0
  302. package/src/team-reruns.js +130 -0
  303. package/src/team-scheduler.js +121 -0
  304. package/src/team-steering.js +21 -0
  305. package/src/team-template-store.js +98 -0
  306. package/src/team-templates.js +117 -0
  307. package/src/team-tool-client.mjs +79 -0
  308. package/src/team-tools.js +235 -0
  309. package/src/teams.js +0 -0
  310. package/src/templates.js +62 -0
  311. package/src/transcript.js +134 -0
  312. package/src/verification-integrity.js +27 -0
  313. package/src/verification.js +183 -0
  314. package/src/watch.js +195 -0
  315. package/src/work-queue.js +47 -0
  316. package/src/workflow-control.js +83 -0
  317. package/src/workspace-api.js +207 -0
  318. package/src/workspace-changes.js +61 -0
  319. package/src/workspace.js +146 -0
package/src/env.js ADDED
@@ -0,0 +1,44 @@
1
+ import { dirname, isAbsolute, join, resolve, sep } from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ const INSTALL_DIR = resolve(dirname(fileURLToPath(import.meta.url)), "..");
4
+ const SECRET_VARS = /* @__PURE__ */ new Set(["CODEFLOW_TOKEN", "CODEFLOW_SSH_HOST", "CODEFLOW_TEAM_CAPABILITY", "CODEFLOW_TEAM_ENDPOINT", "CODEFLOW_INTEGRATIONS_JSON", "CODEFLOW_INTEGRATIONS_FILE", "CODEFLOW_NOTIFICATIONS_JSON", "CODEFLOW_NOTIFICATIONS_FILE"]);
5
+ function registerPrivateEnvNames(names) {
6
+ for (const name of names) if (/^[A-Z_][A-Z0-9_]{0,99}$/.test(name)) SECRET_VARS.add(name);
7
+ }
8
+ function privateEnvironmentValues() {
9
+ return [...SECRET_VARS].map((name) => process.env[name]).filter((value) => !!value && value.length >= 8);
10
+ }
11
+ function childEnv(extra = {}) {
12
+ const env = { ...process.env };
13
+ for (const key of SECRET_VARS) delete env[key];
14
+ for (const key of Object.keys(env)) {
15
+ if (key.startsWith("npm_config_") && key !== "npm_config_local_prefix") continue;
16
+ if (key.startsWith("npm_") || key === "INIT_CWD") delete env[key];
17
+ }
18
+ const merged = { ...env, ...extra };
19
+ merged.PATH = cleanPath(merged.PATH ?? "");
20
+ return merged;
21
+ }
22
+ function cleanPath(path, installDir = INSTALL_DIR) {
23
+ const sepChar = process.platform === "win32" ? ";" : ":";
24
+ const ancestors = /* @__PURE__ */ new Set();
25
+ for (let dir = installDir; ; dir = dirname(dir)) {
26
+ ancestors.add(join(dir, "node_modules", ".bin"));
27
+ if (dirname(dir) === dir) break;
28
+ }
29
+ const cleaned = path.split(sepChar).filter((entry) => {
30
+ if (!isAbsolute(entry)) return false;
31
+ const normalized = resolve(entry);
32
+ if (ancestors.has(normalized)) return false;
33
+ return !normalized.includes(`${sep}@npmcli${sep}run-script${sep}`);
34
+ }).join(sepChar);
35
+ if (cleaned) return cleaned;
36
+ const systemRoot = process.env.SystemRoot ?? process.env.SYSTEMROOT ?? "";
37
+ return process.platform === "win32" ? join(isAbsolute(systemRoot) ? systemRoot : "C:\\Windows", "System32") : "/usr/bin:/bin";
38
+ }
39
+ export {
40
+ childEnv,
41
+ cleanPath,
42
+ privateEnvironmentValues,
43
+ registerPrivateEnvNames
44
+ };
@@ -0,0 +1,8 @@
1
+ const DEFAULT_EXECUTION_BUDGET = { maxSteps: 200, maxMinutes: 240 };
2
+ function coordinatorInvocationLimit(invocations) {
3
+ return Math.max(1, Math.floor(invocations / 4));
4
+ }
5
+ export {
6
+ DEFAULT_EXECUTION_BUDGET,
7
+ coordinatorInvocationLimit
8
+ };
@@ -0,0 +1,50 @@
1
+ import { runPassed } from "./run-status.js";
2
+ import { confidenceInterval } from "./stats.js";
3
+ function taskIntent(value) {
4
+ return typeof value === "string" && ["bug", "feature", "review", "explore"].includes(value) ? value : void 0;
5
+ }
6
+ const median = (values) => {
7
+ const sorted = [...values].sort((a, b) => a - b);
8
+ return sorted.length ? (sorted[Math.floor((sorted.length - 1) / 2)] + sorted[Math.floor(sorted.length / 2)]) / 2 : void 0;
9
+ };
10
+ function outcomeMetrics(runs) {
11
+ const ended = runs.filter((run) => run.status !== "running");
12
+ const verified = ended.filter(runPassed);
13
+ const rated = ended.filter((run) => run.outcome);
14
+ const reworked = ended.filter((run) => run.humanRework?.length);
15
+ return {
16
+ completed: ended.length,
17
+ verified: verified.length,
18
+ rated: rated.length,
19
+ accepted: rated.filter((run) => run.outcome?.decision === "accepted").length,
20
+ shipped: ended.filter((run) => run.merges.length).length,
21
+ medianVerifiedMs: median(verified.flatMap((run) => run.endedAt ? [Math.max(0, run.endedAt - run.createdAt)] : [])),
22
+ reworkSamples: reworked.length,
23
+ medianReworkMinutes: median(reworked.map((run) => run.humanRework.reduce((sum, item) => sum + item.minutes, 0)))
24
+ };
25
+ }
26
+ function workflowRecommendations(runs, repo, intent) {
27
+ const comparable = runs.filter((run) => run.repo === repo && run.status !== "running" && (!intent || run.intent === intent));
28
+ const groups = /* @__PURE__ */ new Map();
29
+ for (const run of comparable) groups.set(run.pattern, [...groups.get(run.pattern) ?? [], run]);
30
+ return [...groups].map(([pattern, samples]) => {
31
+ const metrics = outcomeMetrics(samples);
32
+ return { pattern, ...metrics, confidence: confidenceInterval(metrics.verified, samples.length), runIds: samples.slice(-10).map((run) => run.id) };
33
+ }).sort((a, b) => b.confidence.low - a.confidence.low || b.completed - a.completed || a.pattern.localeCompare(b.pattern));
34
+ }
35
+ function progressSummary(run, steps) {
36
+ const questions = run.team?.messages.filter((message) => message.kind === "question" && message.recipient === "user" && message.status === "open") ?? [];
37
+ if (run.awaiting) return { title: "Your plan decision is needed", detail: run.awaiting.title };
38
+ if (questions.length) return { title: `${questions.length} question${questions.length === 1 ? "" : "s"} need your answer`, detail: questions[0].body };
39
+ if (run.pauseRequested) return { title: "Pausing at a safe checkpoint", detail: "Current work is retained. Resume dispatch when you're ready." };
40
+ const active = steps.filter((step) => step.status === "running" && !step.sideOf);
41
+ if (active.length) return { title: active.every((step) => step.kind === "check") ? "Checking the result" : "Work in progress", detail: active.map((step) => step.title).join(" \xB7 ") };
42
+ if (run.status === "running") return { title: "Preparing the next step", detail: "Waiting for a provider slot, a dependency, or the next workflow action." };
43
+ return { title: runPassed(run) ? "Checks passed" : "Work has stopped", detail: run.error ?? run.stopReason ?? "Review the result and its recorded evidence." };
44
+ }
45
+ export {
46
+ outcomeMetrics,
47
+ progressSummary,
48
+ taskIntent,
49
+ workflowRecommendations
50
+ };
@@ -0,0 +1,144 @@
1
+ import { createHash } from "node:crypto";
2
+ import { constants } from "node:fs";
3
+ import { lstat, mkdir, open, rename, unlink } from "node:fs/promises";
4
+ import { isAbsolute, join } from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ import { CODEFLOW_HOME } from "./config.js";
7
+ import { registerPrivateEnvNames } from "./env.js";
8
+ import { redactExport } from "./privacy.js";
9
+ const EXTENSION_API = { major: 1, minor: 0, capabilities: ["provider-adapter", "workflow-pattern"] };
10
+ const bindings = /* @__PURE__ */ new Map();
11
+ function extensionBindings(pattern, roles) {
12
+ const used = [...new Set([pattern, ...Object.values(roles).flat()].filter((id) => id.startsWith("plugin:")))];
13
+ const result = used.map((id) => {
14
+ const binding = bindings.get(id);
15
+ if (!binding) throw new Error(`Restore the pinned extension for ${id} before running this workflow`);
16
+ return binding;
17
+ });
18
+ return [...new Map(result.map((value) => [value.id, value])).values()].sort((a, b) => a.id.localeCompare(b.id)).map((value) => ({ ...value }));
19
+ }
20
+ function assertExtensionBindings(run) {
21
+ const current = extensionBindings(run.pattern, run.roles);
22
+ if (JSON.stringify(current) !== JSON.stringify(run.extensions ?? [])) throw new Error("This run's provider/workflow extension version changed; restore its exact SHA-256 pin or create a new run");
23
+ }
24
+ const object = (value) => !!value && typeof value === "object" && !Array.isArray(value);
25
+ const identifier = /^[a-z][a-z0-9-]{0,40}$/;
26
+ const bounded = (value, max) => typeof value === "string" && !!value.trim() && value.length <= max && !/[\x00-\x08\x0e-\x1f]/.test(value);
27
+ async function readBundle(path, ceiling) {
28
+ const file = await open(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
29
+ try {
30
+ const info = await file.stat();
31
+ if (!info.isFile() || info.size > ceiling) throw new Error("Extension file exceeds its regular-file bound");
32
+ const buffer = Buffer.alloc(ceiling + 1);
33
+ let read = 0;
34
+ while (read <= ceiling) {
35
+ const next = await file.read(buffer, read, buffer.length - read, read);
36
+ if (!next.bytesRead) break;
37
+ read += next.bytesRead;
38
+ }
39
+ if (read > ceiling) throw new Error("Extension file grew beyond its bound");
40
+ return buffer.subarray(0, read);
41
+ } finally {
42
+ await file.close();
43
+ }
44
+ }
45
+ function negotiateExtension(manifest) {
46
+ if (manifest.api.major !== EXTENSION_API.major || manifest.api.minimumMinor > EXTENSION_API.minor) throw new Error(`Extension requires API ${manifest.api.major}.${manifest.api.minimumMinor}; host supports ${EXTENSION_API.major}.${EXTENSION_API.minor}`);
47
+ const missing = manifest.requires.filter((name) => !EXTENSION_API.capabilities.includes(name));
48
+ if (missing.length) throw new Error(`Unsupported extension capabilities: ${missing.join(", ")}`);
49
+ return { major: EXTENSION_API.major, minor: EXTENSION_API.minor };
50
+ }
51
+ function parseManifests(value) {
52
+ if (!object(value) || value.version !== 1 || !Array.isArray(value.extensions) || value.extensions.length > 16) throw new Error("Configure version 1 with at most 16 explicit extensions");
53
+ const ids = /* @__PURE__ */ new Set();
54
+ return value.extensions.map((raw) => {
55
+ if (!object(raw) || Object.keys(raw).some((key) => !["id", "version", "module", "sha256", "api", "requires"].includes(key)) || typeof raw.id !== "string" || !identifier.test(raw.id) || ids.has(raw.id) || typeof raw.version !== "string" || !/^\d+\.\d+\.\d+(?:-[\w.-]+)?$/.test(raw.version) || !bounded(raw.module, 4e3) || !isAbsolute(raw.module) || !/\.mjs$/.test(raw.module) || typeof raw.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(raw.sha256) || !object(raw.api) || Object.keys(raw.api).some((key) => !["major", "minimumMinor"].includes(key)) || !Number.isInteger(raw.api.major) || !Number.isInteger(raw.api.minimumMinor) || raw.api.minimumMinor < 0 || !Array.isArray(raw.requires) || raw.requires.length > 16 || raw.requires.some((item) => !bounded(item, 100))) throw new Error("Invalid extension identity, pinned bundle, version or capability requirements");
56
+ ids.add(raw.id);
57
+ return structuredClone(raw);
58
+ });
59
+ }
60
+ class Extensions {
61
+ constructor(options) {
62
+ this.options = options;
63
+ }
64
+ statuses = [];
65
+ list() {
66
+ return redactExport(structuredClone(this.statuses));
67
+ }
68
+ async load() {
69
+ const environment = this.options.environment ?? process.env;
70
+ registerPrivateEnvNames(["CODEFLOW_EXTENSIONS_FILE", "CODEFLOW_EXTENSIONS_JSON"]);
71
+ if (environment.CODEFLOW_EXTENSIONS_FILE && environment.CODEFLOW_EXTENSIONS_JSON) throw new Error("Choose one extension configuration source");
72
+ let text = environment.CODEFLOW_EXTENSIONS_JSON;
73
+ if (environment.CODEFLOW_EXTENSIONS_FILE) {
74
+ const path = environment.CODEFLOW_EXTENSIONS_FILE;
75
+ if (!isAbsolute(path)) throw new Error("Extension configuration must have an absolute path");
76
+ text = (await readBundle(path, 128 * 1024)).toString("utf8");
77
+ }
78
+ if (!text) return;
79
+ if (Buffer.byteLength(text) > 128 * 1024) throw new Error("Extension configuration exceeds 128 KiB");
80
+ const manifests = parseManifests(JSON.parse(text));
81
+ for (const manifest of manifests) {
82
+ const status = { id: manifest.id, version: manifest.version, sha256: manifest.sha256, state: "rejected", providers: [], workflows: [] };
83
+ this.statuses.push(status);
84
+ try {
85
+ const negotiated = negotiateExtension(manifest);
86
+ const info = await lstat(manifest.module);
87
+ if (!info.isFile() || info.isSymbolicLink() || info.size > 512 * 1024) throw new Error("Plugin must be a bundled regular .mjs file under 512 KiB");
88
+ const contents = await readBundle(manifest.module, 512 * 1024);
89
+ if (contents.length > 512 * 1024 || createHash("sha256").update(contents).digest("hex") !== manifest.sha256) throw new Error("Plugin bundle changed; review it and update its SHA-256 pin explicitly");
90
+ const directory = join(this.options.home ?? CODEFLOW_HOME, "extension-cache");
91
+ await mkdir(directory, { recursive: true, mode: 448 });
92
+ const cacheInfo = await lstat(directory);
93
+ if (!cacheInfo.isDirectory() || cacheInfo.isSymbolicLink()) throw new Error("Extension cache must be a regular private directory");
94
+ const cached = join(directory, `${manifest.id}-${manifest.sha256}.mjs`), temp = cached + `.${process.pid}.tmp`;
95
+ try {
96
+ const file = await open(temp, "wx", 384);
97
+ try {
98
+ await file.writeFile(contents);
99
+ await file.sync();
100
+ } finally {
101
+ await file.close();
102
+ }
103
+ await rename(temp, cached);
104
+ } finally {
105
+ await unlink(temp).catch(() => void 0);
106
+ }
107
+ const extension = (await import(pathToFileURL(cached).href)).default;
108
+ if (!object(extension) || !object(extension.api) || extension.api.major !== negotiated.major || !Number.isInteger(extension.api.minor) || extension.api.minor < manifest.api.minimumMinor || extension.api.minor > negotiated.minor || Object.keys(extension).some((key) => !["api", "providers", "workflows"].includes(key))) throw new Error("Plugin export does not match its negotiated API");
109
+ const providers = extension.providers ?? [], workflows = extension.workflows ?? [];
110
+ if (!Array.isArray(providers) || !Array.isArray(workflows) || providers.length > 8 || workflows.length > 8 || !providers.length && !workflows.length) throw new Error("Plugin must export 1\u20138 providers or workflows");
111
+ for (const provider of providers) if (!object(provider) || !new RegExp(`^plugin:${manifest.id}/[a-z][a-z0-9-]{0,40}$`).test(provider.id) || !bounded(provider.displayName, 120) || !bounded(provider.binary, 200) || ["available", "catalog", "run"].some((key) => typeof provider[key] !== "function")) throw new Error("Provider needs a namespaced ID and the AgentAdapter contract");
112
+ for (const workflow of workflows) {
113
+ if (!object(workflow) || !new RegExp(`^plugin:${manifest.id}/[a-z][a-z0-9-]{0,40}$`).test(workflow.id) || !bounded(workflow.name, 120) || !bounded(workflow.summary, 500) || !bounded(workflow.description, 8e3) || typeof workflow.run !== "function" || !Array.isArray(workflow.roles) || !workflow.roles.length || workflow.roles.length > 16 || !Array.isArray(workflow.options) || workflow.options.length > 16) throw new Error("Workflow needs a namespaced ID and the Pattern contract");
114
+ const roles = /* @__PURE__ */ new Set();
115
+ for (const role of workflow.roles) {
116
+ if (!object(role) || !identifier.test(role.id) || roles.has(role.id) || !bounded(role.label, 120) || !bounded(role.description, 1e3) || !Array.isArray(role.default) || role.default.length > 6 || role.default.some((id) => typeof id !== "string")) throw new Error("Invalid workflow role");
117
+ roles.add(role.id);
118
+ }
119
+ const options = /* @__PURE__ */ new Set();
120
+ for (const option of workflow.options) {
121
+ if (!object(option) || !identifier.test(option.id) || options.has(option.id) || !bounded(option.label, 120) || !["number", "boolean"].includes(option.type) || typeof option.default !== option.type || option.type === "number" && (!Number.isFinite(option.default) || option.min !== void 0 && !Number.isFinite(option.min) || option.max !== void 0 && !Number.isFinite(option.max) || option.min !== void 0 && option.max !== void 0 && option.min > option.max)) throw new Error("Invalid workflow option");
122
+ options.add(option.id);
123
+ }
124
+ }
125
+ this.options.register(providers, workflows);
126
+ const binding = { id: manifest.id, version: manifest.version, sha256: manifest.sha256 };
127
+ for (const value of [...providers, ...workflows]) bindings.set(value.id, binding);
128
+ status.state = "loaded";
129
+ status.api = negotiated;
130
+ status.providers = providers.map((provider) => provider.id);
131
+ status.workflows = workflows.map((workflow) => workflow.id);
132
+ } catch (error) {
133
+ status.error = error.message.slice(0, 1e3);
134
+ }
135
+ }
136
+ }
137
+ }
138
+ export {
139
+ EXTENSION_API,
140
+ Extensions,
141
+ assertExtensionBindings,
142
+ extensionBindings,
143
+ negotiateExtension
144
+ };
@@ -0,0 +1,107 @@
1
+ import { FilePolicyError, policyIsRestrictive } from "./filesystem-policy.js";
2
+ import { readJson } from "./http.js";
3
+ import { repoRoot } from "./git.js";
4
+ class FilesystemAccess {
5
+ constructor(store, projects, manager) {
6
+ this.store = store;
7
+ this.projects = projects;
8
+ this.manager = manager;
9
+ store.onChange((next) => this.cache.set(next.projectId, next));
10
+ }
11
+ cache = /* @__PURE__ */ new Map();
12
+ operations = /* @__PURE__ */ new Set();
13
+ activating = false;
14
+ failure;
15
+ async all() {
16
+ try {
17
+ const policies = await Promise.all(this.projects.list().map(async (project) => {
18
+ const policy = await this.store.get(project.id);
19
+ this.cache.set(project.id, policy);
20
+ return policy;
21
+ }));
22
+ this.failure = void 0;
23
+ return policies;
24
+ } catch (error) {
25
+ this.failure = error;
26
+ throw error;
27
+ }
28
+ }
29
+ hasRestrictions() {
30
+ return !!this.failure || [...this.cache.values()].some(policyIsRestrictive);
31
+ }
32
+ projectIds(repo, run) {
33
+ const related = /* @__PURE__ */ new Set([repo]);
34
+ const owned = this.manager.relatedRuns(repo);
35
+ for (const item of owned) related.add(item.repo);
36
+ return [.../* @__PURE__ */ new Set([...this.projects.list().filter((project) => project.repos.some((repo2) => related.has(repo2))).map((project) => project.id), ...owned.flatMap((item) => item.project ? [item.project] : []), ...run?.project ? [run.project] : []])];
37
+ }
38
+ async forRepo(repo, run) {
39
+ const policies = await Promise.all(this.projectIds(repo, run).map(async (id) => {
40
+ const policy = await this.store.get(id);
41
+ this.cache.set(id, policy);
42
+ return policy;
43
+ }));
44
+ return [...policies, ...run?.filePolicies ?? []];
45
+ }
46
+ async execution(repo, run) {
47
+ if (this.activating) throw new FilePolicyError("Filesystem policy is being applied; execution preparation is paused", 409);
48
+ return [...await this.all(), ...await this.forRepo(repo, run)];
49
+ }
50
+ restrictedRun(run) {
51
+ const pinned = !!run.filePolicies?.some(policyIsRestrictive);
52
+ if (!this.hasRestrictions()) return pinned;
53
+ return pinned || this.projectIds(run.repo, run).some((id) => {
54
+ const policy = this.cache.get(id);
55
+ return !!policy && policyIsRestrictive(policy);
56
+ });
57
+ }
58
+ list() {
59
+ return this.manager.list().filter((run) => !this.restrictedRun(run));
60
+ }
61
+ page(input = {}) {
62
+ return this.manager.page({ ...input, ...this.hasRestrictions() ? { predicate: (run) => !this.restrictedRun(run) && (input.predicate?.(run) ?? true) } : {} });
63
+ }
64
+ end(req) {
65
+ this.operations.delete(req);
66
+ }
67
+ async sharedGate(req) {
68
+ if (this.activating) throw new FilePolicyError("Filesystem policy is being applied; retry after it finishes", 409);
69
+ this.operations.add(req);
70
+ await this.all();
71
+ }
72
+ async update(projectId, input, activate) {
73
+ if (this.activating || this.operations.size) throw new FilePolicyError("Wait for active application operations to finish before changing filesystem policy", 409);
74
+ this.activating = true;
75
+ try {
76
+ return await this.store.set(projectId, input, async (commit, next) => activate(commit, next));
77
+ } finally {
78
+ this.activating = false;
79
+ }
80
+ }
81
+ /** Register before awaiting body/policy reads. Policy activation cannot race an in-flight command request. */
82
+ async ownerGate(req, url, parts) {
83
+ if (parts[1] === "projects" && parts[3] === "filesystem-policy") return;
84
+ if (this.activating) throw new FilePolicyError("Filesystem policy is being applied; retry after it finishes", 409);
85
+ this.operations.add(req);
86
+ const all = await this.all();
87
+ const globallyRestricted = all.some(policyIsRestrictive);
88
+ const body = ["POST", "PUT", "PATCH"].includes(req.method ?? "") && req.headers["content-type"]?.startsWith("application/json") ? await readJson(req) : {};
89
+ const requested = typeof body.repo === "string" ? body.repo : url.searchParams.get("repo");
90
+ const selected = requested?.startsWith("/") ? await repoRoot(requested).catch(() => requested) : requested;
91
+ const run = parts[1] === "runs" && parts[2] ? await this.manager.getAsync(parts[2]) : void 0;
92
+ const policyProject = parts[1] === "projects" && parts[2] ? this.cache.get(parts[2]) : typeof body.project === "string" ? this.cache.get(body.project) : void 0;
93
+ const targetRestricted = !!run && this.restrictedRun(run) || !!policyProject && policyIsRestrictive(policyProject) || !!selected && (await this.forRepo(selected)).some(policyIsRestrictive);
94
+ const fileApi = parts[1] === "file" && req.method === "GET" || parts[1] === "workspace" && parts[2] === "file" && ["GET", "POST"].includes(req.method ?? "");
95
+ const safeSearch = req.method === "GET" && (["tree", "search"].includes(parts[1] ?? "") || parts[1] === "workspace" && parts[2] === "search" && url.searchParams.get("mode") !== "semantic");
96
+ const filteredDiff = req.method === "GET" && parts[1] === "runs" && parts[3] === "diff";
97
+ if (targetRestricted && !fileApi && !safeSearch && !filteredDiff) throw new FilePolicyError("This project's filesystem policy blocks raw history, configuration and operations that cannot enforce its paths", req.method === "GET" ? 403 : 409);
98
+ if (globallyRestricted) {
99
+ const globalPrivate = ["docs-help", "experience", "journeys", "fs", "notes", "memory", "stats", "automations", "deferred-runs", "coordination", "coordinated-changes", "project-teams", "notifications", "maintenance", "diagnostics", "shells", "serve", "preview", "snapshot", "disk", "cleanup", "models", "doctor", "approvals", "team-capabilities", "team-evaluations", "team-rollout"].includes(parts[1] ?? "");
100
+ const execution = req.method !== "GET" && (["acp-connections", "runs", "readiness", "verification", "compare", "sweeps", "automations", "deferred-runs", "coordination", "coordinated-changes", "project-teams", "remote-workers", "code-hosts", "improve-prompt", "team-link"].includes(parts[1] ?? "") || parts[1] === "workspace" && !fileApi);
101
+ if (globalPrivate || execution || parts[1] === "workspace" && ["references", "dependencies"].includes(parts[2] ?? "") || parts[1] === "workspace" && parts[2] === "search" && url.searchParams.get("mode") === "semantic") throw new FilePolicyError("Custom filesystem restrictions pause app-managed execution and unscoped private data access globally because current backends share host authority", req.method === "GET" ? 403 : 409);
102
+ }
103
+ }
104
+ }
105
+ export {
106
+ FilesystemAccess
107
+ };
@@ -0,0 +1,238 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat, mkdir, open, realpath, rename, rm } from "node:fs/promises";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import { CODEFLOW_HOME } from "./config.js";
6
+ import { sensitiveContextPath } from "./context-inspection.js";
7
+ class FilePolicyError extends Error {
8
+ constructor(message, status = 403, current) {
9
+ super(message);
10
+ this.status = status;
11
+ this.current = current;
12
+ }
13
+ }
14
+ const queues = /* @__PURE__ */ new Map();
15
+ const object = (value) => !!value && typeof value === "object" && !Array.isArray(value);
16
+ const sha = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
17
+ function normalizePolicyPath(raw) {
18
+ if (typeof raw !== "string" || raw.length > 500 || /[\\\x00-\x1f\x7f*?\[\]{}]/.test(raw) || raw.startsWith("/") || /^[a-z]:/i.test(raw)) throw new FilePolicyError("Policy paths must be literal relative repository paths", 400);
19
+ if (raw === ".") return ".";
20
+ const path = raw.replace(/\/$/, "");
21
+ if (!path || path.split("/").some((part) => !part || part === "." || part === ".." || part.toLowerCase() === ".git")) throw new FilePolicyError("Paths must stay inside the repository; traversal, empty components and Git internals are not allowed", 400);
22
+ return path;
23
+ }
24
+ function rules(raw) {
25
+ if (!object(raw) || Object.keys(raw).some((key) => !["allow", "deny"].includes(key))) throw new FilePolicyError("Choose explicit allow and deny path lists", 400);
26
+ const list = (value) => {
27
+ if (!Array.isArray(value) || value.length > 100) throw new FilePolicyError("Use at most 100 literal paths per list", 400);
28
+ return [...new Set(value.map(normalizePolicyPath))].sort();
29
+ };
30
+ return { allow: list(raw.allow), deny: list(raw.deny) };
31
+ }
32
+ function snapshot(projectId, revision, read, write, updatedAt) {
33
+ const base = { version: 1, projectId, revision, read, write, updatedAt };
34
+ return { ...base, hash: sha(base) };
35
+ }
36
+ function defaults(projectId) {
37
+ return snapshot(projectId, 0, { allow: ["."], deny: [] }, { allow: ["."], deny: [] }, 0);
38
+ }
39
+ function validateFilePolicySnapshot(raw) {
40
+ if (!object(raw) || Object.keys(raw).some((key) => !["version", "projectId", "revision", "read", "write", "hash", "updatedAt"].includes(key)) || raw.version !== 1 || typeof raw.projectId !== "string" || !/^[a-zA-Z0-9_-]{1,100}$/.test(raw.projectId) || !Number.isSafeInteger(raw.revision) || raw.revision < 0 || !Number.isSafeInteger(raw.updatedAt) || raw.updatedAt < 0) throw new FilePolicyError("Invalid filesystem policy snapshot", 503);
41
+ const value = snapshot(raw.projectId, raw.revision, rules(raw.read), rules(raw.write), raw.updatedAt);
42
+ if (value.hash !== raw.hash) throw new FilePolicyError("Filesystem policy snapshot hash changed", 503);
43
+ return value;
44
+ }
45
+ const contains = (root, path) => root === "." || path === root || path.startsWith(`${root}/`);
46
+ const folded = (path) => path.normalize("NFKC").toUpperCase().toLowerCase();
47
+ function policyIsRestrictive(policy) {
48
+ return [policy.read, policy.write].some((rules2) => rules2.allow.length !== 1 || rules2.allow[0] !== "." || rules2.deny.length > 0);
49
+ }
50
+ function allowsFilePolicy(policy, operation, rawPath) {
51
+ let path;
52
+ try {
53
+ path = normalizePolicyPath(rawPath);
54
+ } catch {
55
+ return false;
56
+ }
57
+ if (path !== "." && sensitiveContextPath(path)) return false;
58
+ const rules2 = policy[operation];
59
+ return rules2.allow.some((root) => contains(root, path)) && !rules2.deny.some((root) => contains(folded(root), folded(path)));
60
+ }
61
+ function assertPolicyExecution(policies, surface) {
62
+ const restricted = policies.filter(policyIsRestrictive);
63
+ if (restricted.length) throw new FilePolicyError(`${surface} cannot enforce this project's filesystem restrictions. Host, native CLI, Docker and SSH execution remain disabled until an enforcing filtered backend is available. Review the policy deliberately before choosing unrestricted execution.`, 409);
64
+ }
65
+ function filterPolicyDiff(policies, files) {
66
+ return files.filter((file) => policies.every((policy) => allowsFilePolicy(policy, "read", file.path) && (!file.oldPath || allowsFilePolicy(policy, "read", file.oldPath))));
67
+ }
68
+ let resolver;
69
+ let suppressRepositoryCommands;
70
+ function configureFilePolicyResolver(next, options = {}) {
71
+ resolver = next;
72
+ suppressRepositoryCommands = options.suppressRepositoryCommands;
73
+ return () => {
74
+ if (resolver === next) {
75
+ resolver = void 0;
76
+ suppressRepositoryCommands = void 0;
77
+ }
78
+ };
79
+ }
80
+ function filePolicyRegistryActive() {
81
+ return !!resolver;
82
+ }
83
+ function filePolicyMetadataOnly() {
84
+ return suppressRepositoryCommands?.() ?? false;
85
+ }
86
+ async function filterAllowedProjectEntries(repo, entries) {
87
+ if (!resolver) return entries;
88
+ const policies = await resolver(repo);
89
+ return entries.filter((entry) => !sensitiveContextPath(entry.path) && policies.every((policy) => allowsFilePolicy(policy, "read", entry.path) || entry.type === "dir" && policy.read.allow.some((root) => root.startsWith(`${entry.path}/`)) && !policy.read.deny.some((root) => contains(folded(root), folded(entry.path)))));
90
+ }
91
+ async function assertNoSymbolicAncestor(repo, path) {
92
+ const root = await realpath(repo);
93
+ let target = root;
94
+ for (const part of path === "." ? [] : path.split("/")) {
95
+ target = join(target, part);
96
+ let info;
97
+ try {
98
+ info = await lstat(target);
99
+ } catch (error) {
100
+ if (error.code === "ENOENT") return;
101
+ throw error;
102
+ }
103
+ if (info.isSymbolicLink()) throw new FilePolicyError("Symbolic paths cannot bypass filesystem policy");
104
+ }
105
+ }
106
+ async function assertProjectFileAccess(repo, rawPath, operation) {
107
+ if (!resolver) return;
108
+ const path = normalizePolicyPath(rawPath), policies = await resolver(repo);
109
+ if (path !== "." && sensitiveContextPath(path) || policies.some((policy) => !allowsFilePolicy(policy, operation, path))) throw new FilePolicyError(`This project's filesystem policy denies ${operation} access to this path`);
110
+ await assertNoSymbolicAncestor(repo, path);
111
+ }
112
+ async function assertProjectDirectoryAccess(repo, rawPath) {
113
+ if (!resolver) return;
114
+ const path = normalizePolicyPath(rawPath), policies = await resolver(repo);
115
+ const traversable = (policy) => !policy.read.deny.some((root) => contains(folded(root), folded(path))) && (allowsFilePolicy(policy, "read", path) || policy.read.allow.some((root) => path === "." || root.startsWith(`${path}/`)));
116
+ if (path !== "." && sensitiveContextPath(path) || policies.some((policy) => !traversable(policy))) throw new FilePolicyError("This project's filesystem policy denies directory access to this path");
117
+ await assertNoSymbolicAncestor(repo, path);
118
+ }
119
+ class ProjectFilePolicies {
120
+ constructor(home = CODEFLOW_HOME) {
121
+ this.home = home;
122
+ }
123
+ listeners = /* @__PURE__ */ new Set();
124
+ path(projectId) {
125
+ if (!/^[a-zA-Z0-9_-]{1,100}$/.test(projectId)) throw new FilePolicyError("Invalid project identity", 400);
126
+ return resolve(this.home, "filesystem-policies", `${projectId}.json`);
127
+ }
128
+ onChange(listener) {
129
+ this.listeners.add(listener);
130
+ return () => this.listeners.delete(listener);
131
+ }
132
+ async get(projectId) {
133
+ return this.read(projectId, this.path(projectId));
134
+ }
135
+ async set(projectId, input, activate = async (commit) => commit()) {
136
+ const file = this.path(projectId);
137
+ const work = (queues.get(file) ?? Promise.resolve()).catch(() => void 0).then(async () => {
138
+ const previous = await this.read(projectId, file);
139
+ if (!Number.isSafeInteger(input.revision) || input.revision !== previous.revision) throw new FilePolicyError("Filesystem policy changed; reload before saving", 409, previous);
140
+ const next = snapshot(projectId, previous.revision + 1, rules(input.read), rules(input.write), Date.now());
141
+ let committed = false;
142
+ await activate(async () => {
143
+ if (committed) throw new FilePolicyError("Policy activation may commit only once", 409);
144
+ const latest = await this.read(projectId, file);
145
+ if (latest.hash !== previous.hash) throw new FilePolicyError("Filesystem policy changed during activation", 409, latest);
146
+ await this.write(file, next);
147
+ committed = true;
148
+ for (const listener of this.listeners) {
149
+ try {
150
+ listener(structuredClone(next), structuredClone(previous));
151
+ } catch {
152
+ }
153
+ }
154
+ }, structuredClone(next), structuredClone(previous));
155
+ if (!committed) throw new FilePolicyError("Filesystem policy was not activated", 409);
156
+ return structuredClone(next);
157
+ });
158
+ queues.set(file, work);
159
+ try {
160
+ return await work;
161
+ } finally {
162
+ if (queues.get(file) === work) queues.delete(file);
163
+ }
164
+ }
165
+ async read(projectId, file) {
166
+ try {
167
+ await this.safeParent(file);
168
+ const handle = await open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
169
+ try {
170
+ const info = await handle.stat();
171
+ if (!info.isFile() || info.size > 256 * 1024) throw new Error("Policy must be a bounded regular file");
172
+ const raw = JSON.parse(await handle.readFile("utf8"));
173
+ if (!object(raw) || Object.keys(raw).some((key) => !["version", "projectId", "revision", "read", "write", "hash", "updatedAt"].includes(key)) || raw.version !== 1 || raw.projectId !== projectId || !Number.isSafeInteger(raw.revision) || raw.revision < 1 || !Number.isSafeInteger(raw.updatedAt) || raw.updatedAt < 0) throw new Error("Invalid policy version or metadata");
174
+ const value = snapshot(projectId, raw.revision, rules(raw.read), rules(raw.write), raw.updatedAt);
175
+ if (raw.hash !== value.hash) throw new Error("Policy content hash changed");
176
+ return value;
177
+ } finally {
178
+ await handle.close();
179
+ }
180
+ } catch (error) {
181
+ if (error.code === "ENOENT") return defaults(projectId);
182
+ throw new FilePolicyError(`Filesystem policy cannot be read; restricted operations are unavailable: ${error.message}`, 503);
183
+ }
184
+ }
185
+ async safeParent(file) {
186
+ const parent = dirname(file);
187
+ let info;
188
+ try {
189
+ info = await lstat(parent);
190
+ } catch (error) {
191
+ if (error.code === "ENOENT") return;
192
+ throw error;
193
+ }
194
+ if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("Policy storage cannot be a symbolic directory");
195
+ }
196
+ async write(file, value) {
197
+ await mkdir(dirname(file), { recursive: true, mode: 448 });
198
+ await this.safeParent(file);
199
+ const temp = `${file}.${randomUUID()}.tmp`;
200
+ try {
201
+ const handle = await open(temp, "wx", 384);
202
+ try {
203
+ await handle.writeFile(JSON.stringify(value));
204
+ await handle.sync();
205
+ } finally {
206
+ await handle.close();
207
+ }
208
+ await rename(temp, file);
209
+ if (process.platform !== "win32") {
210
+ const parent = await open(dirname(file), "r");
211
+ try {
212
+ await parent.sync();
213
+ } finally {
214
+ await parent.close();
215
+ }
216
+ }
217
+ } finally {
218
+ await rm(temp, { force: true }).catch(() => {
219
+ });
220
+ }
221
+ }
222
+ }
223
+ export {
224
+ FilePolicyError,
225
+ ProjectFilePolicies,
226
+ allowsFilePolicy,
227
+ assertPolicyExecution,
228
+ assertProjectDirectoryAccess,
229
+ assertProjectFileAccess,
230
+ configureFilePolicyResolver,
231
+ filePolicyMetadataOnly,
232
+ filePolicyRegistryActive,
233
+ filterAllowedProjectEntries,
234
+ filterPolicyDiff,
235
+ normalizePolicyPath,
236
+ policyIsRestrictive,
237
+ validateFilePolicySnapshot
238
+ };