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

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
@@ -0,0 +1,87 @@
1
+ /**
2
+ * The seam between the e2b runtime and an E2B-compatible API.
3
+ *
4
+ * {@link StewardSandboxes} is all the runtime asks of a sandbox provider: find
5
+ * the steward's sandbox by its metadata, make one, wake one, run a command in
6
+ * it, list and kill its processes, keep it awake and pause it. The production
7
+ * implementation below is a thin wrapper over the `e2b` SDK, so the runtime
8
+ * works against hosted E2B and anything that serves E2B's API (loam, for one);
9
+ * a test hands the runtime a fake and drives the whole path with no network.
10
+ */
11
+ /** The metadata key a steward's sandbox is found by. */
12
+ export const STEWARD_METADATA_KEY = "chant/steward";
13
+ /** The environment variables that mark a process in the sandbox as a chant run. */
14
+ export const RUN_OP_ENV = "CHANT_E2B_OP";
15
+ export const RUN_ID_ENV = "CHANT_E2B_RUN";
16
+ export const RUN_STARTED_ENV = "CHANT_E2B_STARTED";
17
+ /** The production seam, over the `e2b` SDK. The SDK is imported on first use, so loading the plugin costs nothing. */
18
+ export function e2bSandboxes(conn) {
19
+ const base = { apiKey: conn.apiKey, ...(conn.domain ? { domain: conn.domain } : {}) };
20
+ const sdk = () => import("e2b");
21
+ const wrap = (sb) => ({
22
+ id: sb.sandboxId,
23
+ async run(script, opts = {}) {
24
+ const { CommandExitError } = await sdk();
25
+ // `timeoutMs: 0` lifts the SDK's one-minute default: an op runs as long as it runs.
26
+ const handle = await sb.commands.run(script, {
27
+ background: true,
28
+ timeoutMs: 0,
29
+ ...(opts.envs ? { envs: opts.envs } : {}),
30
+ ...(opts.onStdout ? { onStdout: opts.onStdout } : {}),
31
+ ...(opts.onStderr ? { onStderr: opts.onStderr } : {}),
32
+ });
33
+ opts.onStart?.(handle.pid);
34
+ try {
35
+ const done = await handle.wait();
36
+ return { exitCode: done.exitCode, stdout: done.stdout, stderr: done.stderr };
37
+ }
38
+ catch (err) {
39
+ if (err instanceof CommandExitError)
40
+ return { exitCode: err.exitCode, stdout: err.stdout, stderr: err.stderr };
41
+ throw err;
42
+ }
43
+ },
44
+ async processes() {
45
+ return (await sb.commands.list()).map((p) => ({ pid: p.pid, envs: p.envs ?? {} }));
46
+ },
47
+ async kill(pid) {
48
+ await sb.commands.kill(pid);
49
+ },
50
+ async keepAwake(ms) {
51
+ await sb.setTimeout(ms);
52
+ },
53
+ async pause() {
54
+ await sb.pause();
55
+ },
56
+ });
57
+ return {
58
+ async find(steward) {
59
+ const { Sandbox } = await sdk();
60
+ const page = await Sandbox.list({
61
+ ...base,
62
+ query: { metadata: { [STEWARD_METADATA_KEY]: steward }, state: ["running", "paused"] },
63
+ limit: 2,
64
+ }).nextItems();
65
+ if (page.length > 1) {
66
+ throw new Error(`e2b runtime: ${page.length} sandboxes carry ${STEWARD_METADATA_KEY}=${steward}. ` +
67
+ `A steward is one machine; kill all but one (${page.map((s) => s.sandboxId).join(", ")}).`);
68
+ }
69
+ return page[0] ? { id: page[0].sandboxId, state: page[0].state } : undefined;
70
+ },
71
+ async create(steward, opts) {
72
+ const { Sandbox } = await sdk();
73
+ const sb = await Sandbox.create(opts.template, {
74
+ ...base,
75
+ timeoutMs: opts.awakeMs,
76
+ metadata: { [STEWARD_METADATA_KEY]: steward },
77
+ // Run out of time paused, not killed: the disk, and the memory, are the steward.
78
+ lifecycle: { onTimeout: "pause" },
79
+ });
80
+ return wrap(sb);
81
+ },
82
+ async connect(id, opts) {
83
+ const { Sandbox } = await sdk();
84
+ return wrap(await Sandbox.connect(id, { ...base, timeoutMs: opts.awakeMs }));
85
+ },
86
+ };
87
+ }
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env tsx
2
+ export {};
3
+ //# sourceMappingURL=package-cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"package-cli.d.ts","sourceRoot":"","sources":["../src/package-cli.ts"],"names":[],"mappings":""}
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * `npm run bundle` for the e2b lexicon: writes dist/manifest.json, dist/meta.json,
4
+ * the rules and the skills.
5
+ */
6
+ import { packageLexicon } from "./codegen/package.js";
7
+ import { writeBundleSpec } from "@intentius/chant/codegen/package";
8
+ import { dirname, join } from "path";
9
+ import { fileURLToPath } from "url";
10
+ const distDir = join(dirname(fileURLToPath(import.meta.url)), "..", "dist");
11
+ const verbose = process.argv.includes("--verbose");
12
+ const { spec, stats } = await packageLexicon({ verbose });
13
+ writeBundleSpec(spec, distDir);
14
+ console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules, ${stats.skillCount} skills into ${distDir}`);
@@ -0,0 +1,12 @@
1
+ import type { LexiconPlugin } from "@intentius/chant/lexicon";
2
+ /**
3
+ * e2b lexicon plugin: `chant run <op> --on e2b`, and the `Template` a
4
+ * steward's sandbox is made from.
5
+ *
6
+ * The op runtime is most of the lexicon. Its one declarable kind is written
7
+ * by hand, so `generate` only writes the post-synth barrel, there is no
8
+ * upstream spec to validate against or measure coverage of, and `package`
9
+ * bundles the hand-written catalog.
10
+ */
11
+ export declare const e2bPlugin: LexiconPlugin;
12
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAY9D;;;;;;;;GAQG;AACH,eAAO,MAAM,SAAS,EAAE,aAyDvB,CAAC"}
package/dist/plugin.js ADDED
@@ -0,0 +1,66 @@
1
+ import { createSkillsLoader } from "@intentius/chant/lexicon-plugin-helpers";
2
+ import { e2bSerializer } from "./serializer.js";
3
+ import { e2bConfigSchema } from "./config.js";
4
+ import { createE2bOpRuntime } from "./op/runtime.js";
5
+ import { rules } from "./lint/rules/index.js";
6
+ import { postSynthChecks } from "./lint/post-synth/index.js";
7
+ import { e2bAuditCatalog } from "./lint/audit-catalog.js";
8
+ import { completions } from "./lsp/completions.js";
9
+ import { hover } from "./lsp/hover.js";
10
+ /**
11
+ * e2b lexicon plugin: `chant run <op> --on e2b`, and the `Template` a
12
+ * steward's sandbox is made from.
13
+ *
14
+ * The op runtime is most of the lexicon. Its one declarable kind is written
15
+ * by hand, so `generate` only writes the post-synth barrel, there is no
16
+ * upstream spec to validate against or measure coverage of, and `package`
17
+ * bundles the hand-written catalog.
18
+ */
19
+ export const e2bPlugin = {
20
+ name: "e2b",
21
+ serializer: e2bSerializer,
22
+ configSchema: e2bConfigSchema,
23
+ auditCatalog: () => e2bAuditCatalog,
24
+ // Construction is pure: the key, the steward and the repository are
25
+ // resolved on the first call, so loading the plugin costs nothing.
26
+ opRuntime: createE2bOpRuntime(),
27
+ skills: createSkillsLoader(import.meta.url, [
28
+ {
29
+ file: "chant-e2b-ops.md",
30
+ name: "chant-e2b-ops",
31
+ description: "Run chant ops on a steward sandbox with `--on e2b`, on hosted E2B or any API that speaks E2B's",
32
+ triggers: [{ type: "context", value: "e2b" }],
33
+ },
34
+ ]),
35
+ async generate() {
36
+ const { generate } = await import("./codegen/generate.js");
37
+ generate();
38
+ },
39
+ // Nothing upstream to check the hand-written kind against.
40
+ async validate() { },
41
+ async coverage() { },
42
+ async package(options) {
43
+ const { packageLexicon } = await import("./codegen/package.js");
44
+ const { writeBundleSpec } = await import("@intentius/chant/codegen/package");
45
+ const { join, dirname } = await import("path");
46
+ const { fileURLToPath } = await import("url");
47
+ const { spec } = await packageLexicon(options);
48
+ writeBundleSpec(spec, join(dirname(dirname(fileURLToPath(import.meta.url))), "dist"));
49
+ },
50
+ lintRules() {
51
+ return rules;
52
+ },
53
+ postSynthChecks() {
54
+ return postSynthChecks;
55
+ },
56
+ completionProvider(ctx) {
57
+ return completions(ctx);
58
+ },
59
+ hoverProvider(ctx) {
60
+ return hover(ctx);
61
+ },
62
+ async docs(options) {
63
+ const { generateDocs } = await import("./codegen/docs.js");
64
+ return generateDocs(options);
65
+ },
66
+ };
@@ -0,0 +1,38 @@
1
+ /**
2
+ * The e2b lexicon's one declarable kind: a sandbox template.
3
+ *
4
+ * A steward's sandbox is made from a template (`template` in the profile,
5
+ * default `base`). The runtime needs git and Node in it, and a project usually
6
+ * wants its own tools there too, installed once rather than on every run. A
7
+ * `Template` declares that image; `chant build` writes it out as a Dockerfile
8
+ * and the `e2b template create` command line that builds it.
9
+ */
10
+ import type { Declarable } from "@intentius/chant/declarable";
11
+ export interface TemplateProps {
12
+ /** The template's name on E2B: lowercase letters, digits, dashes and underscores. A profile's `template` names it. */
13
+ name: string;
14
+ /** The image the template starts from, such as `node:22-bookworm`. Pin a tag. */
15
+ fromImage: string;
16
+ /** Shell commands run in order while the template builds, one `RUN` line each. */
17
+ run?: string[];
18
+ /** Environment variables set in every sandbox made from the template. */
19
+ env?: Record<string, string>;
20
+ /** The working directory for the `run` steps and the start command. */
21
+ workdir?: string;
22
+ /** The start command (`--cmd`): E2B runs it when the template is snapshotted, so a sandbox starts with it running. */
23
+ startCmd?: string;
24
+ /** The ready command (`--ready-cmd`): E2B polls it until it exits 0 before it snapshots the template. */
25
+ readyCmd?: string;
26
+ /** vCPUs per sandbox. A positive integer; E2B's default is 2. */
27
+ cpuCount?: number;
28
+ /** Memory per sandbox in MiB. A positive even integer; E2B's default is 1024. */
29
+ memoryMB?: number;
30
+ }
31
+ export interface Template extends Declarable {
32
+ readonly props: TemplateProps;
33
+ }
34
+ /** A sandbox template, serialized to `e2b.<name>.Dockerfile` and an `e2b template create` command. */
35
+ export declare const Template: new (props: TemplateProps) => Template;
36
+ /** The props of a declared `Template`, or `undefined` for anything else. */
37
+ export declare function templateProps(entity: unknown): TemplateProps | undefined;
38
+ //# sourceMappingURL=resources.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resources.d.ts","sourceRoot":"","sources":["../src/resources.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAI9D,MAAM,WAAW,aAAa;IAC5B,sHAAsH;IACtH,IAAI,EAAE,MAAM,CAAC;IACb,iFAAiF;IACjF,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,GAAG,CAAC,EAAE,MAAM,EAAE,CAAC;IACf,yEAAyE;IACzE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7B,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sHAAsH;IACtH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,yGAAyG;IACzG,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iEAAiE;IACjE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,QAAS,SAAQ,UAAU;IAC1C,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC;CAC/B;AAED,sGAAsG;AACtG,eAAO,MAAM,QAAQ,EAA0E,KAC7F,KAAK,EAAE,aAAa,KACjB,QAAQ,CAAC;AAEd,4EAA4E;AAC5E,wBAAgB,aAAa,CAAC,MAAM,EAAE,OAAO,GAAG,aAAa,GAAG,SAAS,CAIxE"}
@@ -0,0 +1,21 @@
1
+ /**
2
+ * The e2b lexicon's one declarable kind: a sandbox template.
3
+ *
4
+ * A steward's sandbox is made from a template (`template` in the profile,
5
+ * default `base`). The runtime needs git and Node in it, and a project usually
6
+ * wants its own tools there too, installed once rather than on every run. A
7
+ * `Template` declares that image; `chant build` writes it out as a Dockerfile
8
+ * and the `e2b template create` command line that builds it.
9
+ */
10
+ import { createResource } from "@intentius/chant/runtime";
11
+ import { TEMPLATE_TYPE } from "./catalog.js";
12
+ /** A sandbox template, serialized to `e2b.<name>.Dockerfile` and an `e2b template create` command. */
13
+ export const Template = /* @__PURE__ */ createResource(TEMPLATE_TYPE, "e2b", {});
14
+ /** The props of a declared `Template`, or `undefined` for anything else. */
15
+ export function templateProps(entity) {
16
+ if (!entity || typeof entity !== "object")
17
+ return undefined;
18
+ if (entity.entityType !== TEMPLATE_TYPE)
19
+ return undefined;
20
+ return (entity.props ?? {});
21
+ }
@@ -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,41 @@
1
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
2
+ import { ts } from "@intentius/chant/lint/typescript";
3
+ import { at, property, templateLiterals } from "./template-literal";
4
+
5
+ /** An image reference with no tag or digest, or the `latest` tag. */
6
+ export function isFloatingImage(image: string): boolean {
7
+ if (image.includes("@")) return false;
8
+ const lastSegment = image.slice(image.lastIndexOf("/") + 1);
9
+ const colon = lastSegment.indexOf(":");
10
+ return colon === -1 || lastSegment.slice(colon + 1) === "latest";
11
+ }
12
+
13
+ /**
14
+ * E2B002: A template's base image is pinned.
15
+ *
16
+ * A steward keeps one sandbox for months, but a template is rebuilt whenever
17
+ * someone runs `e2b template create`. With `fromImage` untagged or on `latest`,
18
+ * two builds of the same declaration can carry different Node or git
19
+ * versions, and a steward made from the second behaves differently with no
20
+ * change in the repository.
21
+ */
22
+ export const pinnedImageRule: LintRule = {
23
+ id: "E2B002",
24
+ severity: "warning",
25
+ category: "correctness",
26
+ description: "A Template's fromImage should name a tag other than latest, or a digest",
27
+
28
+ check(context: LintContext): LintDiagnostic[] {
29
+ const { sourceFile } = context;
30
+ const diagnostics: LintDiagnostic[] = [];
31
+ for (const obj of templateLiterals(sourceFile)) {
32
+ const image = property(obj, "fromImage", sourceFile);
33
+ if (image && ts.isStringLiteralLike(image) && image.text.length > 0 && isFloatingImage(image.text)) {
34
+ diagnostics.push(
35
+ at(sourceFile, image, "E2B002", "warning", `fromImage "${image.text}" is not pinned; name a version tag such as node:22-bookworm, or a digest.`),
36
+ );
37
+ }
38
+ }
39
+ return diagnostics;
40
+ },
41
+ };
@@ -0,0 +1,42 @@
1
+ /** Helpers the e2b lint rules share: finding `new Template({...})` literals and reporting on them. */
2
+
3
+ import type { LintDiagnostic } from "@intentius/chant/lint/rule";
4
+ import { ts } from "@intentius/chant/lint/typescript";
5
+
6
+ /** The object literal passed to each `new Template({...})` in the file. */
7
+ export function templateLiterals(sourceFile: ts.SourceFile): ts.ObjectLiteralExpression[] {
8
+ const found: ts.ObjectLiteralExpression[] = [];
9
+ function visit(node: ts.Node): void {
10
+ if (ts.isNewExpression(node)) {
11
+ const callee = node.expression;
12
+ const name = ts.isIdentifier(callee) ? callee.text : ts.isPropertyAccessExpression(callee) ? callee.name.text : "";
13
+ const arg = node.arguments?.[0];
14
+ if (name === "Template" && arg && ts.isObjectLiteralExpression(arg)) found.push(arg);
15
+ }
16
+ ts.forEachChild(node, visit);
17
+ }
18
+ visit(sourceFile);
19
+ return found;
20
+ }
21
+
22
+ /** The initializer of `key` in an object literal, when it is written there. */
23
+ export function property(obj: ts.ObjectLiteralExpression, key: string, sourceFile: ts.SourceFile): ts.Expression | undefined {
24
+ for (const p of obj.properties) {
25
+ if (ts.isPropertyAssignment(p) && p.name.getText(sourceFile) === key) return p.initializer;
26
+ }
27
+ return undefined;
28
+ }
29
+
30
+ /** A numeric literal's value, a negated one included; `undefined` for anything computed. */
31
+ export function numberLiteral(expr: ts.Expression): number | undefined {
32
+ if (ts.isNumericLiteral(expr)) return Number(expr.text);
33
+ if (ts.isPrefixUnaryExpression(expr) && expr.operator === ts.SyntaxKind.MinusToken && ts.isNumericLiteral(expr.operand)) {
34
+ return -Number(expr.operand.text);
35
+ }
36
+ return undefined;
37
+ }
38
+
39
+ export function at(sourceFile: ts.SourceFile, node: ts.Node, ruleId: string, severity: LintDiagnostic["severity"], message: string): LintDiagnostic {
40
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile));
41
+ return { file: sourceFile.fileName, line: line + 1, column: character + 1, ruleId, severity, message };
42
+ }
@@ -0,0 +1,36 @@
1
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
2
+ import { at, numberLiteral, property, templateLiterals } from "./template-literal";
3
+
4
+ /**
5
+ * E2B001: A template's CPU and memory are sizes E2B accepts.
6
+ *
7
+ * `cpuCount` is a whole number of vCPUs, at least one. `memoryMB` is a whole
8
+ * number of MiB, at least one and even: `e2b template create` refuses an odd
9
+ * one. Both are only checked when written as literals.
10
+ */
11
+ export const templateSizeRule: LintRule = {
12
+ id: "E2B001",
13
+ severity: "error",
14
+ category: "correctness",
15
+ description: "A Template's cpuCount must be a positive integer and its memoryMB a positive even integer",
16
+
17
+ check(context: LintContext): LintDiagnostic[] {
18
+ const { sourceFile } = context;
19
+ const diagnostics: LintDiagnostic[] = [];
20
+ for (const obj of templateLiterals(sourceFile)) {
21
+ const cpu = property(obj, "cpuCount", sourceFile);
22
+ const cpuValue = cpu ? numberLiteral(cpu) : undefined;
23
+ if (cpu && cpuValue !== undefined && !(Number.isInteger(cpuValue) && cpuValue >= 1)) {
24
+ diagnostics.push(at(sourceFile, cpu, "E2B001", "error", `cpuCount ${cpuValue} is not a whole number of vCPUs; use 1 or more.`));
25
+ }
26
+ const mem = property(obj, "memoryMB", sourceFile);
27
+ const memValue = mem ? numberLiteral(mem) : undefined;
28
+ if (mem && memValue !== undefined && !(Number.isInteger(memValue) && memValue >= 2 && memValue % 2 === 0)) {
29
+ diagnostics.push(
30
+ at(sourceFile, mem, "E2B001", "error", `memoryMB ${memValue} is not a positive even number of MiB, which E2B requires.`),
31
+ );
32
+ }
33
+ }
34
+ return diagnostics;
35
+ },
36
+ };
@@ -0,0 +1,18 @@
1
+ import type { Serializer } from "@intentius/chant/serializer";
2
+ import { type TemplateProps } from "./resources.js";
3
+ /**
4
+ * The e2b serializer: each declared `Template` becomes `e2b.<name>.Dockerfile`
5
+ * (`FROM`, then `ENV`, `WORKDIR` and one `RUN` per step), named after the
6
+ * template so several can sit in one directory.
7
+ *
8
+ * The primary output is a JSON index of the templates, keyed by the name each
9
+ * was exported as. Each entry carries the Dockerfile's name and `build`, the
10
+ * `e2b template create` command line that builds the template from it, run in
11
+ * the output directory. A build with no templates (a project that only runs
12
+ * Ops here) gets an empty index.
13
+ */
14
+ export declare const e2bSerializer: Serializer;
15
+ export declare function renderDockerfile(props: TemplateProps): string;
16
+ /** The `e2b template create` command line that builds the template, run where the Dockerfile is. */
17
+ export declare function createCommand(name: string, dockerfile: string, props: TemplateProps): string;
18
+ //# sourceMappingURL=serializer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAoB,MAAM,6BAA6B,CAAC;AAChF,OAAO,EAAiB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAEhE;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa,EAAE,UA+B3B,CAAC;AAUF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,aAAa,GAAG,MAAM,CAM7D;AAED,oGAAoG;AACpG,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,aAAa,GAAG,MAAM,CAO5F"}
@@ -0,0 +1,70 @@
1
+ import { templateProps } from "./resources.js";
2
+ /**
3
+ * The e2b serializer: each declared `Template` becomes `e2b.<name>.Dockerfile`
4
+ * (`FROM`, then `ENV`, `WORKDIR` and one `RUN` per step), named after the
5
+ * template so several can sit in one directory.
6
+ *
7
+ * The primary output is a JSON index of the templates, keyed by the name each
8
+ * was exported as. Each entry carries the Dockerfile's name and `build`, the
9
+ * `e2b template create` command line that builds the template from it, run in
10
+ * the output directory. A build with no templates (a project that only runs
11
+ * Ops here) gets an empty index.
12
+ */
13
+ export const e2bSerializer = {
14
+ name: "e2b",
15
+ rulePrefix: "E2B",
16
+ serialize(entities) {
17
+ const index = {};
18
+ const files = {};
19
+ const templates = [...entities]
20
+ .map(([exportName, entity]) => [exportName, templateProps(entity)])
21
+ .filter((e) => e[1] !== undefined)
22
+ .sort(([a], [b]) => a.localeCompare(b));
23
+ for (const [exportName, props] of templates) {
24
+ const name = props.name || exportName;
25
+ const dockerfile = `e2b.${name}.Dockerfile`;
26
+ files[dockerfile] = renderDockerfile(props);
27
+ index[exportName] = {
28
+ name,
29
+ dockerfile,
30
+ build: createCommand(name, dockerfile, props),
31
+ ...(props.cpuCount !== undefined ? { cpuCount: props.cpuCount } : {}),
32
+ ...(props.memoryMB !== undefined ? { memoryMB: props.memoryMB } : {}),
33
+ };
34
+ }
35
+ return {
36
+ primary: JSON.stringify({ templates: index }, null, 2) + "\n",
37
+ files,
38
+ verbatimFiles: Object.keys(files),
39
+ };
40
+ },
41
+ };
42
+ /** A Dockerfile string literal: Docker reads double-quoted values with JSON's escapes. */
43
+ const quote = (value) => JSON.stringify(value);
44
+ /** A word as one shell argument. */
45
+ function shellWord(value) {
46
+ return /^[A-Za-z0-9._:@+=/,-]+$/.test(value) ? value : `'${value.replace(/'/g, `'\\''`)}'`;
47
+ }
48
+ export function renderDockerfile(props) {
49
+ const lines = [`FROM ${props.fromImage}`];
50
+ for (const [key, value] of Object.entries(props.env ?? {}))
51
+ lines.push(`ENV ${key}=${quote(value)}`);
52
+ if (props.workdir)
53
+ lines.push(`WORKDIR ${props.workdir}`);
54
+ for (const step of props.run ?? [])
55
+ lines.push(`RUN ${step}`);
56
+ return lines.join("\n") + "\n";
57
+ }
58
+ /** The `e2b template create` command line that builds the template, run where the Dockerfile is. */
59
+ export function createCommand(name, dockerfile, props) {
60
+ const args = ["e2b", "template", "create", name, "--dockerfile", dockerfile];
61
+ if (props.startCmd)
62
+ args.push("--cmd", props.startCmd);
63
+ if (props.readyCmd)
64
+ args.push("--ready-cmd", props.readyCmd);
65
+ if (props.cpuCount !== undefined)
66
+ args.push("--cpu-count", String(props.cpuCount));
67
+ if (props.memoryMB !== undefined)
68
+ args.push("--memory-mb", String(props.memoryMB));
69
+ return args.map(shellWord).join(" ");
70
+ }
@@ -0,0 +1,69 @@
1
+ ---
2
+ skill: chant-e2b-ops
3
+ description: Run chant ops on a steward sandbox with `--on e2b`, on hosted E2B or any API that speaks E2B's
4
+ user-invocable: true
5
+ ---
6
+
7
+ # Running chant ops on an e2b steward
8
+
9
+ `chant run <op> --on e2b` runs the op on a steward: one sandbox that outlives
10
+ every run. It keeps a clone of the project and its installed tools on its disk,
11
+ runs one op at a time, and sleeps paused between runs. Any API that speaks
12
+ E2B's hosts one: hosted E2B, or a platform that serves the same API.
13
+
14
+ ## Set it up
15
+
16
+ ```ts
17
+ // chant.config.ts
18
+ import type { ChantConfig } from "@intentius/chant/config";
19
+ import "@intentius/chant-lexicon-e2b";
20
+
21
+ export default {
22
+ lexicons: ["e2b"],
23
+ e2b: {
24
+ profiles: {
25
+ prod: {
26
+ apiKey: { env: "E2B_API_KEY" },
27
+ domain: "e2b.example.com", // omit for hosted E2B
28
+ steward: "prod-steward", // default: the repository's name
29
+ gitToken: { env: "GITHUB_TOKEN" }, // fetch a private repo, push the ledger
30
+ },
31
+ },
32
+ defaultProfile: "prod",
33
+ },
34
+ } satisfies ChantConfig;
35
+ ```
36
+
37
+ The project must depend on `@intentius/chant`, because the steward runs the
38
+ chant the project installs. The template (default `base`) needs git and Node.
39
+
40
+ ## Run, read, cancel
41
+
42
+ ```bash
43
+ chant run prod-apply --on e2b # streams the steps, then pauses the steward
44
+ chant run status prod-apply --on e2b # a run in progress, or the last one recorded
45
+ chant run log prod-apply --on e2b
46
+ chant run cancel prod-apply --on e2b --force
47
+ ```
48
+
49
+ The steward runs the commit this checkout is at, fetched from the repository,
50
+ so **push first**. Uncommitted changes are not sent; the run warns about them.
51
+
52
+ One run at a time: a run while the steward is busy is refused, naming the run
53
+ in progress. A client that dies mid-run (Ctrl-C excepted) leaves the run going;
54
+ `status` reports it, and the steward pauses itself when it is done.
55
+
56
+ ## Gates
57
+
58
+ A gate works as it does locally. The run reaches it in the sandbox, pushes the
59
+ pending fact to the repository's `chant/lifecycle` branch and ends `gated`
60
+ (exit 3). `chant approve <op> <gate>` from any checkout of the repository
61
+ records the answer, and the next `--on e2b` run walks through. The steward also
62
+ pushes its run records, so `chant run log` on any checkout sees its runs.
63
+
64
+ ## Running on a schedule
65
+
66
+ Nothing in E2B fires on a clock. Run `chant run <op> --on e2b` from whatever
67
+ already does: a CI `schedule:` trigger (the github lexicon renders an op's
68
+ `schedule` as one), a CronJob, or `chant operator` on a machine that stays up.
69
+ The job is short, because the steward does the work.
@@ -0,0 +1 @@
1
+ export * from "../index.js";