@intentius/chant-lexicon-fountain 0.37.2 → 0.38.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.
- package/README.md +12 -5
- package/dist/codegen/docs.d.ts.map +1 -1
- package/dist/codegen/package.d.ts.map +1 -1
- package/dist/coverage.d.ts +20 -2
- package/dist/coverage.d.ts.map +1 -1
- package/dist/integrity.json +2 -2
- package/dist/manifest.json +3 -2
- package/dist/serializer.d.ts.map +1 -1
- package/dist/spec/fetch.d.ts +26 -17
- package/dist/spec/fetch.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/codegen/docs.ts +3 -0
- package/src/codegen/package.ts +4 -0
- package/src/coverage.test.ts +25 -1
- package/src/coverage.ts +43 -2
- package/src/describe-resources.test.ts +2 -0
- package/src/serializer.test.ts +12 -2
- package/src/serializer.ts +20 -3
- package/src/spec/fetch.ts +35 -23
- package/src/spec/fountain-openapi.snapshot.json +1 -1
package/README.md
CHANGED
|
@@ -43,11 +43,18 @@ just validate # check the generated artifacts
|
|
|
43
43
|
just docs # build and serve the docs site
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
Generation is pinned to an upstream release artifact — `FOUNTAIN_SPEC_VERSION`
|
|
47
|
+
in `src/spec/fetch.ts`. The same pin produces the same types on any machine,
|
|
48
|
+
so a regeneration that changes the surface means upstream moved, not that
|
|
49
|
+
someone's laptop was online.
|
|
50
|
+
|
|
51
|
+
Without a network it reads the committed snapshot
|
|
52
|
+
(`src/spec/fountain-openapi.snapshot.json`) instead. Either way it prints
|
|
53
|
+
which one it used and at what version.
|
|
54
|
+
|
|
55
|
+
`chant dev coverage` compares the generated surface against that spec and
|
|
56
|
+
reports properties upstream has added, plus the request schemas deliberately
|
|
57
|
+
left unmodeled.
|
|
51
58
|
|
|
52
59
|
## Project structure
|
|
53
60
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"docs.d.ts","sourceRoot":"","sources":["../../src/codegen/docs.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;
|
|
1
|
+
{"version":3,"file":"docs.d.ts","sourceRoot":"","sources":["../../src/codegen/docs.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAqNH;;GAEG;AACH,wBAAsB,YAAY,CAAC,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAyDjF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"package.d.ts","sourceRoot":"","sources":["../../src/codegen/package.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"package.d.ts","sourceRoot":"","sources":["../../src/codegen/package.ts"],"names":[],"mappings":"AAQA;;GAEG;AACH,wBAAsB,cAAc,CAAC,OAAO,CAAC,EAAE;IAAE,OAAO,CAAC,EAAE,OAAO,CAAC;IAAC,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE;;;;;;;;;GAwBpF"}
|
package/dist/coverage.d.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* (lifecycle flags, return attributes, extension constraints) that a
|
|
6
6
|
* three-kind OpenAPI lexicon has no analog for — it would report 0% on
|
|
7
7
|
* everything and mean nothing. What fountain actually needs to know is
|
|
8
|
-
* whether its generated surface still matches upstream,
|
|
9
|
-
*
|
|
8
|
+
* whether its generated surface still matches upstream, measured against the
|
|
9
|
+
* pinned spec release (see spec/fetch.ts):
|
|
10
10
|
*
|
|
11
11
|
* 1. Property coverage — request-schema properties per modeled kind vs
|
|
12
12
|
* what the committed surface baseline exposes. A gap means upstream
|
|
@@ -17,6 +17,22 @@
|
|
|
17
17
|
*/
|
|
18
18
|
/** Request schemas with no typed resource, and why. */
|
|
19
19
|
export declare const EXCLUDED_KINDS: Record<string, string>;
|
|
20
|
+
/**
|
|
21
|
+
* Upstream endpoints that exist but are absent from the OpenAPI spec, and the
|
|
22
|
+
* decision about each.
|
|
23
|
+
*
|
|
24
|
+
* `EXCLUDED_KINDS` cannot carry these. It is keyed on request-schema names and
|
|
25
|
+
* checked against the spec, so an entry for a schema upstream never publishes
|
|
26
|
+
* would match nothing, prove nothing, and quietly keep meaning nothing if the
|
|
27
|
+
* endpoint were later documented under a different name.
|
|
28
|
+
*
|
|
29
|
+
* These are the routes the coverage check is structurally blind to: fountain's
|
|
30
|
+
* spec is generated by OpenApiSpex from controller annotations, so an
|
|
31
|
+
* unannotated controller is invisible to every consumer of the spec, chant
|
|
32
|
+
* included. Recording them here is the only way an unmodeled kind stays a
|
|
33
|
+
* decision on record rather than something nobody noticed.
|
|
34
|
+
*/
|
|
35
|
+
export declare const UNSPECIFIED_ENDPOINTS: Record<string, string>;
|
|
20
36
|
export interface KindCoverage {
|
|
21
37
|
kind: string;
|
|
22
38
|
specProps: number;
|
|
@@ -34,6 +50,8 @@ export interface FountainCoverageReport {
|
|
|
34
50
|
excludedKinds: Record<string, string>;
|
|
35
51
|
/** Request schemas neither modeled nor on the exclusion list. */
|
|
36
52
|
unaccountedKinds: string[];
|
|
53
|
+
/** Upstream endpoints the spec does not describe → the decision about each. */
|
|
54
|
+
unspecifiedEndpoints: Record<string, string>;
|
|
37
55
|
/** Modeled properties as a percentage of upstream request properties. */
|
|
38
56
|
overallPct: number;
|
|
39
57
|
}
|
package/dist/coverage.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../src/coverage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAQH,uDAAuD;AACvD,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"coverage.d.ts","sourceRoot":"","sources":["../src/coverage.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAQH,uDAAuD;AACvD,eAAO,MAAM,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAQjD,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,qBAAqB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAUxD,CAAC;AAEF,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,yEAAyE;IACzE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,yEAAyE;IACzE,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,8CAA8C;IAC9C,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,yDAAyD;IACzD,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACtC,iEAAiE;IACjE,gBAAgB,EAAE,MAAM,EAAE,CAAC;IAC3B,+EAA+E;IAC/E,oBAAoB,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,yEAAyE;IACzE,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,UAAU,eAAe;IACvB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;CAC7D;AAOD,wBAAgB,uBAAuB,CACrC,QAAQ,EAAE,MAAM,GAAG,MAAM,EACzB,OAAO,EAAE,eAAe,GACvB,sBAAsB,CAkDxB;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAGpE;AAED,wBAAgB,aAAa,CAAC,MAAM,EAAE,sBAAsB,GAAG,MAAM,CAkCpE;AAED,sDAAsD;AACtD,wBAAsB,uBAAuB,CAAC,IAAI,CAAC,EAAE;IACnD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,sBAAsB,CAAC,CAsBlC"}
|
package/dist/integrity.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "5c6136425fcb1037a1f6fa6aa10e4179943397ea0af87dadaf990f907e2574f3",
|
|
5
5
|
"meta.json": "6666b7a77db9210a329219c6a5e107e5ae8794a34542f7310ce16d9bf95c64f5",
|
|
6
6
|
"types/index.d.ts": "1dfdcba184fffcf464dac7d1d71ef7abed2ae02a8d638c0de50e0b0317b7eb01",
|
|
7
7
|
"rules/ftn001-no-secret-literals.ts": "897a4ce1ec790b3c1540d32892603bd33ff4bf30eb6ba4cd2565dee356d4962e",
|
|
@@ -17,5 +17,5 @@
|
|
|
17
17
|
"skills/chant-fountain-secrets.md": "27e349a91589510a92e518c7d7824a4a322cab5ef242cf5799373a55cbfcd1cd",
|
|
18
18
|
"skills/chant-fountain-locked-sandboxes.md": "de82f06cb3a08ba6bf3ae45fb9869e21d6da18b9ebe0fc769da8aebaceea7dd1"
|
|
19
19
|
},
|
|
20
|
-
"composite": "
|
|
20
|
+
"composite": "f1a2edc658cd070b0f65d9fafa6c76878e797bf3e14c64b2f773e785fb5b5d08"
|
|
21
21
|
}
|
package/dist/manifest.json
CHANGED
package/dist/serializer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAc,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"serializer.d.ts","sourceRoot":"","sources":["../src/serializer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAc,MAAM,kBAAkB,CAAC;AAyC/D,eAAO,MAAM,kBAAkB,EAAE,UAmDhC,CAAC"}
|
package/dist/spec/fetch.d.ts
CHANGED
|
@@ -1,26 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Fetch the fountain OpenAPI spec.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* follows the rolling-spec model (like fly). A versioned spec artifact per
|
|
9
|
-
* release shipped upstream as a release asset (BinaryBourbon/fountain#140);
|
|
10
|
-
* when the hosted endpoint is reliably current this fetcher can prefer it.
|
|
4
|
+
* Pinned to a release artifact, not a live endpoint. Upstream ships
|
|
5
|
+
* `openapi.json` as a release asset (BinaryBourbon/fountain#147), so there is
|
|
6
|
+
* a tag to pin and generation is reproducible: same pin in, same generated
|
|
7
|
+
* surface out, on any machine, with or without a network.
|
|
11
8
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* upstream
|
|
17
|
-
*
|
|
9
|
+
* It used to fetch `/api/openapi.json` from a running instance. That made the
|
|
10
|
+
* generated types a function of whatever a particular server happened to be
|
|
11
|
+
* serving at the moment someone ran `npm run generate` — two runs a week apart
|
|
12
|
+
* could differ with no diff in chant to explain it. Every other lexicon pins
|
|
13
|
+
* its upstream spec (AWS a CloudFormation zip, each k8s CRD an operator
|
|
14
|
+
* release); this one no longer is the exception.
|
|
15
|
+
*
|
|
16
|
+
* The committed snapshot is now only the offline path. It is not a silent
|
|
17
|
+
* substitute for the pin: both routes log which one was taken and at what
|
|
18
|
+
* version, because a fallback nobody can see is how you end up debugging types
|
|
19
|
+
* that came from somewhere you did not expect.
|
|
20
|
+
*/
|
|
21
|
+
/**
|
|
22
|
+
* The pinned upstream spec.
|
|
23
|
+
*
|
|
24
|
+
* v0.3.0 is the first release to carry the `openapi.json` asset. Bumping this
|
|
25
|
+
* changes the generated surface, so it belongs in its own commit with the
|
|
26
|
+
* regenerated snapshot beside it.
|
|
18
27
|
*/
|
|
28
|
+
export declare const FOUNTAIN_SPEC_VERSION = "v0.3.0";
|
|
19
29
|
/**
|
|
20
|
-
* Fetch the fountain OpenAPI spec
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
* out into per-kind results.
|
|
30
|
+
* Fetch the pinned fountain OpenAPI spec, falling back to the committed
|
|
31
|
+
* snapshot when there is no network. Returns a single-entry map — the whole
|
|
32
|
+
* spec is one document; the parser fans it out into per-kind results.
|
|
24
33
|
*/
|
|
25
34
|
export declare function fetchSchemas(options?: {
|
|
26
35
|
force?: boolean;
|
package/dist/spec/fetch.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/spec/fetch.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"fetch.d.ts","sourceRoot":"","sources":["../../src/spec/fetch.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAQH;;;;;;GAMG;AACH,eAAO,MAAM,qBAAqB,WAAW,CAAC;AAQ9C;;;;GAIG;AACH,wBAAsB,YAAY,CAAC,OAAO,CAAC,EAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAE,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAiB9F"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-fountain",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.38.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Fountain lexicon for chant — sandboxed agent environments, vaults, and agents as typed estate",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -50,7 +50,7 @@
|
|
|
50
50
|
"bundle": "tsx src/package-cli.ts"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
|
53
|
-
"@intentius/chant": "^0.
|
|
53
|
+
"@intentius/chant": "^0.38.0",
|
|
54
54
|
"typescript": "^5.9.3"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
package/src/codegen/docs.ts
CHANGED
|
@@ -67,12 +67,15 @@ kind: Environment
|
|
|
67
67
|
metadata:
|
|
68
68
|
name: team-env
|
|
69
69
|
spec:
|
|
70
|
+
name: team-env
|
|
70
71
|
networking_type: limited
|
|
71
72
|
networking_config:
|
|
72
73
|
allowed_hosts:
|
|
73
74
|
- github.com
|
|
74
75
|
\`\`\`
|
|
75
76
|
|
|
77
|
+
\`metadata.name\` is the resource's declared \`name\`, not the name of the variable you exported it as. fountain reconciles by that name, so renaming the variable does not orphan the resource. An entity declared without a \`name\` falls back to the export name.
|
|
78
|
+
|
|
76
79
|
The output is ejectable — \`fountain apply -f\` accepts it verbatim, so adopting chant here does not trap the manifests behind chant.
|
|
77
80
|
|
|
78
81
|
## Applying the manifest
|
package/src/codegen/package.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { fountainPlugin } from "../plugin";
|
|
|
4
4
|
import { dirname, join } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { readFileSync } from "fs";
|
|
7
|
+
import { FOUNTAIN_SPEC_VERSION } from "../spec/fetch";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Package the fountain lexicon for distribution.
|
|
@@ -21,6 +22,9 @@ export async function packageLexicon(options?: { verbose?: boolean; force?: bool
|
|
|
21
22
|
version: pkgJson.version ?? "0.0.0",
|
|
22
23
|
chantVersion: ">=0.1.0",
|
|
23
24
|
namespace: "Fountain",
|
|
25
|
+
// Which upstream API this surface was generated from. Without it a built
|
|
26
|
+
// lexicon cannot say what it was built against.
|
|
27
|
+
specVersion: FOUNTAIN_SPEC_VERSION,
|
|
24
28
|
}),
|
|
25
29
|
srcDir,
|
|
26
30
|
collectSkills: () => collectSkills(fountainPlugin.skills?.() ?? []),
|
package/src/coverage.test.ts
CHANGED
|
@@ -2,7 +2,8 @@ import { describe, expect, it } from "vitest";
|
|
|
2
2
|
import { readFileSync } from "fs";
|
|
3
3
|
import { join, dirname } from "path";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
|
-
import { computeFountainCoverage, formatVerbose, EXCLUDED_KINDS } from "./coverage";
|
|
5
|
+
import { computeFountainCoverage, formatVerbose, EXCLUDED_KINDS, UNSPECIFIED_ENDPOINTS } from "./coverage";
|
|
6
|
+
import { fetchSchemas } from "./spec/fetch";
|
|
6
7
|
|
|
7
8
|
const srcDir = dirname(fileURLToPath(import.meta.url));
|
|
8
9
|
const spec = readFileSync(join(srcDir, "spec", "fountain-openapi.snapshot.json"), "utf-8");
|
|
@@ -61,3 +62,26 @@ describe("fountain coverage", () => {
|
|
|
61
62
|
}
|
|
62
63
|
});
|
|
63
64
|
});
|
|
65
|
+
|
|
66
|
+
describe("unspecified upstream endpoints", () => {
|
|
67
|
+
it("records a decision for every endpoint the spec cannot describe", () => {
|
|
68
|
+
// The point of the list is the reasoning, not the route string. An entry
|
|
69
|
+
// with an empty or throwaway reason is the omission it exists to prevent.
|
|
70
|
+
for (const [route, reason] of Object.entries(UNSPECIFIED_ENDPOINTS)) {
|
|
71
|
+
expect(route.startsWith("/"), `${route} should be a route path`).toBe(true);
|
|
72
|
+
expect(reason.length, `${route} needs a real reason`).toBeGreaterThan(40);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("does not duplicate anything the spec already covers", async () => {
|
|
77
|
+
// If upstream annotates one of these controllers, the endpoint becomes
|
|
78
|
+
// visible to EXCLUDED_KINDS/unaccountedKinds and this list should shrink
|
|
79
|
+
// rather than shadow it.
|
|
80
|
+
const specs = await fetchSchemas();
|
|
81
|
+
const raw = specs.get("fountain-openapi.json");
|
|
82
|
+
const paths = Object.keys(JSON.parse(raw!.toString("utf-8")).paths ?? {});
|
|
83
|
+
for (const route of Object.keys(UNSPECIFIED_ENDPOINTS)) {
|
|
84
|
+
expect(paths.some((p) => p.endsWith(route))).toBe(false);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
package/src/coverage.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* (lifecycle flags, return attributes, extension constraints) that a
|
|
6
6
|
* three-kind OpenAPI lexicon has no analog for — it would report 0% on
|
|
7
7
|
* everything and mean nothing. What fountain actually needs to know is
|
|
8
|
-
* whether its generated surface still matches upstream,
|
|
9
|
-
*
|
|
8
|
+
* whether its generated surface still matches upstream, measured against the
|
|
9
|
+
* pinned spec release (see spec/fetch.ts):
|
|
10
10
|
*
|
|
11
11
|
* 1. Property coverage — request-schema properties per modeled kind vs
|
|
12
12
|
* what the committed surface baseline exposes. A gap means upstream
|
|
@@ -28,6 +28,36 @@ export const EXCLUDED_KINDS: Record<string, string> = {
|
|
|
28
28
|
PromptRequest: "turn-level input inside a conversation run",
|
|
29
29
|
SecretRequest: "secrets are a write-only sub-resource — upserted by fountainApply",
|
|
30
30
|
VaultSecretRequest: "secrets are a write-only sub-resource — upserted by fountainApply",
|
|
31
|
+
ApplyRequest:
|
|
32
|
+
"the envelope fountainApply builds around a manifest, not a thing anyone declares — " +
|
|
33
|
+
"its contents are the Environment/Vault/Agent resources, which are modeled",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Upstream endpoints that exist but are absent from the OpenAPI spec, and the
|
|
38
|
+
* decision about each.
|
|
39
|
+
*
|
|
40
|
+
* `EXCLUDED_KINDS` cannot carry these. It is keyed on request-schema names and
|
|
41
|
+
* checked against the spec, so an entry for a schema upstream never publishes
|
|
42
|
+
* would match nothing, prove nothing, and quietly keep meaning nothing if the
|
|
43
|
+
* endpoint were later documented under a different name.
|
|
44
|
+
*
|
|
45
|
+
* These are the routes the coverage check is structurally blind to: fountain's
|
|
46
|
+
* spec is generated by OpenApiSpex from controller annotations, so an
|
|
47
|
+
* unannotated controller is invisible to every consumer of the spec, chant
|
|
48
|
+
* included. Recording them here is the only way an unmodeled kind stays a
|
|
49
|
+
* decision on record rather than something nobody noticed.
|
|
50
|
+
*/
|
|
51
|
+
export const UNSPECIFIED_ENDPOINTS: Record<string, string> = {
|
|
52
|
+
"/api-keys": [
|
|
53
|
+
"not modeled, and not modelable as a declarable: an API key's value is",
|
|
54
|
+
"returned once at creation and never readable again. A declared ApiKey could",
|
|
55
|
+
"be created but never diffed or reconciled — chant would report it",
|
|
56
|
+
"permanently unobservable, or recreate it on every apply and hand back a new",
|
|
57
|
+
"secret each time. Same write-only property that excludes SecretRequest.",
|
|
58
|
+
"If minting a key from chant is wanted, the honest shape is an op alongside",
|
|
59
|
+
"fountainRun: hand it to the caller once, never claim it as estate.",
|
|
60
|
+
].join(" "),
|
|
31
61
|
};
|
|
32
62
|
|
|
33
63
|
export interface KindCoverage {
|
|
@@ -48,6 +78,8 @@ export interface FountainCoverageReport {
|
|
|
48
78
|
excludedKinds: Record<string, string>;
|
|
49
79
|
/** Request schemas neither modeled nor on the exclusion list. */
|
|
50
80
|
unaccountedKinds: string[];
|
|
81
|
+
/** Upstream endpoints the spec does not describe → the decision about each. */
|
|
82
|
+
unspecifiedEndpoints: Record<string, string>;
|
|
51
83
|
/** Modeled properties as a percentage of upstream request properties. */
|
|
52
84
|
overallPct: number;
|
|
53
85
|
}
|
|
@@ -111,6 +143,7 @@ export function computeFountainCoverage(
|
|
|
111
143
|
modeledKinds,
|
|
112
144
|
excludedKinds: EXCLUDED_KINDS,
|
|
113
145
|
unaccountedKinds,
|
|
146
|
+
unspecifiedEndpoints: UNSPECIFIED_ENDPOINTS,
|
|
114
147
|
overallPct: totalSpec === 0 ? 0 : Math.round((totalModeled / totalSpec) * 100),
|
|
115
148
|
};
|
|
116
149
|
}
|
|
@@ -138,6 +171,14 @@ export function formatVerbose(report: FountainCoverageReport): string {
|
|
|
138
171
|
lines.push(` ${name} — ${reason}`);
|
|
139
172
|
}
|
|
140
173
|
|
|
174
|
+
const unspecified = Object.entries(report.unspecifiedEndpoints);
|
|
175
|
+
if (unspecified.length > 0) {
|
|
176
|
+
lines.push("", "Upstream endpoints absent from the spec (coverage cannot see these):");
|
|
177
|
+
for (const [route, reason] of unspecified) {
|
|
178
|
+
lines.push(` ${route} — ${reason}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
141
182
|
if (report.unaccountedKinds.length > 0) {
|
|
142
183
|
lines.push(
|
|
143
184
|
"",
|
|
@@ -157,6 +157,8 @@ describe("fountain describeResources", () => {
|
|
|
157
157
|
|
|
158
158
|
describeObservationConformance({
|
|
159
159
|
lexicon: "fountain",
|
|
160
|
+
// No marker channel: every verdict must be `unknown` (#1348).
|
|
161
|
+
ownershipChannel: undefined,
|
|
160
162
|
scenarios: [
|
|
161
163
|
{
|
|
162
164
|
name: "mixed present/absent/foreign against a live listing",
|
package/src/serializer.test.ts
CHANGED
|
@@ -35,7 +35,9 @@ describe("fountain serializer", () => {
|
|
|
35
35
|
|
|
36
36
|
expect(out).toContain("apiVersion: fountain.dev/v1");
|
|
37
37
|
expect(out).toContain("kind: Environment");
|
|
38
|
-
|
|
38
|
+
// The declared name, not the export name — fountain upserts by this.
|
|
39
|
+
expect(out).toContain("name: concierge-env");
|
|
40
|
+
expect(out).not.toContain("name: conciergeEnv");
|
|
39
41
|
expect(out).toContain("networking_type: limited");
|
|
40
42
|
expect(out).toContain("allowed_hosts:");
|
|
41
43
|
expect(out).toContain("- github.com");
|
|
@@ -73,7 +75,15 @@ describe("fountain serializer", () => {
|
|
|
73
75
|
]),
|
|
74
76
|
);
|
|
75
77
|
|
|
76
|
-
|
|
78
|
+
// Resolves to the referenced entity's fountain name, so the reference and
|
|
79
|
+
// the environment's own manifest agree on one identity.
|
|
80
|
+
expect(out).toContain("environment: concierge-env");
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("falls back to the export name when no name is declared", () => {
|
|
84
|
+
const vault = entity("Fountain::V1::Vault", { description: "no name here" });
|
|
85
|
+
const out = fountainSerializer.serialize(new Map([["stagingCreds", vault]]));
|
|
86
|
+
expect(out).toContain("name: stagingCreds");
|
|
77
87
|
});
|
|
78
88
|
|
|
79
89
|
it("quotes YAML-ambiguous strings", () => {
|
package/src/serializer.ts
CHANGED
|
@@ -19,6 +19,20 @@ import { propsOf } from "./entity-props";
|
|
|
19
19
|
|
|
20
20
|
const API_VERSION = "fountain.dev/v1";
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* The name fountain reconciles by.
|
|
24
|
+
*
|
|
25
|
+
* The declared `name` when there is one, the chant export name otherwise. This
|
|
26
|
+
* matters more than it looks: fountain upserts by name, so keying on the export
|
|
27
|
+
* name would mean renaming a TypeScript variable creates a second resource and
|
|
28
|
+
* orphans the first, and a declared `name` would be silently demoted to an
|
|
29
|
+
* ordinary attribute. Same rule the k8s serializer follows for `metadata.name`.
|
|
30
|
+
*/
|
|
31
|
+
function fountainName(exportName: string, entity: Declarable): string {
|
|
32
|
+
const declared = propsOf(entity).name;
|
|
33
|
+
return typeof declared === "string" && declared.length > 0 ? declared : exportName;
|
|
34
|
+
}
|
|
35
|
+
|
|
22
36
|
/** Fountain::V1::Agent → Agent */
|
|
23
37
|
function kindOf(entityType: string): string {
|
|
24
38
|
const parts = entityType.split("::");
|
|
@@ -30,10 +44,13 @@ export const fountainSerializer: Serializer = {
|
|
|
30
44
|
rulePrefix: "FTN",
|
|
31
45
|
|
|
32
46
|
serialize(entities: Map<string, Declarable>): string {
|
|
33
|
-
// Reverse map for reference resolution: Declarable instance → name.
|
|
47
|
+
// Reverse map for reference resolution: Declarable instance → fountain name.
|
|
48
|
+
// Keyed on the fountain name rather than the export name so an agent's
|
|
49
|
+
// `environment` reference resolves to the same identity the environment's
|
|
50
|
+
// own manifest carries.
|
|
34
51
|
const entityNames = new Map<Declarable, string>();
|
|
35
52
|
for (const [name, entity] of entities) {
|
|
36
|
-
entityNames.set(entity, name);
|
|
53
|
+
entityNames.set(entity, fountainName(name, entity));
|
|
37
54
|
}
|
|
38
55
|
|
|
39
56
|
const visitor: SerializerVisitor = {
|
|
@@ -65,7 +82,7 @@ export const fountainSerializer: Serializer = {
|
|
|
65
82
|
const manifest = {
|
|
66
83
|
apiVersion: API_VERSION,
|
|
67
84
|
kind: kindOf(entity.entityType),
|
|
68
|
-
metadata: { name },
|
|
85
|
+
metadata: { name: fountainName(name, entity) },
|
|
69
86
|
spec,
|
|
70
87
|
};
|
|
71
88
|
docs.push(toYaml(manifest));
|
package/src/spec/fetch.ts
CHANGED
|
@@ -1,20 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Fetch the fountain OpenAPI spec.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* follows the rolling-spec model (like fly). A versioned spec artifact per
|
|
9
|
-
* release shipped upstream as a release asset (BinaryBourbon/fountain#140);
|
|
10
|
-
* when the hosted endpoint is reliably current this fetcher can prefer it.
|
|
4
|
+
* Pinned to a release artifact, not a live endpoint. Upstream ships
|
|
5
|
+
* `openapi.json` as a release asset (BinaryBourbon/fountain#147), so there is
|
|
6
|
+
* a tag to pin and generation is reproducible: same pin in, same generated
|
|
7
|
+
* surface out, on any machine, with or without a network.
|
|
11
8
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* upstream
|
|
17
|
-
*
|
|
9
|
+
* It used to fetch `/api/openapi.json` from a running instance. That made the
|
|
10
|
+
* generated types a function of whatever a particular server happened to be
|
|
11
|
+
* serving at the moment someone ran `npm run generate` — two runs a week apart
|
|
12
|
+
* could differ with no diff in chant to explain it. Every other lexicon pins
|
|
13
|
+
* its upstream spec (AWS a CloudFormation zip, each k8s CRD an operator
|
|
14
|
+
* release); this one no longer is the exception.
|
|
15
|
+
*
|
|
16
|
+
* The committed snapshot is now only the offline path. It is not a silent
|
|
17
|
+
* substitute for the pin: both routes log which one was taken and at what
|
|
18
|
+
* version, because a fallback nobody can see is how you end up debugging types
|
|
19
|
+
* that came from somewhere you did not expect.
|
|
18
20
|
*/
|
|
19
21
|
|
|
20
22
|
import { join, dirname } from "path";
|
|
@@ -23,29 +25,39 @@ import { readFileSync } from "fs";
|
|
|
23
25
|
import { fileURLToPath } from "url";
|
|
24
26
|
import { fetchWithCache } from "@intentius/chant/codegen/fetch";
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
/**
|
|
29
|
+
* The pinned upstream spec.
|
|
30
|
+
*
|
|
31
|
+
* v0.3.0 is the first release to carry the `openapi.json` asset. Bumping this
|
|
32
|
+
* changes the generated surface, so it belongs in its own commit with the
|
|
33
|
+
* regenerated snapshot beside it.
|
|
34
|
+
*/
|
|
35
|
+
export const FOUNTAIN_SPEC_VERSION = "v0.3.0";
|
|
36
|
+
|
|
37
|
+
const SCHEMA_URL = `https://github.com/BinaryBourbon/fountain/releases/download/${FOUNTAIN_SPEC_VERSION}/openapi.json`;
|
|
27
38
|
|
|
28
|
-
const CACHE_FILE = join(homedir(), ".chant",
|
|
39
|
+
const CACHE_FILE = join(homedir(), ".chant", `fountain-openapi-${FOUNTAIN_SPEC_VERSION}.json`);
|
|
29
40
|
|
|
30
41
|
const SNAPSHOT_FILE = join(dirname(fileURLToPath(import.meta.url)), "fountain-openapi.snapshot.json");
|
|
31
42
|
|
|
32
43
|
/**
|
|
33
|
-
* Fetch the fountain OpenAPI spec
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* out into per-kind results.
|
|
44
|
+
* Fetch the pinned fountain OpenAPI spec, falling back to the committed
|
|
45
|
+
* snapshot when there is no network. Returns a single-entry map — the whole
|
|
46
|
+
* spec is one document; the parser fans it out into per-kind results.
|
|
37
47
|
*/
|
|
38
48
|
export async function fetchSchemas(options?: { force?: boolean }): Promise<Map<string, Buffer>> {
|
|
39
49
|
let raw: Buffer;
|
|
40
50
|
try {
|
|
41
51
|
raw = await fetchWithCache({ url: SCHEMA_URL, cacheFile: CACHE_FILE }, options?.force);
|
|
42
|
-
//
|
|
43
|
-
//
|
|
52
|
+
// A 404 body is still a body. Parsing proves we got a spec and not an
|
|
53
|
+
// error page that would otherwise be cached and generated from.
|
|
44
54
|
JSON.parse(raw.toString("utf-8"));
|
|
55
|
+
console.error(`[fountain] spec: pinned release ${FOUNTAIN_SPEC_VERSION}`);
|
|
45
56
|
} catch (err) {
|
|
46
57
|
console.error(
|
|
47
|
-
`[fountain]
|
|
48
|
-
`
|
|
58
|
+
`[fountain] spec: committed snapshot — could not fetch pinned release ${FOUNTAIN_SPEC_VERSION} ` +
|
|
59
|
+
`(${err instanceof Error ? err.message.split("\n")[0] : err}). ` +
|
|
60
|
+
`Refresh it with \`npm run generate -- --force\` on a networked machine.`,
|
|
49
61
|
);
|
|
50
62
|
raw = readFileSync(SNAPSHOT_FILE);
|
|
51
63
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"components":{"responses":{},"schemas":{"SecretListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Secret"},"type":"array","x-struct":null,"x-validate":null}},"required":["data"],"title":"SecretListResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.SecretListResponse","x-validate":null},"ConversationCreateRequest":{"properties":{"agent_id":{"format":"uuid","type":"string","x-struct":null,"x-validate":null},"images":{"description":"Optional images to attach to the initial prompt.","items":{"$ref":"#/components/schemas/ImageInput"},"nullable":true,"type":"array","x-struct":null,"x-validate":null},"prompt":{"description":"Optional first turn prompt.","type":"string","x-struct":null,"x-validate":null},"sprite_name":{"description":"Override the auto-generated sprite name.","type":"string","x-struct":null,"x-validate":null},"vault_id":{"description":"Optional vault whose secrets override the environment's baseline at sprite spawn. Must satisfy the agent's allowed_vault_ids when that allowlist is set.","format":"uuid","nullable":true,"type":"string","x-struct":null,"x-validate":null}},"required":["agent_id"],"title":"ConversationCreateRequest","type":"object","x-struct":"Elixir.FountainWeb.Schemas.ConversationCreateRequest","x-validate":null},"Secret":{"description":"A named secret. Values are write-only — the API never returns them.","properties":{"environment_id":{"format":"uuid","type":"string","x-struct":null,"x-validate":null},"id":{"format":"uuid","type":"string","x-struct":null,"x-validate":null},"inserted_at":{"format":"date-time","type":"string","x-struct":null,"x-validate":null},"key":{"type":"string","x-struct":null,"x-validate":null},"updated_at":{"format":"date-time","type":"string","x-struct":null,"x-validate":null}},"required":["id","key","environment_id"],"title":"Secret","type":"object","x-struct":"Elixir.FountainWeb.Schemas.Secret","x-validate":null},"VaultUpdate":{"properties":{"description":{"type":"string","x-struct":null,"x-validate":null},"metadata":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"name":{"maxLength":200,"minLength":1,"type":"string","x-struct":null,"x-validate":null}},"title":"VaultUpdate","type":"object","x-struct":"Elixir.FountainWeb.Schemas.VaultUpdate","x-validate":null},"ImageInput":{"description":"A base64-encoded image to attach to a prompt.","properties":{"data":{"description":"Base64-encoded image bytes.","type":"string","x-struct":null,"x-validate":null},"media_type":{"description":"MIME type of the image.","enum":["image/png","image/jpeg","image/gif","image/webp"],"type":"string","x-struct":null,"x-validate":null}},"required":["data","media_type"],"title":"ImageInput","type":"object","x-struct":"Elixir.FountainWeb.Schemas.ImageInput","x-validate":null},"Repository":{"properties":{"mount_path":{"pattern":"^/","type":"string","x-struct":null,"x-validate":null},"url":{"format":"uri","pattern":"^https://","type":"string","x-struct":null,"x-validate":null}},"required":["url","mount_path"],"title":"Repository","type":"object","x-struct":"Elixir.FountainWeb.Schemas.Repository","x-validate":null},"EnvironmentUpdate":{"properties":{"env_vars":{"additionalProperties":{"type":"string","x-struct":null,"x-validate":null},"type":"object","x-struct":null,"x-validate":null},"metadata":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"name":{"maxLength":200,"minLength":1,"type":"string","x-struct":null,"x-validate":null},"networking_config":{"additionalProperties":true,"description":"Refines networking_type: limited. allowed_hosts is the only key honored today; unknown keys are ignored. Under limited, egress is restricted to the allowlisted domains. With no allowed_hosts (or an empty list), the sandbox denies all egress by default — this is a deny-all, not an allow-all.","properties":{"allowed_hosts":{"description":"Domains the sandbox may reach when networking_type is limited.","items":{"type":"string","x-struct":null,"x-validate":null},"type":"array","x-struct":null,"x-validate":null}},"type":"object","x-struct":null,"x-validate":null},"networking_type":{"enum":["unrestricted","limited"],"type":"string","x-struct":null,"x-validate":null},"packages":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"repositories":{"items":{"$ref":"#/components/schemas/Repository"},"type":"array","x-struct":null,"x-validate":null},"setup_script":{"type":"string","x-struct":null,"x-validate":null}},"title":"EnvironmentUpdate","type":"object","x-struct":"Elixir.FountainWeb.Schemas.EnvironmentUpdate","x-validate":null},"VaultSecret":{"description":"A named secret in a vault. Values are write-only — the API never returns them.","properties":{"id":{"format":"uuid","type":"string","x-struct":null,"x-validate":null},"inserted_at":{"format":"date-time","type":"string","x-struct":null,"x-validate":null},"key":{"type":"string","x-struct":null,"x-validate":null},"updated_at":{"format":"date-time","type":"string","x-struct":null,"x-validate":null},"vault_id":{"format":"uuid","type":"string","x-struct":null,"x-validate":null}},"required":["id","key","vault_id"],"title":"VaultSecret","type":"object","x-struct":"Elixir.FountainWeb.Schemas.VaultSecret","x-validate":null},"TurnListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Turn"},"type":"array","x-struct":null,"x-validate":null}},"required":["data"],"title":"TurnListResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.TurnListResponse","x-validate":null},"Sandbox":{"description":"One sprite lifespan owned by a conversation.","properties":{"id":{"format":"uuid","type":"string","x-struct":null,"x-validate":null},"sprite_name":{"type":"string","x-struct":null,"x-validate":null},"status":{"enum":["pending","starting","ready","terminated","failed"],"type":"string","x-struct":null,"x-validate":null}},"required":["id","sprite_name","status"],"title":"Sandbox","type":"object","x-struct":"Elixir.FountainWeb.Schemas.Sandbox","x-validate":null},"Error":{"properties":{"error":{"type":"string","x-struct":null,"x-validate":null}},"required":["error"],"title":"Error","type":"object","x-struct":"Elixir.FountainWeb.Schemas.Error","x-validate":null},"VaultSecretResponse":{"properties":{"data":{"$ref":"#/components/schemas/VaultSecret"}},"required":["data"],"title":"VaultSecretResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.VaultSecretResponse","x-validate":null},"EnvironmentResponse":{"properties":{"data":{"$ref":"#/components/schemas/Environment"}},"required":["data"],"title":"EnvironmentResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.EnvironmentResponse","x-validate":null},"ChangesetError":{"description":"Validation errors keyed by field, with each value an array of messages.","properties":{"errors":{"additionalProperties":{"items":{"type":"string","x-struct":null,"x-validate":null},"type":"array","x-struct":null,"x-validate":null},"type":"object","x-struct":null,"x-validate":null}},"required":["errors"],"title":"ChangesetError","type":"object","x-struct":"Elixir.FountainWeb.Schemas.ChangesetError","x-validate":null},"Agent":{"description":"An AI agent definition: runtime, model, skills, MCP, env.","properties":{"allowed_vault_ids":{"description":"Vaults a conversation may attach to this agent. null (default) allows any vault the tenant owns; an empty list forbids attaching any vault; a non-empty list is an allowlist. Vault values override the agent's environment on key collision, so this scopes who can override reviewed config.","items":{"format":"uuid","type":"string","x-struct":null,"x-validate":null},"nullable":true,"type":"array","x-struct":null,"x-validate":null},"description":{"type":"string","x-struct":null,"x-validate":null},"environment_id":{"format":"uuid","nullable":true,"type":"string","x-struct":null,"x-validate":null},"id":{"format":"uuid","type":"string","x-struct":null,"x-validate":null},"inserted_at":{"format":"date-time","type":"string","x-struct":null,"x-validate":null},"mcp_servers":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"metadata":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"model":{"description":"Canonical provider/model_id (e.g. anthropic/claude-sonnet-4-6).","pattern":"^[a-z0-9_-]+/[a-z0-9._-]+$","type":"string","x-struct":null,"x-validate":null},"name":{"type":"string","x-struct":null,"x-validate":null},"runtime":{"enum":["claude","codex","gemini","opencode"],"type":"string","x-struct":null,"x-validate":null},"skills":{"description":"Each entry is either inline (`{name, content}` — full SKILL.md text written to the sprite) or github (`{source, ref?, name?}` — installed on the sprite via the skills.sh CLI, optionally pinned to a tag/branch/sha via `ref`). Exactly one of `content` or `source` must be set on each entry.","items":{"properties":{"content":{"description":"Full SKILL.md body for inline entries.","type":"string","x-struct":null,"x-validate":null},"name":{"description":"Skill name (required for inline entries).","type":"string","x-struct":null,"x-validate":null},"ref":{"description":"Optional tag, branch, or sha pinning a github-sourced skill (installed as `owner/repo@ref`). Without it the default branch is fetched at spawn time.","pattern":"^[A-Za-z0-9._/-]+$","type":"string","x-struct":null,"x-validate":null},"source":{"description":"GitHub `owner/repo` for skills.sh-sourced entries.","pattern":"^[A-Za-z0-9._/-]+$","type":"string","x-struct":null,"x-validate":null}},"type":"object","x-struct":null,"x-validate":null},"type":"array","x-struct":null,"x-validate":null},"system":{"description":"System prompt.","type":"string","x-struct":null,"x-validate":null},"updated_at":{"format":"date-time","type":"string","x-struct":null,"x-validate":null}},"required":["id","name","model","runtime"],"title":"Agent","type":"object","x-struct":"Elixir.FountainWeb.Schemas.Agent","x-validate":null},"Conversation":{"description":"One chat with one agent inside one sandbox.","properties":{"agent_id":{"format":"uuid","nullable":true,"type":"string","x-struct":null,"x-validate":null},"id":{"format":"uuid","type":"string","x-struct":null,"x-validate":null},"inserted_at":{"format":"date-time","type":"string","x-struct":null,"x-validate":null},"runtime":{"enum":["claude","codex","gemini","opencode"],"type":"string","x-struct":null,"x-validate":null},"runtime_session_id":{"nullable":true,"type":"string","x-struct":null,"x-validate":null},"sandbox":{"nullable":true,"oneOf":[{"$ref":"#/components/schemas/Sandbox"}],"x-struct":null,"x-validate":null},"sandbox_id":{"format":"uuid","nullable":true,"type":"string","x-struct":null,"x-validate":null},"status":{"enum":["pending","running","idle","completed","failed","terminated"],"type":"string","x-struct":null,"x-validate":null},"updated_at":{"format":"date-time","type":"string","x-struct":null,"x-validate":null},"vault_id":{"format":"uuid","nullable":true,"type":"string","x-struct":null,"x-validate":null}},"required":["id","runtime","status"],"title":"Conversation","type":"object","x-struct":"Elixir.FountainWeb.Schemas.Conversation","x-validate":null},"EnvironmentListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Environment"},"type":"array","x-struct":null,"x-validate":null}},"required":["data"],"title":"EnvironmentListResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.EnvironmentListResponse","x-validate":null},"VaultSecretRequest":{"properties":{"key":{"type":"string","x-struct":null,"x-validate":null},"value":{"description":"Secret value (write-only).","type":"string","x-struct":null,"x-validate":null}},"required":["key","value"],"title":"VaultSecretRequest","type":"object","x-struct":"Elixir.FountainWeb.Schemas.VaultSecretRequest","x-validate":null},"PromptRequest":{"properties":{"images":{"description":"Optional images to attach to this prompt.","items":{"$ref":"#/components/schemas/ImageInput"},"nullable":true,"type":"array","x-struct":null,"x-validate":null},"prompt":{"type":"string","x-struct":null,"x-validate":null}},"required":["prompt"],"title":"PromptRequest","type":"object","x-struct":"Elixir.FountainWeb.Schemas.PromptRequest","x-validate":null},"Turn":{"description":"One prompt → exit_code cycle within a conversation.","properties":{"ended_at":{"format":"date-time","nullable":true,"type":"string","x-struct":null,"x-validate":null},"exit_code":{"nullable":true,"type":"integer","x-struct":null,"x-validate":null},"id":{"format":"uuid","type":"string","x-struct":null,"x-validate":null},"image_count":{"description":"Number of images attached to this turn.","type":"integer","x-struct":null,"x-validate":null},"inserted_at":{"format":"date-time","type":"string","x-struct":null,"x-validate":null},"prompt":{"type":"string","x-struct":null,"x-validate":null},"started_at":{"format":"date-time","nullable":true,"type":"string","x-struct":null,"x-validate":null},"status":{"enum":["pending","running","completed","failed","interrupted"],"type":"string","x-struct":null,"x-validate":null},"turn_number":{"type":"integer","x-struct":null,"x-validate":null}},"required":["id","turn_number","prompt","status"],"title":"Turn","type":"object","x-struct":"Elixir.FountainWeb.Schemas.Turn","x-validate":null},"VaultSecretListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/VaultSecret"},"type":"array","x-struct":null,"x-validate":null}},"required":["data"],"title":"VaultSecretListResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.VaultSecretListResponse","x-validate":null},"HealthResponse":{"properties":{"status":{"example":"ok","type":"string","x-struct":null,"x-validate":null}},"required":["status"],"title":"HealthResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.HealthResponse","x-validate":null},"VaultResponse":{"properties":{"data":{"$ref":"#/components/schemas/Vault"}},"required":["data"],"title":"VaultResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.VaultResponse","x-validate":null},"PromptResponse":{"properties":{"status":{"example":"queued","type":"string","x-struct":null,"x-validate":null}},"required":["status"],"title":"PromptResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.PromptResponse","x-validate":null},"AgentListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Agent"},"type":"array","x-struct":null,"x-validate":null}},"required":["data"],"title":"AgentListResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.AgentListResponse","x-validate":null},"AgentUpdate":{"properties":{"allowed_vault_ids":{"description":"Vaults a conversation may attach to this agent. null (default) allows any vault the tenant owns; an empty list forbids attaching any vault; a non-empty list is an allowlist.","items":{"format":"uuid","type":"string","x-struct":null,"x-validate":null},"nullable":true,"type":"array","x-struct":null,"x-validate":null},"description":{"type":"string","x-struct":null,"x-validate":null},"environment_id":{"format":"uuid","nullable":true,"type":"string","x-struct":null,"x-validate":null},"mcp_servers":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"metadata":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"model":{"pattern":"^[a-z0-9_-]+/[a-z0-9._-]+$","type":"string","x-struct":null,"x-validate":null},"name":{"maxLength":200,"minLength":1,"type":"string","x-struct":null,"x-validate":null},"runtime":{"enum":["claude","codex","gemini","opencode"],"type":"string","x-struct":null,"x-validate":null},"skills":{"description":"Each entry is either inline (`{name, content}` — full SKILL.md text written to the sprite) or github (`{source, ref?, name?}` — installed on the sprite via the skills.sh CLI, optionally pinned to a tag/branch/sha via `ref`). Exactly one of `content` or `source` must be set on each entry.","items":{"properties":{"content":{"description":"Full SKILL.md body for inline entries.","type":"string","x-struct":null,"x-validate":null},"name":{"description":"Skill name (required for inline entries).","type":"string","x-struct":null,"x-validate":null},"ref":{"description":"Optional tag, branch, or sha pinning a github-sourced skill (installed as `owner/repo@ref`). Without it the default branch is fetched at spawn time.","pattern":"^[A-Za-z0-9._/-]+$","type":"string","x-struct":null,"x-validate":null},"source":{"description":"GitHub `owner/repo` for skills.sh-sourced entries.","pattern":"^[A-Za-z0-9._/-]+$","type":"string","x-struct":null,"x-validate":null}},"type":"object","x-struct":null,"x-validate":null},"type":"array","x-struct":null,"x-validate":null},"system":{"type":"string","x-struct":null,"x-validate":null}},"title":"AgentUpdate","type":"object","x-struct":"Elixir.FountainWeb.Schemas.AgentUpdate","x-validate":null},"ConversationListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Conversation"},"type":"array","x-struct":null,"x-validate":null}},"required":["data"],"title":"ConversationListResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.ConversationListResponse","x-validate":null},"VaultRequest":{"properties":{"description":{"type":"string","x-struct":null,"x-validate":null},"metadata":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"name":{"maxLength":200,"minLength":1,"type":"string","x-struct":null,"x-validate":null}},"required":["name"],"title":"VaultRequest","type":"object","x-struct":"Elixir.FountainWeb.Schemas.VaultRequest","x-validate":null},"ConversationResponse":{"properties":{"data":{"$ref":"#/components/schemas/Conversation"}},"required":["data"],"title":"ConversationResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.ConversationResponse","x-validate":null},"Vault":{"description":"A free-floating bag of env-var overrides selected at conversation creation. Vault values override an environment's baseline secrets when the same key is set on both.","properties":{"description":{"type":"string","x-struct":null,"x-validate":null},"id":{"format":"uuid","type":"string","x-struct":null,"x-validate":null},"inserted_at":{"format":"date-time","type":"string","x-struct":null,"x-validate":null},"metadata":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"name":{"type":"string","x-struct":null,"x-validate":null},"updated_at":{"format":"date-time","type":"string","x-struct":null,"x-validate":null}},"required":["id","name"],"title":"Vault","type":"object","x-struct":"Elixir.FountainWeb.Schemas.Vault","x-validate":null},"SecretResponse":{"properties":{"data":{"$ref":"#/components/schemas/Secret"}},"required":["data"],"title":"SecretResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.SecretResponse","x-validate":null},"SecretRequest":{"properties":{"key":{"type":"string","x-struct":null,"x-validate":null},"value":{"description":"Secret value (write-only).","type":"string","x-struct":null,"x-validate":null}},"required":["key","value"],"title":"SecretRequest","type":"object","x-struct":"Elixir.FountainWeb.Schemas.SecretRequest","x-validate":null},"AgentRequest":{"properties":{"allowed_vault_ids":{"description":"Vaults a conversation may attach to this agent. null (default) allows any vault the tenant owns; an empty list forbids attaching any vault; a non-empty list is an allowlist.","items":{"format":"uuid","type":"string","x-struct":null,"x-validate":null},"nullable":true,"type":"array","x-struct":null,"x-validate":null},"description":{"type":"string","x-struct":null,"x-validate":null},"environment_id":{"format":"uuid","nullable":true,"type":"string","x-struct":null,"x-validate":null},"mcp_servers":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"metadata":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"model":{"pattern":"^[a-z0-9_-]+/[a-z0-9._-]+$","type":"string","x-struct":null,"x-validate":null},"name":{"maxLength":200,"minLength":1,"type":"string","x-struct":null,"x-validate":null},"runtime":{"enum":["claude","codex","gemini","opencode"],"type":"string","x-struct":null,"x-validate":null},"skills":{"description":"Each entry is either inline (`{name, content}` — full SKILL.md text written to the sprite) or github (`{source, ref?, name?}` — installed on the sprite via the skills.sh CLI, optionally pinned to a tag/branch/sha via `ref`). Exactly one of `content` or `source` must be set on each entry.","items":{"properties":{"content":{"description":"Full SKILL.md body for inline entries.","type":"string","x-struct":null,"x-validate":null},"name":{"description":"Skill name (required for inline entries).","type":"string","x-struct":null,"x-validate":null},"ref":{"description":"Optional tag, branch, or sha pinning a github-sourced skill (installed as `owner/repo@ref`). Without it the default branch is fetched at spawn time.","pattern":"^[A-Za-z0-9._/-]+$","type":"string","x-struct":null,"x-validate":null},"source":{"description":"GitHub `owner/repo` for skills.sh-sourced entries.","pattern":"^[A-Za-z0-9._/-]+$","type":"string","x-struct":null,"x-validate":null}},"type":"object","x-struct":null,"x-validate":null},"type":"array","x-struct":null,"x-validate":null},"system":{"type":"string","x-struct":null,"x-validate":null}},"required":["name","model","runtime"],"title":"AgentRequest","type":"object","x-struct":"Elixir.FountainWeb.Schemas.AgentRequest","x-validate":null},"EnvironmentRequest":{"properties":{"env_vars":{"additionalProperties":{"type":"string","x-struct":null,"x-validate":null},"type":"object","x-struct":null,"x-validate":null},"metadata":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"name":{"maxLength":200,"minLength":1,"type":"string","x-struct":null,"x-validate":null},"networking_config":{"additionalProperties":true,"description":"Refines networking_type: limited. allowed_hosts is the only key honored today; unknown keys are ignored. Under limited, egress is restricted to the allowlisted domains. With no allowed_hosts (or an empty list), the sandbox denies all egress by default — this is a deny-all, not an allow-all.","properties":{"allowed_hosts":{"description":"Domains the sandbox may reach when networking_type is limited.","items":{"type":"string","x-struct":null,"x-validate":null},"type":"array","x-struct":null,"x-validate":null}},"type":"object","x-struct":null,"x-validate":null},"networking_type":{"enum":["unrestricted","limited"],"type":"string","x-struct":null,"x-validate":null},"packages":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"repositories":{"items":{"$ref":"#/components/schemas/Repository"},"type":"array","x-struct":null,"x-validate":null},"setup_script":{"type":"string","x-struct":null,"x-validate":null}},"required":["name"],"title":"EnvironmentRequest","type":"object","x-struct":"Elixir.FountainWeb.Schemas.EnvironmentRequest","x-validate":null},"Environment":{"description":"A reusable sandbox environment: packages, env vars, repos, networking.","properties":{"env_vars":{"additionalProperties":{"type":"string","x-struct":null,"x-validate":null},"type":"object","x-struct":null,"x-validate":null},"id":{"format":"uuid","type":"string","x-struct":null,"x-validate":null},"inserted_at":{"format":"date-time","type":"string","x-struct":null,"x-validate":null},"metadata":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"name":{"type":"string","x-struct":null,"x-validate":null},"networking_config":{"additionalProperties":true,"description":"Refines networking_type: limited. allowed_hosts is the only key honored today; unknown keys are ignored. Under limited, egress is restricted to the allowlisted domains. With no allowed_hosts (or an empty list), the sandbox denies all egress by default — this is a deny-all, not an allow-all.","properties":{"allowed_hosts":{"description":"Domains the sandbox may reach when networking_type is limited.","items":{"type":"string","x-struct":null,"x-validate":null},"type":"array","x-struct":null,"x-validate":null}},"type":"object","x-struct":null,"x-validate":null},"networking_type":{"enum":["unrestricted","limited"],"type":"string","x-struct":null,"x-validate":null},"packages":{"additionalProperties":true,"type":"object","x-struct":null,"x-validate":null},"repositories":{"items":{"$ref":"#/components/schemas/Repository"},"type":"array","x-struct":null,"x-validate":null},"setup_script":{"type":"string","x-struct":null,"x-validate":null},"updated_at":{"format":"date-time","type":"string","x-struct":null,"x-validate":null}},"required":["id","name"],"title":"Environment","type":"object","x-struct":"Elixir.FountainWeb.Schemas.Environment","x-validate":null},"VaultListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Vault"},"type":"array","x-struct":null,"x-validate":null}},"required":["data"],"title":"VaultListResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.VaultListResponse","x-validate":null},"AgentResponse":{"properties":{"data":{"$ref":"#/components/schemas/Agent"}},"required":["data"],"title":"AgentResponse","type":"object","x-struct":"Elixir.FountainWeb.Schemas.AgentResponse","x-validate":null}},"securitySchemes":{"bearer":{"description":"ADMIN_TOKEN configured at boot.","scheme":"bearer","type":"http"}}},"info":{"description":"HTTP API for Agent on Demand. The same surface backs the LiveView UI\nand the `aod` CLI; if it's not here, it doesn't exist yet.\n\nAll `/api/*` endpoints require a bearer token (`ADMIN_TOKEN`).\n","title":"Agent on Demand","version":"0.1.0"},"openapi":"3.0.0","paths":{"/api/agents":{"get":{"callbacks":{},"operationId":"FountainWeb.AgentController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentListResponse"}}},"description":"Agents"}},"summary":"List agents","tags":["Agents"]},"post":{"callbacks":{},"operationId":"FountainWeb.AgentController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentRequest"}}},"description":"Agent attributes","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}},"description":"Agent"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Create an agent","tags":["Agents"]}},"/api/agents/{id}":{"delete":{"callbacks":{},"operationId":"FountainWeb.AgentController.delete","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"204":{"description":"Deleted"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Delete an agent","tags":["Agents"]},"get":{"callbacks":{},"operationId":"FountainWeb.AgentController.show","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}},"description":"Agent"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Get an agent","tags":["Agents"]},"patch":{"callbacks":{},"description":"Every field is optional; the server merges into the existing record.","operationId":"FountainWeb.AgentController.update (2)","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentUpdate"}}},"description":"Partial agent attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}},"description":"Agent"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Update an agent (partial)","tags":["Agents"]},"put":{"callbacks":{},"description":"Every field is optional; the server merges into the existing record.","operationId":"FountainWeb.AgentController.update","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentUpdate"}}},"description":"Partial agent attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}},"description":"Agent"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Update an agent (partial)","tags":["Agents"]}},"/api/conversations":{"get":{"callbacks":{},"operationId":"FountainWeb.ConversationController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationListResponse"}}},"description":"Conversations"}},"summary":"List conversations","tags":["Conversations"]},"post":{"callbacks":{},"description":"Creates a sandbox + conversation pair, starts the runtime in a fresh sprite, and (if `prompt` is supplied) sends it as turn 1. Pass `X-Fountain-Parent-Conversation-Id` header to record which conversation spawned this one. Legacy `X-AoD-Parent-Conversation-Id` is still accepted for sprites provisioned before the rename.","operationId":"FountainWeb.ConversationController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationCreateRequest"}}},"description":"Conversation attrs","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationResponse"}}},"description":"Conversation"},"402":{"content":{"application/json":{"schema":{"properties":{"error":{"type":"string","x-struct":null,"x-validate":null},"upgrade_url":{"type":"string","x-struct":null,"x-validate":null}},"type":"object","x-struct":null,"x-validate":null}}},"description":"Subscription required"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Agent not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Start a conversation","tags":["Conversations"]}},"/api/conversations/{conversation_id}/interrupt":{"post":{"callbacks":{},"operationId":"FountainWeb.ConversationController.interrupt","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"204":{"description":"Interrupted"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No turn running"}},"summary":"Interrupt the running turn","tags":["Conversations"]}},"/api/conversations/{conversation_id}/prompts":{"post":{"callbacks":{},"description":"Queues a new turn. If the ConversationServer has been GC'd (e.g. across a BEAM restart) a fresh sprite is provisioned and the runtime resumes via its session id.","operationId":"FountainWeb.ConversationController.prompt","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptRequest"}}},"description":"Prompt","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptResponse"}}},"description":"Queued"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Busy"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Send another prompt","tags":["Conversations"]}},"/api/conversations/{conversation_id}/stream":{"get":{"callbacks":{},"description":"Server-Sent Events stream of the conversation's log events. The `Last-Event-ID` request header resumes from a known event id; missed events are replayed before the live tail begins. Keep-alive heartbeats every 15s as `: heartbeat` comments.","operationId":"FountainWeb.ConversationController.stream","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}},{"description":"Resume after this event id (integer as string). Missing or unparseable values are treated as 0.","in":"header","name":"Last-Event-ID","required":false,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"200":{"content":{"text/event-stream":{"schema":{"type":"string","x-struct":null,"x-validate":null}}},"description":"SSE stream"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Stream log events (SSE)","tags":["Conversations"]}},"/api/conversations/{conversation_id}/terminate":{"post":{"callbacks":{},"description":"Tears down the sprite and marks the conversation `terminated`. Idempotent for already-dead conversations.","operationId":"FountainWeb.ConversationController.terminate","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"204":{"description":"Terminated"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Terminate a conversation","tags":["Conversations"]}},"/api/conversations/{conversation_id}/turns":{"get":{"callbacks":{},"operationId":"FountainWeb.ConversationController.turns","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TurnListResponse"}}},"description":"Turns"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"List turns in a conversation","tags":["Conversations"]}},"/api/conversations/{id}":{"delete":{"callbacks":{},"description":"Tears down the sprite if alive, then deletes the conversation row (cascades to turns and log events).","operationId":"FountainWeb.ConversationController.delete","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"204":{"description":"Deleted"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Delete a conversation","tags":["Conversations"]},"get":{"callbacks":{},"operationId":"FountainWeb.ConversationController.show","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationResponse"}}},"description":"Conversation"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Get a conversation","tags":["Conversations"]}},"/api/environments":{"get":{"callbacks":{},"operationId":"FountainWeb.EnvironmentController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentListResponse"}}},"description":"Environments"}},"summary":"List environments","tags":["Environments"]},"post":{"callbacks":{},"operationId":"FountainWeb.EnvironmentController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentRequest"}}},"description":"Environment attributes","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentResponse"}}},"description":"Environment"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Create an environment","tags":["Environments"]}},"/api/environments/{environment_id}/secrets":{"get":{"callbacks":{},"operationId":"FountainWeb.SecretController.index","parameters":[{"description":"","in":"path","name":"environment_id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecretListResponse"}}},"description":"Secrets"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"List secrets in an environment","tags":["Secrets"]},"post":{"callbacks":{},"description":"Sets the value for `key`. If the key exists, the value is overwritten. Values are write-only — subsequent reads never return them.","operationId":"FountainWeb.SecretController.create","parameters":[{"description":"","in":"path","name":"environment_id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecretRequest"}}},"description":"Secret","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecretResponse"}}},"description":"Secret"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Upsert a secret","tags":["Secrets"]}},"/api/environments/{environment_id}/secrets/{id}":{"delete":{"callbacks":{},"operationId":"FountainWeb.SecretController.delete","parameters":[{"description":"","in":"path","name":"environment_id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}},{"description":"Secret key.","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"204":{"description":"Deleted"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Delete a secret by key","tags":["Secrets"]}},"/api/environments/{id}":{"delete":{"callbacks":{},"operationId":"FountainWeb.EnvironmentController.delete","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"204":{"description":"Deleted"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Delete an environment","tags":["Environments"]},"get":{"callbacks":{},"operationId":"FountainWeb.EnvironmentController.show","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentResponse"}}},"description":"Environment"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Get an environment","tags":["Environments"]},"patch":{"callbacks":{},"description":"Every field is optional; the server merges into the existing record.","operationId":"FountainWeb.EnvironmentController.update (2)","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentUpdate"}}},"description":"Partial environment attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentResponse"}}},"description":"Environment"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Update an environment (partial)","tags":["Environments"]},"put":{"callbacks":{},"description":"Every field is optional; the server merges into the existing record.","operationId":"FountainWeb.EnvironmentController.update","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentUpdate"}}},"description":"Partial environment attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentResponse"}}},"description":"Environment"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Update an environment (partial)","tags":["Environments"]}},"/api/vaults":{"get":{"callbacks":{},"operationId":"FountainWeb.VaultController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultListResponse"}}},"description":"Vaults"}},"summary":"List vaults","tags":["Vaults"]},"post":{"callbacks":{},"operationId":"FountainWeb.VaultController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultRequest"}}},"description":"Vault attributes","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultResponse"}}},"description":"Vault"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Create a vault","tags":["Vaults"]}},"/api/vaults/{id}":{"delete":{"callbacks":{},"operationId":"FountainWeb.VaultController.delete","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"204":{"description":"Deleted"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Delete a vault","tags":["Vaults"]},"get":{"callbacks":{},"operationId":"FountainWeb.VaultController.show","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultResponse"}}},"description":"Vault"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Get a vault","tags":["Vaults"]},"patch":{"callbacks":{},"description":"Every field is optional; the server merges into the existing record.","operationId":"FountainWeb.VaultController.update (2)","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultUpdate"}}},"description":"Partial vault attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultResponse"}}},"description":"Vault"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Update a vault (partial)","tags":["Vaults"]},"put":{"callbacks":{},"description":"Every field is optional; the server merges into the existing record.","operationId":"FountainWeb.VaultController.update","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultUpdate"}}},"description":"Partial vault attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultResponse"}}},"description":"Vault"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Update a vault (partial)","tags":["Vaults"]}},"/api/vaults/{vault_id}/secrets":{"get":{"callbacks":{},"operationId":"FountainWeb.VaultSecretController.index","parameters":[{"description":"","in":"path","name":"vault_id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultSecretListResponse"}}},"description":"Vault Secrets"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"List secrets in a vault","tags":["Vault Secrets"]},"post":{"callbacks":{},"description":"Sets the value for `key`. If the key exists, the value is overwritten. Values are write-only — subsequent reads never return them.","operationId":"FountainWeb.VaultSecretController.create","parameters":[{"description":"","in":"path","name":"vault_id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultSecretRequest"}}},"description":"Vault Secret","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultSecretResponse"}}},"description":"Vault Secret"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Upsert a vault secret","tags":["Vault Secrets"]}},"/api/vaults/{vault_id}/secrets/{id}":{"delete":{"callbacks":{},"operationId":"FountainWeb.VaultSecretController.delete","parameters":[{"description":"","in":"path","name":"vault_id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}},{"description":"Secret key.","in":"path","name":"id","required":true,"schema":{"type":"string","x-struct":null,"x-validate":null}}],"responses":{"204":{"description":"Deleted"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Delete a vault secret by key","tags":["Vault Secrets"]}},"/health":{"get":{"callbacks":{},"description":"Public, unauthenticated. Returns `{\"status\": \"ok\"}` if the app is up.","operationId":"FountainWeb.HealthController.show","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}},"description":"Health response"}},"security":[],"summary":"Liveness probe","tags":["Health"]}}},"security":[{"bearer":[]}],"servers":[{"url":"http://localhost:4000","variables":{}}],"tags":[]}
|
|
1
|
+
{"components":{"responses":{},"schemas":{"SecretListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Secret"},"type":"array"}},"required":["data"],"title":"SecretListResponse","type":"object"},"ConversationCreateRequest":{"properties":{"agent_id":{"format":"uuid","type":"string"},"images":{"description":"Optional images to attach to the initial prompt.","items":{"$ref":"#/components/schemas/ImageInput"},"nullable":true,"type":"array"},"prompt":{"description":"Optional first turn prompt.","type":"string"},"sprite_name":{"description":"Override the auto-generated sprite name.","type":"string"},"vault_id":{"description":"Optional vault whose secrets override the environment's baseline at sprite spawn. Must satisfy the agent's allowed_vault_ids when that allowlist is set.","format":"uuid","nullable":true,"type":"string"}},"required":["agent_id"],"title":"ConversationCreateRequest","type":"object"},"Secret":{"description":"A named secret. Values are write-only — the API never returns them.","properties":{"environment_id":{"format":"uuid","type":"string"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"key":{"type":"string"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","key","environment_id"],"title":"Secret","type":"object"},"VaultUpdate":{"properties":{"description":{"type":"string"},"metadata":{"additionalProperties":true,"type":"object"},"name":{"maxLength":200,"minLength":1,"type":"string"}},"title":"VaultUpdate","type":"object"},"ImageInput":{"description":"A base64-encoded image to attach to a prompt.","properties":{"data":{"description":"Base64-encoded image bytes.","type":"string"},"media_type":{"description":"MIME type of the image.","enum":["image/png","image/jpeg","image/gif","image/webp"],"type":"string"}},"required":["data","media_type"],"title":"ImageInput","type":"object"},"Repository":{"properties":{"mount_path":{"pattern":"^/","type":"string"},"url":{"format":"uri","pattern":"^https://","type":"string"}},"required":["url","mount_path"],"title":"Repository","type":"object"},"EnvironmentUpdate":{"properties":{"env_vars":{"additionalProperties":{"type":"string"},"type":"object"},"metadata":{"additionalProperties":true,"type":"object"},"name":{"maxLength":200,"minLength":1,"type":"string"},"networking_config":{"additionalProperties":true,"description":"Refines networking_type: limited. allowed_hosts is the only key honored today; unknown keys are ignored. Under limited, egress is restricted to the allowlisted domains. With no allowed_hosts (or an empty list), the sandbox denies all egress by default — this is a deny-all, not an allow-all.","properties":{"allowed_hosts":{"description":"Domains the sandbox may reach when networking_type is limited.","items":{"type":"string"},"type":"array"}},"type":"object"},"networking_type":{"enum":["unrestricted","limited"],"type":"string"},"packages":{"additionalProperties":true,"type":"object"},"repositories":{"items":{"$ref":"#/components/schemas/Repository"},"type":"array"},"setup_script":{"type":"string"}},"title":"EnvironmentUpdate","type":"object"},"VaultSecret":{"description":"A named secret in a vault. Values are write-only — the API never returns them.","properties":{"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"key":{"type":"string"},"updated_at":{"format":"date-time","type":"string"},"vault_id":{"format":"uuid","type":"string"}},"required":["id","key","vault_id"],"title":"VaultSecret","type":"object"},"TurnListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Turn"},"type":"array"}},"required":["data"],"title":"TurnListResponse","type":"object"},"Sandbox":{"description":"One sprite lifespan owned by a conversation.","properties":{"id":{"format":"uuid","type":"string"},"sprite_name":{"type":"string"},"status":{"enum":["pending","starting","ready","terminated","failed"],"type":"string"}},"required":["id","sprite_name","status"],"title":"Sandbox","type":"object"},"Error":{"properties":{"error":{"type":"string"}},"required":["error"],"title":"Error","type":"object"},"ReadinessResponse":{"properties":{"checks":{"additionalProperties":{"enum":["ok","error"],"type":"string"},"description":"Per-dependency result. `ok` or `error`, with no further detail.","example":{"database":"ok"},"type":"object"},"status":{"enum":["ok","error"],"example":"ok","type":"string"}},"required":["status","checks"],"title":"ReadinessResponse","type":"object"},"VaultSecretResponse":{"properties":{"data":{"$ref":"#/components/schemas/VaultSecret"}},"required":["data"],"title":"VaultSecretResponse","type":"object"},"EnvironmentResponse":{"properties":{"data":{"$ref":"#/components/schemas/Environment"}},"required":["data"],"title":"EnvironmentResponse","type":"object"},"ChangesetError":{"description":"Validation errors keyed by field, with each value an array of messages.","properties":{"errors":{"additionalProperties":{"items":{"type":"string"},"type":"array"},"type":"object"}},"required":["errors"],"title":"ChangesetError","type":"object"},"Agent":{"description":"An AI agent definition: runtime, model, skills, MCP, env.","properties":{"allowed_vault_ids":{"description":"Vaults a conversation may attach to this agent. null (default) allows any vault the tenant owns; an empty list forbids attaching any vault; a non-empty list is an allowlist. Vault values override the agent's environment on key collision, so this scopes who can override reviewed config.","items":{"format":"uuid","type":"string"},"nullable":true,"type":"array"},"description":{"type":"string"},"environment_id":{"format":"uuid","nullable":true,"type":"string"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"mcp_servers":{"additionalProperties":true,"type":"object"},"metadata":{"additionalProperties":true,"type":"object"},"model":{"description":"Canonical provider/model_id (e.g. anthropic/claude-sonnet-4-6).","pattern":"^[a-z0-9_-]+/[a-z0-9._-]+$","type":"string"},"name":{"type":"string"},"runtime":{"enum":["claude","codex","gemini","opencode"],"type":"string"},"skills":{"description":"Each entry is either inline (`{name, content}` — full SKILL.md text written to the sprite) or github (`{source, ref?, name?}` — installed on the sprite via the skills.sh CLI, optionally pinned to a tag/branch/sha via `ref`). Exactly one of `content` or `source` must be set on each entry.","items":{"properties":{"content":{"description":"Full SKILL.md body for inline entries.","type":"string"},"name":{"description":"Skill name (required for inline entries).","type":"string"},"ref":{"description":"Optional tag, branch, or sha pinning a github-sourced skill (installed as `owner/repo@ref`). Without it the default branch is fetched at spawn time.","pattern":"^[A-Za-z0-9._/-]+$","type":"string"},"source":{"description":"GitHub `owner/repo` for skills.sh-sourced entries.","pattern":"^[A-Za-z0-9._/-]+$","type":"string"}},"type":"object"},"type":"array"},"system":{"description":"System prompt.","type":"string"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","name","model","runtime"],"title":"Agent","type":"object"},"ApplyResult":{"properties":{"action":{"enum":["created","updated","error"],"type":"string"},"errors":{"additionalProperties":true,"nullable":true,"type":"object"},"kind":{"type":"string"},"name":{"type":"string"},"secrets":{"items":{"$ref":"#/components/schemas/ApplySecretResult"},"type":"array"}},"required":["kind","name","action"],"title":"ApplyResult","type":"object"},"Conversation":{"description":"One chat with one agent inside one sandbox.","properties":{"agent_id":{"format":"uuid","nullable":true,"type":"string"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"runtime":{"enum":["claude","codex","gemini","opencode"],"type":"string"},"runtime_session_id":{"nullable":true,"type":"string"},"sandbox":{"nullable":true,"oneOf":[{"$ref":"#/components/schemas/Sandbox"}]},"sandbox_id":{"format":"uuid","nullable":true,"type":"string"},"status":{"enum":["pending","running","idle","completed","failed","terminated"],"type":"string"},"updated_at":{"format":"date-time","type":"string"},"vault_id":{"format":"uuid","nullable":true,"type":"string"}},"required":["id","runtime","status"],"title":"Conversation","type":"object"},"EnvironmentListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Environment"},"type":"array"}},"required":["data"],"title":"EnvironmentListResponse","type":"object"},"VaultSecretRequest":{"properties":{"key":{"type":"string"},"value":{"description":"Secret value (write-only).","type":"string"}},"required":["key","value"],"title":"VaultSecretRequest","type":"object"},"PromptRequest":{"properties":{"images":{"description":"Optional images to attach to this prompt.","items":{"$ref":"#/components/schemas/ImageInput"},"nullable":true,"type":"array"},"prompt":{"type":"string"}},"required":["prompt"],"title":"PromptRequest","type":"object"},"Turn":{"description":"One prompt → exit_code cycle within a conversation.","properties":{"ended_at":{"format":"date-time","nullable":true,"type":"string"},"exit_code":{"nullable":true,"type":"integer"},"id":{"format":"uuid","type":"string"},"image_count":{"description":"Number of images attached to this turn.","type":"integer"},"inserted_at":{"format":"date-time","type":"string"},"prompt":{"type":"string"},"started_at":{"format":"date-time","nullable":true,"type":"string"},"status":{"enum":["pending","running","completed","failed","interrupted"],"type":"string"},"turn_number":{"type":"integer"}},"required":["id","turn_number","prompt","status"],"title":"Turn","type":"object"},"VaultSecretListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/VaultSecret"},"type":"array"}},"required":["data"],"title":"VaultSecretListResponse","type":"object"},"HealthResponse":{"properties":{"status":{"example":"ok","type":"string"}},"required":["status"],"title":"HealthResponse","type":"object"},"VaultResponse":{"properties":{"data":{"$ref":"#/components/schemas/Vault"}},"required":["data"],"title":"VaultResponse","type":"object"},"ManifestResource":{"description":"One compiled document from a fountain.yml manifest. `spec` matches the create/update schema for the kind, plus an inline `secrets` map (Environment and Vault). Agent specs may reference an environment by name via `environment`; the server resolves it to `environment_id`.","properties":{"kind":{"enum":["Environment","Vault","Agent"],"type":"string"},"name":{"maxLength":200,"minLength":1,"type":"string"},"spec":{"additionalProperties":true,"type":"object"}},"required":["kind","name"],"title":"ManifestResource","type":"object"},"ApplySecretResult":{"description":"Outcome for one secret key. Values are never echoed back.","properties":{"action":{"enum":["upserted","error"],"type":"string"},"errors":{"additionalProperties":true,"nullable":true,"type":"object"},"key":{"type":"string"}},"required":["key","action"],"title":"ApplySecretResult","type":"object"},"PromptResponse":{"properties":{"status":{"example":"queued","type":"string"}},"required":["status"],"title":"PromptResponse","type":"object"},"AgentListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Agent"},"type":"array"}},"required":["data"],"title":"AgentListResponse","type":"object"},"AgentUpdate":{"properties":{"allowed_vault_ids":{"description":"Vaults a conversation may attach to this agent. null (default) allows any vault the tenant owns; an empty list forbids attaching any vault; a non-empty list is an allowlist.","items":{"format":"uuid","type":"string"},"nullable":true,"type":"array"},"description":{"type":"string"},"environment_id":{"format":"uuid","nullable":true,"type":"string"},"mcp_servers":{"additionalProperties":true,"type":"object"},"metadata":{"additionalProperties":true,"type":"object"},"model":{"pattern":"^[a-z0-9_-]+/[a-z0-9._-]+$","type":"string"},"name":{"maxLength":200,"minLength":1,"type":"string"},"runtime":{"enum":["claude","codex","gemini","opencode"],"type":"string"},"skills":{"description":"Each entry is either inline (`{name, content}` — full SKILL.md text written to the sprite) or github (`{source, ref?, name?}` — installed on the sprite via the skills.sh CLI, optionally pinned to a tag/branch/sha via `ref`). Exactly one of `content` or `source` must be set on each entry.","items":{"properties":{"content":{"description":"Full SKILL.md body for inline entries.","type":"string"},"name":{"description":"Skill name (required for inline entries).","type":"string"},"ref":{"description":"Optional tag, branch, or sha pinning a github-sourced skill (installed as `owner/repo@ref`). Without it the default branch is fetched at spawn time.","pattern":"^[A-Za-z0-9._/-]+$","type":"string"},"source":{"description":"GitHub `owner/repo` for skills.sh-sourced entries.","pattern":"^[A-Za-z0-9._/-]+$","type":"string"}},"type":"object"},"type":"array"},"system":{"type":"string"}},"title":"AgentUpdate","type":"object"},"ConversationListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Conversation"},"type":"array"}},"required":["data"],"title":"ConversationListResponse","type":"object"},"VaultRequest":{"properties":{"description":{"type":"string"},"metadata":{"additionalProperties":true,"type":"object"},"name":{"maxLength":200,"minLength":1,"type":"string"}},"required":["name"],"title":"VaultRequest","type":"object"},"ConversationResponse":{"properties":{"data":{"$ref":"#/components/schemas/Conversation"}},"required":["data"],"title":"ConversationResponse","type":"object"},"Vault":{"description":"A free-floating bag of env-var overrides selected at conversation creation. Vault values override an environment's baseline secrets when the same key is set on both.","properties":{"description":{"type":"string"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"metadata":{"additionalProperties":true,"type":"object"},"name":{"type":"string"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","name"],"title":"Vault","type":"object"},"ApplyResponse":{"properties":{"data":{"properties":{"results":{"items":{"$ref":"#/components/schemas/ApplyResult"},"type":"array"}},"required":["results"],"type":"object"}},"required":["data"],"title":"ApplyResponse","type":"object"},"SecretResponse":{"properties":{"data":{"$ref":"#/components/schemas/Secret"}},"required":["data"],"title":"SecretResponse","type":"object"},"SecretRequest":{"properties":{"key":{"type":"string"},"value":{"description":"Secret value (write-only).","type":"string"}},"required":["key","value"],"title":"SecretRequest","type":"object"},"AgentRequest":{"properties":{"allowed_vault_ids":{"description":"Vaults a conversation may attach to this agent. null (default) allows any vault the tenant owns; an empty list forbids attaching any vault; a non-empty list is an allowlist.","items":{"format":"uuid","type":"string"},"nullable":true,"type":"array"},"description":{"type":"string"},"environment_id":{"format":"uuid","nullable":true,"type":"string"},"mcp_servers":{"additionalProperties":true,"type":"object"},"metadata":{"additionalProperties":true,"type":"object"},"model":{"pattern":"^[a-z0-9_-]+/[a-z0-9._-]+$","type":"string"},"name":{"maxLength":200,"minLength":1,"type":"string"},"runtime":{"enum":["claude","codex","gemini","opencode"],"type":"string"},"skills":{"description":"Each entry is either inline (`{name, content}` — full SKILL.md text written to the sprite) or github (`{source, ref?, name?}` — installed on the sprite via the skills.sh CLI, optionally pinned to a tag/branch/sha via `ref`). Exactly one of `content` or `source` must be set on each entry.","items":{"properties":{"content":{"description":"Full SKILL.md body for inline entries.","type":"string"},"name":{"description":"Skill name (required for inline entries).","type":"string"},"ref":{"description":"Optional tag, branch, or sha pinning a github-sourced skill (installed as `owner/repo@ref`). Without it the default branch is fetched at spawn time.","pattern":"^[A-Za-z0-9._/-]+$","type":"string"},"source":{"description":"GitHub `owner/repo` for skills.sh-sourced entries.","pattern":"^[A-Za-z0-9._/-]+$","type":"string"}},"type":"object"},"type":"array"},"system":{"type":"string"}},"required":["name","model","runtime"],"title":"AgentRequest","type":"object"},"EnvironmentRequest":{"properties":{"env_vars":{"additionalProperties":{"type":"string"},"type":"object"},"metadata":{"additionalProperties":true,"type":"object"},"name":{"maxLength":200,"minLength":1,"type":"string"},"networking_config":{"additionalProperties":true,"description":"Refines networking_type: limited. allowed_hosts is the only key honored today; unknown keys are ignored. Under limited, egress is restricted to the allowlisted domains. With no allowed_hosts (or an empty list), the sandbox denies all egress by default — this is a deny-all, not an allow-all.","properties":{"allowed_hosts":{"description":"Domains the sandbox may reach when networking_type is limited.","items":{"type":"string"},"type":"array"}},"type":"object"},"networking_type":{"enum":["unrestricted","limited"],"type":"string"},"packages":{"additionalProperties":true,"type":"object"},"repositories":{"items":{"$ref":"#/components/schemas/Repository"},"type":"array"},"setup_script":{"type":"string"}},"required":["name"],"title":"EnvironmentRequest","type":"object"},"Environment":{"description":"A reusable sandbox environment: packages, env vars, repos, networking.","properties":{"env_vars":{"additionalProperties":{"type":"string"},"type":"object"},"id":{"format":"uuid","type":"string"},"inserted_at":{"format":"date-time","type":"string"},"metadata":{"additionalProperties":true,"type":"object"},"name":{"type":"string"},"networking_config":{"additionalProperties":true,"description":"Refines networking_type: limited. allowed_hosts is the only key honored today; unknown keys are ignored. Under limited, egress is restricted to the allowlisted domains. With no allowed_hosts (or an empty list), the sandbox denies all egress by default — this is a deny-all, not an allow-all.","properties":{"allowed_hosts":{"description":"Domains the sandbox may reach when networking_type is limited.","items":{"type":"string"},"type":"array"}},"type":"object"},"networking_type":{"enum":["unrestricted","limited"],"type":"string"},"packages":{"additionalProperties":true,"type":"object"},"repositories":{"items":{"$ref":"#/components/schemas/Repository"},"type":"array"},"setup_script":{"type":"string"},"updated_at":{"format":"date-time","type":"string"}},"required":["id","name"],"title":"Environment","type":"object"},"VaultListResponse":{"properties":{"data":{"items":{"$ref":"#/components/schemas/Vault"},"type":"array"}},"required":["data"],"title":"VaultListResponse","type":"object"},"AgentResponse":{"properties":{"data":{"$ref":"#/components/schemas/Agent"}},"required":["data"],"title":"AgentResponse","type":"object"},"ApplyRequest":{"properties":{"resources":{"items":{"$ref":"#/components/schemas/ManifestResource"},"type":"array"}},"required":["resources"],"title":"ApplyRequest","type":"object"}},"securitySchemes":{"bearer":{"description":"ADMIN_TOKEN configured at boot.","scheme":"bearer","type":"http"}}},"info":{"description":"HTTP API for Agent on Demand. The same surface backs the LiveView UI\nand the `aod` CLI; if it's not here, it doesn't exist yet.\n\nAll `/api/*` endpoints require a bearer token (`ADMIN_TOKEN`).\n","title":"Agent on Demand","version":"0.3.0"},"openapi":"3.0.0","paths":{"/api/agents":{"get":{"callbacks":{},"operationId":"FountainWeb.AgentController.index","parameters":[{"description":"Case-insensitive substring match on the agent name.","in":"query","name":"search","required":false,"schema":{"type":"string"}},{"description":"Comma-separated runtimes, e.g. `claude,codex`.","in":"query","name":"runtime","required":false,"schema":{"type":"string"}},{"description":"Comma-separated environment ids.","in":"query","name":"environment_id","required":false,"schema":{"type":"string"}},{"description":"Only agents with at least one skill.","in":"query","name":"has_skills","required":false,"schema":{"type":"boolean"}},{"description":"Only agents with at least one MCP server.","in":"query","name":"has_mcp","required":false,"schema":{"type":"boolean"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentListResponse"}}},"description":"Agents"}},"summary":"List agents","tags":["Agents"]},"post":{"callbacks":{},"operationId":"FountainWeb.AgentController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentRequest"}}},"description":"Agent attributes","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}},"description":"Agent"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Create an agent","tags":["Agents"]}},"/api/agents/{id}":{"delete":{"callbacks":{},"operationId":"FountainWeb.AgentController.delete","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Delete an agent","tags":["Agents"]},"get":{"callbacks":{},"operationId":"FountainWeb.AgentController.show","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}},"description":"Agent"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Get an agent","tags":["Agents"]},"patch":{"callbacks":{},"description":"Every field is optional; the server merges into the existing record.","operationId":"FountainWeb.AgentController.update (2)","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentUpdate"}}},"description":"Partial agent attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}},"description":"Agent"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Update an agent (partial)","tags":["Agents"]},"put":{"callbacks":{},"description":"Every field is optional; the server merges into the existing record.","operationId":"FountainWeb.AgentController.update","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentUpdate"}}},"description":"Partial agent attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentResponse"}}},"description":"Agent"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Update an agent (partial)","tags":["Agents"]}},"/api/apply":{"post":{"callbacks":{},"description":"Applies all resources from a compiled fountain.yml manifest in one request. Resources are reconciled by name — environments first, then vaults, then agents — so agent specs may reference an environment by name via `spec.environment`. Application is best-effort per resource: the response is 200 even when individual resources fail, with per-resource errors in the result entries.","operationId":"FountainWeb.ApplyController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplyRequest"}}},"description":"Compiled manifest","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ApplyResponse"}}},"description":"Per-resource results"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Apply a compiled manifest (bulk upsert)","tags":["Apply"]}},"/api/conversations":{"get":{"callbacks":{},"operationId":"FountainWeb.ConversationController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationListResponse"}}},"description":"Conversations"}},"summary":"List conversations","tags":["Conversations"]},"post":{"callbacks":{},"description":"Creates a sandbox + conversation pair, starts the runtime in a fresh sprite, and (if `prompt` is supplied) sends it as turn 1. Pass `X-Fountain-Parent-Conversation-Id` header to record which conversation spawned this one. Legacy `X-AoD-Parent-Conversation-Id` is still accepted for sprites provisioned before the rename.","operationId":"FountainWeb.ConversationController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationCreateRequest"}}},"description":"Conversation attrs","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationResponse"}}},"description":"Conversation"},"402":{"content":{"application/json":{"schema":{"properties":{"error":{"type":"string"},"upgrade_url":{"type":"string"}},"type":"object"}}},"description":"Subscription required"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Agent not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Start a conversation","tags":["Conversations"]}},"/api/conversations/{conversation_id}/interrupt":{"post":{"callbacks":{},"operationId":"FountainWeb.ConversationController.interrupt","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Interrupted"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"409":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"No turn running"}},"summary":"Interrupt the running turn","tags":["Conversations"]}},"/api/conversations/{conversation_id}/prompts":{"post":{"callbacks":{},"description":"Queues a new turn. If the ConversationServer has been GC'd (e.g. across a BEAM restart) a fresh sprite is provisioned and the runtime resumes via its session id.","operationId":"FountainWeb.ConversationController.prompt","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptRequest"}}},"description":"Prompt","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PromptResponse"}}},"description":"Queued"},"400":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Busy"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Send another prompt","tags":["Conversations"]}},"/api/conversations/{conversation_id}/stream":{"get":{"callbacks":{},"description":"Server-Sent Events stream of the conversation's log events. The `Last-Event-ID` request header resumes from a known event id; missed events are replayed before the live tail begins. Keep-alive heartbeats every 15s as `: heartbeat` comments.","operationId":"FountainWeb.ConversationController.stream","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string"}},{"description":"Resume after this event id (integer as string). Missing or unparseable values are treated as 0.","in":"header","name":"Last-Event-ID","required":false,"schema":{"type":"string"}},{"description":"Comma-separated subset of `stdout`, `stderr`, `stage`. Omitted or empty means all three.","in":"query","name":"streams","required":false,"schema":{"type":"string"}},{"description":"`false`/`0` drains the buffered events and closes immediately, rather than holding the connection open for the live tail. Defaults to true.","in":"query","name":"wait","required":false,"schema":{"type":"string"}}],"responses":{"200":{"content":{"text/event-stream":{"schema":{"type":"string"}}},"description":"SSE stream"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Stream log events (SSE)","tags":["Conversations"]}},"/api/conversations/{conversation_id}/terminate":{"post":{"callbacks":{},"description":"Tears down the sprite and marks the conversation `terminated`. Idempotent for already-dead conversations.","operationId":"FountainWeb.ConversationController.terminate","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Terminated"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Terminate a conversation","tags":["Conversations"]}},"/api/conversations/{conversation_id}/turns":{"get":{"callbacks":{},"operationId":"FountainWeb.ConversationController.turns","parameters":[{"description":"","in":"path","name":"conversation_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TurnListResponse"}}},"description":"Turns"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"List turns in a conversation","tags":["Conversations"]}},"/api/conversations/{id}":{"delete":{"callbacks":{},"description":"Tears down the sprite if alive, then deletes the conversation row (cascades to turns and log events).","operationId":"FountainWeb.ConversationController.delete","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Delete a conversation","tags":["Conversations"]},"get":{"callbacks":{},"operationId":"FountainWeb.ConversationController.show","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConversationResponse"}}},"description":"Conversation"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Get a conversation","tags":["Conversations"]}},"/api/environments":{"get":{"callbacks":{},"operationId":"FountainWeb.EnvironmentController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentListResponse"}}},"description":"Environments"}},"summary":"List environments","tags":["Environments"]},"post":{"callbacks":{},"operationId":"FountainWeb.EnvironmentController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentRequest"}}},"description":"Environment attributes","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentResponse"}}},"description":"Environment"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Create an environment","tags":["Environments"]}},"/api/environments/{environment_id}/secrets":{"get":{"callbacks":{},"operationId":"FountainWeb.SecretController.index","parameters":[{"description":"","in":"path","name":"environment_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecretListResponse"}}},"description":"Secrets"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"List secrets in an environment","tags":["Secrets"]},"post":{"callbacks":{},"description":"Sets the value for `key`. If the key exists, the value is overwritten. Values are write-only — subsequent reads never return them.","operationId":"FountainWeb.SecretController.create","parameters":[{"description":"","in":"path","name":"environment_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecretRequest"}}},"description":"Secret","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SecretResponse"}}},"description":"Secret"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Upsert a secret","tags":["Secrets"]}},"/api/environments/{environment_id}/secrets/{id}":{"delete":{"callbacks":{},"operationId":"FountainWeb.SecretController.delete","parameters":[{"description":"","in":"path","name":"environment_id","required":true,"schema":{"type":"string"}},{"description":"Secret key.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Delete a secret by key","tags":["Secrets"]}},"/api/environments/{id}":{"delete":{"callbacks":{},"operationId":"FountainWeb.EnvironmentController.delete","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Delete an environment","tags":["Environments"]},"get":{"callbacks":{},"operationId":"FountainWeb.EnvironmentController.show","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentResponse"}}},"description":"Environment"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Get an environment","tags":["Environments"]},"patch":{"callbacks":{},"description":"Every field is optional; the server merges into the existing record.","operationId":"FountainWeb.EnvironmentController.update (2)","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentUpdate"}}},"description":"Partial environment attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentResponse"}}},"description":"Environment"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Update an environment (partial)","tags":["Environments"]},"put":{"callbacks":{},"description":"Every field is optional; the server merges into the existing record.","operationId":"FountainWeb.EnvironmentController.update","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentUpdate"}}},"description":"Partial environment attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/EnvironmentResponse"}}},"description":"Environment"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Update an environment (partial)","tags":["Environments"]}},"/api/vaults":{"get":{"callbacks":{},"operationId":"FountainWeb.VaultController.index","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultListResponse"}}},"description":"Vaults"}},"summary":"List vaults","tags":["Vaults"]},"post":{"callbacks":{},"operationId":"FountainWeb.VaultController.create","parameters":[],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultRequest"}}},"description":"Vault attributes","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultResponse"}}},"description":"Vault"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Create a vault","tags":["Vaults"]}},"/api/vaults/{id}":{"delete":{"callbacks":{},"operationId":"FountainWeb.VaultController.delete","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Delete a vault","tags":["Vaults"]},"get":{"callbacks":{},"operationId":"FountainWeb.VaultController.show","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultResponse"}}},"description":"Vault"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Get a vault","tags":["Vaults"]},"patch":{"callbacks":{},"description":"Every field is optional; the server merges into the existing record.","operationId":"FountainWeb.VaultController.update (2)","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultUpdate"}}},"description":"Partial vault attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultResponse"}}},"description":"Vault"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Update a vault (partial)","tags":["Vaults"]},"put":{"callbacks":{},"description":"Every field is optional; the server merges into the existing record.","operationId":"FountainWeb.VaultController.update","parameters":[{"description":"","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultUpdate"}}},"description":"Partial vault attributes","required":false},"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultResponse"}}},"description":"Vault"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Update a vault (partial)","tags":["Vaults"]}},"/api/vaults/{vault_id}/secrets":{"get":{"callbacks":{},"operationId":"FountainWeb.VaultSecretController.index","parameters":[{"description":"","in":"path","name":"vault_id","required":true,"schema":{"type":"string"}}],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultSecretListResponse"}}},"description":"Vault Secrets"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"List secrets in a vault","tags":["Vault Secrets"]},"post":{"callbacks":{},"description":"Sets the value for `key`. If the key exists, the value is overwritten. Values are write-only — subsequent reads never return them.","operationId":"FountainWeb.VaultSecretController.create","parameters":[{"description":"","in":"path","name":"vault_id","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultSecretRequest"}}},"description":"Vault Secret","required":false},"responses":{"201":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VaultSecretResponse"}}},"description":"Vault Secret"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"},"422":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChangesetError"}}},"description":"Validation error"}},"summary":"Upsert a vault secret","tags":["Vault Secrets"]}},"/api/vaults/{vault_id}/secrets/{id}":{"delete":{"callbacks":{},"operationId":"FountainWeb.VaultSecretController.delete","parameters":[{"description":"","in":"path","name":"vault_id","required":true,"schema":{"type":"string"}},{"description":"Secret key.","in":"path","name":"id","required":true,"schema":{"type":"string"}}],"responses":{"204":{"description":"Deleted"},"404":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}},"description":"Not found"}},"summary":"Delete a vault secret by key","tags":["Vault Secrets"]}},"/health":{"get":{"callbacks":{},"description":"Public, unauthenticated. Returns `{\"status\": \"ok\"}` if the app is up. Checks no dependencies by design — ask `/health/ready` whether this instance can actually serve.","operationId":"FountainWeb.HealthController.show","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthResponse"}}},"description":"Health response"}},"security":[],"summary":"Liveness probe","tags":["Health"]}},"/health/ready":{"get":{"callbacks":{},"description":"Public, unauthenticated. Returns 200 when this instance can serve requests, or 503 when a dependency it cannot work without is unavailable. Individual checks report `ok` or `error` and nothing further.","operationId":"FountainWeb.HealthController.ready","parameters":[],"responses":{"200":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReadinessResponse"}}},"description":"Ready"},"503":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReadinessResponse"}}},"description":"Not ready"}},"security":[],"summary":"Readiness probe","tags":["Health"]}}},"security":[{"bearer":[]}],"servers":[{"url":"http://localhost:4000","variables":{}}],"tags":[]}
|