@intentius/chant-lexicon-e2b 0.0.0-stage → 0.124.32

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 (123) hide show
  1. package/README.md +147 -2
  2. package/dist/catalog.d.ts +19 -0
  3. package/dist/catalog.d.ts.map +1 -0
  4. package/dist/catalog.js +58 -0
  5. package/dist/codegen/docs-cli.d.ts +3 -0
  6. package/dist/codegen/docs-cli.d.ts.map +1 -0
  7. package/dist/codegen/docs-cli.js +6 -0
  8. package/dist/codegen/docs.d.ts +15 -0
  9. package/dist/codegen/docs.d.ts.map +1 -0
  10. package/dist/codegen/docs.js +123 -0
  11. package/dist/codegen/generate-cli.d.ts +3 -0
  12. package/dist/codegen/generate-cli.d.ts.map +1 -0
  13. package/dist/codegen/generate-cli.js +4 -0
  14. package/dist/codegen/generate.d.ts +11 -0
  15. package/dist/codegen/generate.d.ts.map +1 -0
  16. package/dist/codegen/generate.js +20 -0
  17. package/dist/codegen/package.d.ts +10 -0
  18. package/dist/codegen/package.d.ts.map +1 -0
  19. package/dist/codegen/package.js +34 -0
  20. package/dist/config.d.ts +113 -0
  21. package/dist/config.d.ts.map +1 -0
  22. package/dist/config.js +82 -0
  23. package/dist/index.d.ts +7 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +6 -0
  26. package/dist/integrity.json +15 -0
  27. package/dist/lint/audit-catalog.d.ts +11 -0
  28. package/dist/lint/audit-catalog.d.ts.map +1 -0
  29. package/dist/lint/audit-catalog.js +15 -0
  30. package/dist/lint/post-synth/e2b011-template-name.d.ts +12 -0
  31. package/dist/lint/post-synth/e2b011-template-name.d.ts.map +1 -0
  32. package/dist/lint/post-synth/e2b011-template-name.js +48 -0
  33. package/dist/lint/post-synth/e2b012-steward-tools.d.ts +20 -0
  34. package/dist/lint/post-synth/e2b012-steward-tools.d.ts.map +1 -0
  35. package/dist/lint/post-synth/e2b012-steward-tools.js +53 -0
  36. package/dist/lint/post-synth/index.d.ts +3 -0
  37. package/dist/lint/post-synth/index.d.ts.map +1 -0
  38. package/dist/lint/post-synth/index.js +6 -0
  39. package/dist/lint/rules/index.d.ts +6 -0
  40. package/dist/lint/rules/index.d.ts.map +1 -0
  41. package/dist/lint/rules/index.js +6 -0
  42. package/dist/lint/rules/pinned-image.d.ts +14 -0
  43. package/dist/lint/rules/pinned-image.d.ts.map +1 -0
  44. package/dist/lint/rules/pinned-image.js +36 -0
  45. package/dist/lint/rules/template-literal.d.ts +11 -0
  46. package/dist/lint/rules/template-literal.d.ts.map +1 -0
  47. package/dist/lint/rules/template-literal.js +39 -0
  48. package/dist/lint/rules/template-size.d.ts +10 -0
  49. package/dist/lint/rules/template-size.d.ts.map +1 -0
  50. package/dist/lint/rules/template-size.js +31 -0
  51. package/dist/lsp/completions.d.ts +7 -0
  52. package/dist/lsp/completions.d.ts.map +1 -0
  53. package/dist/lsp/completions.js +10 -0
  54. package/dist/lsp/hover.d.ts +7 -0
  55. package/dist/lsp/hover.d.ts.map +1 -0
  56. package/dist/lsp/hover.js +30 -0
  57. package/dist/manifest.json +6 -0
  58. package/dist/meta.json +45 -0
  59. package/dist/okf/index.md +14 -0
  60. package/dist/okf/rules/E2B001.md +15 -0
  61. package/dist/okf/rules/E2B002.md +15 -0
  62. package/dist/okf/rules/E2B011.md +15 -0
  63. package/dist/okf/rules/E2B012.md +15 -0
  64. package/dist/okf/types/Template.md +16 -0
  65. package/dist/op/runtime.d.ts +119 -0
  66. package/dist/op/runtime.d.ts.map +1 -0
  67. package/dist/op/runtime.js +522 -0
  68. package/dist/op/sandbox.d.ts +67 -0
  69. package/dist/op/sandbox.d.ts.map +1 -0
  70. package/dist/op/sandbox.js +87 -0
  71. package/dist/package-cli.d.ts +3 -0
  72. package/dist/package-cli.d.ts.map +1 -0
  73. package/dist/package-cli.js +14 -0
  74. package/dist/plugin.d.ts +12 -0
  75. package/dist/plugin.d.ts.map +1 -0
  76. package/dist/plugin.js +66 -0
  77. package/dist/resources.d.ts +38 -0
  78. package/dist/resources.d.ts.map +1 -0
  79. package/dist/resources.js +21 -0
  80. package/dist/rules/e2b011-template-name.ts +51 -0
  81. package/dist/rules/e2b012-steward-tools.ts +58 -0
  82. package/dist/rules/pinned-image.ts +41 -0
  83. package/dist/rules/template-literal.ts +42 -0
  84. package/dist/rules/template-size.ts +36 -0
  85. package/dist/serializer.d.ts +18 -0
  86. package/dist/serializer.d.ts.map +1 -0
  87. package/dist/serializer.js +70 -0
  88. package/dist/skills/chant-e2b-ops.md +69 -0
  89. package/dist/types/index.d.ts +1 -0
  90. package/package.json +67 -4
  91. package/src/catalog.ts +72 -0
  92. package/src/codegen/docs-cli.ts +7 -0
  93. package/src/codegen/docs.ts +130 -0
  94. package/src/codegen/generate-cli.ts +5 -0
  95. package/src/codegen/generate.ts +22 -0
  96. package/src/codegen/package.ts +39 -0
  97. package/src/config.ts +102 -0
  98. package/src/index.ts +6 -0
  99. package/src/lint/audit-catalog.ts +29 -0
  100. package/src/lint/post-synth/e2b011-template-name.ts +51 -0
  101. package/src/lint/post-synth/e2b012-steward-tools.ts +58 -0
  102. package/src/lint/post-synth/index.ts +9 -0
  103. package/src/lint/post-synth/post-synth.test.ts +59 -0
  104. package/src/lint/rules/index.ts +9 -0
  105. package/src/lint/rules/pinned-image.ts +41 -0
  106. package/src/lint/rules/rules.test.ts +46 -0
  107. package/src/lint/rules/template-literal.ts +42 -0
  108. package/src/lint/rules/template-size.ts +36 -0
  109. package/src/lsp/completions.test.ts +23 -0
  110. package/src/lsp/completions.ts +13 -0
  111. package/src/lsp/hover.test.ts +29 -0
  112. package/src/lsp/hover.ts +35 -0
  113. package/src/op/ledger-sync.test.ts +136 -0
  114. package/src/op/runtime.test.ts +394 -0
  115. package/src/op/runtime.ts +573 -0
  116. package/src/op/sandbox.ts +138 -0
  117. package/src/package-cli.ts +16 -0
  118. package/src/plugin.test.ts +39 -0
  119. package/src/plugin.ts +79 -0
  120. package/src/resources.ts +50 -0
  121. package/src/serializer.test.ts +88 -0
  122. package/src/serializer.ts +73 -0
  123. package/src/skills/chant-e2b-ops.md +69 -0
package/package.json CHANGED
@@ -1,6 +1,69 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-e2b",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.124.32",
4
+ "type": "module",
5
+ "description": "e2b lexicon for chant — run Ops on a steward sandbox on E2B, or any API that speaks E2B's, and declare the template it is made from",
6
+ "license": "Apache-2.0",
7
+ "homepage": "https://github.com/INTENTIUS/chant#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/INTENTIUS/chant.git",
11
+ "directory": "lexicons/e2b"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/INTENTIUS/chant/issues"
15
+ },
16
+ "keywords": [
17
+ "chant",
18
+ "e2b",
19
+ "sandbox",
20
+ "ops",
21
+ "template"
22
+ ],
23
+ "publishConfig": {
24
+ "access": "public"
25
+ },
26
+ "files": [
27
+ "src/",
28
+ "dist/"
29
+ ],
30
+ "exports": {
31
+ ".": {
32
+ "development": "./src/index.ts",
33
+ "types": "./dist/index.d.ts",
34
+ "default": "./dist/index.js"
35
+ },
36
+ "./*": {
37
+ "development": "./src/*.ts",
38
+ "types": "./dist/*.d.ts",
39
+ "default": "./dist/*.js"
40
+ },
41
+ "./lint/post-synth": {
42
+ "development": "./src/lint/post-synth/index.ts",
43
+ "types": "./dist/lint/post-synth/index.d.ts",
44
+ "default": "./dist/lint/post-synth/index.js"
45
+ },
46
+ "./manifest": "./dist/manifest.json",
47
+ "./meta": "./dist/meta.json",
48
+ "./types": "./dist/types/index.d.ts"
49
+ },
50
+ "scripts": {
51
+ "generate": "tsx src/codegen/generate-cli.ts",
52
+ "bundle": "tsx src/package-cli.ts",
53
+ "docs": "tsx src/codegen/docs-cli.ts",
54
+ "prepack": "npm run generate && npm run bundle && npm run build",
55
+ "build": "../../scripts/tsc7.sh -p tsconfig.build.json --sourceMap false && tsc-alias -p tsconfig.build.json"
56
+ },
57
+ "dependencies": {
58
+ "e2b": "^2.56.0"
59
+ },
60
+ "peerDependencies": {
61
+ "@intentius/chant": "^0.124.32",
62
+ "zod": "^4.3.6"
63
+ },
64
+ "devDependencies": {
65
+ "@intentius/chant": "*",
66
+ "tsc-alias": "^1.8.17",
67
+ "typescript": "^5.9.3"
68
+ }
69
+ }
package/src/catalog.ts ADDED
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The one kind the e2b lexicon declares, described once.
3
+ *
4
+ * The LSP's completions and hover, the bundle's `dist/meta.json` and the docs'
5
+ * resource reference all read this table, so a property added to `Template`
6
+ * is added here and reaches all three.
7
+ */
8
+
9
+ import type { LexiconEntry } from "@intentius/chant/lsp/lexicon-providers";
10
+
11
+ export const TEMPLATE_TYPE = "E2B::Template";
12
+
13
+ export interface PropertyDoc {
14
+ type: string;
15
+ required?: boolean;
16
+ description: string;
17
+ }
18
+
19
+ /** `Template`'s properties, in the order the hover lists them. */
20
+ export const TEMPLATE_PROPERTIES: Record<string, PropertyDoc> = {
21
+ name: {
22
+ type: "string",
23
+ required: true,
24
+ description:
25
+ "The template's name on E2B: lowercase letters, digits, dashes and underscores. A profile's `template` names it.",
26
+ },
27
+ fromImage: {
28
+ type: "string",
29
+ required: true,
30
+ description: "The image the template starts from, such as `node:22-bookworm`. Pin a tag.",
31
+ },
32
+ run: {
33
+ type: "string[]",
34
+ description: "Shell commands run in order while the template builds, one `RUN` line each.",
35
+ },
36
+ env: {
37
+ type: "Record<string, string>",
38
+ description: "Environment variables set in every sandbox made from the template.",
39
+ },
40
+ workdir: {
41
+ type: "string",
42
+ description: "The working directory for the `run` steps and the start command.",
43
+ },
44
+ startCmd: {
45
+ type: "string",
46
+ description: "The start command (`--cmd`): E2B runs it when the template is snapshotted, so a sandbox starts with it running.",
47
+ },
48
+ readyCmd: {
49
+ type: "string",
50
+ description: "The ready command (`--ready-cmd`): E2B polls it until it exits 0 before it snapshots the template.",
51
+ },
52
+ cpuCount: {
53
+ type: "number",
54
+ description: "vCPUs per sandbox. A positive integer; E2B's default is 2.",
55
+ },
56
+ memoryMB: {
57
+ type: "number",
58
+ description: "Memory per sandbox in MiB. A positive even integer; E2B's default is 1024.",
59
+ },
60
+ };
61
+
62
+ /** The registry the LSP and `dist/meta.json` read: one resource entry. */
63
+ export const LEXICON_ENTRIES: Record<string, LexiconEntry> = {
64
+ Template: {
65
+ resourceType: TEMPLATE_TYPE,
66
+ kind: "resource",
67
+ lexicon: "e2b",
68
+ propertyConstraints: Object.fromEntries(
69
+ Object.entries(TEMPLATE_PROPERTIES).map(([name, p]) => [name, { type: p.type, required: p.required ?? false }]),
70
+ ),
71
+ },
72
+ };
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env tsx
2
+ import { generateDocs } from "./docs";
3
+
4
+ generateDocs({ verbose: true }).catch((err) => {
5
+ console.error(err);
6
+ process.exit(1);
7
+ });
@@ -0,0 +1,130 @@
1
+ /**
2
+ * e2b documentation generator.
3
+ *
4
+ * Calls the core docsPipeline. The how-to and reference pages are authored
5
+ * MDX under `docs/pages/`, each tagged with its Diátaxis quadrant; this file
6
+ * holds the overview and the serialization page. writeDocsSite puts the pages
7
+ * in the main docs site under docs/src/content/docs/lexicons/e2b/ (chant #3993).
8
+ */
9
+
10
+ import { dirname, join } from "path";
11
+ import { fileURLToPath } from "url";
12
+ import { docsPipeline, writeDocsSite, type DocsConfig } from "@intentius/chant/codegen/docs";
13
+
14
+ const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
15
+
16
+ const overview = `The **e2b** lexicon runs chant Ops on a steward: one [E2B](https://e2b.dev) sandbox that outlives every run. \`chant run <op> --on e2b\` hands the run to it. The steward keeps a clone of the project and its installed tools on its disk, runs one Op at a time, and sleeps paused between runs. Any API that speaks E2B's can host it, hosted E2B or a platform of your own.
17
+
18
+ The lexicon also declares one kind, \`Template\`, for the image a steward is made from. \`chant build\` writes each template as \`e2b.<name>.Dockerfile\`, and the \`e2b template create\` command line that builds it.
19
+
20
+ Install it with:
21
+
22
+ \`\`\`bash
23
+ npm install --save-dev @intentius/chant-lexicon-e2b
24
+ \`\`\`
25
+
26
+ ## Quick Start
27
+
28
+ Point a profile at your E2B account in \`chant.config.ts\`:
29
+
30
+ \`\`\`typescript
31
+ import type { ChantConfig } from "@intentius/chant/config";
32
+ import "@intentius/chant-lexicon-e2b";
33
+
34
+ export default {
35
+ lexicons: ["e2b"],
36
+ e2b: {
37
+ profiles: {
38
+ prod: {
39
+ apiKey: { env: "E2B_API_KEY" },
40
+ template: "chant-steward",
41
+ gitToken: { env: "GITHUB_TOKEN" },
42
+ },
43
+ },
44
+ defaultProfile: "prod",
45
+ },
46
+ } satisfies ChantConfig;
47
+ \`\`\`
48
+
49
+ Declare the template it names, with the tools the project's Ops call:
50
+
51
+ \`\`\`typescript
52
+ import { Template } from "@intentius/chant-lexicon-e2b";
53
+
54
+ const steward = new Template({
55
+ name: "chant-steward",
56
+ fromImage: "node:22-bookworm",
57
+ run: ["apt-get update && apt-get install -y --no-install-recommends jq && rm -rf /var/lib/apt/lists/*"],
58
+ cpuCount: 2,
59
+ memoryMB: 2048,
60
+ });
61
+
62
+ export { steward };
63
+ \`\`\`
64
+
65
+ Then build the template once and run an Op on the steward:
66
+
67
+ \`\`\`bash
68
+ chant build src --lexicon e2b -o dist/e2b.json
69
+ (cd dist && e2b template create chant-steward --dockerfile e2b.chant-steward.Dockerfile --cpu-count 2 --memory-mb 2048)
70
+ chant run prod-apply --on e2b
71
+ \`\`\``;
72
+
73
+ const outputFormat = `The e2b lexicon writes each \`Template\` as a Dockerfile named after the template, so several can share a directory, and indexes them with the command that builds each one.
74
+
75
+ ## Building
76
+
77
+ \`e2b.<name>.Dockerfile\` starts \`FROM\` the template's \`fromImage\`. Each \`env\` entry becomes an \`ENV\` line and each \`run\` step a \`RUN\` line, in order, with \`WORKDIR\` between them when \`workdir\` is set:
78
+
79
+ \`\`\`dockerfile
80
+ FROM node:22-bookworm
81
+ ENV CI="1"
82
+ RUN apt-get update && apt-get install -y --no-install-recommends jq && rm -rf /var/lib/apt/lists/*
83
+ \`\`\`
84
+
85
+ The primary output is a JSON index of the templates, keyed by the name each was exported as. \`build\` is the \`e2b template create\` command line for the template. It names the Dockerfile and passes \`--cmd\`, \`--ready-cmd\`, \`--cpu-count\` and \`--memory-mb\` only when the declaration sets them:
86
+
87
+ \`\`\`json
88
+ {
89
+ "templates": {
90
+ "steward": {
91
+ "name": "chant-steward",
92
+ "dockerfile": "e2b.chant-steward.Dockerfile",
93
+ "build": "e2b template create chant-steward --dockerfile e2b.chant-steward.Dockerfile --cpu-count 2 --memory-mb 2048",
94
+ "cpuCount": 2,
95
+ "memoryMB": 2048
96
+ }
97
+ }
98
+ }
99
+ \`\`\`
100
+
101
+ A project that declares no template gets \`{ "templates": {} }\`.
102
+
103
+ ## Applying
104
+
105
+ chant does not build templates. Run each template's \`build\` command in the output directory, once, and again when the declaration changes. A profile's \`template\` then names the template, and the next steward made from that profile starts from it. A steward that already exists keeps the sandbox it has; delete it to have the next run make a new one.`;
106
+
107
+ /**
108
+ * Generate the e2b lexicon's docs.
109
+ */
110
+ export async function generateDocs(options?: { verbose?: boolean }): Promise<void> {
111
+ const config: DocsConfig = {
112
+ name: "e2b",
113
+ displayName: "E2B",
114
+ description: "Run chant Ops on a steward sandbox on E2B, and declare the template it is made from",
115
+ distDir: join(pkgDir, "dist"),
116
+ outDir: join(pkgDir, "docs"),
117
+ srcDir: join(pkgDir, "src"),
118
+ basePath: process.env.DOCS_BASE_PATH ?? "/chant/lexicons/e2b/",
119
+ overview,
120
+ outputFormat,
121
+ serviceFromType: (type: string) => type.split("::")[1] ?? type,
122
+ };
123
+
124
+ const result = docsPipeline(config);
125
+ writeDocsSite(config, result);
126
+
127
+ if (options?.verbose) {
128
+ console.error(`Generated docs: ${result.stats.resources} resources, ${result.stats.rules} rules`);
129
+ }
130
+ }
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env tsx
2
+ /** `npm run generate` for the e2b lexicon: writes the post-synth barrel. */
3
+ import { generate } from "./generate";
4
+
5
+ console.error(`Generated the e2b post-synth barrel: ${generate()} checks`);
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Generation for the e2b lexicon.
3
+ *
4
+ * The lexicon's one kind is written by hand (../resources.ts), so there is no
5
+ * upstream spec to generate types from. What is generated is the static
6
+ * barrel of post-synth checks, `src/lint/post-synth/index.ts`, the same file
7
+ * every lexicon's generate step writes (#409).
8
+ */
9
+
10
+ import { writeFileSync } from "fs";
11
+ import { dirname, join } from "path";
12
+ import { moduleSourceDir } from "@intentius/chant/module-source";
13
+ import { renderPostSynthBarrelForDir } from "@intentius/chant/codegen/generate-post-synth-barrel";
14
+
15
+ /** Write the post-synth barrel and return how many checks it lists. */
16
+ export function generate(): number {
17
+ const srcDir = dirname(moduleSourceDir(import.meta.url));
18
+ const postSynthDir = join(srcDir, "lint", "post-synth");
19
+ const content = renderPostSynthBarrelForDir(postSynthDir, import.meta.url);
20
+ writeFileSync(join(postSynthDir, "index.ts"), content);
21
+ return (content.match(/^ \w/gm) ?? []).length;
22
+ }
@@ -0,0 +1,39 @@
1
+ import { packagePipeline, collectSkills } from "@intentius/chant/codegen/package";
2
+ import { readFileSync } from "fs";
3
+ import { dirname, join } from "path";
4
+ import { moduleSourceDir } from "@intentius/chant/module-source";
5
+ import { LEXICON_ENTRIES } from "../catalog";
6
+ import { e2bPlugin } from "../plugin";
7
+
8
+ /**
9
+ * Package the e2b lexicon: `dist/manifest.json`, `dist/meta.json`, the rules
10
+ * and the skills. There is no generation step to run first; the registry is
11
+ * the hand-written catalog in ../catalog.ts.
12
+ */
13
+ export async function packageLexicon(options?: { verbose?: boolean; force?: boolean }) {
14
+ const srcDir = dirname(moduleSourceDir(import.meta.url));
15
+ const pkgJson = JSON.parse(readFileSync(join(srcDir, "..", "package.json"), "utf-8")) as { version?: string };
16
+
17
+ return packagePipeline(
18
+ {
19
+ generate: async () => ({
20
+ lexiconJSON: JSON.stringify(LEXICON_ENTRIES, null, 2),
21
+ typesDTS: 'export * from "../index";\n',
22
+ indexTS: "",
23
+ resources: Object.values(LEXICON_ENTRIES).filter((e) => e.kind === "resource").length,
24
+ properties: 0,
25
+ enums: 0,
26
+ warnings: [],
27
+ }),
28
+ buildManifest: () => ({
29
+ name: "e2b",
30
+ version: pkgJson.version ?? "0.0.0",
31
+ chantVersion: ">=0.1.0",
32
+ namespace: "E2B",
33
+ }),
34
+ srcDir,
35
+ collectSkills: () => collectSkills(e2bPlugin.skills?.() ?? []),
36
+ },
37
+ options,
38
+ );
39
+ }
package/src/config.ts ADDED
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The `e2b` namespace in `chant.config.ts`.
3
+ *
4
+ * ```ts
5
+ * import type { ChantConfig } from "@intentius/chant/config";
6
+ * import "@intentius/chant-lexicon-e2b"; // brings the `e2b` key into ChantConfig
7
+ *
8
+ * export default {
9
+ * lexicons: ["e2b"],
10
+ * e2b: {
11
+ * profiles: {
12
+ * prod: {
13
+ * apiKey: { env: "E2B_API_KEY" },
14
+ * domain: "e2b.loam.example.com", // omit for hosted E2B
15
+ * steward: "prod-steward",
16
+ * gitToken: { env: "GITHUB_TOKEN" },
17
+ * },
18
+ * },
19
+ * defaultProfile: "prod",
20
+ * },
21
+ * } satisfies ChantConfig;
22
+ * ```
23
+ *
24
+ * One schema, three consumers, as for fountain's namespace: core validates the
25
+ * namespace against it at load, the type below is derived from it, and that
26
+ * type augments `ChantConfig`. A project with no `e2b.profiles` keeps working:
27
+ * the runtime falls back to `E2B_API_KEY`, `E2B_DOMAIN` and `CHANT_GIT_TOKEN`.
28
+ */
29
+
30
+ import { z } from "zod";
31
+ import type { ChantConfig } from "@intentius/chant/config";
32
+
33
+ /** A secret named by its environment variable, never a literal value. */
34
+ const envRef = z.strictObject({ env: z.string() });
35
+
36
+ export const e2bProfileSchema = z.strictObject({
37
+ /** The E2B API key, by environment variable. Default `E2B_API_KEY`. */
38
+ apiKey: envRef.optional(),
39
+ /**
40
+ * The E2B domain the SDK talks to: `api.<domain>` and `<port>-<id>.<domain>`.
41
+ * Omitted, the SDK's own default (hosted E2B, or `E2B_DOMAIN`).
42
+ */
43
+ domain: z.string().optional(),
44
+ /** The template a steward's sandbox is made from. Default `base`. It needs git and Node. */
45
+ template: z.string().optional(),
46
+ /**
47
+ * The steward's name: the one sandbox every run of this profile lands on,
48
+ * found again by its `chant/steward` metadata. Default: the repository's name.
49
+ */
50
+ steward: z.string().optional(),
51
+ /**
52
+ * The repository the sandbox clones, as an https URL. Default: `origin`'s
53
+ * URL, with an ssh URL rewritten to https.
54
+ */
55
+ repo: z.string().optional(),
56
+ /**
57
+ * A token for that repository, by environment variable, so the sandbox can
58
+ * fetch a private repo and push chant's ledger branch. It is handed to each
59
+ * command in the sandbox and never written to its disk.
60
+ */
61
+ gitToken: envRef.optional(),
62
+ });
63
+
64
+ export const e2bConfigSchema = z.strictObject({
65
+ profiles: z.record(z.string(), e2bProfileSchema).optional(),
66
+ /** Profile used when a run names none. */
67
+ defaultProfile: z.string().optional(),
68
+ });
69
+
70
+ export type E2bProfile = z.infer<typeof e2bProfileSchema>;
71
+ export type E2bConfig = z.infer<typeof e2bConfigSchema>;
72
+
73
+ declare module "@intentius/chant/config" {
74
+ interface ChantConfig {
75
+ e2b?: E2bConfig;
76
+ }
77
+ }
78
+
79
+ /** Compile-time proof that the augmentation reaches `ChantConfig`, as in fountain's config. */
80
+ export type E2bConfigNamespace = NonNullable<ChantConfig["e2b"]>;
81
+
82
+ /**
83
+ * The named profile, else `defaultProfile`, else `undefined`. A name that is
84
+ * not declared is an error rather than a fall back to the default: running
85
+ * against the wrong steward because a profile name was misspelt is the one
86
+ * mistake a runtime must not make.
87
+ */
88
+ export function resolveProfile(config: Pick<ChantConfig, "e2b"> | undefined, name?: string): E2bProfile | undefined {
89
+ const profiles = config?.e2b?.profiles;
90
+ if (name !== undefined) {
91
+ const named = profiles?.[name];
92
+ if (!named) {
93
+ throw new Error(
94
+ `e2b runtime: no profile "${name}" under e2b.profiles in chant.config.ts. ` +
95
+ `Declare it, or drop --profile to use e2b.defaultProfile.`,
96
+ );
97
+ }
98
+ return named;
99
+ }
100
+ const fallback = config?.e2b?.defaultProfile;
101
+ return fallback !== undefined ? profiles?.[fallback] : undefined;
102
+ }
package/src/index.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { e2bPlugin } from "./plugin";
2
+ export { e2bSerializer } from "./serializer";
3
+ export { Template, type TemplateProps } from "./resources";
4
+ export { e2bConfigSchema, e2bProfileSchema, resolveProfile, type E2bConfig, type E2bProfile } from "./config";
5
+ export { createE2bOpRuntime, type E2bOpRuntimeOptions } from "./op/runtime";
6
+ export { e2bSandboxes, STEWARD_METADATA_KEY, type StewardSandbox, type StewardSandboxes } from "./op/sandbox";
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The e2b lexicon's chant audit catalog: metadata for the E2B post-synth
3
+ * checks, contributed via `e2bPlugin.auditCatalog()`.
4
+ *
5
+ * Every check reads the chant model (`ctx.entities`), not the emitted files,
6
+ * so they fire on `chant build` and not on an audit of a hand-written
7
+ * `e2b.Dockerfile`.
8
+ */
9
+
10
+ import type { RuleMeta } from "@intentius/chant/audit/catalog";
11
+
12
+ function rule(id: string, tier: RuleMeta["tier"], title: string, remediation: string): RuleMeta {
13
+ return { id, tier, fixKind: "guidance", category: "correctness", title, remediation, yamlBased: false };
14
+ }
15
+
16
+ export const e2bAuditCatalog: Record<string, RuleMeta> = {
17
+ E2B011: rule(
18
+ "E2B011",
19
+ "merge-worthy",
20
+ "Template name is not one E2B accepts, or two templates share it",
21
+ "Name the template with lowercase letters, digits, dashes and underscores, and give each template its own name.",
22
+ ),
23
+ E2B012: rule(
24
+ "E2B012",
25
+ "report-only",
26
+ "Template shows no sign of git or Node",
27
+ "Start from a `node` image, or add a `run` step that installs git and Node.",
28
+ ),
29
+ };
@@ -0,0 +1,51 @@
1
+ /**
2
+ * E2B011: a template's name is one E2B accepts, and only one template has it.
3
+ *
4
+ * E2B names templates with lowercase letters, digits, dashes and underscores;
5
+ * `e2b template create` refuses anything else. The name is also the profile's
6
+ * `template` and the stem of the Dockerfile the serializer writes, so two
7
+ * templates with one name would overwrite each other's file.
8
+ */
9
+
10
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
11
+ import { templateProps } from "../../resources";
12
+
13
+ export const TEMPLATE_NAME = /^[a-z0-9_-]+$/;
14
+
15
+ export const e2b011: PostSynthCheck = {
16
+ id: "E2B011",
17
+ description: "A Template's name must be lowercase letters, digits, dashes and underscores, and unique",
18
+
19
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
20
+ const diagnostics: PostSynthDiagnostic[] = [];
21
+ const seen = new Map<string, string>();
22
+ for (const [entityName, entity] of ctx.entities) {
23
+ const props = templateProps(entity);
24
+ if (!props) continue;
25
+ const name = props.name;
26
+ if (typeof name !== "string" || !TEMPLATE_NAME.test(name)) {
27
+ diagnostics.push({
28
+ checkId: "E2B011",
29
+ severity: "error",
30
+ message: `Template "${entityName}" has name ${JSON.stringify(name)}. E2B template names are lowercase letters, digits, dashes and underscores.`,
31
+ entity: entityName,
32
+ lexicon: "e2b",
33
+ });
34
+ continue;
35
+ }
36
+ const first = seen.get(name);
37
+ if (first !== undefined) {
38
+ diagnostics.push({
39
+ checkId: "E2B011",
40
+ severity: "error",
41
+ message: `Templates "${first}" and "${entityName}" are both named "${name}"; their e2b.${name}.Dockerfile files would overwrite each other.`,
42
+ entity: entityName,
43
+ lexicon: "e2b",
44
+ });
45
+ } else {
46
+ seen.set(name, entityName);
47
+ }
48
+ }
49
+ return diagnostics;
50
+ },
51
+ };
@@ -0,0 +1,58 @@
1
+ /**
2
+ * E2B012: a template has the git and Node a steward needs.
3
+ *
4
+ * The runtime clones the repository with git and runs the project's own chant
5
+ * with Node, and it stops a run at once on a sandbox that lacks either. This
6
+ * check looks for both in the declaration: Node from a `node` image or a
7
+ * step that installs it, git from a full (not `-slim` or `-alpine`) `node` image, which
8
+ * ships it, or a step that installs it. It cannot see inside other images, so
9
+ * it warns rather than fails: a template from an image that already has both
10
+ * can say so with a `run` step such as `git --version && node --version`.
11
+ */
12
+
13
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
14
+ import { templateProps, type TemplateProps } from "../../resources";
15
+
16
+ function imageRepoAndTag(image: string): { repo: string; tag: string } {
17
+ const ref = image.toLowerCase().split("@")[0];
18
+ const segment = ref.slice(ref.lastIndexOf("/") + 1);
19
+ const [repo, tag = ""] = segment.split(":");
20
+ return { repo, tag };
21
+ }
22
+
23
+ /** Which of git and Node the declaration visibly provides. */
24
+ export function stewardTools(props: TemplateProps): { git: boolean; node: boolean } {
25
+ const { repo, tag } = imageRepoAndTag(props.fromImage ?? "");
26
+ const steps = (props.run ?? []).join("\n");
27
+ const nodeImage = repo === "node";
28
+ return {
29
+ node: nodeImage || /\b(nodejs|node|nvm|fnm|volta)\b/.test(steps),
30
+ git: (nodeImage && !/slim|alpine/.test(tag)) || /\bgit\b/.test(steps),
31
+ };
32
+ }
33
+
34
+ export const e2b012: PostSynthCheck = {
35
+ id: "E2B012",
36
+ description: "A Template a steward runs on needs git and Node",
37
+
38
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
39
+ const diagnostics: PostSynthDiagnostic[] = [];
40
+ for (const [name, entity] of ctx.entities) {
41
+ const props = templateProps(entity);
42
+ if (!props) continue;
43
+ const tools = stewardTools(props);
44
+ const missing = [tools.git ? undefined : "git", tools.node ? undefined : "Node"].filter((t): t is string => t !== undefined);
45
+ if (missing.length === 0) continue;
46
+ diagnostics.push({
47
+ checkId: "E2B012",
48
+ severity: "warning",
49
+ message:
50
+ `Template "${name}" shows no sign of ${missing.join(" or ")}. A steward needs both; ` +
51
+ `start from a node image or add a run step that installs ${missing.length === 2 ? "them" : "it"}.`,
52
+ entity: name,
53
+ lexicon: "e2b",
54
+ });
55
+ }
56
+ return diagnostics;
57
+ },
58
+ };
@@ -0,0 +1,9 @@
1
+ // Code generated by chant generate. DO NOT EDIT.
2
+ import type { PostSynthCheck } from "@intentius/chant/lint/post-synth";
3
+ import { e2b011 } from "./e2b011-template-name";
4
+ import { e2b012 } from "./e2b012-steward-tools";
5
+
6
+ export const postSynthChecks: PostSynthCheck[] = [
7
+ e2b011,
8
+ e2b012,
9
+ ];
@@ -0,0 +1,59 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import type { Declarable } from "@intentius/chant/declarable";
3
+ import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
4
+ import { Template, type TemplateProps } from "../../resources";
5
+ import { postSynthChecks } from "./index";
6
+ import { e2b011 } from "./e2b011-template-name";
7
+ import { e2b012, stewardTools } from "./e2b012-steward-tools";
8
+
9
+ function ctx(...templates: Array<[string, TemplateProps]>): PostSynthContext {
10
+ const entities = new Map(templates.map(([n, p]) => [n, new Template(p) as unknown as Declarable]));
11
+ return { outputs: new Map(), entities, buildResult: { outputs: new Map(), entities, warnings: [], errors: [], sourceFileCount: 1 } };
12
+ }
13
+
14
+ const GOOD: TemplateProps = { name: "chant-steward", fromImage: "node:22-bookworm" };
15
+
16
+ describe("e2b post-synth checks", () => {
17
+ test("the barrel lists the two checks", () => {
18
+ expect(postSynthChecks.map((c) => c.id)).toEqual(["E2B011", "E2B012"]);
19
+ });
20
+
21
+ test("a full node image passes both", () => {
22
+ for (const check of postSynthChecks) expect(check.check(ctx(["steward", GOOD])), check.id).toEqual([]);
23
+ });
24
+
25
+ describe("E2B011: template name", () => {
26
+ test("fails a name E2B refuses", () => {
27
+ for (const name of ["Chant-Steward", "chant steward", "chant.steward", ""]) {
28
+ const diags = e2b011.check(ctx(["steward", { ...GOOD, name }]));
29
+ expect(diags, name).toHaveLength(1);
30
+ expect(diags[0].severity).toBe("error");
31
+ }
32
+ });
33
+
34
+ test("fails a second template with the same name", () => {
35
+ const diags = e2b011.check(ctx(["a", GOOD], ["b", GOOD]));
36
+ expect(diags).toHaveLength(1);
37
+ expect(diags[0]).toMatchObject({ entity: "b" });
38
+ expect(diags[0].message).toContain('"a" and "b"');
39
+ });
40
+ });
41
+
42
+ describe("E2B012: git and Node", () => {
43
+ test("stewardTools reads the image and the run steps", () => {
44
+ expect(stewardTools(GOOD)).toEqual({ git: true, node: true });
45
+ expect(stewardTools({ ...GOOD, fromImage: "node:22-bookworm-slim" })).toEqual({ git: false, node: true });
46
+ expect(stewardTools({ ...GOOD, fromImage: "node:22-alpine" })).toEqual({ git: false, node: true });
47
+ expect(stewardTools({ name: "x", fromImage: "alpine:3.20", run: ["apk add --no-cache git nodejs npm"] })).toEqual({ git: true, node: true });
48
+ expect(stewardTools({ name: "x", fromImage: "debian:12", run: ["apt-get install -y git nodejs"] })).toEqual({ git: true, node: true });
49
+ expect(stewardTools({ name: "x", fromImage: "debian:12" })).toEqual({ git: false, node: false });
50
+ });
51
+
52
+ test("warns, naming what is missing", () => {
53
+ const [d] = e2b012.check(ctx(["steward", { ...GOOD, fromImage: "node:22-bookworm-slim" }]));
54
+ expect(d).toMatchObject({ checkId: "E2B012", severity: "warning" });
55
+ expect(d.message).toContain("no sign of git");
56
+ expect(d.message).not.toContain("Node");
57
+ });
58
+ });
59
+ });