@turri-dev/cli 0.0.0-stage → 0.1.1

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 (315) hide show
  1. package/.pi/themes/turri-dark.json +80 -0
  2. package/.pi/themes/turri-light.json +82 -0
  3. package/LICENSE +21 -0
  4. package/README.md +272 -2
  5. package/assets/jellyfish/dark-00.png +0 -0
  6. package/assets/jellyfish/dark-01.png +0 -0
  7. package/assets/jellyfish/dark-02.png +0 -0
  8. package/assets/jellyfish/dark-03.png +0 -0
  9. package/assets/jellyfish/dark-04.png +0 -0
  10. package/assets/jellyfish/dark-05.png +0 -0
  11. package/assets/jellyfish/dark-06.png +0 -0
  12. package/assets/jellyfish/dark-07.png +0 -0
  13. package/assets/jellyfish/dark-08.png +0 -0
  14. package/assets/jellyfish/dark-09.png +0 -0
  15. package/assets/jellyfish/dark-10.png +0 -0
  16. package/assets/jellyfish/dark-11.png +0 -0
  17. package/assets/jellyfish/dark-12.png +0 -0
  18. package/assets/jellyfish/dark-13.png +0 -0
  19. package/assets/jellyfish/dark-14.png +0 -0
  20. package/assets/jellyfish/fallback.txt +252 -0
  21. package/assets/jellyfish/light-00.png +0 -0
  22. package/assets/jellyfish/light-01.png +0 -0
  23. package/assets/jellyfish/light-02.png +0 -0
  24. package/assets/jellyfish/light-03.png +0 -0
  25. package/assets/jellyfish/light-04.png +0 -0
  26. package/assets/jellyfish/light-05.png +0 -0
  27. package/assets/jellyfish/light-06.png +0 -0
  28. package/assets/jellyfish/light-07.png +0 -0
  29. package/assets/jellyfish/light-08.png +0 -0
  30. package/assets/jellyfish/light-09.png +0 -0
  31. package/assets/jellyfish/light-10.png +0 -0
  32. package/assets/jellyfish/light-11.png +0 -0
  33. package/assets/jellyfish/light-12.png +0 -0
  34. package/assets/jellyfish/light-13.png +0 -0
  35. package/assets/jellyfish/light-14.png +0 -0
  36. package/bin/turri.mjs +29 -0
  37. package/dist/ask.js +149 -0
  38. package/dist/brand.js +50 -0
  39. package/dist/btw.js +174 -0
  40. package/dist/chat-models.js +37 -0
  41. package/dist/chat-skills.js +18 -0
  42. package/dist/cli.js +178 -0
  43. package/dist/config-command.js +197 -0
  44. package/dist/config-edit.js +367 -0
  45. package/dist/config.js +489 -0
  46. package/dist/conform-cli.js +29 -0
  47. package/dist/cultivate-cli.js +110 -0
  48. package/dist/drift-cli.js +25 -0
  49. package/dist/environment.js +70 -0
  50. package/dist/extension.js +2372 -0
  51. package/dist/genome.js +301 -0
  52. package/dist/graph.js +246 -0
  53. package/dist/identity.js +35 -0
  54. package/dist/init-config.js +511 -0
  55. package/dist/judge.js +62 -0
  56. package/dist/lineage-cli.js +13 -0
  57. package/dist/loop-cli.js +132 -0
  58. package/dist/loop-tools.js +225 -0
  59. package/dist/mature-tools.js +193 -0
  60. package/dist/model-check.js +117 -0
  61. package/dist/module-report.js +9 -0
  62. package/dist/pi-launch.js +31 -0
  63. package/dist/pi.js +813 -0
  64. package/dist/plain/check.js +76 -0
  65. package/dist/plain/concepts.js +213 -0
  66. package/dist/plain/errors.js +43 -0
  67. package/dist/plain/files.js +57 -0
  68. package/dist/plain/highlight.js +195 -0
  69. package/dist/plain/index.js +15 -0
  70. package/dist/plain/liquid.js +129 -0
  71. package/dist/plain/markdown.js +208 -0
  72. package/dist/plain/parser.js +752 -0
  73. package/dist/plain/plain.tmLanguage.json +124 -0
  74. package/dist/plain/spec.js +291 -0
  75. package/dist/plain/stdlib.js +44 -0
  76. package/dist/question-dialog.js +125 -0
  77. package/dist/questioning.js +109 -0
  78. package/dist/quiet.js +125 -0
  79. package/dist/regenerate/activity.js +187 -0
  80. package/dist/regenerate/agent-env.js +190 -0
  81. package/dist/regenerate/agent-shell.js +114 -0
  82. package/dist/regenerate/arbiter.js +133 -0
  83. package/dist/regenerate/audit.js +203 -0
  84. package/dist/regenerate/cases.js +83 -0
  85. package/dist/regenerate/change-view.js +102 -0
  86. package/dist/regenerate/choose-conformance.js +117 -0
  87. package/dist/regenerate/chunk-plan.js +625 -0
  88. package/dist/regenerate/codebase.js +75 -0
  89. package/dist/regenerate/command-check.js +312 -0
  90. package/dist/regenerate/compaction.js +20 -0
  91. package/dist/regenerate/confine.js +310 -0
  92. package/dist/regenerate/conform.js +133 -0
  93. package/dist/regenerate/conformance-agents.js +955 -0
  94. package/dist/regenerate/conformance.js +936 -0
  95. package/dist/regenerate/conversation-writes.js +100 -0
  96. package/dist/regenerate/credentials.js +88 -0
  97. package/dist/regenerate/debug-log.js +228 -0
  98. package/dist/regenerate/debugger.js +90 -0
  99. package/dist/regenerate/delivery.js +19 -0
  100. package/dist/regenerate/diagnose-job.js +40 -0
  101. package/dist/regenerate/diagnose.js +81 -0
  102. package/dist/regenerate/diff-view.js +240 -0
  103. package/dist/regenerate/drift.js +194 -0
  104. package/dist/regenerate/engine.js +118 -0
  105. package/dist/regenerate/environment-error.js +220 -0
  106. package/dist/regenerate/job-log.js +102 -0
  107. package/dist/regenerate/job-panel.js +336 -0
  108. package/dist/regenerate/jobs-tree.js +300 -0
  109. package/dist/regenerate/jobs.js +427 -0
  110. package/dist/regenerate/lanes.js +32 -0
  111. package/dist/regenerate/lineage.js +384 -0
  112. package/dist/regenerate/loop.js +1435 -0
  113. package/dist/regenerate/masking.js +95 -0
  114. package/dist/regenerate/notes.js +397 -0
  115. package/dist/regenerate/os-sandbox.js +243 -0
  116. package/dist/regenerate/paths.js +165 -0
  117. package/dist/regenerate/plain-palette.js +83 -0
  118. package/dist/regenerate/prompt.js +704 -0
  119. package/dist/regenerate/records.js +294 -0
  120. package/dist/regenerate/regenerate.js +598 -0
  121. package/dist/regenerate/regression.js +181 -0
  122. package/dist/regenerate/release.js +262 -0
  123. package/dist/regenerate/requires.js +241 -0
  124. package/dist/regenerate/resources.js +164 -0
  125. package/dist/regenerate/self-check.js +111 -0
  126. package/dist/regenerate/services.js +155 -0
  127. package/dist/regenerate/shell.js +319 -0
  128. package/dist/regenerate/side-panel.js +278 -0
  129. package/dist/regenerate/spec-map-view.js +574 -0
  130. package/dist/regenerate/spec-map.js +561 -0
  131. package/dist/regenerate/spec-steps.js +527 -0
  132. package/dist/regenerate/state.js +10 -0
  133. package/dist/regenerate/steer.js +91 -0
  134. package/dist/regenerate/steering.js +119 -0
  135. package/dist/regenerate/stop-report.js +161 -0
  136. package/dist/regenerate/submit-work.js +71 -0
  137. package/dist/regenerate/test-kinds.js +124 -0
  138. package/dist/regenerate/test-scripts.js +695 -0
  139. package/dist/regenerate/testing.js +406 -0
  140. package/dist/regenerate/transcript-view.js +1 -0
  141. package/dist/regenerate/unit-gate.js +20 -0
  142. package/dist/regenerate/unit-review.js +347 -0
  143. package/dist/regenerate/web.js +256 -0
  144. package/dist/regenerate/workspace.js +99 -0
  145. package/dist/regenerate-cli.js +68 -0
  146. package/dist/release-agent.js +114 -0
  147. package/dist/release-cli.js +131 -0
  148. package/dist/release-tools.js +148 -0
  149. package/dist/review/live.js +165 -0
  150. package/dist/rules.js +47 -0
  151. package/dist/self-invocation.js +110 -0
  152. package/dist/setup.js +245 -0
  153. package/dist/skill-progress.js +78 -0
  154. package/dist/skill.js +149 -0
  155. package/dist/skills.js +74 -0
  156. package/dist/spec-diff-cli.js +37 -0
  157. package/dist/spec-tools.js +155 -0
  158. package/dist/stack.js +158 -0
  159. package/dist/stacks.js +97 -0
  160. package/dist/stdlib.js +100 -0
  161. package/dist/terminal-images.js +46 -0
  162. package/dist/tools.js +242 -0
  163. package/dist/validate-cli.js +52 -0
  164. package/docs/README.md +58 -0
  165. package/docs/authoring-plain.md +245 -0
  166. package/docs/config.md +353 -0
  167. package/docs/deploy.md +109 -0
  168. package/docs/development.md +75 -0
  169. package/docs/headless.md +148 -0
  170. package/docs/images/how-turri-works.svg +50 -0
  171. package/docs/images/turri-banner.png +0 -0
  172. package/docs/installation.md +118 -0
  173. package/docs/parser.md +108 -0
  174. package/docs/regenerating.md +1116 -0
  175. package/docs/sandbox.md +233 -0
  176. package/docs/skills-and-gates.md +254 -0
  177. package/docs/theming.md +76 -0
  178. package/docs/using.md +454 -0
  179. package/genome/rules/bullet-continuation.md +69 -0
  180. package/genome/rules/concise-specs-examples.md +431 -0
  181. package/genome/rules/concise-specs-integration-examples.md +270 -0
  182. package/genome/rules/concise-specs.md +80 -0
  183. package/genome/rules/definitions.md +107 -0
  184. package/genome/rules/examples/integration-embedded-testing/run_conformance_tests_java.sh +163 -0
  185. package/genome/rules/examples/integration-embedded-testing/run_unittests_java.sh +51 -0
  186. package/genome/rules/exported-concepts.md +39 -0
  187. package/genome/rules/func-specs.md +129 -0
  188. package/genome/rules/impl-reqs.md +68 -0
  189. package/genome/rules/import-modules.md +48 -0
  190. package/genome/rules/integration-embedded-testing.md +276 -0
  191. package/genome/rules/integration-embedded.md +171 -0
  192. package/genome/rules/integration-standalone.md +135 -0
  193. package/genome/rules/integrations.md +137 -0
  194. package/genome/rules/linked-resources.md +69 -0
  195. package/genome/rules/module-structure.md +105 -0
  196. package/genome/rules/project-model.md +88 -0
  197. package/genome/rules/required-concepts.md +44 -0
  198. package/genome/rules/requires-modules.md +80 -0
  199. package/genome/rules/stdlib.md +73 -0
  200. package/genome/rules/test-reqs.md +57 -0
  201. package/genome/skills/add-acceptance-test/SKILL.md +100 -0
  202. package/genome/skills/add-concept/SKILL.md +71 -0
  203. package/genome/skills/add-functional-spec/SKILL.md +84 -0
  204. package/genome/skills/add-functional-specs/SKILL.md +118 -0
  205. package/genome/skills/add-implementation-requirement/SKILL.md +86 -0
  206. package/genome/skills/add-resource/SKILL.md +105 -0
  207. package/genome/skills/add-template/SKILL.md +64 -0
  208. package/genome/skills/add-test-requirement/SKILL.md +73 -0
  209. package/genome/skills/analyze-2-func-specs/SKILL.md +101 -0
  210. package/genome/skills/analyze-func-specs/SKILL.md +123 -0
  211. package/genome/skills/analyze-if-func-spec-too-complex/SKILL.md +211 -0
  212. package/genome/skills/break-down-func-spec/SKILL.md +205 -0
  213. package/genome/skills/brood/SKILL.md +229 -0
  214. package/genome/skills/brood/references/intent.md +35 -0
  215. package/genome/skills/brood/references/product.md +101 -0
  216. package/genome/skills/brood/references/stack.md +61 -0
  217. package/genome/skills/brood/references/testing.md +56 -0
  218. package/genome/skills/bud/SKILL.md +148 -0
  219. package/genome/skills/bud/references/checklist.md +44 -0
  220. package/genome/skills/bud/references/intent.md +30 -0
  221. package/genome/skills/check-plain-env/SKILL.md +285 -0
  222. package/genome/skills/consolidate-concepts/SKILL.md +192 -0
  223. package/genome/skills/create-import-module/SKILL.md +104 -0
  224. package/genome/skills/create-requires-module/SKILL.md +109 -0
  225. package/genome/skills/debug-specs/SKILL.md +188 -0
  226. package/genome/skills/implement-conformance-testing-script/SKILL.md +238 -0
  227. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_cypress.ps1 +337 -0
  228. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_golang.ps1 +111 -0
  229. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_java.sh +136 -0
  230. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.ps1 +103 -0
  231. package/genome/skills/implement-conformance-testing-script/assets/run_conformance_tests_python.sh +150 -0
  232. package/genome/skills/implement-unit-testing-script/SKILL.md +160 -0
  233. package/genome/skills/implement-unit-testing-script/assets/run_unittests_flutter.ps1 +87 -0
  234. package/genome/skills/implement-unit-testing-script/assets/run_unittests_golang.ps1 +73 -0
  235. package/genome/skills/implement-unit-testing-script/assets/run_unittests_java.sh +72 -0
  236. package/genome/skills/implement-unit-testing-script/assets/run_unittests_python.ps1 +81 -0
  237. package/genome/skills/implement-unit-testing-script/assets/run_unittests_python.sh +98 -0
  238. package/genome/skills/implement-unit-testing-script/assets/run_unittests_react.ps1 +87 -0
  239. package/genome/skills/init-plain-project/SKILL.md +117 -0
  240. package/genome/skills/init-plain-project/references/checklist.md +30 -0
  241. package/genome/skills/mature/SKILL.md +126 -0
  242. package/genome/skills/refactor-module/SKILL.md +196 -0
  243. package/genome/skills/resolve-section-ownership/SKILL.md +64 -0
  244. package/genome/skills/resolve-spec-conflict/SKILL.md +96 -0
  245. package/genome/skills/spawn/SKILL.md +163 -0
  246. package/package.json +68 -4
  247. package/stdlib/backend/common.plain +25 -0
  248. package/stdlib/backend/conformance/common.plain +12 -0
  249. package/stdlib/backend/conformance/go-gin.plain +9 -0
  250. package/stdlib/backend/conformance/go-nethttp.plain +9 -0
  251. package/stdlib/backend/conformance/java-springboot.plain +9 -0
  252. package/stdlib/backend/conformance/python-django.plain +9 -0
  253. package/stdlib/backend/conformance/python-fastapi.plain +9 -0
  254. package/stdlib/backend/conformance/python-flask.plain +9 -0
  255. package/stdlib/backend/conformance/rust-axum.plain +9 -0
  256. package/stdlib/backend/conformance/typescript-express.plain +9 -0
  257. package/stdlib/backend/conformance/typescript-fastify.plain +9 -0
  258. package/stdlib/backend/conformance/typescript-nestjs.plain +9 -0
  259. package/stdlib/backend/go-gin.plain +18 -0
  260. package/stdlib/backend/go-nethttp.plain +18 -0
  261. package/stdlib/backend/java-springboot.plain +19 -0
  262. package/stdlib/backend/python-django.plain +19 -0
  263. package/stdlib/backend/python-fastapi.plain +23 -0
  264. package/stdlib/backend/python-flask.plain +23 -0
  265. package/stdlib/backend/rust-axum.plain +17 -0
  266. package/stdlib/backend/typescript-express.plain +19 -0
  267. package/stdlib/backend/typescript-fastify.plain +19 -0
  268. package/stdlib/backend/typescript-nestjs.plain +19 -0
  269. package/stdlib/cli/common.plain +17 -0
  270. package/stdlib/cli/conformance/common.plain +11 -0
  271. package/stdlib/cli/conformance/go-cobra.plain +9 -0
  272. package/stdlib/cli/conformance/go-flag.plain +9 -0
  273. package/stdlib/cli/conformance/java-picocli.plain +9 -0
  274. package/stdlib/cli/conformance/python-argparse.plain +9 -0
  275. package/stdlib/cli/conformance/python-click.plain +9 -0
  276. package/stdlib/cli/conformance/python-typer.plain +9 -0
  277. package/stdlib/cli/conformance/rust-clap.plain +9 -0
  278. package/stdlib/cli/conformance/typescript-commander.plain +9 -0
  279. package/stdlib/cli/go-cobra.plain +14 -0
  280. package/stdlib/cli/go-flag.plain +14 -0
  281. package/stdlib/cli/java-picocli.plain +15 -0
  282. package/stdlib/cli/python-argparse.plain +18 -0
  283. package/stdlib/cli/python-click.plain +16 -0
  284. package/stdlib/cli/python-typer.plain +16 -0
  285. package/stdlib/cli/rust-clap.plain +14 -0
  286. package/stdlib/cli/typescript-commander.plain +17 -0
  287. package/stdlib/frontend/angular.plain +17 -0
  288. package/stdlib/frontend/common.plain +17 -0
  289. package/stdlib/frontend/conformance/angular.plain +5 -0
  290. package/stdlib/frontend/conformance/common.plain +11 -0
  291. package/stdlib/frontend/conformance/react-vite.plain +5 -0
  292. package/stdlib/frontend/conformance/sveltekit-spa.plain +5 -0
  293. package/stdlib/frontend/conformance/vue-vite.plain +5 -0
  294. package/stdlib/frontend/react-vite.plain +16 -0
  295. package/stdlib/frontend/sveltekit-spa.plain +16 -0
  296. package/stdlib/frontend/vue-vite.plain +16 -0
  297. package/stdlib/lang/go.plain +13 -0
  298. package/stdlib/lang/java.plain +12 -0
  299. package/stdlib/lang/python.plain +18 -0
  300. package/stdlib/lang/rust.plain +12 -0
  301. package/stdlib/lang/typescript.plain +13 -0
  302. package/stdlib/library/common.plain +15 -0
  303. package/stdlib/library/conformance/common.plain +9 -0
  304. package/stdlib/library/conformance/go.plain +9 -0
  305. package/stdlib/library/conformance/java-gradle.plain +9 -0
  306. package/stdlib/library/conformance/java-maven.plain +9 -0
  307. package/stdlib/library/conformance/python.plain +9 -0
  308. package/stdlib/library/conformance/rust.plain +9 -0
  309. package/stdlib/library/conformance/typescript.plain +9 -0
  310. package/stdlib/library/go.plain +12 -0
  311. package/stdlib/library/java-gradle.plain +13 -0
  312. package/stdlib/library/java-maven.plain +13 -0
  313. package/stdlib/library/python.plain +13 -0
  314. package/stdlib/library/rust.plain +12 -0
  315. package/stdlib/library/typescript.plain +15 -0
@@ -0,0 +1,243 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { existsSync, realpathSync } from "node:fs";
3
+ import { basename, dirname, join, resolve, sep } from "node:path";
4
+ /** The caps set inside the sandbox before the command runs: processes, and file size in 1024-byte blocks (8 GiB). */
5
+ export const PROCESS_CAP = 4096;
6
+ export const FILE_SIZE_CAP_BLOCKS = 8 * 1024 * 1024;
7
+ /** A folder as the kernel sees it - symlinks resolved as far as it exists - since that is what both sandboxes check. */
8
+ function real(path) {
9
+ // Neither sandbox runs on Windows: the paths are kept as they are, so what is generated is the same everywhere.
10
+ if (process.platform === "win32")
11
+ return path;
12
+ let head = resolve(path);
13
+ const tail = [];
14
+ for (;;) {
15
+ if (existsSync(head))
16
+ return join(realpathSync(head), ...tail.reverse());
17
+ const parent = dirname(head);
18
+ if (parent === head)
19
+ return resolve(path);
20
+ tail.push(basename(head));
21
+ head = parent;
22
+ }
23
+ }
24
+ /** What of macOS a command needs to read beyond the toolchains, besides `/usr`, `/bin`, `/System`: the loader's caches, the time zone, the developer tools. */
25
+ export const MACOS_MACHINE = [
26
+ "/usr",
27
+ "/bin",
28
+ "/sbin",
29
+ "/System",
30
+ "/Library/Apple",
31
+ "/Library/Developer",
32
+ "/Library/Java",
33
+ "/Library/Frameworks",
34
+ "/Library/Preferences",
35
+ "/private/etc",
36
+ "/private/var/db/timezone",
37
+ "/private/var/db/dyld",
38
+ "/private/var/select",
39
+ "/opt/homebrew",
40
+ "/opt/local",
41
+ "/Applications/Xcode.app",
42
+ "/dev",
43
+ ];
44
+ /** What of Linux a command needs to read beyond the toolchains. */
45
+ export const LINUX_MACHINE = ["/usr", "/bin", "/sbin", "/lib", "/lib32", "/lib64", "/libx32", "/etc", "/opt", "/nix", "/snap", "/run/systemd/resolve"];
46
+ /**
47
+ * What of the machine a command reads, from the environment it runs in: the system's folders, every folder on its PATH,
48
+ * the folders its toolchain variables point at, and the project's `toolchains:`. Only the PATH folders themselves, not
49
+ * their parents - `~/.cargo/bin`, not `~/.cargo`, which holds a registry's credentials - but for a version manager's
50
+ * install: a `bin` whose parent is a version, `…/installs/node/24.16.0/bin`, links its tools into the install beside it
51
+ * (`npm` into `lib/node_modules`), and an install holds the toolchain, never a credential.
52
+ */
53
+ export function machineFor(env, toolchainVariables, extra = [], platform = process.platform) {
54
+ const base = platform === "darwin" ? MACOS_MACHINE : platform === "linux" ? LINUX_MACHINE : [];
55
+ const onPath = (env.PATH ?? "").split(platform === "win32" ? ";" : ":").filter((dir) => dir.startsWith("/"));
56
+ const installs = onPath.flatMap((dir) => {
57
+ const install = /^(.*\/v?\d+\.\d+(?:\.\d+)?[^/]*)\/bin\/?$/.exec(dir)?.[1];
58
+ return install !== undefined ? [install] : [];
59
+ });
60
+ const located = toolchainVariables.map((name) => env[name]).filter((dir) => dir !== undefined && dir.startsWith("/"));
61
+ return [...new Set([...base, ...onPath, ...installs, ...located, ...extra])];
62
+ }
63
+ function sbplString(path) {
64
+ return `"${path.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
65
+ }
66
+ /**
67
+ * The Seatbelt profile for a fence. Everything is allowed but what it names - reading file contents outside the
68
+ * allow-list, writing outside the writable folders, the code record, Unix sockets outside the folders, signals and
69
+ * process information beyond the sandbox - later rules winning over earlier ones. File metadata stays readable, as
70
+ * resolving a path needs.
71
+ */
72
+ export function seatbeltProfile(fence, userDirs = darwinUserDirs()) {
73
+ // The machine's folders as given - they are the system's own spelling; the fence's with their symlinks resolved.
74
+ const subpaths = (paths, resolveLinks = true) => paths.map((path) => ` (subpath ${sbplString(resolveLinks ? real(path) : path)})`).join("\n");
75
+ const readable = [...fence.read, ...fence.write, ...fence.writable];
76
+ const writable = [...fence.write, ...fence.writable];
77
+ return [
78
+ "(version 1)",
79
+ "(allow default)",
80
+ ";; Reading: the machine's allow-list and the fence, nothing else.",
81
+ "(deny file-read-data file-read-xattr)",
82
+ "(allow file-read-data file-read-xattr",
83
+ ' (literal "/")',
84
+ ' (literal "/etc") (literal "/var") (literal "/tmp") (literal "/private/var/run/resolv.conf")',
85
+ subpaths(fence.machine, false),
86
+ subpaths(readable),
87
+ ")",
88
+ ";; Writing: its own folders, the module's home, TMPDIR and caches, the devices.",
89
+ "(deny file-write*)",
90
+ "(allow file-write*",
91
+ subpaths(writable),
92
+ ' (literal "/dev/null") (literal "/dev/zero") (literal "/dev/tty") (literal "/dev/dtracehelper")',
93
+ ' (regex #"^/dev/fd/")',
94
+ ")",
95
+ ...darwinToolCaches(userDirs),
96
+ ";; The code record is the harness's. (A wildcard deny does not override an allow of one operation: each is named.)",
97
+ ...(fence.deny.length > 0 ? [`(deny file-read-data file-read-xattr file-write*${fence.deny.map((path) => ` (literal ${sbplString(real(path))})`).join("")})`] : []),
98
+ ";; No Unix socket outside its folders: the Docker daemon, an SSH agent, the person's services.",
99
+ "(deny network-outbound (remote unix-socket))",
100
+ `(allow network-outbound (remote unix-socket (path-literal "/private/var/run/mDNSResponder")) (remote unix-socket (subpath ${sbplString(real(fence.cwd))}))${writable.map((path) => ` (remote unix-socket (subpath ${sbplString(real(path))}))`).join("")})`,
101
+ ";; Signals and process information: only within this sandbox.",
102
+ "(deny signal)",
103
+ "(allow signal (target same-sandbox))",
104
+ "(deny process-info*)",
105
+ "(allow process-info* (target same-sandbox))",
106
+ "(allow process-info-pidinfo process-info-setcontrol (target self))",
107
+ "",
108
+ ].join("\n");
109
+ }
110
+ /** macOS's per-user temporary and cache folders, as `getconf` gives them - asked once. */
111
+ let darwinDirs;
112
+ export function darwinUserDirs() {
113
+ if (darwinDirs !== undefined)
114
+ return darwinDirs;
115
+ const ask = (name) => {
116
+ if (process.platform !== "darwin")
117
+ return undefined;
118
+ const run = spawnSync("getconf", [name], { encoding: "utf8", timeout: 5000 });
119
+ const dir = run.status === 0 ? run.stdout.trim().replace(/\/$/, "") : "";
120
+ return dir !== "" ? real(dir) : undefined;
121
+ };
122
+ darwinDirs = { temp: ask("DARWIN_USER_TEMP_DIR"), cache: ask("DARWIN_USER_CACHE_DIR") };
123
+ return darwinDirs;
124
+ }
125
+ /**
126
+ * The developer tools' own scratch files on macOS: `xcrun` (behind `/usr/bin/git`, `clang`, `make`) keeps a database in
127
+ * the per-user temporary folder, and clang its module cache in the per-user cache folder - whatever TMPDIR says.
128
+ */
129
+ function darwinToolCaches({ temp, cache }) {
130
+ const rules = [];
131
+ const regex = (path) => path.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
132
+ if (temp !== undefined)
133
+ rules.push(`(allow file-read-data file-write* (regex #"^${regex(temp)}/xcrun_db"))`);
134
+ if (cache !== undefined)
135
+ rules.push(`(allow file-read-data file-write* (subpath ${sbplString(join(cache, "clang"))}) (subpath ${sbplString(join(cache, "com.apple.dt.xcrun"))}))`);
136
+ return rules.length > 0 ? [";; The developer tools' own scratch files.", ...rules] : [];
137
+ }
138
+ /** The bubblewrap arguments for a fence, up to the command. `proc: "bind"` binds the host's /proc where mounting one is refused. */
139
+ export function bubblewrapArgs(fence, options = { proc: "mount" }) {
140
+ const args = ["--die-with-parent", "--unshare-pid", "--unshare-ipc", "--unshare-uts", "--unshare-cgroup-try"];
141
+ for (const path of fence.machine)
142
+ args.push("--ro-bind-try", path, path);
143
+ args.push(...(options.proc === "mount" ? ["--proc", "/proc"] : ["--ro-bind", "/proc", "/proc"]));
144
+ args.push("--dev", "/dev", "--tmpfs", "/tmp");
145
+ for (const path of fence.read)
146
+ args.push("--ro-bind", real(path), real(path));
147
+ for (const path of [...fence.write, ...fence.writable])
148
+ args.push("--bind", real(path), real(path));
149
+ // The record: covered by an empty file, unreadable and unwritable.
150
+ for (const path of fence.deny)
151
+ if (existsSync(path))
152
+ args.push("--ro-bind", "/dev/null", real(path));
153
+ args.push("--chdir", real(fence.cwd));
154
+ return args;
155
+ }
156
+ /** The caps, set inside the sandbox, then the command itself. */
157
+ function capped(argv) {
158
+ const caps = `ulimit -S -u ${PROCESS_CAP} 2>/dev/null; ulimit -S -f ${FILE_SIZE_CAP_BLOCKS} 2>/dev/null; exec "$@"`;
159
+ return ["/bin/bash", "-c", caps, "turri", ...argv];
160
+ }
161
+ /** How a command is run behind the wall that is up: for the host runner's `wrap`. Where none is, it runs as it is. */
162
+ export function sandboxWrap(wall, fence) {
163
+ if (wall.kind === "seatbelt")
164
+ return (argv) => ["/usr/bin/sandbox-exec", "-p", seatbeltProfile(fence), ...capped(argv)];
165
+ if (wall.kind === "bubblewrap")
166
+ return (argv) => ["bwrap", ...bubblewrapArgs(fence, { proc: wall.proc }), "--", ...capped(argv)];
167
+ return (argv) => argv;
168
+ }
169
+ /** What bubblewrap's refusal to start says is missing, and how to give it - each failure has a message of its own. */
170
+ export function whyBubblewrapFailed(output) {
171
+ if (/ENOENT|not found|No such file or directory: 'bwrap'|spawnSync bwrap/i.test(output) && !/Can't mount/.test(output)) {
172
+ return { why: "bubblewrap is not installed", fix: "install it: `apt install bubblewrap` (or your distribution's package)" };
173
+ }
174
+ if (/Creating new namespace failed: Operation not permitted|No permissions to create a new namespace/.test(output)) {
175
+ return {
176
+ why: "this machine does not let an unprivileged process create a user namespace",
177
+ fix: "in Docker, run the container as a user that is not root (`--user`), with `--security-opt seccomp=unconfined`; on the host, `sysctl -w kernel.unprivileged_userns_clone=1`",
178
+ };
179
+ }
180
+ if (/setting up uid map: Permission denied|Failed to make \/ slave: Permission denied/.test(output)) {
181
+ return {
182
+ why: "AppArmor keeps bubblewrap from setting up its namespace",
183
+ fix: "in Docker, add `--security-opt apparmor=unconfined`; on Ubuntu 24.04, `sysctl -w kernel.apparmor_restrict_unprivileged_userns=0` or an AppArmor profile for bwrap",
184
+ };
185
+ }
186
+ if (/Can't mount proc on (\/newroot)?\/proc/.test(output)) {
187
+ return { why: "the container masks /proc", fix: "in Docker, add `--security-opt systempaths=unconfined`" };
188
+ }
189
+ return { why: `bubblewrap did not start: ${output.trim().split("\n")[0] ?? "no reason given"}` };
190
+ }
191
+ let probed;
192
+ /** For tests: forget what was found, or say what to find. */
193
+ export function setSandboxForTests(wall) {
194
+ probed = wall;
195
+ }
196
+ /**
197
+ * Which wall is up on this machine - tried once, by starting a sandbox that runs `true` - and, when none starts, why
198
+ * and how to give it one.
199
+ */
200
+ export function sandboxAvailable() {
201
+ if (probed !== undefined)
202
+ return probed;
203
+ probed = probe();
204
+ return probed;
205
+ }
206
+ function probe() {
207
+ if (process.platform === "darwin") {
208
+ if (!existsSync("/usr/bin/sandbox-exec"))
209
+ return { kind: "none", why: "this macOS has no sandbox-exec (Seatbelt)" };
210
+ const run = spawnSync("/usr/bin/sandbox-exec", ["-p", "(version 1)(allow default)", "/usr/bin/true"], { encoding: "utf8", timeout: 10_000 });
211
+ return run.status === 0 ? { kind: "seatbelt" } : { kind: "none", why: `Seatbelt did not start: ${(run.stderr || String(run.error ?? "")).trim()}` };
212
+ }
213
+ if (process.platform === "linux") {
214
+ const tryProc = (proc) => spawnSync("bwrap", ["--ro-bind", "/", "/", ...(proc === "mount" ? ["--proc", "/proc"] : ["--ro-bind", "/proc", "/proc"]), "--dev", "/dev", "--unshare-pid", "--", "true"], {
215
+ encoding: "utf8",
216
+ timeout: 10_000,
217
+ });
218
+ const mounted = tryProc("mount");
219
+ if (mounted.status === 0)
220
+ return { kind: "bubblewrap", proc: "mount" };
221
+ const output = `${mounted.stderr ?? ""}${mounted.error !== undefined ? String(mounted.error) : ""}`;
222
+ if (/Can't mount proc on (\/newroot)?\/proc/.test(output) && tryProc("bind").status === 0)
223
+ return { kind: "bubblewrap", proc: "bind" };
224
+ return { kind: "none", ...whyBubblewrapFailed(output) };
225
+ }
226
+ if (process.platform === "win32")
227
+ return { kind: "none", why: "Windows has no OS sandbox turri drives: the command check and the audit hold alone" };
228
+ return { kind: "none", why: `${process.platform} has no OS sandbox turri drives` };
229
+ }
230
+ /** Whether `path` is one of the fence's roots or under one - for the tests and the audit. */
231
+ export function underAny(path, roots) {
232
+ const target = real(path);
233
+ return roots.some((root) => {
234
+ const base = real(root);
235
+ return target === base || target.startsWith(base + sep);
236
+ });
237
+ }
238
+ /** Where the agents' commands run, in one line for the person: which walls are up, or that only the shared ones are. */
239
+ export function sandboxSummary(wall = sandboxAvailable()) {
240
+ if (wall.kind === "none")
241
+ return { level: "limited", text: `agents' commands run on this machine behind the command check and the audit - no OS sandbox: ${wall.why} (turri doctor)` };
242
+ return { level: "ok", text: `agents' commands run on this machine behind the command check, ${wall.kind === "seatbelt" ? "Seatbelt" : "bubblewrap"} and the audit` };
243
+ }
@@ -0,0 +1,165 @@
1
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { dirname, join, resolve, sep } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ /**
5
+ * The ***plain standard library: import modules for generic pieces of software that ship with turri,
6
+ * imported as `stdlib/backend/python-fastapi` - a reserved name (see plain/stdlib.ts).
7
+ */
8
+ export const STDLIB_DIR = join(dirname(dirname(dirname(fileURLToPath(import.meta.url)))), "stdlib");
9
+ /**
10
+ * Where a module's generated code and tests live.
11
+ *
12
+ * Everything a module produces sits under `plain_modules/<module>/`, next to the specs it
13
+ * came from and inside the project's own repository: `code/` holds the implementation and
14
+ * its unit tests, `tests/` the black-box conformance tests - of every functionality built so
15
+ * far, the required modules' included, organised however suits them.
16
+ * The build state is written into `code/`, so what a codebase contains travels with that
17
+ * codebase instead of living in a scratch directory that can be cleared without anyone
18
+ * noticing the specs and the code have drifted apart.
19
+ */
20
+ export const MODULES_DIR = "plain_modules";
21
+ export function moduleDir(moduleName, cwd = process.cwd()) {
22
+ return join(cwd, MODULES_DIR, moduleName);
23
+ }
24
+ /** Implementation and unit tests: `plain_modules/<module>/code`. */
25
+ export function defaultBuildDir(moduleName, cwd = process.cwd()) {
26
+ return join(moduleDir(moduleName, cwd), "code");
27
+ }
28
+ /**
29
+ * The directory every module's output hangs off, `plain_modules` by default.
30
+ *
31
+ * Callers override this to build somewhere else - a sandbox, a test fixture - and the
32
+ * layout underneath it stays the same.
33
+ */
34
+ /**
35
+ * The project a spec belongs to: the nearest folder, from the spec's own up, that holds a
36
+ * `config.turri.yaml`, a stack's `config.turri.<stack>.yaml`, the `config.yaml` turri read before it had a file of
37
+ * its own, or a `plain_modules` - where its code is built, its config read, its testing scripts found. Without any, the
38
+ * spec's own folder. So `plain/hubspot.plain`, beside `plain/config.turri.yaml` and `plain/plain_modules`, is built there,
39
+ * wherever turri was started;
40
+ * specs at the root of their project are built where they always were.
41
+ */
42
+ export function projectDirOf(plainFile) {
43
+ const start = dirname(resolve(plainFile));
44
+ for (let dir = start;; dir = dirname(dir)) {
45
+ if (existsSync(join(dir, "config.yaml")) || existsSync(join(dir, MODULES_DIR)) || hasModuleConfig(dir))
46
+ return dir;
47
+ if (dirname(dir) === dir)
48
+ return start;
49
+ }
50
+ }
51
+ /** Whether a folder holds a config file - config.turri.yaml, a stack's config.turri.<stack>.yaml, or one in the old naming - which makes it a project too. */
52
+ function hasModuleConfig(dir) {
53
+ try {
54
+ return readdirSync(dir).some((name) => /^config\..+\.yaml$/.test(name));
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ }
60
+ export function modulesRoot(cwd = process.cwd()) {
61
+ return join(cwd, MODULES_DIR);
62
+ }
63
+ export function buildDirUnder(root, moduleName) {
64
+ return join(root, moduleName, "code");
65
+ }
66
+ export function testsDirUnder(root, moduleName) {
67
+ return join(root, moduleName, "tests");
68
+ }
69
+ /**
70
+ * The `plain_modules` a directory sits in: what holds every module's code and tests. A
71
+ * directory outside one - a sandbox, an explicit `--build-dir` - is its own root, so this
72
+ * never silently widens to a whole disk.
73
+ */
74
+ export function modulesRootOf(dir) {
75
+ const parts = resolve(dir).split(sep);
76
+ const index = parts.lastIndexOf(MODULES_DIR);
77
+ return index === -1 ? resolve(dir) : parts.slice(0, index + 1).join(sep);
78
+ }
79
+ /**
80
+ * `.turri` inside the modules root: what turri keeps beside the code - the package
81
+ * caches, the modules' homes and temporary folders, the copies test runs happen in, the
82
+ * outputs kept for read_output. Ignored by the project's history.
83
+ */
84
+ export function modulesScratchDir(dir) {
85
+ return join(modulesRootOf(dir), ".turri");
86
+ }
87
+ /** Make `path` inside the modules' scratch, which ignores itself. Returns `path`. */
88
+ export function ensureInModulesScratch(dir, path) {
89
+ return ensureInScratch(modulesScratchDir(dir), path);
90
+ }
91
+ /** Where the project keeps what `deploy` puts out: `dist/`, beside `plain_modules`. */
92
+ export const DIST_DIR = "dist";
93
+ /**
94
+ * Where a person tries a module: `<project>/dist/<module>`, the module's code as its last commit has
95
+ * it. Outside `plain_modules`, so no agent can reach it, and replaced whole by every deploy.
96
+ */
97
+ export function distDirFor(projectDir, moduleName) {
98
+ return join(projectDir, DIST_DIR, moduleName);
99
+ }
100
+ /**
101
+ * The project a directory belongs to: the one `plain_modules` sits in, when the directory is
102
+ * inside it; otherwise the directory's parent for a build folder, or the directory itself.
103
+ */
104
+ export function projectRootOf(dir) {
105
+ const parts = resolve(dir).split(sep);
106
+ const index = parts.lastIndexOf(MODULES_DIR);
107
+ return index === -1 ? resolve(dir) : parts.slice(0, index).join(sep);
108
+ }
109
+ /**
110
+ * `.turri` in the project: the harness's own scratch space - copies to try things in,
111
+ * saved agent sessions. Never part of the project's history, so it ignores itself - but for `skills/`, the
112
+ * project's own skills.
113
+ */
114
+ export function scratchDir(dir) {
115
+ const parts = resolve(dir).split(sep);
116
+ const index = parts.lastIndexOf(MODULES_DIR);
117
+ const project = index === -1 ? dirname(resolve(dir)) : parts.slice(0, index).join(sep);
118
+ return join(project, ".turri");
119
+ }
120
+ /**
121
+ * Where a skill run keeps its state - the manifest an interrupted run resumes from, the evidence
122
+ * each phase's gate is judged on: `.turri/runs/<skill>` in the project, beside the rest of
123
+ * turri's scratch, ignored by the project's history like it.
124
+ */
125
+ export function skillRunDir(cwd, skillName) {
126
+ const scratch = join(resolve(cwd), ".turri");
127
+ return ensureInScratch(scratch, join(scratch, "runs", skillName));
128
+ }
129
+ /**
130
+ * What the scratch dir's .gitignore says: everything in it is ignored but `skills/` - the project's own skills
131
+ * (skills.ts), which are written by hand and kept in its history.
132
+ */
133
+ export const SCRATCH_IGNORE = "*\n!skills/\n!skills/**\n";
134
+ /** Make `path`, inside a scratch dir, and see that the scratch dir ignores itself, its skills apart. Returns `path`. */
135
+ export function ensureInScratch(scratch, path) {
136
+ mkdirSync(path, { recursive: true });
137
+ const ignore = join(scratch, ".gitignore");
138
+ // One written before skills were kept here ignored them too: it is brought up to date.
139
+ if (!existsSync(ignore) || readFileSync(ignore, "utf8") === "*\n")
140
+ writeFileSync(ignore, SCRATCH_IGNORE);
141
+ return path;
142
+ }
143
+ /** Where agent sessions are saved, so what an agent did can be read - and resumed - later. */
144
+ export function sessionsDir(project) {
145
+ const scratch = scratchDir(join(project, MODULES_DIR));
146
+ return ensureInScratch(scratch, join(scratch, "sessions"));
147
+ }
148
+ /**
149
+ * Template search order, matching file_utils.get_template_directories: the plain file's
150
+ * own directory first, then a sibling `template/` directory if there is one (where a
151
+ * project keeps its import modules), then anything passed in, then PLAIN_TEMPLATE_DIRS.
152
+ * A `stdlib/...` import is not looked for in them: it is turri's (see plain/stdlib.ts).
153
+ */
154
+ export function resolveTemplateDirs(plainFile, extra = []) {
155
+ const own = dirname(resolve(plainFile));
156
+ const dirs = [own];
157
+ const sibling = join(own, "template");
158
+ if (existsSync(sibling))
159
+ dirs.push(sibling);
160
+ dirs.push(...extra.map((dir) => resolve(dir)));
161
+ const fromEnv = process.env.PLAIN_TEMPLATE_DIRS;
162
+ if (fromEnv)
163
+ dirs.push(...fromEnv.split(":").filter(Boolean).map((dir) => resolve(dir)));
164
+ return [...new Set(dirs)];
165
+ }
@@ -0,0 +1,83 @@
1
+ /** On a dark surface - turri-dark's #0f0f0f, pi's dark ones. Every colour at least 7:1 on it, and 4.5:1 on the fills. */
2
+ const DARK = {
3
+ roles: {
4
+ section: { colour: "#fcd34d", bold: true },
5
+ concept: { colour: "#7dd3fc" },
6
+ link: { colour: "#c4b5fd", underline: true },
7
+ literal: { colour: "#fdba74" },
8
+ keyword: { colour: "#f9a8d4", bold: true },
9
+ module: { colour: "#5eead4" },
10
+ comment: { colour: "#b4bfcf", italic: true },
11
+ bullet: { colour: "#b4bfcf" },
12
+ },
13
+ added: "#86efac",
14
+ removed: "#fca5a5",
15
+ addedFill: "#1f4d2e",
16
+ removedFill: "#5c2323",
17
+ muted: "#808080",
18
+ };
19
+ /** On a light surface - turri-light's white, pi's light ones. Every colour at least 7:1 on it, and 5:1 on the fills. */
20
+ const LIGHT = {
21
+ roles: {
22
+ section: { colour: "#0a1fd4", bold: true },
23
+ concept: { colour: "#155e75" },
24
+ link: { colour: "#6d28d9", underline: true },
25
+ literal: { colour: "#92400e" },
26
+ keyword: { colour: "#9d174d", bold: true },
27
+ module: { colour: "#065f46" },
28
+ comment: { colour: "#475569", italic: true },
29
+ bullet: { colour: "#475569" },
30
+ },
31
+ added: "#166534",
32
+ removed: "#991b1b",
33
+ addedFill: "#d3f5dc",
34
+ removedFill: "#fcd9d9",
35
+ muted: "#6b7280",
36
+ };
37
+ const rgb = (hex) => {
38
+ const value = Number.parseInt(hex.slice(1), 16);
39
+ return [(value >> 16) & 255, (value >> 8) & 255, value & 255];
40
+ };
41
+ /** A colour's SGR parameters: 24-bit, or the nearest of the 256 where the terminal has no more. */
42
+ function sgr(hex, background, truecolor) {
43
+ const [r, g, b] = rgb(hex);
44
+ if (truecolor)
45
+ return `${background ? 48 : 38};2;${r};${g};${b}`;
46
+ const step = (value) => (value < 48 ? 0 : value < 115 ? 1 : Math.min(5, Math.floor((value - 35) / 40)));
47
+ return `${background ? 48 : 38};5;${16 + 36 * step(r) + 6 * step(g) + step(b)}`;
48
+ }
49
+ const paintWith = (ink, text, truecolor) => {
50
+ const on = [sgr(ink.colour, false, truecolor), ...(ink.bold ? ["1"] : []), ...(ink.italic ? ["3"] : []), ...(ink.underline ? ["4"] : [])].join(";");
51
+ const off = ["39", ...(ink.bold ? ["22"] : []), ...(ink.italic ? ["23"] : []), ...(ink.underline ? ["24"] : [])].join(";");
52
+ return `\x1b[${on}m${text}\x1b[${off}m`;
53
+ };
54
+ /** A fill behind a whole row: set again after every reset inside it, so nothing in the row ends it early. */
55
+ const fillWith = (hex, text, truecolor) => {
56
+ const on = `\x1b[${sgr(hex, true, truecolor)}m`;
57
+ return `${on}${text.replace(/\x1b\[(?:0|49)?m/g, (reset) => `${reset}${on}`)}\x1b[49m`;
58
+ };
59
+ /**
60
+ * Brand blue, for what is shown to do what its spec says - regen's ◆. Brand blue itself on a light surface, 9.72:1
61
+ * on white; on a dark one, where it is illegible (1.97:1 on brand black - see docs/theming.md), its tint 55% of the way
62
+ * to white, 7.34:1 on brand black.
63
+ */
64
+ export const BRAND_BLUE = { light: "#0a1fd4", dark: "#919aec" };
65
+ /** Text in brand blue on a theme - its tint on a dark one - in as many colours as the terminal has. */
66
+ export function brandBlue(theme) {
67
+ const colour = theme.appearance === "light" ? BRAND_BLUE.light : BRAND_BLUE.dark;
68
+ const truecolor = (theme.getColorMode?.() ?? "truecolor") === "truecolor";
69
+ return (text) => paintWith({ colour, bold: true }, text, truecolor);
70
+ }
71
+ /** How a spec diff is painted on a theme: its palette by whether the theme is dark or light, in as many colours as the terminal has. */
72
+ export function plainPaint(theme) {
73
+ const palette = theme.appearance === "light" ? LIGHT : DARK;
74
+ const truecolor = (theme.getColorMode?.() ?? "truecolor") === "truecolor";
75
+ return {
76
+ syntax: (role, text) => paintWith(palette.roles[role], text, truecolor),
77
+ added: (text) => paintWith({ colour: palette.added, bold: true }, text, truecolor),
78
+ removed: (text) => paintWith({ colour: palette.removed, bold: true }, text, truecolor),
79
+ addedBand: (text) => fillWith(palette.addedFill, text, truecolor),
80
+ removedBand: (text) => fillWith(palette.removedFill, text, truecolor),
81
+ muted: (text) => paintWith({ colour: palette.muted }, text, truecolor),
82
+ };
83
+ }