@intentius/chant-lexicon-fountain 0.33.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/README.md +63 -0
  2. package/dist/codegen/docs-cli.d.ts +3 -0
  3. package/dist/codegen/docs-cli.d.ts.map +1 -0
  4. package/dist/codegen/docs.d.ts +16 -0
  5. package/dist/codegen/docs.d.ts.map +1 -0
  6. package/dist/codegen/generate-cli.d.ts +3 -0
  7. package/dist/codegen/generate-cli.d.ts.map +1 -0
  8. package/dist/codegen/generate-lexicon.d.ts +24 -0
  9. package/dist/codegen/generate-lexicon.d.ts.map +1 -0
  10. package/dist/codegen/generate-typescript.d.ts +11 -0
  11. package/dist/codegen/generate-typescript.d.ts.map +1 -0
  12. package/dist/codegen/generate.d.ts +15 -0
  13. package/dist/codegen/generate.d.ts.map +1 -0
  14. package/dist/codegen/naming.d.ts +14 -0
  15. package/dist/codegen/naming.d.ts.map +1 -0
  16. package/dist/codegen/package.d.ts +17 -0
  17. package/dist/codegen/package.d.ts.map +1 -0
  18. package/dist/composites/concierge-stack.d.ts +44 -0
  19. package/dist/composites/concierge-stack.d.ts.map +1 -0
  20. package/dist/coverage.d.ts +56 -0
  21. package/dist/coverage.d.ts.map +1 -0
  22. package/dist/describe-resources.d.ts +45 -0
  23. package/dist/describe-resources.d.ts.map +1 -0
  24. package/dist/detect.d.ts +10 -0
  25. package/dist/detect.d.ts.map +1 -0
  26. package/dist/entity-props.d.ts +11 -0
  27. package/dist/entity-props.d.ts.map +1 -0
  28. package/dist/export-resources.d.ts +29 -0
  29. package/dist/export-resources.d.ts.map +1 -0
  30. package/dist/generated/index.d.ts +5 -0
  31. package/dist/generated/index.d.ts.map +1 -0
  32. package/dist/generated/runtime.d.ts +5 -0
  33. package/dist/generated/runtime.d.ts.map +1 -0
  34. package/dist/import/generator.d.ts +15 -0
  35. package/dist/import/generator.d.ts.map +1 -0
  36. package/dist/import/parser.d.ts +19 -0
  37. package/dist/import/parser.d.ts.map +1 -0
  38. package/dist/index.d.ts +8 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/init-templates.d.ts +13 -0
  41. package/dist/init-templates.d.ts.map +1 -0
  42. package/dist/integrity.json +21 -0
  43. package/dist/lint/audit-catalog.d.ts +15 -0
  44. package/dist/lint/audit-catalog.d.ts.map +1 -0
  45. package/dist/lint/post-synth/ftn010-networking-explicit.d.ts +11 -0
  46. package/dist/lint/post-synth/ftn010-networking-explicit.d.ts.map +1 -0
  47. package/dist/lint/post-synth/ftn011-no-unrestricted-networking.d.ts +11 -0
  48. package/dist/lint/post-synth/ftn011-no-unrestricted-networking.d.ts.map +1 -0
  49. package/dist/lint/post-synth/ftn012-no-cloud-credential-env.d.ts +3 -0
  50. package/dist/lint/post-synth/ftn012-no-cloud-credential-env.d.ts.map +1 -0
  51. package/dist/lint/post-synth/ftn013-substitution-resolvable.d.ts +3 -0
  52. package/dist/lint/post-synth/ftn013-substitution-resolvable.d.ts.map +1 -0
  53. package/dist/lint/post-synth/ftn014-vault-shadowing.d.ts +3 -0
  54. package/dist/lint/post-synth/ftn014-vault-shadowing.d.ts.map +1 -0
  55. package/dist/lint/post-synth/ftn015-mcp-secret-env-substitution.d.ts +3 -0
  56. package/dist/lint/post-synth/ftn015-mcp-secret-env-substitution.d.ts.map +1 -0
  57. package/dist/lint/post-synth/ftn016-runtime-model-valid.d.ts +3 -0
  58. package/dist/lint/post-synth/ftn016-runtime-model-valid.d.ts.map +1 -0
  59. package/dist/lint/post-synth/ftn017-unique-resource-names.d.ts +11 -0
  60. package/dist/lint/post-synth/ftn017-unique-resource-names.d.ts.map +1 -0
  61. package/dist/lint/post-synth/index.d.ts +3 -0
  62. package/dist/lint/post-synth/index.d.ts.map +1 -0
  63. package/dist/lint/rules/ftn001-no-secret-literals.d.ts +3 -0
  64. package/dist/lint/rules/ftn001-no-secret-literals.d.ts.map +1 -0
  65. package/dist/lint/rules/index.d.ts +5 -0
  66. package/dist/lint/rules/index.d.ts.map +1 -0
  67. package/dist/lsp/completions.d.ts +7 -0
  68. package/dist/lsp/completions.d.ts.map +1 -0
  69. package/dist/lsp/hover.d.ts +4 -0
  70. package/dist/lsp/hover.d.ts.map +1 -0
  71. package/dist/manifest.json +6 -0
  72. package/dist/meta.json +69 -0
  73. package/dist/op/activities/fountain-apply.d.ts +76 -0
  74. package/dist/op/activities/fountain-apply.d.ts.map +1 -0
  75. package/dist/op/activities/fountain-run.d.ts +36 -0
  76. package/dist/op/activities/fountain-run.d.ts.map +1 -0
  77. package/dist/op/activities/index.d.ts +11 -0
  78. package/dist/op/activities/index.d.ts.map +1 -0
  79. package/dist/package-cli.d.ts +3 -0
  80. package/dist/package-cli.d.ts.map +1 -0
  81. package/dist/plugin.d.ts +8 -0
  82. package/dist/plugin.d.ts.map +1 -0
  83. package/dist/reference-catalog.d.ts +16 -0
  84. package/dist/reference-catalog.d.ts.map +1 -0
  85. package/dist/rules/ftn001-no-secret-literals.ts +71 -0
  86. package/dist/rules/ftn010-networking-explicit.ts +37 -0
  87. package/dist/rules/ftn011-no-unrestricted-networking.ts +36 -0
  88. package/dist/rules/ftn012-no-cloud-credential-env.ts +58 -0
  89. package/dist/rules/ftn013-substitution-resolvable.ts +93 -0
  90. package/dist/rules/ftn014-vault-shadowing.ts +61 -0
  91. package/dist/rules/ftn015-mcp-secret-env-substitution.ts +49 -0
  92. package/dist/rules/ftn016-runtime-model-valid.ts +50 -0
  93. package/dist/rules/ftn017-unique-resource-names.ts +43 -0
  94. package/dist/serializer.d.ts +3 -0
  95. package/dist/serializer.d.ts.map +1 -0
  96. package/dist/skills/chant-fountain-locked-sandboxes.md +34 -0
  97. package/dist/skills/chant-fountain-secrets.md +29 -0
  98. package/dist/skills/chant-fountain.md +45 -0
  99. package/dist/spec/fetch.d.ts +28 -0
  100. package/dist/spec/fetch.d.ts.map +1 -0
  101. package/dist/spec/parse.d.ts +58 -0
  102. package/dist/spec/parse.d.ts.map +1 -0
  103. package/dist/types/index.d.ts +60 -0
  104. package/dist/validate-cli.d.ts +3 -0
  105. package/dist/validate-cli.d.ts.map +1 -0
  106. package/dist/validate.d.ts +15 -0
  107. package/dist/validate.d.ts.map +1 -0
  108. package/package.json +61 -0
  109. package/src/codegen/docs-cli.ts +7 -0
  110. package/src/codegen/docs.ts +278 -0
  111. package/src/codegen/generate-cli.ts +8 -0
  112. package/src/codegen/generate-lexicon.ts +58 -0
  113. package/src/codegen/generate-typescript.ts +69 -0
  114. package/src/codegen/generate.ts +109 -0
  115. package/src/codegen/naming.ts +45 -0
  116. package/src/codegen/package.ts +32 -0
  117. package/src/composites/composites.test.ts +35 -0
  118. package/src/composites/concierge-stack.ts +71 -0
  119. package/src/coverage.test.ts +63 -0
  120. package/src/coverage.ts +178 -0
  121. package/src/describe-resources.test.ts +203 -0
  122. package/src/describe-resources.ts +192 -0
  123. package/src/detect.ts +37 -0
  124. package/src/entity-props.ts +22 -0
  125. package/src/export-resources.ts +103 -0
  126. package/src/generated/.gitkeep +0 -0
  127. package/src/generated/index.d.ts +60 -0
  128. package/src/generated/index.ts +10 -0
  129. package/src/generated/lexicon-fountain.json +69 -0
  130. package/src/generated/runtime.ts +4 -0
  131. package/src/import/generator.ts +112 -0
  132. package/src/import/parser.ts +83 -0
  133. package/src/import/roundtrip.test.ts +113 -0
  134. package/src/index.ts +17 -0
  135. package/src/init-templates.ts +60 -0
  136. package/src/lint/audit-catalog.ts +107 -0
  137. package/src/lint/post-synth/ftn010-networking-explicit.ts +37 -0
  138. package/src/lint/post-synth/ftn011-no-unrestricted-networking.ts +36 -0
  139. package/src/lint/post-synth/ftn012-no-cloud-credential-env.ts +58 -0
  140. package/src/lint/post-synth/ftn013-substitution-resolvable.ts +93 -0
  141. package/src/lint/post-synth/ftn014-vault-shadowing.ts +61 -0
  142. package/src/lint/post-synth/ftn015-mcp-secret-env-substitution.ts +49 -0
  143. package/src/lint/post-synth/ftn016-runtime-model-valid.ts +50 -0
  144. package/src/lint/post-synth/ftn017-unique-resource-names.ts +43 -0
  145. package/src/lint/post-synth/index.ts +21 -0
  146. package/src/lint/post-synth/post-synth.test.ts +133 -0
  147. package/src/lint/rules/ftn001-no-secret-literals.ts +71 -0
  148. package/src/lint/rules/index.ts +7 -0
  149. package/src/lint/rules/rules.test.ts +126 -0
  150. package/src/lsp/completions.test.ts +51 -0
  151. package/src/lsp/completions.ts +21 -0
  152. package/src/lsp/hover.test.ts +47 -0
  153. package/src/lsp/hover.ts +70 -0
  154. package/src/op/activities/fountain-apply.test.ts +208 -0
  155. package/src/op/activities/fountain-apply.ts +249 -0
  156. package/src/op/activities/fountain-run.ts +95 -0
  157. package/src/op/activities/index.ts +28 -0
  158. package/src/package-cli.ts +26 -0
  159. package/src/plugin.test.ts +73 -0
  160. package/src/plugin.ts +170 -0
  161. package/src/reference-catalog.ts +27 -0
  162. package/src/serializer.test.ts +109 -0
  163. package/src/serializer.ts +143 -0
  164. package/src/skills/chant-fountain-locked-sandboxes.md +34 -0
  165. package/src/skills/chant-fountain-secrets.md +29 -0
  166. package/src/skills/chant-fountain.md +45 -0
  167. package/src/spec/fetch.ts +53 -0
  168. package/src/spec/fountain-openapi.snapshot.json +1 -0
  169. package/src/spec/parse.ts +309 -0
  170. package/src/validate-cli.ts +13 -0
  171. package/src/validate.ts +34 -0
@@ -0,0 +1,73 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { fountainPlugin } from "./plugin";
3
+ import { isLexiconPlugin } from "@intentius/chant/lexicon";
4
+
5
+ describe("fountain plugin", () => {
6
+ it("is a valid LexiconPlugin", () => {
7
+ expect(isLexiconPlugin(fountainPlugin)).toBe(true);
8
+ });
9
+
10
+ it("has the correct name", () => {
11
+ expect(fountainPlugin.name).toBe("fountain");
12
+ });
13
+
14
+ it("has a serializer", () => {
15
+ expect(fountainPlugin.serializer).toBeDefined();
16
+ });
17
+
18
+ it("namespaces its MCP tool so it can load beside other lexicons", () => {
19
+ const tools = fountainPlugin.mcpTools?.() ?? [];
20
+ expect(tools.map((t) => t.name)).toEqual(["fountain:diff"]);
21
+ });
22
+
23
+ it("serves the resource catalog over MCP", async () => {
24
+ const resources = fountainPlugin.mcpResources?.() ?? [];
25
+ expect(resources.map((r) => r.uri)).toEqual(["fountain:resource-catalog"]);
26
+
27
+ const catalog = JSON.parse(await resources[0].handler()) as Array<{
28
+ className: string;
29
+ kind: string;
30
+ }>;
31
+ const resourceKinds = catalog.filter((e) => e.kind === "resource").map((e) => e.className);
32
+ expect(resourceKinds.sort()).toEqual(["Agent", "Environment", "Vault"]);
33
+ });
34
+
35
+ it("exposes every post-synth check and lint rule", () => {
36
+ expect(fountainPlugin.lintRules?.()).toHaveLength(1);
37
+ expect(fountainPlugin.postSynthChecks?.()).toHaveLength(8);
38
+ });
39
+
40
+ it("carries audit metadata for every rule it ships", () => {
41
+ const catalog = fountainPlugin.auditCatalog?.() ?? {};
42
+ const ruleIds = [
43
+ ...(fountainPlugin.lintRules?.() ?? []).map((r) => r.id),
44
+ ...(fountainPlugin.postSynthChecks?.() ?? []).map((c) => c.id),
45
+ ];
46
+
47
+ expect(ruleIds.sort()).toEqual(Object.keys(catalog).sort());
48
+ });
49
+
50
+ it("marks its checks entity-based so audit does not claim they fire on YAML", () => {
51
+ const catalog = fountainPlugin.auditCatalog?.() ?? {};
52
+ // Every fountain check reads ctx.entities, so none can fire against
53
+ // standalone fountain YAML. Flipping one to true would misreport it.
54
+ expect(Object.values(catalog).every((m) => m.yamlBased === false)).toBe(true);
55
+ });
56
+
57
+ it("scaffolds a closed sandbox by default", () => {
58
+ const def = fountainPlugin.initTemplates?.()?.src["fountain.ts"] ?? "";
59
+ expect(def).toContain("ConciergeStack");
60
+
61
+ const open = fountainPlugin.initTemplates?.("open")?.src["fountain.ts"] ?? "";
62
+ expect(open).toContain("networking_type");
63
+ expect(open).not.toContain("ConciergeStack");
64
+ });
65
+
66
+ it("loads all three skills", () => {
67
+ expect(fountainPlugin.skills?.().map((s) => s.name).sort()).toEqual([
68
+ "chant-fountain",
69
+ "chant-fountain-locked-sandboxes",
70
+ "chant-fountain-secrets",
71
+ ]);
72
+ });
73
+ });
package/src/plugin.ts ADDED
@@ -0,0 +1,170 @@
1
+ import type { LexiconPlugin, SkillDefinition, IntrinsicDef } from "@intentius/chant/lexicon";
2
+ import type { LintRule } from "@intentius/chant/lint/rule";
3
+ import type { PostSynthCheck } from "@intentius/chant/lint/post-synth";
4
+ import type { CompletionContext, CompletionItem, HoverContext, HoverInfo } from "@intentius/chant/lsp/types";
5
+ import type { McpToolContribution, McpResourceContribution } from "@intentius/chant/mcp/types";
6
+ import {
7
+ createSkillsLoader,
8
+ createDiffTool,
9
+ createCatalogResource,
10
+ } from "@intentius/chant/lexicon-plugin-helpers";
11
+ import { fountainSerializer } from "./serializer";
12
+ import { rules } from "./lint/rules";
13
+ import { postSynthChecks } from "./lint/post-synth";
14
+ import { fountainAuditCatalog } from "./lint/audit-catalog";
15
+ import { fountainReferenceCatalog } from "./reference-catalog";
16
+ import { detectFountainTemplate } from "./detect";
17
+ import { fountainInitTemplates } from "./init-templates";
18
+ import { FountainParser } from "./import/parser";
19
+ import { FountainGenerator } from "./import/generator";
20
+ import { completions } from "./lsp/completions";
21
+ import { hover } from "./lsp/hover";
22
+
23
+ /**
24
+ * fountain lexicon plugin.
25
+ *
26
+ * Implements all required LexiconPlugin lifecycle methods.
27
+ */
28
+ export const fountainPlugin: LexiconPlugin = {
29
+ name: "fountain",
30
+ serializer: fountainSerializer,
31
+
32
+ // ── Required lifecycle methods ────────────────────────────────
33
+
34
+ async generate(options?: { verbose?: boolean }): Promise<void> {
35
+ const { generate } = await import("./codegen/generate");
36
+ await generate(options);
37
+ },
38
+
39
+ async validate(options?: { verbose?: boolean }): Promise<void> {
40
+ const { validate } = await import("./validate");
41
+ const { printValidationResult } = await import("@intentius/chant/codegen/validate");
42
+ const result = await validate();
43
+ printValidationResult(result);
44
+ },
45
+
46
+ async coverage(options?: { verbose?: boolean; minOverall?: number }): Promise<void> {
47
+ const { analyzeFountainCoverage } = await import("./coverage");
48
+ await analyzeFountainCoverage(options);
49
+ },
50
+
51
+ async package(options?: { verbose?: boolean; force?: boolean }): Promise<void> {
52
+ const { packageLexicon } = await import("./codegen/package");
53
+ const { writeBundleSpec } = await import("@intentius/chant/codegen/package");
54
+ const { join, dirname } = await import("path");
55
+ const { fileURLToPath } = await import("url");
56
+
57
+ const { spec, stats } = await packageLexicon(options);
58
+ const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
59
+ writeBundleSpec(spec, join(pkgDir, "dist"));
60
+
61
+ console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
62
+ },
63
+
64
+ // ── Optional extensions ────────────────────────────────────
65
+
66
+ lintRules() {
67
+ return rules;
68
+ },
69
+
70
+ postSynthChecks() {
71
+ return postSynthChecks;
72
+ },
73
+
74
+ auditCatalog: () => fountainAuditCatalog,
75
+
76
+ skills: createSkillsLoader(import.meta.url, [
77
+ {
78
+ file: "chant-fountain.md",
79
+ name: "chant-fountain",
80
+ description:
81
+ "Declare, lint, and reconcile fountain Environments, Vaults, and Agents from a chant project",
82
+ triggers: [
83
+ { type: "context" as const, value: "fountain" },
84
+ { type: "context" as const, value: "founta.inevitable.fyi" },
85
+ ],
86
+ },
87
+ {
88
+ file: "chant-fountain-secrets.md",
89
+ name: "chant-fountain-secrets",
90
+ description: "Handle fountain secrets, env vars, and ${VAR} substitution safely from chant",
91
+ triggers: [
92
+ { type: "context" as const, value: "fountain secrets" },
93
+ { type: "context" as const, value: "vault" },
94
+ ],
95
+ },
96
+ {
97
+ file: "chant-fountain-locked-sandboxes.md",
98
+ name: "chant-fountain-locked-sandboxes",
99
+ description:
100
+ "Declare locked-down fountain environments for untrusted agents and run conversations against them",
101
+ triggers: [
102
+ { type: "context" as const, value: "sandbox" },
103
+ { type: "context" as const, value: "networking_type" },
104
+ ],
105
+ },
106
+ ]),
107
+
108
+ mcpTools(): McpToolContribution[] {
109
+ return [
110
+ createDiffTool(
111
+ fountainSerializer,
112
+ "Compare current fountain build output (fountain manifests + fountain-plan.json) against previous version",
113
+ "fountain",
114
+ ),
115
+ ];
116
+ },
117
+
118
+ mcpResources(): McpResourceContribution[] {
119
+ return [
120
+ createCatalogResource(
121
+ import.meta.url,
122
+ "Fountain Resource Types",
123
+ "The three fountain workload kinds declarable from chant: Environment, Vault, Agent (conversations are runs, started by the fountainRun op)",
124
+ "lexicon-fountain.json",
125
+ "fountain",
126
+ ),
127
+ ];
128
+ },
129
+
130
+ initTemplates(template?: string) {
131
+ return fountainInitTemplates(template);
132
+ },
133
+
134
+ detectTemplate(data: unknown) {
135
+ return detectFountainTemplate(data);
136
+ },
137
+
138
+ templateParser() {
139
+ return new FountainParser();
140
+ },
141
+
142
+ templateGenerator() {
143
+ return new FountainGenerator();
144
+ },
145
+
146
+ async exportResources(options) {
147
+ const { exportResources } = await import("./export-resources");
148
+ return exportResources(options);
149
+ },
150
+
151
+ async describeResources(options) {
152
+ const { describeResources } = await import("./describe-resources");
153
+ return describeResources(options);
154
+ },
155
+
156
+ referenceCatalog: fountainReferenceCatalog,
157
+
158
+ completionProvider(ctx: CompletionContext) {
159
+ return completions(ctx);
160
+ },
161
+
162
+ hoverProvider(ctx: HoverContext) {
163
+ return hover(ctx);
164
+ },
165
+
166
+ async docs(options?) {
167
+ const { generateDocs } = await import("./codegen/docs");
168
+ return generateDocs(options);
169
+ },
170
+ };
@@ -0,0 +1,27 @@
1
+ import type { ReferenceCatalog } from "@intentius/chant/lexicon";
2
+
3
+ /**
4
+ * Fountain reference catalog — how observed fountain resources reference
5
+ * each other, so `chant graph --live` reconstructs the topology.
6
+ *
7
+ * One relationship matters: an Agent runs in an Environment
8
+ * (`environment_id`). `describeResources` puts `environment_id` in the
9
+ * agent's observed attributes and indexes environments by `id` (their
10
+ * physicalId), so the edge resolves with no enrichment pass.
11
+ *
12
+ * Vaults are deliberately edge-free here: vault↔agent binding is a
13
+ * conversation-time choice (scoped by `allowed_vault_ids`), not standing
14
+ * topology.
15
+ */
16
+ export const fountainReferenceCatalog: ReferenceCatalog = {
17
+ identities: [{ kind: "Fountain::V1::Environment", ids: ["id"] }],
18
+ refs: [
19
+ {
20
+ from: "Fountain::V1::Agent",
21
+ path: "environment_id",
22
+ targetKind: "Fountain::V1::Environment",
23
+ relation: "reference",
24
+ label: "environment",
25
+ },
26
+ ],
27
+ };
@@ -0,0 +1,109 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { fountainSerializer } from "./serializer";
3
+ import type { Declarable } from "@intentius/chant";
4
+
5
+ function primary(result: unknown): string {
6
+ return typeof result === "string" ? result : (result as { primary: string }).primary;
7
+ }
8
+
9
+ function entity(entityType: string, props: Record<string, unknown>): Declarable {
10
+ return { entityType, lexicon: "fountain", ...props } as unknown as Declarable;
11
+ }
12
+
13
+ describe("fountain serializer", () => {
14
+ it("has the correct name and rule prefix", () => {
15
+ expect(fountainSerializer.name).toBe("fountain");
16
+ expect(fountainSerializer.rulePrefix).toBe("FTN");
17
+ });
18
+
19
+ it("serializes an empty map to an empty string", () => {
20
+ expect(fountainSerializer.serialize(new Map())).toBe("");
21
+ });
22
+
23
+ it("emits the fountain-plan.json sidecar for the applier", () => {
24
+ const env = entity("Fountain::V1::Environment", { name: "e" });
25
+ const result = fountainSerializer.serialize(new Map([["e", env]]));
26
+ expect(typeof result).toBe("object");
27
+ const files = (result as { files: Record<string, string> }).files;
28
+ const plan = JSON.parse(files["fountain-plan.json"]);
29
+ expect(plan.e.kind).toBe("Environment");
30
+ });
31
+
32
+ it("emits a fountain manifest per entity", () => {
33
+ const env = entity("Fountain::V1::Environment", {
34
+ name: "concierge-env",
35
+ networking_type: "limited",
36
+ networking_config: { allowed_hosts: ["github.com"] },
37
+ });
38
+
39
+ const out = primary(fountainSerializer.serialize(new Map([["conciergeEnv", env]])));
40
+
41
+ expect(out).toContain("apiVersion: fountain.dev/v1");
42
+ expect(out).toContain("kind: Environment");
43
+ expect(out).toContain("name: conciergeEnv");
44
+ expect(out).toContain("networking_type: limited");
45
+ expect(out).toContain("allowed_hosts:");
46
+ expect(out).toContain("- github.com");
47
+ });
48
+
49
+ it("separates multiple entities with document markers", () => {
50
+ const env = entity("Fountain::V1::Environment", { name: "e" });
51
+ const vault = entity("Fountain::V1::Vault", { name: "v" });
52
+
53
+ const out = primary(
54
+ fountainSerializer.serialize(
55
+ new Map([
56
+ ["env", env],
57
+ ["vault", vault],
58
+ ]),
59
+ ),
60
+ );
61
+
62
+ expect(out).toContain("---\n");
63
+ expect(out).toContain("kind: Environment");
64
+ expect(out).toContain("kind: Vault");
65
+ });
66
+
67
+ it("resolves a cross-resource reference to the referenced entity name", () => {
68
+ const env = entity("Fountain::V1::Environment", { name: "concierge-env" });
69
+ const agent = entity("Fountain::V1::Agent", {
70
+ name: "researcher",
71
+ model: "anthropic/claude-sonnet-4-6",
72
+ runtime: "claude",
73
+ environment: env,
74
+ });
75
+
76
+ const out = primary(
77
+ fountainSerializer.serialize(
78
+ new Map([
79
+ ["conciergeEnv", env],
80
+ ["researcher", agent],
81
+ ]),
82
+ ),
83
+ );
84
+
85
+ expect(out).toContain("environment: conciergeEnv");
86
+ });
87
+
88
+ it("quotes YAML-ambiguous strings", () => {
89
+ const vault = entity("Fountain::V1::Vault", {
90
+ name: "v",
91
+ description: "true",
92
+ });
93
+
94
+ const out = primary(fountainSerializer.serialize(new Map([["v", vault]])));
95
+ expect(out).toContain('description: "true"');
96
+ });
97
+
98
+ it("does not use YAML aliases or tags for substitution references", () => {
99
+ const agent = entity("Fountain::V1::Agent", {
100
+ name: "a",
101
+ model: "anthropic/claude-sonnet-4-6",
102
+ runtime: "claude",
103
+ mcp_servers: { github: { env: { GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_PAT}" } } },
104
+ });
105
+
106
+ const out = primary(fountainSerializer.serialize(new Map([["a", agent]])));
107
+ expect(out).toContain('GITHUB_PERSONAL_ACCESS_TOKEN: "${GITHUB_PAT}"');
108
+ });
109
+ });
@@ -0,0 +1,143 @@
1
+ import type { Serializer, SerializerResult, Declarable } from "@intentius/chant";
2
+ import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
3
+ import { propsOf } from "./entity-props";
4
+
5
+ /**
6
+ * Fountain serializer — emits fountain's native manifest format
7
+ * (`apiVersion: fountain.dev/v1`), one YAML document per resource, as a
8
+ * single multi-document stream. The output stays `fountain apply -f`
9
+ * compatible, so anyone can eject from chant and keep the artifacts.
10
+ *
11
+ * Cross-resource references (e.g. `agent.environment`) serialize to the
12
+ * referenced entity's name — fountain's CLI resolves names to ids at apply.
13
+ */
14
+
15
+ const API_VERSION = "fountain.dev/v1";
16
+
17
+ /** Fountain::V1::Agent → Agent */
18
+ function kindOf(entityType: string): string {
19
+ const parts = entityType.split("::");
20
+ return parts[parts.length - 1];
21
+ }
22
+
23
+ export const fountainSerializer: Serializer = {
24
+ name: "fountain",
25
+ rulePrefix: "FTN",
26
+
27
+ serialize(entities: Map<string, Declarable>): string | SerializerResult {
28
+ // Reverse map for reference resolution: Declarable instance → name.
29
+ const entityNames = new Map<Declarable, string>();
30
+ for (const [name, entity] of entities) {
31
+ entityNames.set(entity, name);
32
+ }
33
+
34
+ const visitor: SerializerVisitor = {
35
+ // A reference to another fountain resource serializes to its name.
36
+ attrRef(logicalName, _attribute) {
37
+ return logicalName;
38
+ },
39
+ resourceRef(logicalName) {
40
+ return logicalName;
41
+ },
42
+ propertyDeclarable(entity, walk) {
43
+ const props: Record<string, unknown> = {};
44
+ for (const [key, val] of Object.entries(propsOf(entity))) {
45
+ if (val === undefined) continue;
46
+ props[key] = walk(val);
47
+ }
48
+ return props;
49
+ },
50
+ };
51
+
52
+ const docs: string[] = [];
53
+ const plan: Record<string, { kind: string; spec: Record<string, unknown> }> = {};
54
+ for (const [name, entity] of entities) {
55
+ const spec: Record<string, unknown> = {};
56
+ for (const [key, val] of Object.entries(propsOf(entity))) {
57
+ if (val === undefined) continue;
58
+ spec[key] = walkValue(val, entityNames, visitor);
59
+ }
60
+
61
+ const manifest = {
62
+ apiVersion: API_VERSION,
63
+ kind: kindOf(entity.entityType),
64
+ metadata: { name },
65
+ spec,
66
+ };
67
+ docs.push(toYaml(manifest));
68
+ plan[name] = { kind: manifest.kind, spec };
69
+ }
70
+
71
+ const yaml = docs.join("---\n");
72
+ if (docs.length === 0) return yaml;
73
+
74
+ // Primary output is the ejectable `fountain apply -f` YAML; the JSON
75
+ // sidecar is the fountainApply op's input (same data, no YAML parser
76
+ // needed on the apply side — the fly plan.json pattern).
77
+ return { primary: yaml, files: { "fountain-plan.json": JSON.stringify(plan, null, 2) } };
78
+ },
79
+ };
80
+
81
+ // ── Minimal YAML emitter ───────────────────────────────────────────
82
+ // The manifest shape is plain JSON-compatible data (maps, arrays, scalars),
83
+ // so a small emitter keeps the lexicon dependency-free. Strings are quoted
84
+ // whenever they could be misread as another YAML type.
85
+
86
+ function toYaml(value: unknown, indent = 0): string {
87
+ const pad = " ".repeat(indent);
88
+
89
+ if (Array.isArray(value)) {
90
+ if (value.length === 0) return `${pad}[]\n`;
91
+ let out = "";
92
+ for (const item of value) {
93
+ if (isScalar(item)) {
94
+ out += `${pad}- ${scalar(item)}\n`;
95
+ } else {
96
+ const body = toYaml(item, indent + 1);
97
+ out += `${pad}-\n${body}`;
98
+ }
99
+ }
100
+ return out;
101
+ }
102
+
103
+ if (value !== null && typeof value === "object") {
104
+ const entries = Object.entries(value as Record<string, unknown>);
105
+ if (entries.length === 0) return `${pad}{}\n`;
106
+ let out = "";
107
+ for (const [k, v] of entries) {
108
+ if (isScalar(v)) {
109
+ out += `${pad}${key(k)}: ${scalar(v)}\n`;
110
+ } else if (Array.isArray(v) && v.length === 0) {
111
+ out += `${pad}${key(k)}: []\n`;
112
+ } else if (v !== null && typeof v === "object" && Object.keys(v as object).length === 0) {
113
+ out += `${pad}${key(k)}: {}\n`;
114
+ } else {
115
+ out += `${pad}${key(k)}:\n${toYaml(v, indent + 1)}`;
116
+ }
117
+ }
118
+ return out;
119
+ }
120
+
121
+ return `${pad}${scalar(value)}\n`;
122
+ }
123
+
124
+ function isScalar(v: unknown): boolean {
125
+ return v === null || ["string", "number", "boolean"].includes(typeof v);
126
+ }
127
+
128
+ const PLAIN_STRING = /^[A-Za-z0-9._/-][A-Za-z0-9._/ -]*$/;
129
+ const YAML_AMBIGUOUS = /^(true|false|null|yes|no|on|off|~|[+-]?[0-9.]+([eE][+-]?[0-9]+)?)$/i;
130
+
131
+ function scalar(v: unknown): string {
132
+ if (v === null) return "null";
133
+ if (typeof v === "number" || typeof v === "boolean") return String(v);
134
+ const s = String(v);
135
+ if (s === "" || !PLAIN_STRING.test(s) || YAML_AMBIGUOUS.test(s) || s.includes("\n")) {
136
+ return JSON.stringify(s);
137
+ }
138
+ return s;
139
+ }
140
+
141
+ function key(k: string): string {
142
+ return PLAIN_STRING.test(k) && !YAML_AMBIGUOUS.test(k) ? k : JSON.stringify(k);
143
+ }
@@ -0,0 +1,34 @@
1
+ ---
2
+ skill: chant-fountain-locked-sandboxes
3
+ description: Declare locked-down fountain environments for untrusted or security-sensitive agents, and run conversations against them
4
+ user-invocable: true
5
+ ---
6
+
7
+ # Locked-Down Sandboxes and Conversation Runs
8
+
9
+ ## The posture
10
+
11
+ For an agent handling anything sensitive (a repo checkout, an internal API), declare the environment closed by construction:
12
+
13
+ ```ts
14
+ export const lockedEnv = new Environment({
15
+ name: "concierge-env",
16
+ networking_type: "limited",
17
+ networking_config: { allowed_hosts: ["registry.npmjs.org", "github.com"] },
18
+ metadata: { "managed-by": "chant" },
19
+ });
20
+ ```
21
+
22
+ Semantics (from fountain's own schema): `unrestricted` is a no-op — sprites are open by default. `limited` restricts egress to `allowed_hosts`; with **no** hosts (or an empty list) it denies all egress — deny-all, not allow-all. So `limited` + `[]` is the isolation mode.
23
+
24
+ Pair it with a closed vault policy on the agent — `allowed_vault_ids: []` — so no conversation can override the reviewed environment at spawn, and give the sandbox **no cloud credentials of any kind**: anything readable inside is exfiltratable by prompt injection. Services the agent needs live outside the sandbox behind their own auth; the sandbox gets at most a conversation-scoped token.
25
+
26
+ Lint posture: FTN010/FTN011 fire on missing/unrestricted networking at warning severity by default. For repos whose environments are all concierge-class, promote them to error via project rules.
27
+
28
+ ## Drift is the alarm
29
+
30
+ The environment's config is the enforcement boundary, so watch it: `chant lifecycle diff --live` flags a UI edit that adds a secret, opens networking, or drops the marker. Wire it into a scheduled watch — an out-of-band change to a locked environment is an incident, not housekeeping.
31
+
32
+ ## Running conversations
33
+
34
+ Conversations are runs, not resources. Use the `fountainRun` activity: resolves the agent by name, starts (optionally with a prompt and an allowlisted vault), polls to `completed | failed | timed_out`, and terminates at its deadline so a hung sandbox never outlives the op. Multi-turn interaction (follow-up prompts, interrupt) is fountain's own conversations API — keep chant to the lifecycle edges.
@@ -0,0 +1,29 @@
1
+ ---
2
+ skill: chant-fountain-secrets
3
+ description: Handle fountain secrets, env vars, and ${VAR} substitution safely from chant
4
+ user-invocable: true
5
+ ---
6
+
7
+ # Fountain Secrets and Substitution
8
+
9
+ ## The model
10
+
11
+ Everything materialized into a fountain sandbox must be presumed exfiltrated once untrusted agent code runs. Order of preference:
12
+
13
+ 1. **`${VAR}` substitution references** in agent config (MCP server env, system prompts). Resolved at spawn from the merged environment + vault sets. Never a value in source.
14
+ 2. **Environment secrets** (`spec.secrets`) — encrypted at rest, write-only over the API (values are never returned once stored). `fountainApply` upserts them through the sub-resource; a changed value cannot be detected, only overwritten.
15
+ 3. **`env_vars`** — plaintext config only. FTN012 errors on credential-shaped keys or values here.
16
+
17
+ Never put a literal credential anywhere in a declaration: FTN001 catches known shapes (AWS keys, GitHub/Slack tokens, `sk-`/`ftn_` keys, private key material) at the AST; FTN015 errors on secret-shaped MCP env keys that are not `${VAR}` references.
18
+
19
+ ## Vault semantics
20
+
21
+ Vault values **win on key collision** with the environment, silently, at spawn. FTN014 warns when a declared vault shadows a declared environment key, so the override is visible in review. Which vaults may attach to an agent is scoped by `allowed_vault_ids` (nullable: `null` = any tenant vault, `[]` = none, list = allowlist) — set `[]` on agents whose environment must not be overridable.
22
+
23
+ ## Build-time resolution check
24
+
25
+ FTN013 warns when an agent references `${VAR}` and its declared environment has no such key (env_vars or secret keys). A vault can legitimately supply it at conversation create — the warning is "confirm this is intentional," not "this is broken."
26
+
27
+ ## Round-trips and their limits
28
+
29
+ `chant import --from` exports live resources but never secrets: values are write-only upstream, and secret keys are not on the typed request surface. Re-declare imported environments' secrets through your secret provider. Upstream discussion of a reference-based model that would fix this: BinaryBourbon/fountain#148.
@@ -0,0 +1,45 @@
1
+ ---
2
+ skill: chant-fountain
3
+ description: Declare, lint, and reconcile fountain Environments, Vaults, and Agents from a chant project
4
+ user-invocable: true
5
+ ---
6
+
7
+ # Fountain Workloads as Typed Estate
8
+
9
+ ## What this lexicon covers
10
+
11
+ [fountain](https://github.com/BinaryBourbon/fountain) runs coding agents in sandboxed VMs. Its workload layer is three kinds — `Environment` (sandbox baseline), `Vault` (env-var overrides), `Agent` (a runnable agent config) — and this lexicon declares them as typed chant resources. Conversations are runs, not resources: start them with the `fountainRun` op, never declare them.
12
+
13
+ The source of truth is the TypeScript in `src/`. `chant build` serializes it to fountain's own manifest YAML (ejectable — `fountain apply -f` accepts it verbatim) plus a `fountain-plan.json` sidecar that `fountainApply` reconciles against the API directly: create-if-new, update-by-name, opt-in owned-only prune keyed on the `managed-by: chant` metadata marker.
14
+
15
+ ## Authoring
16
+
17
+ ```ts
18
+ import { Environment, Agent } from "@intentius/chant-lexicon-fountain";
19
+
20
+ export const env = new Environment({
21
+ name: "team-env",
22
+ networking_type: "limited", // FTN010 requires explicit intent
23
+ networking_config: { allowed_hosts: ["github.com"] },
24
+ metadata: { "managed-by": "chant" }, // enables owned-only reconcile/prune
25
+ });
26
+
27
+ export const helper = new Agent({
28
+ name: "helper",
29
+ model: "anthropic/claude-sonnet-4-6",
30
+ runtime: "claude",
31
+ environment: env, // typed ref — dangling name = build error
32
+ skills: [{ source: "owner/skills-repo", ref: "v1.0.0" }], // always pin a ref
33
+ });
34
+ ```
35
+
36
+ ## Endpoint and auth
37
+
38
+ `FOUNTAIN_ENDPOINT` (defaults to the hosted instance) + `FOUNTAIN_TOKEN` (mint via `POST /api/auth/token` with email+password, or the account UI). The same code applies to a local `mix phx.server` fountain by pointing `FOUNTAIN_ENDPOINT` at it — registration and token mint work headless, so CI needs no browser.
39
+
40
+ ## The loop
41
+
42
+ 1. `chant build` — synthesize + lint (FTN rules catch open networking, credential literals, unresolvable `${VAR}` refs before review).
43
+ 2. `chant run <apply op>` or call `fountainApply` — reconcile. Idempotent by name.
44
+ 3. `chant lifecycle diff --live` — drift: a UI edit to an owned Environment shows up here.
45
+ 4. `chant import --from` — adopt UI-built resources into typed files (secrets stay behind; see the secrets skill).