@domesystems/templates 0.3.8 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/cli.mjs +40 -5
- package/src/generate.mjs +115 -185
- package/src/manifest.mjs +106 -36
- package/src/runtime.mjs +20 -8
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { spawn, spawnSync } from "node:child_process";
|
|
4
|
+
import { parse as parseYAML } from "yaml";
|
|
4
5
|
import { readManifest, readPublishManifest } from "./manifest.mjs";
|
|
5
6
|
import { generateDomeTerraform } from "./generate.mjs";
|
|
6
7
|
import { imageForRuntime, renderCompose } from "./runtime.mjs";
|
|
@@ -10,15 +11,15 @@ import { DEFAULT_REGISTRY, clearSelectedTarget, deleteProfile, directRegistry, d
|
|
|
10
11
|
|
|
11
12
|
function ansi(code, value, options) { return options.color && process.stdout.isTTY ? `\u001B[${code}m${value}\u001B[0m` : value; }
|
|
12
13
|
function line(kind, value, options) { const styles = { info: ["•", "36"], success: ["✓", "32"], warning: ["!", "33"], error: ["✗", "31"] }; const [mark, code] = styles[kind]; return `${ansi(code, mark, options)} ${value}`; }
|
|
13
|
-
function usage() { return "Usage:\n templates login [--server <Dome application URL>|--registry <Registry URL>] [--github]\n templates logout [--server <Dome application URL>|--registry <Registry URL>]\n templates doctor <pull|up|publish> [package] [--dir <directory>] [--server <URL>|--registry <URL>] [--json]\n templates pull <scope/package[@version]> [--dir <directory>] [--server <URL>|--registry <URL>]\n templates generate [--dir <directory>] [--output <file>] [--force]\n templates up [--dir <directory>] [--port <host-port>] [--server <URL>|--registry <URL>] [--import-job <job-id>] [--skip-provision|--provision] [--no-start] [--detach] [--debug] [--verbose]\n templates publish [--dir <directory>] [--dry-run] [--server <Dome application URL>|--registry <Registry URL>]\n"; }
|
|
14
|
+
function usage() { return "Usage:\n templates login [--server <Dome application URL>|--registry <Registry URL>] [--github]\n templates logout [--server <Dome application URL>|--registry <Registry URL>]\n templates doctor <pull|up|publish> [package] [--dir <directory>] [--server <URL>|--registry <URL>] [--json]\n templates pull <scope/package[@version]> [--dir <directory>] [--server <URL>|--registry <URL>]\n templates generate [--dir <directory>] [--output <file>] [--force]\n templates up [--dir <directory>] [--agent <agent-name>] [--port <host-port>] [--server <URL>|--registry <URL>] [--import-job <job-id>] [--skip-provision|--provision] [--no-start] [--detach] [--debug] [--verbose]\n templates publish [--dir <directory>] [--dry-run] [--server <Dome application URL>|--registry <Registry URL>]\n"; }
|
|
14
15
|
function parse(argv) {
|
|
15
16
|
if (!argv.length || argv.includes("--help") || argv.includes("-h")) return { help: true };
|
|
16
17
|
const [command, ...rest] = argv;
|
|
17
|
-
const options = { server: undefined, directory: undefined, output: "dome.tf", force: false, port: 3000, dryRun: false, json: false, github: false, importJob: undefined, provision: true, forceProvision: false, start: true, detach: false, debug: false, verbose: false, color: !process.env.NO_COLOR };
|
|
18
|
+
const options = { server: undefined, directory: undefined, output: "dome.tf", force: false, port: 3000, dryRun: false, json: false, github: false, importJob: undefined, agent: undefined, provision: true, forceProvision: false, start: true, detach: false, debug: false, verbose: false, color: !process.env.NO_COLOR };
|
|
18
19
|
const values = [];
|
|
19
20
|
for (let i = 0; i < rest.length; i += 1) {
|
|
20
21
|
const arg = rest[i];
|
|
21
|
-
if (arg === "--server" || arg === "--registry" || arg === "--dir" || arg === "--output" || arg === "--port" || arg === "--color" || arg === "--import-job") { const value = rest[++i]; if (!value) throw new Error(`${arg} requires a value.`); if (arg === "--server") options.server = normalizeServer(value); else if (arg === "--registry") options.registry = normalizeServer(value); else if (arg === "--dir") options.directory = value; else if (arg === "--output") options.output = value; else if (arg === "--import-job") options.importJob = value; else if (arg === "--color") { if (!["auto", "always", "never"].includes(value)) throw new Error("--color must be auto, always, or never."); options.color = value === "always" || (value === "auto" && !process.env.NO_COLOR); } else { options.port = Number(value); if (!Number.isInteger(options.port) || options.port < 1 || options.port > 65535) throw new Error("--port requires an integer from 1 to 65535."); } }
|
|
22
|
+
if (arg === "--server" || arg === "--registry" || arg === "--dir" || arg === "--output" || arg === "--port" || arg === "--color" || arg === "--import-job" || arg === "--agent") { const value = rest[++i]; if (!value) throw new Error(`${arg} requires a value.`); if (arg === "--server") options.server = normalizeServer(value); else if (arg === "--registry") options.registry = normalizeServer(value); else if (arg === "--dir") options.directory = value; else if (arg === "--output") options.output = value; else if (arg === "--import-job") options.importJob = value; else if (arg === "--agent") options.agent = value; else if (arg === "--color") { if (!["auto", "always", "never"].includes(value)) throw new Error("--color must be auto, always, or never."); options.color = value === "always" || (value === "auto" && !process.env.NO_COLOR); } else { options.port = Number(value); if (!Number.isInteger(options.port) || options.port < 1 || options.port > 65535) throw new Error("--port requires an integer from 1 to 65535."); } }
|
|
22
23
|
else if (arg === "--github") options.github = true; else if (arg === "--dry-run") options.dryRun = true; else if (arg === "--json") options.json = true; else if (arg === "--force") options.force = true; else if (arg === "--skip-provision") options.provision = false; else if (arg === "--provision") options.forceProvision = true; else if (arg === "--no-start") options.start = false; else if (arg === "--detach") options.detach = true; else if (arg === "--debug") options.debug = true; else if (arg === "--verbose") options.verbose = true; else if (arg.startsWith("-")) throw new Error(`Unknown option: ${arg}`); else values.push(arg);
|
|
23
24
|
}
|
|
24
25
|
if (options.server && options.registry) throw new Error("Use either --server or --registry, not both.");
|
|
@@ -59,6 +60,21 @@ function generate(options, output) {
|
|
|
59
60
|
const file = generateDomeTerraform(target, { output: options.output, force: options.force, manifest });
|
|
60
61
|
output(line("success", `Generated ${file}`, options));
|
|
61
62
|
}
|
|
63
|
+
function runtimeFor(directory, manifest, selectedAgent) {
|
|
64
|
+
const candidates = selectedAgent ? manifest.agents.filter((agent) => agent.name === selectedAgent) : manifest.agents.filter((agent) => agent.type === "external");
|
|
65
|
+
if (candidates.length !== 1) throw new Error(`templates up requires exactly one external agent; found ${candidates.length}. Pass --agent <name> to select one.`);
|
|
66
|
+
const agent = candidates[0];
|
|
67
|
+
if (agent.type !== "external") throw new Error(`templates up cannot start managed agent ${agent.name}; Dome runs its loop.`);
|
|
68
|
+
const configPath = path.join("agents", agent.name);
|
|
69
|
+
const configFile = path.join(directory, configPath, "agent.yaml");
|
|
70
|
+
if (!fs.existsSync(configFile)) throw new Error(`External agent ${agent.name} needs ${configPath}/agent.yaml.`);
|
|
71
|
+
const runner = parseYAML(fs.readFileSync(configFile, "utf8"))?.runner;
|
|
72
|
+
if (!runner?.pattern || !runner?.language) throw new Error(`${configPath}/agent.yaml needs runner.pattern and runner.language.`);
|
|
73
|
+
const keys = agent.keys ?? [];
|
|
74
|
+
const key = keys.find((candidate) => candidate.name === "local") ?? (keys.length === 1 ? keys[0] : null);
|
|
75
|
+
if (!key) throw new Error(`External agent ${agent.name} needs exactly one key or a key named local for templates up.`);
|
|
76
|
+
return { ...agent, runtime: runner, configPath, localKey: key.name };
|
|
77
|
+
}
|
|
62
78
|
function run(command, args, cwd) { const result = spawnSync(command, args, { cwd, stdio: "inherit" }); if (result.error || result.status !== 0) throw new Error(`${command} ${args.join(" ")} failed.`); }
|
|
63
79
|
export function composeCommands({ detach = false } = {}) {
|
|
64
80
|
const prefix = ["compose", "-f", ".dome-compose.yaml"];
|
|
@@ -94,8 +110,27 @@ function configured(target) { const env = path.join(target, ".env"); return fs.e
|
|
|
94
110
|
export function importMadeNoChanges(job) { const summary = job?.plan_summary ?? job?.planSummary; if (!summary) return false; return ["create_count", "update_count", "import_count"].every((key) => Number(summary[key] ?? summary[key.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())] ?? 0) === 0); }
|
|
95
111
|
export function importHasPendingOutputs(job) { return (job?.outputs_state ?? job?.outputsState) === "pending"; }
|
|
96
112
|
export function importCreatedResources(job) { const summary = job?.plan_summary ?? job?.planSummary; return Number(summary?.create_count ?? summary?.createCount ?? 0) > 0; }
|
|
97
|
-
async function configureRuntime(target, jobID, options, output) { const gateways = await runJSON("dome", ["gateways", "list", "--json"], target); const gatewayURL = gateways?.data?.gateways?.find((candidate) => candidate.is_default)?.endpoints?.gateway_url; if (!gatewayURL) throw new Error("The import did not configure a default Gateway for the runtime."); const revealed = await runJSON("dome", ["import", "outputs", jobID, "--json"], target); const
|
|
98
|
-
async function up(options, output) {
|
|
113
|
+
async function configureRuntime(target, jobID, agentName, keyName, options, output) { const gateways = await runJSON("dome", ["gateways", "list", "--json"], target); const gatewayURL = gateways?.data?.gateways?.find((candidate) => candidate.is_default)?.endpoints?.gateway_url; if (!gatewayURL) throw new Error("The import did not configure a default Gateway for the runtime."); const revealed = await runJSON("dome", ["import", "outputs", jobID, "--json"], target); const tokenKey = `${agentName.replace(/[^A-Za-z0-9_]/g, "_").replace(/^[^A-Za-z_]/, "_")}_${keyName.replace(/[^A-Za-z0-9_]/g, "_").replace(/^[^A-Za-z_]/, "_")}_token`; const token = revealed?.data?.outputs?.[tokenKey]; if (typeof token !== "string") throw new Error(`The import did not produce the ${keyName} key for ${agentName}.`); writeRuntimeEnvironment(target, { DOME_TOKEN: token, DOME_GATEWAY_URL: gatewayURL }); output(line("success", `Configured the ${agentName} runtime with its imported credentials.`, options)); }
|
|
114
|
+
async function up(options, output) {
|
|
115
|
+
const target = path.resolve(process.cwd(), options.directory ?? ".");
|
|
116
|
+
const manifest = readManifest(target);
|
|
117
|
+
const runtimeAgent = runtimeFor(target, manifest, options.agent);
|
|
118
|
+
const needsCredentials = !configured(target);
|
|
119
|
+
output(line("info", `Checking ${target}`, options));
|
|
120
|
+
fs.writeFileSync(path.join(target, ".dome-compose.yaml"), renderCompose(runtimeAgent.runtime, options.port, { debug: options.debug, configPath: runtimeAgent.configPath }));
|
|
121
|
+
if (options.importJob) await configureRuntime(target, options.importJob, runtimeAgent.name, runtimeAgent.localKey, options, output);
|
|
122
|
+
else if (options.provision && (needsCredentials || options.forceProvision)) {
|
|
123
|
+
output(line("info", "Setting up Dome resources", options));
|
|
124
|
+
let imported;
|
|
125
|
+
try { imported = await runJSON("dome", ["import", "dome.tf", "--json"], target); }
|
|
126
|
+
catch (caught) { const dashboardURL = importDashboardURL(target); throw new Error(`${caught.message}${dashboardURL ? ` View the import details: ${dashboardURL}` : ""}`); }
|
|
127
|
+
const job = imported?.data?.job;
|
|
128
|
+
if (!job?.id) throw new Error("Dome import did not return a job ID.");
|
|
129
|
+
if (importCreatedResources(job) && importHasPendingOutputs(job)) await configureRuntime(target, job.id, runtimeAgent.name, runtimeAgent.localKey, options, output);
|
|
130
|
+
else if (needsCredentials) throw new Error(`Dome matched existing resources and did not create a new runtime token. Recover a pending import with templates up --dir ${options.directory ?? "."} --import-job <job-id>, or rotate the local agent key and set DOME_TOKEN and DOME_GATEWAY_URL in .env.`);
|
|
131
|
+
}
|
|
132
|
+
if (options.start) { const compose = composeCommands(options); run("docker", compose.pull, target); run("docker", compose.up, target); }
|
|
133
|
+
}
|
|
99
134
|
async function publish(options, output, fetchImplementation) { const target = path.resolve(process.cwd(), options.directory ?? "."); const manifest = readPublishManifest(target); const source = publishSource(target); const name = manifest.metadata.name; const [scope, slug] = name.split("/"); const archive = options.dryRun ? null : createArchive(source); output(line("info", `Checking ${name}@${manifest.metadata.version}`, options)); const payload = { name, version: manifest.metadata.version, title: manifest.metadata.title, summary: manifest.metadata.summary ?? "", sha256: archive?.sha256, size_bytes: archive?.buffer.length, source: { repository: manifest.metadata.source.repository, path: manifest.metadata.source.path, commit: source.commit, remote: source.remote, files: source.files } };
|
|
100
135
|
const remote = remoteTarget(options); let registry; let attempt;
|
|
101
136
|
try { ({ target: registry, body: attempt } = await registryRequest(remote, "/v1/publish-attempts", { method: "POST", body: { ...payload, dry_run: options.dryRun }, authenticated: true, fetchImplementation })); } catch (caught) {
|
package/src/generate.mjs
CHANGED
|
@@ -8,213 +8,143 @@ const label = (value) => {
|
|
|
8
8
|
};
|
|
9
9
|
const q = (value) => JSON.stringify(String(value));
|
|
10
10
|
const attr = (name, value) => `${name.padEnd(19)} = ${value}`;
|
|
11
|
-
const block = (type, name, body) => `resource ${q(type)} ${q(name)} {\n${body.map((line) => ` ${line}`).join("\n")}\n}`;
|
|
12
|
-
const
|
|
13
|
-
const
|
|
11
|
+
const block = (type, name, body) => `resource ${q(type)} ${q(name)} {\n${body.map((line) => line === "" ? "" : ` ${line}`).join("\n")}\n}`;
|
|
12
|
+
const required = (object, key, message) => { if (object?.[key] === undefined || object[key] === "") throw new Error(message); return object[key]; };
|
|
13
|
+
const expression = (value) => typeof value === "string" && /^(var|dome_[a-z_]+\.)[A-Za-z0-9_.]+$/.test(value) ? value : q(value);
|
|
14
14
|
|
|
15
|
-
function
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function secretVariable(secret) {
|
|
21
|
-
return secret?.variable ? label(secret.variable) : null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function connectionBlocks(connection, gatewayLabel, variables) {
|
|
25
|
-
const connectionLabel = label(required(connection, "name", "Every catalog connection needs a name."));
|
|
26
|
-
const auth = connection.auth ?? { method: "none" };
|
|
27
|
-
const lines = [
|
|
28
|
-
attr("workspace_id", "var.workspace_id"),
|
|
29
|
-
attr("name", q(connection.name)),
|
|
30
|
-
attr("protocol", q(required(connection, "protocol", `Connection ${connection.name} needs a protocol.`))),
|
|
31
|
-
attr("url", q(required(connection, "url", `Connection ${connection.name} needs a URL.`))),
|
|
32
|
-
attr("auth_method", q(auth.method)),
|
|
33
|
-
];
|
|
34
|
-
if (auth.credential) lines.push(attr("credential_type", q(auth.credential)));
|
|
35
|
-
if (auth.oauth) {
|
|
36
|
-
const oauth = Object.entries(auth.oauth).map(([key, value]) => {
|
|
37
|
-
const destination = key === "scopes" ? "default_scopes" : key;
|
|
38
|
-
return `${destination} = ${Array.isArray(value) ? `[${value.map(q).join(", ")}]` : q(value)}`;
|
|
39
|
-
});
|
|
40
|
-
lines.push(`oauth = { ${oauth.join(", ")} }`);
|
|
41
|
-
}
|
|
42
|
-
const secret = auth.secret;
|
|
43
|
-
if (secretVariable(secret)) {
|
|
44
|
-
const key = auth.header ?? "Authorization";
|
|
45
|
-
variables.add(secretVariable(secret));
|
|
46
|
-
lines.push(attr("secret_values", `{ ${q(key)} = var.${secretVariable(secret)} }`));
|
|
47
|
-
}
|
|
48
|
-
const output = [block("dome_mcp_connection", connectionLabel, lines)];
|
|
49
|
-
for (const tool of [...(connection.permit ?? []), ...(connection.forbid ?? [])]) {
|
|
50
|
-
output.push(block("dome_gateway_tool", nameFor(connectionLabel, tool), [
|
|
51
|
-
attr("workspace_id", "var.workspace_id"),
|
|
52
|
-
attr("gateway", `dome_gateway.${gatewayLabel}.name`),
|
|
53
|
-
attr("mcp_connection", `dome_mcp_connection.${connectionLabel}.name`),
|
|
54
|
-
attr("tool", q(tool)),
|
|
55
|
-
]));
|
|
56
|
-
}
|
|
57
|
-
return output;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
function modelBlocks(model, pool, gatewayLabel, variables) {
|
|
61
|
-
const modelLabel = label(required(model, "name", "catalog.model needs a name."));
|
|
62
|
-
const poolLabel = label(required(pool, "name", "catalog.pool needs a name."));
|
|
63
|
-
const secret = model.secret;
|
|
64
|
-
const modelLines = [
|
|
65
|
-
attr("workspace_id", "var.workspace_id"),
|
|
66
|
-
attr("name", q(model.name)),
|
|
67
|
-
attr("provider_id", q(required(model, "provider", "catalog.model needs a provider."))),
|
|
68
|
-
attr("provider_config", `jsonencode({ model = ${q(required(model, "model_id", "catalog.model needs a model_id."))} })`),
|
|
69
|
-
];
|
|
70
|
-
if (secretVariable(secret)) {
|
|
71
|
-
const variableName = secretVariable(secret);
|
|
72
|
-
// Dome shared API-key credentials are stored against the provider's
|
|
73
|
-
// managed header. Its default is x-api-key; Authorization is not a
|
|
74
|
-
// substitute and is rejected by the Management API.
|
|
75
|
-
const secretHeader = model.secret_header ?? "x-api-key";
|
|
76
|
-
variables.add(variableName);
|
|
77
|
-
modelLines.push(
|
|
78
|
-
attr("auth_method", q("api-key")),
|
|
79
|
-
attr("credential_type", q("shared")),
|
|
80
|
-
attr("managed_header_name", q(secretHeader)),
|
|
81
|
-
attr("secret_values", `{ ${q(secretHeader)} = var.${variableName} }`),
|
|
82
|
-
);
|
|
83
|
-
}
|
|
84
|
-
return [
|
|
85
|
-
block("dome_llm_connection", modelLabel, modelLines),
|
|
86
|
-
block("dome_llm_pool", poolLabel, [
|
|
87
|
-
attr("workspace_id", "var.workspace_id"),
|
|
88
|
-
attr("name", q(pool.name)),
|
|
89
|
-
// Generated templates must coexist with any pool already installed in
|
|
90
|
-
// the target workspace. A workspace permits only one default pool, so
|
|
91
|
-
// never make a reusable template's pool the default during provisioning.
|
|
92
|
-
attr("is_default", "false"),
|
|
93
|
-
]),
|
|
94
|
-
block("dome_llm_pool_member", modelLabel, [
|
|
95
|
-
attr("workspace_id", "var.workspace_id"),
|
|
96
|
-
attr("pool", `dome_llm_pool.${poolLabel}.name`),
|
|
97
|
-
attr("llm_connection", `dome_llm_connection.${modelLabel}.name`),
|
|
98
|
-
]),
|
|
99
|
-
block("dome_gateway_llm_pool", poolLabel, [
|
|
100
|
-
attr("workspace_id", "var.workspace_id"),
|
|
101
|
-
attr("gateway", `dome_gateway.${gatewayLabel}.name`),
|
|
102
|
-
attr("llm_pool", `dome_llm_pool.${poolLabel}.name`),
|
|
103
|
-
]),
|
|
104
|
-
];
|
|
15
|
+
function variableBlocks(variables) {
|
|
16
|
+
return Object.entries(variables).map(([name, definition]) => {
|
|
17
|
+
const value = definition && typeof definition === "object" ? definition : {};
|
|
18
|
+
return `variable ${q(name)} {\n type = string${value.sensitive ? "\n sensitive = true" : ""}\n}`;
|
|
19
|
+
});
|
|
105
20
|
}
|
|
106
21
|
|
|
107
|
-
function rulesBlock(
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
return [block("dome_agent_rules_bundle", agentLabel,
|
|
114
|
-
attr("workspace_id", "var.workspace_id"),
|
|
115
|
-
attr("agent", `dome_agent.${agentLabel}.name`),
|
|
116
|
-
"files = {",
|
|
117
|
-
` ${q(filename)} = <<-CEDAR`,
|
|
118
|
-
...contents.split("\n").map((line) => ` ${line}`),
|
|
119
|
-
" CEDAR",
|
|
120
|
-
"}",
|
|
121
|
-
])];
|
|
22
|
+
function rulesBlock(agent, resourceType, agentLabel) {
|
|
23
|
+
const files = agent.rules?.files;
|
|
24
|
+
if (!files || !Object.keys(files).length) return [];
|
|
25
|
+
const body = [attr("workspace_id", "var.workspace_id"), attr("agent", `${resourceType}.${agentLabel}.name`), "files = {"];
|
|
26
|
+
for (const [name, file] of Object.entries(files)) body.push(` ${q(name)} = <<-CEDAR`, ...file.content.split("\n").map((line) => line.trimEnd() ? ` ${line.trimEnd()}` : ""), " CEDAR");
|
|
27
|
+
body.push("}");
|
|
28
|
+
return [block("dome_agent_rules_bundle", agentLabel, body)];
|
|
122
29
|
}
|
|
123
30
|
|
|
124
|
-
function
|
|
125
|
-
const filters = guards?.filters ?? [];
|
|
31
|
+
function filterBlocks(filters) {
|
|
126
32
|
const blocks = [];
|
|
127
33
|
const bindings = new Map();
|
|
128
34
|
for (const filter of filters) {
|
|
129
|
-
const filterLabel = label(required(filter, "name", "Every
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const
|
|
134
|
-
if (!
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
if (!kind) throw new Error(`Guard ${filter.name} must bind to a model or tool.`);
|
|
141
|
-
const target = kind === "model" ? label(binding.model) : label(binding.tool);
|
|
142
|
-
const key = `${kind}:${target}:${binding.direction ?? "request"}`;
|
|
143
|
-
bindings.set(key, [...(bindings.get(key) ?? []), filterLabel]);
|
|
35
|
+
const filterLabel = label(required(filter, "name", "Every filter needs a name."));
|
|
36
|
+
if (!filter.config || typeof filter.config !== "object") throw new Error(`Filter ${filter.name} needs config.`);
|
|
37
|
+
blocks.push(block("dome_filter", filterLabel, [attr("workspace_id", "var.workspace_id"), attr("name", q(filter.name)), attr("description", q(filter.description ?? filter.name)), attr("config", `jsonencode(${JSON.stringify(filter.config)})`)]));
|
|
38
|
+
for (const binding of filter.bindings ?? []) {
|
|
39
|
+
const kind = binding.llm_connection ? "llm" : binding.tool_connection ? "tool" : null;
|
|
40
|
+
if (!kind) throw new Error(`Filter ${filter.name} bindings must name llm_connection or tool_connection.`);
|
|
41
|
+
const connection = binding.llm_connection ?? binding.tool_connection;
|
|
42
|
+
const direction = binding.direction ?? "request";
|
|
43
|
+
const key = `${kind}:${connection}:${direction}`;
|
|
44
|
+
bindings.set(key, [...(bindings.get(key) ?? []), filterLabel]);
|
|
45
|
+
}
|
|
144
46
|
}
|
|
145
|
-
for (const [key,
|
|
146
|
-
const [kind,
|
|
147
|
-
const resource = kind === "
|
|
148
|
-
const attribute = kind === "
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
attr(attribute, `${kind === "model" ? "dome_llm_connection" : "dome_mcp_connection"}.${target}.name`),
|
|
152
|
-
attr("direction", q(direction)),
|
|
153
|
-
attr("filters", `[${filterLabels.map((item) => `dome_filter.${item}.name`).join(", ")}]`),
|
|
154
|
-
]));
|
|
47
|
+
for (const [key, names] of bindings) {
|
|
48
|
+
const [kind, connection, direction] = key.split(":");
|
|
49
|
+
const resource = kind === "llm" ? "dome_llm_connection_filters" : "dome_mcp_connection_filters";
|
|
50
|
+
const attribute = kind === "llm" ? "llm_connection" : "mcp_connection";
|
|
51
|
+
const target = kind === "llm" ? "dome_llm_connection" : "dome_mcp_connection";
|
|
52
|
+
blocks.push(block(resource, `${label(connection)}_${direction}`, [attr("workspace_id", "var.workspace_id"), attr(attribute, `${target}.${label(connection)}.name`), attr("direction", q(direction)), attr("filters", `[${names.map((name) => `dome_filter.${name}.name`).join(", ")}]`)]));
|
|
155
53
|
}
|
|
156
54
|
return blocks;
|
|
157
55
|
}
|
|
158
56
|
|
|
159
|
-
function quotaBlocks(quotas,
|
|
160
|
-
return
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
]);
|
|
57
|
+
function quotaBlocks(quotas, agents) {
|
|
58
|
+
return quotas.map((quota) => {
|
|
59
|
+
const subjectType = required(quota, "subject_type", "Every quota needs subject_type.");
|
|
60
|
+
const subject = required(quota, "subject", "Every quota needs subject.");
|
|
61
|
+
const resource = subjectType === "agent" ? (agents.get(subject) === "managed" ? "dome_managed_agent" : "dome_agent") : subjectType === "tool_connection" ? "dome_mcp_connection" : subjectType === "model" ? "dome_llm_connection" : null;
|
|
62
|
+
if (!resource) throw new Error(`Quota ${quota.name ?? ""} has unsupported subject_type ${subjectType}.`);
|
|
63
|
+
return block("dome_quota", label(required(quota, "name", "Every quota needs a name.")), [attr("workspace_id", "var.workspace_id"), attr("dimension", q(required(quota, "dimension", "Every quota needs dimension."))), attr("unit", q(required(quota, "unit", "Every quota needs unit."))), attr("subject_type", q(subjectType)), attr("subject", `${resource}.${label(subject)}.name`), attr("scope", q(quota.scope ?? "total")), attr("window", q(required(quota, "window", "Every quota needs window."))), attr("limit_amount", String(required(quota, "limit_amount", "Every quota needs limit_amount."))), attr("name", q(quota.name))]);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function managedModelReference(manifest, name) {
|
|
68
|
+
if (manifest.pools.some((pool) => pool.name === name)) return `dome_llm_pool.${label(name)}.name`;
|
|
69
|
+
if (manifest.models.some((model) => model.name === name)) return `dome_llm_connection.${label(name)}.name`;
|
|
70
|
+
throw new Error(`Managed agent default_model ${name} must name a pool or model.`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function triggerBlocks(triggers) {
|
|
74
|
+
return triggers.map((trigger) => {
|
|
75
|
+
const kind = required(trigger, "kind", `Trigger ${trigger.name} needs kind.`);
|
|
76
|
+
if (!["schedule", "webhook"].includes(kind)) throw new Error(`Trigger ${trigger.name} has unsupported kind ${kind}.`);
|
|
77
|
+
if (kind === "schedule" && !trigger.cron) throw new Error(`Schedule trigger ${trigger.name} needs cron.`);
|
|
78
|
+
const lines = [attr("workspace_id", "var.workspace_id"), attr("name", q(required(trigger, "name", "Every trigger needs a name."))), ...(trigger.description ? [attr("description", q(trigger.description))] : []), attr("kind", q(kind))];
|
|
79
|
+
for (const key of ["enabled", "cron", "timezone", "input", "max_concurrent_runs", "catch_up", "catch_up_deadline_seconds", "max_retries"]) if (trigger[key] !== undefined) lines.push(attr(key, typeof trigger[key] === "string" ? q(trigger[key]) : String(trigger[key])));
|
|
80
|
+
const targets = trigger.targets ?? [];
|
|
81
|
+
if (!Array.isArray(targets) || !targets.length) throw new Error(`Trigger ${trigger.name} needs at least one target.`);
|
|
82
|
+
lines.push(`targets = [${targets.map((target) => {
|
|
83
|
+
if (target?.kind !== "managed_agent" || typeof target.name !== "string") throw new Error(`Trigger ${trigger.name} targets must name a managed_agent.`);
|
|
84
|
+
return `{ kind = "managed_agent", name = dome_managed_agent.${label(target.name)}.name${target.disabled === undefined ? "" : `, disabled = ${Boolean(target.disabled)}`} }`;
|
|
85
|
+
}).join(", ")}]`);
|
|
86
|
+
return block("dome_trigger", label(trigger.name), lines);
|
|
179
87
|
});
|
|
180
88
|
}
|
|
181
89
|
|
|
182
|
-
export function renderDomeTerraform(manifest
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
90
|
+
export function renderDomeTerraform(manifest) {
|
|
91
|
+
if (!manifest.gateways.length) throw new Error("template.yaml must declare at least one gateway.");
|
|
92
|
+
const blocks = [];
|
|
93
|
+
for (const gateway of manifest.gateways) {
|
|
94
|
+
const gatewayLabel = label(gateway.name);
|
|
95
|
+
blocks.push(block("dome_gateway", gatewayLabel, [attr("workspace_id", "var.workspace_id"), attr("name", q(required(gateway, "name", "Every gateway needs a name."))), ...(gateway.description ? [attr("description", q(gateway.description))] : []), attr("is_default", String(Boolean(gateway.is_default)))]));
|
|
96
|
+
for (const group of gateway.tools ?? []) for (const tool of group.tools ?? []) blocks.push(block("dome_gateway_tool", `${gatewayLabel}_${label(group.tool_connection)}_${label(tool)}`, [attr("workspace_id", "var.workspace_id"), attr("gateway", `dome_gateway.${gatewayLabel}.name`), attr("mcp_connection", `dome_mcp_connection.${label(required(group, "tool_connection", "Gateway tools need tool_connection."))}.name`), attr("tool", q(tool))]));
|
|
97
|
+
for (const pool of gateway.pools ?? []) blocks.push(block("dome_gateway_llm_pool", `${gatewayLabel}_${label(pool)}`, [attr("workspace_id", "var.workspace_id"), attr("gateway", `dome_gateway.${gatewayLabel}.name`), attr("llm_pool", `dome_llm_pool.${label(pool)}.name`)]));
|
|
98
|
+
}
|
|
99
|
+
for (const connection of manifest.tool_connections) {
|
|
100
|
+
const auth = connection.auth_method ?? "none";
|
|
101
|
+
const lines = [attr("workspace_id", "var.workspace_id"), attr("name", q(connection.name)), attr("protocol", q(required(connection, "protocol", `Tool connection ${connection.name} needs protocol.`))), attr("url", q(required(connection, "url", `Tool connection ${connection.name} needs url.`))), attr("auth_method", q(auth)), attr("credential_type", q(connection.credential_type ?? (auth === "none" ? "none" : "shared")))];
|
|
102
|
+
if (connection.managed_header_name) lines.push(attr("managed_header_name", q(connection.managed_header_name)));
|
|
103
|
+
if (connection.secret_values) lines.push(attr("secret_values", `{ ${Object.entries(connection.secret_values).map(([key, value]) => `${q(key)} = ${expression(value)}`).join(", ")} }`));
|
|
104
|
+
if (connection.oauth) lines.push(attr("oauth", `jsonencode(${JSON.stringify(connection.oauth)})`));
|
|
105
|
+
blocks.push(block("dome_mcp_connection", label(connection.name), lines));
|
|
106
|
+
}
|
|
107
|
+
for (const model of manifest.models) {
|
|
108
|
+
const lines = [attr("workspace_id", "var.workspace_id"), attr("name", q(model.name)), attr("provider_id", q(required(model, "provider_id", `Model ${model.name} needs provider_id.`))), attr("provider_config", `jsonencode(${JSON.stringify(required(model, "provider_config", `Model ${model.name} needs provider_config.`))})`)];
|
|
109
|
+
for (const key of ["auth_method", "credential_type", "managed_header_name"]) if (model[key]) lines.push(attr(key, q(model[key])));
|
|
110
|
+
if (model.secret_values) lines.push(attr("secret_values", `{ ${Object.entries(model.secret_values).map(([key, value]) => `${q(key)} = ${expression(value)}`).join(", ")} }`));
|
|
111
|
+
blocks.push(block("dome_llm_connection", label(model.name), lines));
|
|
112
|
+
}
|
|
113
|
+
for (const pool of manifest.pools) {
|
|
114
|
+
const poolLabel = label(pool.name);
|
|
115
|
+
blocks.push(block("dome_llm_pool", poolLabel, [attr("workspace_id", "var.workspace_id"), attr("name", q(pool.name)), attr("is_default", String(Boolean(pool.is_default)))]));
|
|
116
|
+
for (const member of pool.members ?? []) blocks.push(block("dome_llm_pool_member", `${poolLabel}_${label(member)}`, [attr("workspace_id", "var.workspace_id"), attr("pool", `dome_llm_pool.${poolLabel}.name`), attr("llm_connection", `dome_llm_connection.${label(member)}.name`)]));
|
|
199
117
|
}
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (
|
|
205
|
-
|
|
118
|
+
for (const agent of manifest.agents) {
|
|
119
|
+
const agentLabel = label(agent.name); const managed = agent.type === "managed"; const resourceType = managed ? "dome_managed_agent" : "dome_agent";
|
|
120
|
+
const lines = [attr("workspace_id", "var.workspace_id"), attr("name", q(agent.name))];
|
|
121
|
+
if (!managed) lines.push(attr("allowed_gateways", `[${(agent.allowed_gateways ?? []).map((gateway) => `dome_gateway.${label(gateway)}.name`).join(", ")}]`));
|
|
122
|
+
if (!managed && agent.act_as) {
|
|
123
|
+
const { method, required: actAsRequired, hmac_secret: hmacSecret, allowed_subjects: allowedSubjects } = agent.act_as;
|
|
124
|
+
if (!method) throw new Error(`Agent ${agent.name} act_as needs method.`);
|
|
125
|
+
lines.push(attr("act_as", `{ method = ${q(method)}, required = ${Boolean(actAsRequired)} }`));
|
|
126
|
+
if (hmacSecret) lines.push(attr("act_as_hmac_secret", expression(hmacSecret)));
|
|
127
|
+
if (allowedSubjects) lines.push(attr("actas_allowed_subjects", `[${allowedSubjects.map(q).join(", ")}]`));
|
|
128
|
+
}
|
|
129
|
+
if (managed) {
|
|
130
|
+
if (!agent.system_prompt?.content || !agent.default_model) throw new Error(`Managed agent ${agent.name} needs system_prompt and default_model.`);
|
|
131
|
+
lines.push(attr("system_prompt", "<<-PROMPT"), ...agent.system_prompt.content.split("\n").map((line) => line.trimEnd()), "PROMPT", attr("default_model", managedModelReference(manifest, agent.default_model)), attr("gateway", `dome_gateway.${label(agent.gateway)}.name`));
|
|
132
|
+
}
|
|
133
|
+
blocks.push(block(resourceType, agentLabel, lines), ...rulesBlock(agent, resourceType, agentLabel));
|
|
134
|
+
if (!managed) for (const key of agent.keys ?? []) blocks.push(block("dome_agent_key", `${agentLabel}_${label(required(key, "name", `Agent ${agent.name} keys need a name.`))}`, [attr("workspace_id", "var.workspace_id"), attr("agent", `dome_agent.${agentLabel}.name`), attr("name", q(key.name))]));
|
|
206
135
|
}
|
|
207
|
-
blocks.push(
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
136
|
+
blocks.push(...filterBlocks(manifest.filters), ...quotaBlocks(manifest.quotas, new Map(manifest.agents.map((agent) => [agent.name, agent.type]))), ...triggerBlocks(manifest.triggers));
|
|
137
|
+
const header = `# Generated by @domesystems/templates from template.yaml. Do not edit.\nterraform {\n required_providers {\n dome = { source = ${q("dome-systems/dome")}, version = ${q(">= 0.1.0")} }\n }\n}\n\nvariable "workspace_id" { type = string }`;
|
|
138
|
+
const outputs = manifest.agents.filter((agent) => agent.type === "external").flatMap((agent) => (agent.keys ?? []).map((key) => {
|
|
139
|
+
const agentLabel = label(agent.name); const keyLabel = label(key.name);
|
|
140
|
+
return `output ${q(`${agentLabel}_${keyLabel}_token`)} {\n value = dome_agent_key.${agentLabel}_${keyLabel}.token\n sensitive = true\n}`;
|
|
141
|
+
}));
|
|
142
|
+
return [header, variableBlocks(manifest.variables).join("\n\n"), blocks.join("\n\n"), outputs.join("\n\n")].filter(Boolean).join("\n\n") + "\n";
|
|
213
143
|
}
|
|
214
144
|
|
|
215
145
|
export function generateDomeTerraform(directory, { output = "dome.tf", force = false, manifest }) {
|
|
216
146
|
const target = path.resolve(directory, output);
|
|
217
147
|
if (fs.existsSync(target) && !force) throw new Error(`${target} already exists. Use --force to replace it.`);
|
|
218
|
-
fs.writeFileSync(target, renderDomeTerraform(manifest
|
|
148
|
+
fs.writeFileSync(target, renderDomeTerraform(manifest));
|
|
219
149
|
return target;
|
|
220
150
|
}
|
package/src/manifest.mjs
CHANGED
|
@@ -2,23 +2,112 @@ import fs from "node:fs";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parseDocument } from "yaml";
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
const
|
|
7
|
-
if (
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
function parseYAML(file, label) {
|
|
6
|
+
const document = parseDocument(fs.readFileSync(file, "utf8"));
|
|
7
|
+
if (document.errors.length) throw new Error(`Invalid ${label}: ${document.errors.map((error) => error.message).join("; ")}`);
|
|
8
|
+
return document.toJS() ?? {};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function sourcePath(root, reference, label) {
|
|
12
|
+
if (typeof reference !== "string" || !reference.trim()) throw new Error(`${label} path must be a non-empty string.`);
|
|
13
|
+
const target = path.resolve(root, reference);
|
|
14
|
+
if (target !== root && !target.startsWith(`${root}${path.sep}`)) throw new Error(`${label} path must stay inside the template directory.`);
|
|
15
|
+
if (!fs.statSync(target, { throwIfNoEntry: false })?.isFile()) throw new Error(`${label} path does not exist: ${reference}.`);
|
|
16
|
+
return target;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function expandAgent(root, entry, index) {
|
|
20
|
+
if (!entry || typeof entry !== "object") throw new Error(`agents[${index}] must be an object or path.`);
|
|
21
|
+
if (entry.path === undefined) return entry;
|
|
22
|
+
if (Object.keys(entry).length !== 1) throw new Error(`agents[${index}] cannot combine path with other fields.`);
|
|
23
|
+
const agent = parseYAML(sourcePath(root, entry.path, `agents[${index}]`), `agent fragment ${entry.path}`);
|
|
24
|
+
if (!agent || typeof agent !== "object" || Array.isArray(agent)) throw new Error(`Agent fragment ${entry.path} must be an object.`);
|
|
25
|
+
return agent;
|
|
26
|
+
}
|
|
10
27
|
|
|
11
|
-
|
|
12
|
-
if (
|
|
13
|
-
|
|
28
|
+
function expandFilters(root, filters) {
|
|
29
|
+
if (filters === undefined) return [];
|
|
30
|
+
if (!Array.isArray(filters)) throw new Error("filters must be a list.");
|
|
31
|
+
return filters.flatMap((entry, index) => {
|
|
32
|
+
if (!entry || typeof entry !== "object") throw new Error(`filters[${index}] must be an object or path.`);
|
|
33
|
+
if (entry.path === undefined) return [entry];
|
|
34
|
+
if (Object.keys(entry).length !== 1) throw new Error(`filters[${index}] cannot combine path with other fields.`);
|
|
35
|
+
const parsed = parseYAML(sourcePath(root, entry.path, `filters[${index}]`), `filter fragment ${entry.path}`);
|
|
36
|
+
const items = Array.isArray(parsed) ? parsed : parsed.filters;
|
|
37
|
+
if (!Array.isArray(items)) throw new Error(`filters[${index}] fragment must contain a filters list.`);
|
|
38
|
+
return items;
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeFilterConfigs(root, filters) {
|
|
43
|
+
return filters.map((filter, index) => {
|
|
44
|
+
if (!filter?.config?.path) return filter;
|
|
45
|
+
if (Object.keys(filter.config).length !== 1) throw new Error(`filters[${index}].config cannot combine path with other fields.`);
|
|
46
|
+
const file = sourcePath(root, filter.config.path, `filters[${index}].config`);
|
|
47
|
+
try {
|
|
48
|
+
return { ...filter, config: JSON.parse(fs.readFileSync(file, "utf8")) };
|
|
49
|
+
} catch (error) {
|
|
50
|
+
throw new Error(`filters[${index}].config path must contain JSON: ${error.message}`);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeRuleFiles(root, agent, index) {
|
|
56
|
+
const files = agent.rules?.files;
|
|
57
|
+
if (files === undefined) return agent;
|
|
58
|
+
if (!files || typeof files !== "object" || Array.isArray(files)) throw new Error(`agents[${index}].rules.files must be a filename map.`);
|
|
59
|
+
const normalized = {};
|
|
60
|
+
for (const [name, source] of Object.entries(files)) {
|
|
61
|
+
if (!source || typeof source !== "object" || Array.isArray(source)) throw new Error(`agents[${index}].rules.files.${name} must declare path or inline.`);
|
|
62
|
+
const hasPath = typeof source.path === "string";
|
|
63
|
+
const hasInline = typeof source.inline === "string";
|
|
64
|
+
if (hasPath === hasInline) throw new Error(`agents[${index}].rules.files.${name} must declare exactly one of path or inline.`);
|
|
65
|
+
normalized[name] = hasPath ? { content: fs.readFileSync(sourcePath(root, source.path, `agents[${index}].rules.files.${name}`), "utf8"), source: source.path } : { content: source.inline, source: "inline" };
|
|
14
66
|
}
|
|
67
|
+
return { ...agent, rules: { ...agent.rules, files: normalized } };
|
|
68
|
+
}
|
|
15
69
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
70
|
+
function normalizeManagedAgent(root, agent, index) {
|
|
71
|
+
if (agent.type !== "managed") return agent;
|
|
72
|
+
const prompt = agent.system_prompt;
|
|
73
|
+
if (!prompt || typeof prompt !== "object" || Array.isArray(prompt)) throw new Error(`agents[${index}].system_prompt must declare path or inline.`);
|
|
74
|
+
const hasPath = typeof prompt.path === "string";
|
|
75
|
+
const hasInline = typeof prompt.inline === "string";
|
|
76
|
+
if (hasPath === hasInline) throw new Error(`agents[${index}].system_prompt must declare exactly one of path or inline.`);
|
|
77
|
+
if (typeof agent.gateway !== "string" || !agent.gateway.trim()) throw new Error(`Managed agent ${agent.name} needs gateway.`);
|
|
78
|
+
if (agent.allowed_gateways !== undefined || agent.trigger !== undefined) throw new Error(`Managed agent ${agent.name} uses gateway and top-level triggers; allowed_gateways and trigger are not supported.`);
|
|
79
|
+
return { ...agent, system_prompt: { content: hasPath ? fs.readFileSync(sourcePath(root, prompt.path, `agents[${index}].system_prompt`), "utf8") : prompt.inline, source: hasPath ? prompt.path : "inline" } };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function named(items, label) {
|
|
83
|
+
const names = new Set();
|
|
84
|
+
for (const item of items ?? []) {
|
|
85
|
+
if (typeof item?.name !== "string" || !item.name.trim()) throw new Error(`${label} entries need a name.`);
|
|
86
|
+
if (names.has(item.name)) throw new Error(`${label} names must be unique; ${item.name} appears more than once.`);
|
|
87
|
+
names.add(item.name);
|
|
20
88
|
}
|
|
21
|
-
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function readManifest(templateDirectory) {
|
|
92
|
+
const root = path.resolve(templateDirectory);
|
|
93
|
+
const manifestPath = path.join(root, "template.yaml");
|
|
94
|
+
if (!fs.statSync(manifestPath, { throwIfNoEntry: false })?.isFile()) throw new Error(`No template.yaml found in ${templateDirectory}.`);
|
|
95
|
+
const manifest = parseYAML(manifestPath, "template.yaml");
|
|
96
|
+
if (manifest.apiVersion !== "templates.domesystems.ai/v1" || manifest.kind !== "Template") throw new Error("template.yaml must declare apiVersion templates.domesystems.ai/v1 and kind Template.");
|
|
97
|
+
if (!Array.isArray(manifest.agents) || manifest.agents.length === 0) throw new Error("template.yaml must declare at least one agent.");
|
|
98
|
+
const agents = manifest.agents.map((entry, index) => {
|
|
99
|
+
const agent = normalizeManagedAgent(root, normalizeRuleFiles(root, expandAgent(root, entry, index), index), index);
|
|
100
|
+
if (typeof agent.name !== "string" || !agent.name.trim()) throw new Error(`agents[${index}].name is required.`);
|
|
101
|
+
const type = agent.type;
|
|
102
|
+
if (!type) throw new Error(`agents[${index}].type is required.`);
|
|
103
|
+
if (!["external", "managed"].includes(type)) throw new Error(`agents[${index}].type must be external or managed.`);
|
|
104
|
+
if (type === "managed" && agent.keys?.length) throw new Error(`agents[${index}] is managed and cannot declare keys.`);
|
|
105
|
+
return { ...agent, type };
|
|
106
|
+
});
|
|
107
|
+
const filters = normalizeFilterConfigs(root, expandFilters(root, manifest.filters));
|
|
108
|
+
const triggers = manifest.triggers ?? [];
|
|
109
|
+
named(agents, "agents"); named(manifest.gateways ?? [], "gateways"); named(manifest.tool_connections ?? [], "tool_connections"); named(manifest.models ?? [], "models"); named(manifest.pools ?? [], "pools"); named(filters, "filters"); named(triggers, "triggers");
|
|
110
|
+
return { ...manifest, variables: manifest.variables ?? {}, gateways: manifest.gateways ?? [], tool_connections: manifest.tool_connections ?? [], models: manifest.models ?? [], pools: manifest.pools ?? [], filters, quotas: manifest.quotas ?? [], triggers, agents };
|
|
22
111
|
}
|
|
23
112
|
|
|
24
113
|
const PACKAGE_NAME = /^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?\/[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$/;
|
|
@@ -26,30 +115,11 @@ const VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.
|
|
|
26
115
|
|
|
27
116
|
export function readPublishManifest(templateDirectory) {
|
|
28
117
|
const manifest = readManifest(templateDirectory);
|
|
29
|
-
if (manifest.apiVersion !== "templates.domesystems.ai/v1" || manifest.kind !== "Template") {
|
|
30
|
-
throw new Error("template.yaml must declare apiVersion templates.domesystems.ai/v1 and kind Template.");
|
|
31
|
-
}
|
|
32
118
|
const metadata = manifest.metadata ?? {};
|
|
33
|
-
if (!PACKAGE_NAME.test(metadata.name ?? ""))
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
if (!VERSION.test(metadata.version ?? "")) {
|
|
37
|
-
throw new Error("template.yaml metadata.version must be a valid SemVer version.");
|
|
38
|
-
}
|
|
39
|
-
if (typeof metadata.title !== "string" || !metadata.title.trim()) {
|
|
40
|
-
throw new Error("template.yaml metadata.title is required to publish a template.");
|
|
41
|
-
}
|
|
119
|
+
if (!PACKAGE_NAME.test(metadata.name ?? "")) throw new Error("template.yaml metadata.name must be a lowercase scoped name such as im2nguyen/tinyfish.");
|
|
120
|
+
if (!VERSION.test(metadata.version ?? "")) throw new Error("template.yaml metadata.version must be a valid SemVer version.");
|
|
121
|
+
if (typeof metadata.title !== "string" || !metadata.title.trim()) throw new Error("template.yaml metadata.title is required to publish a template.");
|
|
42
122
|
const source = metadata.source;
|
|
43
|
-
if (!source || typeof source.repository !== "string" || typeof source.path !== "string" || !source.path.trim())
|
|
44
|
-
throw new Error("template.yaml metadata.source.repository and metadata.source.path are required to publish a template.");
|
|
45
|
-
}
|
|
46
|
-
let repository;
|
|
47
|
-
try { repository = new URL(source.repository); } catch { throw new Error("metadata.source.repository must be a GitHub HTTPS URL."); }
|
|
48
|
-
if (repository.protocol !== "https:" || repository.hostname !== "github.com" || !/^\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+\/?$/.test(repository.pathname)) {
|
|
49
|
-
throw new Error("metadata.source.repository must be a public https://github.com/<owner>/<repository> URL.");
|
|
50
|
-
}
|
|
51
|
-
if (source.path.split(/[\\/]+/).some((part) => !part || part === "." || part === "..")) {
|
|
52
|
-
throw new Error("metadata.source.path must be a relative repository path without traversal.");
|
|
53
|
-
}
|
|
123
|
+
if (!source || typeof source.repository !== "string" || typeof source.path !== "string" || !source.path.trim()) throw new Error("template.yaml metadata.source.repository and metadata.source.path are required to publish a template.");
|
|
54
124
|
return manifest;
|
|
55
125
|
}
|
package/src/runtime.mjs
CHANGED
|
@@ -6,18 +6,30 @@ const runtimeImages = {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
export function imageForRuntime(runtime) {
|
|
9
|
-
if (runtime.image) return runtime.image;
|
|
10
9
|
const key = `${runtime.language}:${runtime.pattern}`;
|
|
11
10
|
const image = runtimeImages[key];
|
|
12
|
-
if (!image) {
|
|
13
|
-
|
|
14
|
-
throw new Error(`Unsupported runtime ${key}. Set app.runtime.image explicitly, or use one of: ${supported}.`);
|
|
15
|
-
}
|
|
16
|
-
return `${image}:${runtime.image_tag ?? "latest"}`;
|
|
11
|
+
if (!image) throw new Error(`Unsupported runtime ${key}. Use one of: ${Object.keys(runtimeImages).join(", ")}.`);
|
|
12
|
+
return `${image}:latest`;
|
|
17
13
|
}
|
|
18
14
|
|
|
19
|
-
export function renderCompose(runtime, hostPort = 3000, { debug = false } = {}) {
|
|
15
|
+
export function renderCompose(runtime, hostPort = 3000, { debug = false, configPath = "app" } = {}) {
|
|
20
16
|
const image = imageForRuntime(runtime);
|
|
21
17
|
const diagnostics = debug ? " environment:\n DOME_DEBUG: \"true\"\n" : "";
|
|
22
|
-
return `# Generated by @domesystems/templates. Do not edit; update template.yaml instead
|
|
18
|
+
return `# Generated by @domesystems/templates. Do not edit; update template.yaml instead.
|
|
19
|
+
services:
|
|
20
|
+
app:
|
|
21
|
+
image: ${image}
|
|
22
|
+
env_file: .env
|
|
23
|
+
${diagnostics} volumes:
|
|
24
|
+
- ./${configPath}:/config:ro
|
|
25
|
+
- ./template.yaml:/template.yaml:ro
|
|
26
|
+
ports:
|
|
27
|
+
- "${hostPort}:3000"
|
|
28
|
+
healthcheck:
|
|
29
|
+
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:3000/health', timeout=2)"]
|
|
30
|
+
interval: 30s
|
|
31
|
+
timeout: 3s
|
|
32
|
+
start_period: 10s
|
|
33
|
+
retries: 3
|
|
34
|
+
`;
|
|
23
35
|
}
|