@better-openclaw/core 1.0.25 → 1.0.26
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/dist/addon-stack.cjs +55 -3
- package/dist/addon-stack.cjs.map +1 -1
- package/dist/addon-stack.d.cts.map +1 -1
- package/dist/addon-stack.d.mts.map +1 -1
- package/dist/addon-stack.mjs +54 -2
- package/dist/addon-stack.mjs.map +1 -1
- package/dist/addon-stack.test.cjs +113 -1
- package/dist/addon-stack.test.cjs.map +1 -1
- package/dist/addon-stack.test.mjs +112 -0
- package/dist/addon-stack.test.mjs.map +1 -1
- package/dist/compose-validation.test.cjs +1 -1
- package/dist/composer.cjs +1 -1
- package/dist/composer.test.cjs +1 -1
- package/dist/deployers/strip-host-ports.cjs +1 -1
- package/dist/generate.cjs +1 -1
- package/dist/generate.test.cjs +1 -1
- package/dist/generators/env.cjs +1 -1
- package/dist/generators/postgres-init.cjs +5 -0
- package/dist/generators/postgres-init.cjs.map +1 -1
- package/dist/generators/postgres-init.d.cts.map +1 -1
- package/dist/generators/postgres-init.d.mts.map +1 -1
- package/dist/generators/postgres-init.mjs +5 -0
- package/dist/generators/postgres-init.mjs.map +1 -1
- package/dist/generators/skills.cjs +1 -1
- package/dist/generators/skills.d.cts.map +1 -1
- package/dist/generators/skills.d.mts.map +1 -1
- package/dist/generators/skills.mjs +141 -0
- package/dist/generators/skills.mjs.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/presets/presets.test.cjs +1 -1
- package/dist/{schema-CKBRu-Rt.d.cts → schema-BQnZrcw8.d.cts} +6 -1
- package/dist/{schema-CKBRu-Rt.d.cts.map → schema-BQnZrcw8.d.cts.map} +1 -1
- package/dist/{schema-Dn-_Jpb6.d.mts → schema-SBpL0bdI.d.mts} +6 -1
- package/dist/{schema-Dn-_Jpb6.d.mts.map → schema-SBpL0bdI.d.mts.map} +1 -1
- package/dist/schema.cjs +11 -2
- package/dist/schema.cjs.map +1 -1
- package/dist/schema.d.cts +1 -1
- package/dist/schema.d.mts +1 -1
- package/dist/schema.mjs +10 -1
- package/dist/schema.mjs.map +1 -1
- package/dist/services/definitions/burnlink.cjs +142 -0
- package/dist/services/definitions/burnlink.cjs.map +1 -0
- package/dist/services/definitions/burnlink.d.cts +7 -0
- package/dist/services/definitions/burnlink.d.cts.map +1 -0
- package/dist/services/definitions/burnlink.d.mts +7 -0
- package/dist/services/definitions/burnlink.d.mts.map +1 -0
- package/dist/services/definitions/burnlink.mjs +141 -0
- package/dist/services/definitions/burnlink.mjs.map +1 -0
- package/dist/services/definitions/hindsight.cjs +130 -0
- package/dist/services/definitions/hindsight.cjs.map +1 -0
- package/dist/services/definitions/hindsight.d.cts +7 -0
- package/dist/services/definitions/hindsight.d.cts.map +1 -0
- package/dist/services/definitions/hindsight.d.mts +7 -0
- package/dist/services/definitions/hindsight.d.mts.map +1 -0
- package/dist/services/definitions/hindsight.mjs +129 -0
- package/dist/services/definitions/hindsight.mjs.map +1 -0
- package/dist/services/definitions/index.cjs +9 -0
- package/dist/services/definitions/index.cjs.map +1 -1
- package/dist/services/definitions/index.d.cts +4 -1
- package/dist/services/definitions/index.d.cts.map +1 -1
- package/dist/services/definitions/index.d.mts +4 -1
- package/dist/services/definitions/index.d.mts.map +1 -1
- package/dist/services/definitions/index.mjs +7 -1
- package/dist/services/definitions/index.mjs.map +1 -1
- package/dist/services/definitions/opensandbox.cjs +149 -0
- package/dist/services/definitions/opensandbox.cjs.map +1 -0
- package/dist/services/definitions/opensandbox.d.cts +7 -0
- package/dist/services/definitions/opensandbox.d.cts.map +1 -0
- package/dist/services/definitions/opensandbox.d.mts +7 -0
- package/dist/services/definitions/opensandbox.d.mts.map +1 -0
- package/dist/services/definitions/opensandbox.mjs +148 -0
- package/dist/services/definitions/opensandbox.mjs.map +1 -0
- package/dist/{skills-BlzpHmpH.cjs → skills-BSF7iNa4.cjs} +142 -1
- package/dist/{skills-BlzpHmpH.cjs.map → skills-BSF7iNa4.cjs.map} +1 -1
- package/dist/types.d.cts +1 -1
- package/dist/types.d.mts +1 -1
- package/dist/validator.cjs +1 -1
- package/package.json +1 -1
- package/src/addon-stack.test.ts +158 -0
- package/src/addon-stack.ts +48 -0
- package/src/generators/postgres-init.ts +2 -0
- package/src/generators/skills.ts +142 -0
- package/src/schema.ts +7 -0
- package/src/services/definitions/burnlink.ts +142 -0
- package/src/services/definitions/hindsight.ts +131 -0
- package/src/services/definitions/index.ts +10 -0
- package/src/services/definitions/opensandbox.ts +156 -0
package/dist/addon-stack.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_skills = require("./skills-
|
|
2
|
+
const require_skills = require("./skills-BSF7iNa4.cjs");
|
|
3
3
|
const require_generators_postgres_init = require("./generators/postgres-init.cjs");
|
|
4
4
|
const require_composer = require("./composer.cjs");
|
|
5
5
|
const require_services_registry = require("./services/registry.cjs");
|
|
@@ -470,6 +470,54 @@ function generateAddonStack(rawInput) {
|
|
|
470
470
|
skillCount++;
|
|
471
471
|
}
|
|
472
472
|
const proxyRoutes = buildProxyRoutes(deployableServices);
|
|
473
|
+
const additionalFiles = {};
|
|
474
|
+
if (deployableServices.some((s) => s.definition.id === "opensandbox")) additionalFiles["sandbox.toml"] = [
|
|
475
|
+
"[server]",
|
|
476
|
+
"host = \"0.0.0.0\"",
|
|
477
|
+
"port = 8080",
|
|
478
|
+
"log_level = \"INFO\"",
|
|
479
|
+
"api_key = \"${OPEN_SANDBOX_API_KEY}\"",
|
|
480
|
+
"",
|
|
481
|
+
"[runtime]",
|
|
482
|
+
"type = \"docker\"",
|
|
483
|
+
"execd_image = \"opensandbox/execd:v1.0.6\"",
|
|
484
|
+
"",
|
|
485
|
+
"[docker]",
|
|
486
|
+
"network_mode = \"bridge\"",
|
|
487
|
+
"drop_capabilities = [\"NET_ADMIN\", \"SYS_ADMIN\", \"SYS_PTRACE\", \"MKNOD\", \"NET_RAW\", \"SYS_RAWIO\"]",
|
|
488
|
+
"no_new_privileges = true",
|
|
489
|
+
"pids_limit = 512",
|
|
490
|
+
"",
|
|
491
|
+
"[secure_runtime]",
|
|
492
|
+
"type = \"gvisor\"",
|
|
493
|
+
""
|
|
494
|
+
].join("\n");
|
|
495
|
+
const prePullImages = [];
|
|
496
|
+
if (deployableServices.some((s) => s.definition.id === "opensandbox")) prePullImages.push({
|
|
497
|
+
image: "opensandbox/server:v1.0.6",
|
|
498
|
+
priority: 1
|
|
499
|
+
}, {
|
|
500
|
+
image: "opensandbox/execd:v1.0.6",
|
|
501
|
+
priority: 1
|
|
502
|
+
}, {
|
|
503
|
+
image: "opensandbox/desktop:latest",
|
|
504
|
+
priority: 1
|
|
505
|
+
}, {
|
|
506
|
+
image: "opensandbox/chrome:latest",
|
|
507
|
+
priority: 1
|
|
508
|
+
}, {
|
|
509
|
+
image: "opensandbox/code-interpreter:python",
|
|
510
|
+
priority: 2
|
|
511
|
+
}, {
|
|
512
|
+
image: "opensandbox/code-interpreter:node",
|
|
513
|
+
priority: 2
|
|
514
|
+
}, {
|
|
515
|
+
image: "opensandbox/code-interpreter:latest",
|
|
516
|
+
priority: 3
|
|
517
|
+
}, {
|
|
518
|
+
image: "opensandbox/vscode:latest",
|
|
519
|
+
priority: 3
|
|
520
|
+
});
|
|
473
521
|
const volumeMap = {};
|
|
474
522
|
for (const v of allVolumes) volumeMap[v] = null;
|
|
475
523
|
const composeDoc = { services };
|
|
@@ -482,6 +530,7 @@ function generateAddonStack(rawInput) {
|
|
|
482
530
|
skillFiles,
|
|
483
531
|
openclawConfigPatch: { skills: { entries: skillEntries } },
|
|
484
532
|
proxyRoutes,
|
|
533
|
+
additionalFiles,
|
|
485
534
|
metadata: {
|
|
486
535
|
serviceCount: Object.keys(services).length,
|
|
487
536
|
skillCount,
|
|
@@ -489,7 +538,8 @@ function generateAddonStack(rawInput) {
|
|
|
489
538
|
resolvedServices: deployableServices.map((s) => s.definition.id),
|
|
490
539
|
skippedServices,
|
|
491
540
|
generatedSecretKeys,
|
|
492
|
-
portAssignments: portConflicts.assignments
|
|
541
|
+
portAssignments: portConflicts.assignments,
|
|
542
|
+
prePullImages
|
|
493
543
|
},
|
|
494
544
|
warnings
|
|
495
545
|
};
|
|
@@ -625,6 +675,7 @@ function emptyResultBase() {
|
|
|
625
675
|
skillFiles: {},
|
|
626
676
|
openclawConfigPatch: { skills: { entries: {} } },
|
|
627
677
|
proxyRoutes: [],
|
|
678
|
+
additionalFiles: {},
|
|
628
679
|
metadata: {
|
|
629
680
|
serviceCount: 0,
|
|
630
681
|
skillCount: 0,
|
|
@@ -632,7 +683,8 @@ function emptyResultBase() {
|
|
|
632
683
|
resolvedServices: [],
|
|
633
684
|
skippedServices: [],
|
|
634
685
|
generatedSecretKeys: [],
|
|
635
|
-
portAssignments: {}
|
|
686
|
+
portAssignments: {},
|
|
687
|
+
prePullImages: []
|
|
636
688
|
},
|
|
637
689
|
warnings: []
|
|
638
690
|
};
|
package/dist/addon-stack.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"addon-stack.cjs","names":["AddonStackInputSchema","getServiceById","resolve","buildCompanionService","getDbRequirements","quotedStr","generateSkillFiles","YAML_OPTIONS","AddonStackUpdateInputSchema"],"sources":["../src/addon-stack.ts"],"sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { stringify } from \"yaml\";\nimport { parse as parseYaml } from \"yaml\";\nimport { buildCompanionService, buildPostgresSetup, quotedStr, YAML_OPTIONS } from \"./composer.js\";\nimport { getDbRequirements } from \"./generators/postgres-init.js\";\nimport { generateSkillFiles } from \"./generators/skills.js\";\nimport { resolve } from \"./resolver.js\";\nimport { AddonStackInputSchema, AddonStackUpdateInputSchema } from \"./schema.js\";\nimport { getServiceById } from \"./services/registry.js\";\nimport type {\n\tAddonStackInput,\n\tAddonStackResult,\n\tAddonStackUpdateInput,\n\tAddonStackUpdateResult,\n\tComposeOptions,\n\tProxyRoute,\n\tResolvedService,\n\tResolverOutput,\n\tServiceDefinition,\n\tSkippedService,\n} from \"./types.js\";\n\n// ── Constants ────────────────────────────────────────────────────────────────\n\n/** Services that Clawexa's cloud-init already provisions (or are mandatory platform services). */\nconst INFRA_SERVICE_IDS = new Set([\n\t\"openclaw-gateway\",\n\t\"openclaw-cli\",\n\t\"redis\",\n\t\"postgresql\",\n\t\"open-webui\",\n\t\"caddy\",\n\t\"traefik\",\n\t\"postgres-setup\",\n\t// Mandatory platform services provisioned by Clawexa cloud-init\n\t\"convex\",\n\t\"convex-dashboard\",\n\t\"mission-control\",\n]);\n\n/** Env keys managed by Clawexa's cloud-init — never include in addon env output. */\nconst CLAWEXA_MANAGED_ENV_KEYS = new Set([\n\t\"COMPOSE_FILE\",\n\t\"COMPOSE_PROFILES\",\n\t\"OPENCLAW_VERSION\",\n\t\"OPENCLAW_GATEWAY_TOKEN\",\n\t\"OPENCLAW_GATEWAY_PORT\",\n\t\"OPENCLAW_BRIDGE_PORT\",\n\t\"OPENCLAW_GATEWAY_BIND\",\n\t\"OPENCLAW_CONFIG_DIR\",\n\t\"OPENCLAW_WORKSPACE_DIR\",\n\t\"REDIS_PASSWORD\",\n\t\"REDIS_HOST\",\n\t\"REDIS_PORT\",\n\t\"POSTGRES_USER\",\n\t\"POSTGRES_PASSWORD\",\n\t\"POSTGRES_DB\",\n\t\"POSTGRES_HOST\",\n\t\"POSTGRES_PORT\",\n]);\n\n// ── Helpers ──────────────────────────────────────────────────────────────────\n\n/** Sanitize instanceId into a valid Docker Compose project name. */\nfunction sanitizeProjectName(instanceId: string): string {\n\treturn instanceId\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9-]/g, \"-\")\n\t\t.replace(/^-+|-+$/g, \"\")\n\t\t.replace(/-{2,}/g, \"-\")\n\t\t.slice(0, 64) || \"addon\";\n}\n\n/** Generate a cryptographically secure hex secret of the given byte length. */\nfunction generateHexSecret(bytes: number): string {\n\treturn randomBytes(bytes).toString(\"hex\");\n}\n\n/** Generate a cryptographically secure base64url secret of the given byte length. */\nfunction generateBase64UrlSecret(bytes: number): string {\n\treturn randomBytes(bytes).toString(\"base64url\");\n}\n\n/**\n * Check if a service requires user-provided credentials that are missing.\n * Returns the list of missing credential keys, or empty if all are satisfied.\n *\n * When `generateSecrets` is true, empty-default secrets are auto-generated\n * (passwords, tokens, etc.) and are NOT considered missing. Only secrets\n * with `validation` regex patterns (indicating specific format like API keys)\n * are flagged as missing when not provided by the user.\n */\nfunction getMissingCredentials(\n\tdef: ServiceDefinition,\n\tuserCredentials: Record<string, string> | undefined,\n\tgenerateSecrets: boolean,\n): string[] {\n\tconst missing: string[] = [];\n\tfor (const env of def.environment) {\n\t\t// Skip if not required or not a secret\n\t\tif (!env.required || !env.secret) continue;\n\t\t// Skip if it has a non-empty default value or is a reference\n\t\tif (env.defaultValue && env.defaultValue.length > 0) continue;\n\t\t// Skip if user provided the credential\n\t\tif (userCredentials?.[env.key]) continue;\n\t\t// When generateSecrets is true, empty secrets are auto-generated\n\t\t// Only flag as missing if the env var has a validation pattern\n\t\t// (indicating it needs a specific format like an API key)\n\t\tif (generateSecrets && !env.validation) continue;\n\n\t\tmissing.push(env.key);\n\t}\n\treturn missing;\n}\n\n/**\n * Apply env quirks (from service definition) to the generated env values.\n * Handles: empty_string_crashes → set fixed value, min_length → generate longer secret,\n * must_sync → ensure two keys have the same value.\n */\nfunction applyEnvQuirks(\n\tdef: ServiceDefinition,\n\tenvValues: Map<string, string>,\n\tgenerateSecrets: boolean,\n): void {\n\tif (!def.envQuirks) return;\n\n\tfor (const quirk of def.envQuirks) {\n\t\tswitch (quirk.issue) {\n\t\t\tcase \"empty_string_crashes\": {\n\t\t\t\tif (quirk.fix.type === \"set_value\" && quirk.fix.value !== undefined) {\n\t\t\t\t\tenvValues.set(quirk.key, quirk.fix.value);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"min_length\": {\n\t\t\t\tif (!generateSecrets) break;\n\t\t\t\tconst current = envValues.get(quirk.key) || \"\";\n\t\t\t\tconst minBytes = quirk.fix.minBytes || 24;\n\t\t\t\tconst minHexLen = minBytes * 2;\n\t\t\t\tif (current.length < minHexLen) {\n\t\t\t\t\tif (quirk.fix.type === \"generate_hex\") {\n\t\t\t\t\t\tenvValues.set(quirk.key, generateHexSecret(minBytes));\n\t\t\t\t\t} else if (quirk.fix.type === \"generate_base64url\") {\n\t\t\t\t\t\tenvValues.set(quirk.key, generateBase64UrlSecret(minBytes));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"must_sync\": {\n\t\t\t\tif (quirk.fix.type === \"sync_with\" && quirk.fix.syncKey) {\n\t\t\t\t\tconst sourceValue = envValues.get(quirk.key) || envValues.get(quirk.fix.syncKey);\n\t\t\t\t\tif (sourceValue) {\n\t\t\t\t\t\tenvValues.set(quirk.key, sourceValue);\n\t\t\t\t\t\tenvValues.set(quirk.fix.syncKey, sourceValue);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Build proxy routes from resolved addon services.\n */\nfunction buildProxyRoutes(services: ResolvedService[]): ProxyRoute[] {\n\tconst routes: ProxyRoute[] = [];\n\tfor (const { definition: def } of services) {\n\t\tconst exposedPort = def.ports.find((p) => p.exposed);\n\t\tif (!exposedPort) continue;\n\n\t\troutes.push({\n\t\t\tserviceId: def.id,\n\t\t\tpath: def.proxyPath || `/${def.id}`,\n\t\t\tport: exposedPort.container,\n\t\t\tprotocol: \"http\",\n\t\t\tstripPrefix: true,\n\t\t});\n\t}\n\treturn routes;\n}\n\n/**\n * Build a minimal ComposeOptions suitable for addon stack generation.\n * No proxy, no gateway, no hardening by default.\n */\nfunction buildAddonComposeOptions(projectName: string, input: AddonStackInput): ComposeOptions {\n\treturn {\n\t\tprojectName,\n\t\tproxy: \"none\",\n\t\tgpu: false,\n\t\tplatform: input.platform ?? \"linux/amd64\",\n\t\tdeployment: \"clawexa\",\n\t\topenclawVersion: input.openclawVersion ?? \"latest\",\n\t\topenclawImage: \"official\",\n\t\thardened: false, // Clawexa default: no cap_drop/security_opt\n\t\topenclawInstallMethod: \"docker\",\n\t};\n}\n\n/**\n * Resolve port conflicts between addon services and reserved ports.\n * Returns a map of serviceId → { originalPort → assignedPort }.\n */\nfunction resolvePortConflicts(\n\taddonServices: ResolvedService[],\n\treservedPorts: number[],\n\tportOverrides?: Record<string, Record<string, number>>,\n): { assignments: Record<string, number>; overrides: Record<string, Record<string, number>> } {\n\tconst usedPorts = new Set(reservedPorts);\n\tconst assignments: Record<string, number> = {};\n\tconst overrides: Record<string, Record<string, number>> = {};\n\n\tfor (const { definition: def } of addonServices) {\n\t\tfor (const port of def.ports) {\n\t\t\tif (!port.exposed) continue;\n\n\t\t\t// Check for user-specified override\n\t\t\tconst userOverride = portOverrides?.[def.id]?.[String(port.host)];\n\t\t\tif (userOverride) {\n\t\t\t\tusedPorts.add(userOverride);\n\t\t\t\tassignments[`${def.id}:${port.container}`] = userOverride;\n\t\t\t\tif (!overrides[def.id]) overrides[def.id] = {};\n\t\t\t\toverrides[def.id][String(port.host)] = userOverride;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tlet assignedPort = port.host;\n\t\t\tif (usedPorts.has(assignedPort)) {\n\t\t\t\t// Auto-reassign to port+1000 range\n\t\t\t\tassignedPort = port.host + 1000;\n\t\t\t\twhile (usedPorts.has(assignedPort)) {\n\t\t\t\t\tassignedPort++;\n\t\t\t\t}\n\t\t\t\tif (!overrides[def.id]) overrides[def.id] = {};\n\t\t\t\toverrides[def.id][String(port.host)] = assignedPort;\n\t\t\t}\n\t\t\tusedPorts.add(assignedPort);\n\t\t\tassignments[`${def.id}:${port.container}`] = assignedPort;\n\t\t}\n\t}\n\n\treturn { assignments, overrides };\n}\n\n// ── Main: generateAddonStack ─────────────────────────────────────────────────\n\n/**\n * Generates a Docker Compose override stack containing only addon services\n * for Clawexa managed instances. Infrastructure services (gateway, redis,\n * postgres, open-webui, caddy) are excluded since Clawexa's cloud-init\n * already provisions them.\n *\n * This function never throws. Errors are reported via `warnings` and\n * `metadata.skippedServices`.\n */\nexport function generateAddonStack(rawInput: AddonStackInput): AddonStackResult {\n\tconst warnings: string[] = [];\n\tconst skippedServices: SkippedService[] = [];\n\tconst generatedSecretKeys: string[] = [];\n\n\t// 1. Parse & validate input\n\tlet input: AddonStackInput;\n\ttry {\n\t\tinput = AddonStackInputSchema.parse(rawInput);\n\t} catch (err) {\n\t\treturn emptyResult(`Invalid input: ${err instanceof Error ? err.message : String(err)}`);\n\t}\n\n\tconst projectName = sanitizeProjectName(input.instanceId);\n\n\t// 2. Filter out infrastructure service IDs from request\n\tconst addonServiceIds = input.services.filter((id) => {\n\t\tif (INFRA_SERVICE_IDS.has(id)) {\n\t\t\twarnings.push(`Service \"${id}\" is managed by Clawexa infrastructure and was excluded.`);\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t});\n\n\tif (addonServiceIds.length === 0) {\n\t\treturn emptyResult(\"No addon services requested (all were infrastructure services).\");\n\t}\n\n\t// 3. Validate all service IDs exist in registry\n\tconst validServiceIds: string[] = [];\n\tfor (const id of addonServiceIds) {\n\t\tconst svc = getServiceById(id);\n\t\tif (!svc) {\n\t\t\tskippedServices.push({\n\t\t\t\tserviceId: id,\n\t\t\t\treason: \"unknown_service\",\n\t\t\t\tdetails: `Service \"${id}\" does not exist in the registry.`,\n\t\t\t});\n\t\t} else {\n\t\t\tvalidServiceIds.push(id);\n\t\t}\n\t}\n\n\tif (validServiceIds.length === 0) {\n\t\treturn {\n\t\t\t...emptyResultBase(),\n\t\t\tmetadata: {\n\t\t\t\t...emptyResultBase().metadata,\n\t\t\t\tskippedServices,\n\t\t\t},\n\t\t\twarnings: [...warnings, \"No valid addon services to deploy.\"],\n\t\t};\n\t}\n\n\t// 4. Resolve dependencies\n\tlet resolved: ResolverOutput;\n\ttry {\n\t\tresolved = resolve({\n\t\t\tservices: validServiceIds,\n\t\t\tskillPacks: input.skillPacks,\n\t\t\taiProviders: input.aiProviders,\n\t\t\tplatform: input.platform ?? \"linux/amd64\",\n\t\t});\n\t} catch (err) {\n\t\treturn {\n\t\t\t...emptyResultBase(),\n\t\t\tmetadata: {\n\t\t\t\t...emptyResultBase().metadata,\n\t\t\t\tskippedServices,\n\t\t\t},\n\t\t\twarnings: [\n\t\t\t\t...warnings,\n\t\t\t\t`Dependency resolution failed: ${err instanceof Error ? err.message : String(err)}`,\n\t\t\t],\n\t\t};\n\t}\n\n\t// Forward resolver warnings\n\tfor (const w of resolved.warnings) {\n\t\twarnings.push(w.message);\n\t}\n\n\t// 5. Filter resolved services: keep only addon services (not infra)\n\tconst addonResolved: ResolvedService[] = [];\n\tfor (const svc of resolved.services) {\n\t\tif (INFRA_SERVICE_IDS.has(svc.definition.id)) continue;\n\t\taddonResolved.push(svc);\n\t}\n\n\t// 6. Check credentials, images, platform, GPU for each addon service\n\tconst deployableServices: ResolvedService[] = [];\n\tfor (const svc of addonResolved) {\n\t\tconst def = svc.definition;\n\n\t\t// Check for git-based services without prebuilt image\n\t\tif (def.gitSource && def.buildContext && !def.image) {\n\t\t\tconst prebuilt = def.prebuiltImage || input.prebuiltImages[def.id];\n\t\t\tif (!prebuilt) {\n\t\t\t\tskippedServices.push({\n\t\t\t\t\tserviceId: def.id,\n\t\t\t\t\treason: \"no_image\",\n\t\t\t\t\tdetails: `Service \"${def.name}\" requires building from source but no pre-built image is available.`,\n\t\t\t\t});\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\n\t\t// Check GPU requirement — skip if host has no GPU support\n\t\tif (def.gpuRequired && !input.gpu) {\n\t\t\tskippedServices.push({\n\t\t\t\tserviceId: def.id,\n\t\t\t\treason: \"gpu_required\",\n\t\t\t\tdetails: `Service \"${def.name}\" requires a GPU but the host does not have GPU support.`,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Check user-provided credentials\n\t\tconst userCreds = input.credentials[def.id];\n\t\tconst missing = getMissingCredentials(def, userCreds, input.generateSecrets);\n\t\tif (missing.length > 0) {\n\t\t\tskippedServices.push({\n\t\t\t\tserviceId: def.id,\n\t\t\t\treason: \"missing_credentials\",\n\t\t\t\tdetails: `Service \"${def.name}\" requires credentials: ${missing.join(\", \")}`,\n\t\t\t\trequiredCredentials: missing,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\n\t\tdeployableServices.push(svc);\n\t}\n\n\tif (deployableServices.length === 0) {\n\t\treturn {\n\t\t\t...emptyResultBase(),\n\t\t\tmetadata: {\n\t\t\t\t...emptyResultBase().metadata,\n\t\t\t\tskippedServices,\n\t\t\t},\n\t\t\twarnings: [...warnings, \"No deployable addon services after filtering.\"],\n\t\t};\n\t}\n\n\t// 7. Resolve port conflicts (include ports from existingServices)\n\tconst allReservedPorts = [...input.reservedPorts];\n\tfor (const existingId of input.existingServices) {\n\t\tconst existingDef = getServiceById(existingId);\n\t\tif (existingDef) {\n\t\t\tfor (const port of existingDef.ports) {\n\t\t\t\tif (port.exposed) allReservedPorts.push(port.host);\n\t\t\t}\n\t\t}\n\t}\n\tconst portConflicts = resolvePortConflicts(\n\t\tdeployableServices,\n\t\tallReservedPorts,\n\t\tinput.portOverrides,\n\t);\n\n\t// Build a fake \"full\" resolved output for buildCompanionService\n\t// It needs to see all services to resolve depends_on references\n\tconst addonResolvedOutput: ResolverOutput = {\n\t\tservices: deployableServices,\n\t\taddedDependencies: resolved.addedDependencies,\n\t\tremovedConflicts: resolved.removedConflicts,\n\t\twarnings: resolved.warnings,\n\t\terrors: [],\n\t\tisValid: true,\n\t\testimatedMemoryMB: deployableServices.reduce(\n\t\t\t(sum, s) => sum + (s.definition.minMemoryMB ?? 128),\n\t\t\t0,\n\t\t),\n\t\taiProviders: input.aiProviders ?? [],\n\t\tgsdRuntimes: [],\n\t};\n\n\t// 8. Build compose options (no hardening for Clawexa)\n\tconst composeOptions = buildAddonComposeOptions(projectName, input);\n\tif (Object.keys(portConflicts.overrides).length > 0) {\n\t\tcomposeOptions.portOverrides = portConflicts.overrides;\n\t}\n\n\t// 9. Build per-service entries\n\tconst services: Record<string, Record<string, unknown>> = {};\n\tconst allVolumes = new Set<string>();\n\tconst envValues = new Map<string, string>();\n\n\tfor (const svc of deployableServices) {\n\t\tconst def = svc.definition;\n\t\ttry {\n\t\t\t// Handle prebuilt image substitution for git-based services\n\t\t\tlet effectiveDef = def;\n\t\t\tif (def.gitSource && def.buildContext && !def.image) {\n\t\t\t\tconst prebuiltImage = def.prebuiltImage || input.prebuiltImages[def.id];\n\t\t\t\tif (prebuiltImage) {\n\t\t\t\t\tconst [img, tag] = prebuiltImage.includes(\":\")\n\t\t\t\t\t\t? prebuiltImage.split(\":\")\n\t\t\t\t\t\t: [prebuiltImage, \"latest\"];\n\t\t\t\t\teffectiveDef = {\n\t\t\t\t\t\t...def,\n\t\t\t\t\t\timage: img,\n\t\t\t\t\t\timageTag: tag,\n\t\t\t\t\t\tgitSource: undefined,\n\t\t\t\t\t\tbuildContext: undefined,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst { entry, volumeNames } = buildCompanionService(\n\t\t\t\teffectiveDef,\n\t\t\t\taddonResolvedOutput,\n\t\t\t\tcomposeOptions,\n\t\t\t\tallVolumes,\n\t\t\t);\n\n\t\t\t// Remove profiles from the service entry\n\t\t\tdelete (entry as Record<string, unknown>).profiles;\n\n\t\t\t// Remove depends_on references to infrastructure services\n\t\t\tif (entry.depends_on) {\n\t\t\t\tconst deps = entry.depends_on as Record<string, { condition: string }>;\n\t\t\t\tfor (const depId of Object.keys(deps)) {\n\t\t\t\t\tif (INFRA_SERVICE_IDS.has(depId)) {\n\t\t\t\t\t\tdelete deps[depId];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (Object.keys(deps).length === 0) {\n\t\t\t\t\tdelete entry.depends_on;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tservices[def.id] = entry;\n\t\t\tfor (const v of volumeNames) allVolumes.add(v);\n\n\t\t\t// Inject user-provided credentials into env\n\t\t\tconst userCreds = input.credentials[def.id];\n\t\t\tif (userCreds) {\n\t\t\t\tfor (const [key, value] of Object.entries(userCreds)) {\n\t\t\t\t\tenvValues.set(key, value);\n\t\t\t\t}\n\t\t\t\t// Sync referenced keys: if a user provides e.g. DB_POSTGRESDB_PASSWORD\n\t\t\t\t// and the env var's defaultValue is \"${N8N_DB_PASSWORD}\", sync the ref key\n\t\t\t\t// so postgres-setup uses the same password.\n\t\t\t\tfor (const envVar of def.environment) {\n\t\t\t\t\tif (\n\t\t\t\t\t\tuserCreds[envVar.key] &&\n\t\t\t\t\t\tenvVar.defaultValue?.startsWith(\"${\") &&\n\t\t\t\t\t\tenvVar.defaultValue?.endsWith(\"}\")\n\t\t\t\t\t) {\n\t\t\t\t\t\tconst refKey = envVar.defaultValue.slice(2, -1);\n\t\t\t\t\t\tenvValues.set(refKey, userCreds[envVar.key]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tskippedServices.push({\n\t\t\t\tserviceId: def.id,\n\t\t\t\treason: \"resolution_error\",\n\t\t\t\tdetails: `Failed to build compose entry: ${err instanceof Error ? err.message : String(err)}`,\n\t\t\t});\n\t\t\twarnings.push(`Failed to process service \"${def.name}\": ${err instanceof Error ? err.message : String(err)}`);\n\t\t}\n\t}\n\n\t// 10. Build postgres-setup if any addon needs a DB\n\t// We need to check if any of our deployable services require DB setup\n\t// and if postgresql is in the infrastructure (it is for Clawexa)\n\tconst dbReqs = getDbRequirements(addonResolvedOutput);\n\tif (dbReqs.length > 0) {\n\t\t// Build a custom postgres-setup that references the existing PostgreSQL\n\t\t// We can't use buildPostgresSetup directly because it checks for postgresql\n\t\t// in the resolved services. Instead, build it manually.\n\t\tconst scriptLines = [\"echo '=== PostgreSQL database setup (addon) ==='\", \"FAILED=0\"];\n\n\t\tfor (const req of dbReqs) {\n\t\t\tscriptLines.push(\n\t\t\t\t`echo \"Setting up database '${req.dbName}' with user '${req.dbUser}'...\"`,\n\t\t\t\t`psql -c \"SELECT 1 FROM pg_roles WHERE rolname='${req.dbUser}'\" | grep -q 1 || psql -c \"CREATE ROLE ${req.dbUser} WITH LOGIN PASSWORD '$$${req.passwordEnvVar}'\"`,\n\t\t\t\t`psql -c \"ALTER ROLE ${req.dbUser} WITH LOGIN PASSWORD '$$${req.passwordEnvVar}'\"`,\n\t\t\t\t`psql -tc \"SELECT 1 FROM pg_database WHERE datname='${req.dbName}'\" | grep -q 1 || psql -c \"CREATE DATABASE ${req.dbName} OWNER ${req.dbUser}\"`,\n\t\t\t\t`psql -c \"GRANT ALL PRIVILEGES ON DATABASE ${req.dbName} TO ${req.dbUser}\" || FAILED=1`,\n\t\t\t\t`echo \" Done: ${req.dbName}\"`,\n\t\t\t);\n\t\t}\n\t\tscriptLines.push(\"echo '=== All databases ready ==='\", \"exit $$FAILED\");\n\n\t\tconst dbEnv: Record<string, string> = {\n\t\t\tPGHOST: \"postgresql\",\n\t\t\tPGUSER: \"${POSTGRES_USER:-openclaw}\",\n\t\t\tPGDATABASE: \"${POSTGRES_DB:-openclaw}\",\n\t\t\tPGPASSWORD: \"${POSTGRES_PASSWORD}\",\n\t\t};\n\t\tfor (const req of dbReqs) {\n\t\t\tdbEnv[req.passwordEnvVar] = `\\${${req.passwordEnvVar}}`;\n\t\t}\n\n\t\tservices[\"postgres-setup\"] = {\n\t\t\timage: \"postgres:17-alpine\",\n\t\t\tdepends_on: {\n\t\t\t\tpostgresql: { condition: \"service_healthy\" },\n\t\t\t},\n\t\t\tenvironment: dbEnv,\n\t\t\tentrypoint: [\"/bin/sh\", \"-c\"],\n\t\t\tcommand: [scriptLines.join(\"\\n\")],\n\t\t\trestart: quotedStr(\"no\"),\n\t\t\tnetworks: [\"openclaw-network\"],\n\t\t};\n\n\t\t// Update addon services that need DB to depend on postgres-setup\n\t\tfor (const req of dbReqs) {\n\t\t\tconst svcEntry = services[req.serviceId];\n\t\t\tif (svcEntry) {\n\t\t\t\tconst deps = (svcEntry.depends_on as Record<string, { condition: string }>) || {};\n\t\t\t\tdeps[\"postgres-setup\"] = { condition: \"service_completed_successfully\" };\n\t\t\t\tsvcEntry.depends_on = deps;\n\t\t\t}\n\t\t}\n\t}\n\n\t// 11. Generate secrets and env file\n\tconst envLines: string[] = [\n\t\t\"# ═══════════════════════════════════════════════════════════════════════════════\",\n\t\t\"# OpenClaw Addon Stack Environment\",\n\t\t`# Instance: ${input.instanceId}`,\n\t\t`# Generated at ${new Date().toISOString()}`,\n\t\t\"# ═══════════════════════════════════════════════════════════════════════════════\",\n\t\t\"\",\n\t];\n\n\t// DB passwords first\n\tif (dbReqs.length > 0) {\n\t\tenvLines.push(\"# ── Per-Service Database Passwords ──────────────────────────────────────\");\n\t\tfor (const req of dbReqs) {\n\t\t\tconst secretValue = input.generateSecrets ? generateHexSecret(24) : \"\";\n\t\t\tenvValues.set(req.passwordEnvVar, secretValue);\n\t\t\tif (secretValue) generatedSecretKeys.push(req.passwordEnvVar);\n\t\t\tenvLines.push(`# PostgreSQL password for ${req.serviceName} (db: ${req.dbName}, user: ${req.dbUser})`);\n\t\t\tenvLines.push(`${req.passwordEnvVar}=${secretValue}`);\n\t\t\tenvLines.push(\"\");\n\t\t}\n\t}\n\n\t// Per-service env vars\n\tconst seenKeys = new Set<string>([...CLAWEXA_MANAGED_ENV_KEYS, ...dbReqs.map((r) => r.passwordEnvVar)]);\n\tconst envVarGroups: AddonStackResult[\"envVars\"] = [];\n\n\tfor (const svc of deployableServices) {\n\t\tconst def = svc.definition;\n\t\tconst allEnvVars = [...def.environment, ...def.openclawEnvVars];\n\t\tif (allEnvVars.length === 0) continue;\n\n\t\tconst groupVars: AddonStackResult[\"envVars\"][number][\"vars\"] = [];\n\n\t\tenvLines.push(`# ── ${def.icon} ${def.name} ──────────────────────────────────────`);\n\n\t\tfor (const envVar of allEnvVars) {\n\t\t\tif (seenKeys.has(envVar.key)) continue;\n\t\t\tseenKeys.add(envVar.key);\n\n\t\t\t// Check if user provided this credential\n\t\t\tconst userValue = input.credentials[def.id]?.[envVar.key];\n\t\t\tlet actualValue: string;\n\n\t\t\tif (userValue !== undefined) {\n\t\t\t\tactualValue = userValue;\n\t\t\t} else if (envVar.secret) {\n\t\t\t\t// Resolve references like ${N8N_DB_PASSWORD}\n\t\t\t\tif (envVar.defaultValue.startsWith(\"${\") && envVar.defaultValue.endsWith(\"}\")) {\n\t\t\t\t\tconst refKey = envVar.defaultValue.slice(2, -1);\n\t\t\t\t\tactualValue = envValues.get(refKey) || envVar.defaultValue;\n\t\t\t\t} else if (input.generateSecrets) {\n\t\t\t\t\tactualValue = generateHexSecret(24);\n\t\t\t\t\tgeneratedSecretKeys.push(envVar.key);\n\t\t\t\t} else {\n\t\t\t\t\tactualValue = envVar.defaultValue;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tactualValue = envVar.defaultValue;\n\t\t\t}\n\n\t\t\tenvValues.set(envVar.key, actualValue);\n\n\t\t\tenvLines.push(`# ${envVar.description}`);\n\t\t\tenvLines.push(`${envVar.key}=${actualValue}`);\n\t\t\tenvLines.push(\"\");\n\n\t\t\tgroupVars.push({\n\t\t\t\tkey: envVar.key,\n\t\t\t\tdescription: envVar.description,\n\t\t\t\tvalue: actualValue,\n\t\t\t\tsecret: envVar.secret,\n\t\t\t});\n\t\t}\n\n\t\tif (groupVars.length > 0) {\n\t\t\tenvVarGroups.push({\n\t\t\t\tserviceName: def.name,\n\t\t\t\tvars: groupVars,\n\t\t\t});\n\t\t}\n\t}\n\n\t// Apply env quirks after all values are generated\n\tfor (const svc of deployableServices) {\n\t\tapplyEnvQuirks(svc.definition, envValues, input.generateSecrets);\n\t}\n\n\t// Rebuild env lines from envValues (quirks may have modified values or introduced new keys)\n\tconst quirkedKeys = new Set<string>();\n\tconst finalEnvLines: string[] = [];\n\tfor (const line of envLines) {\n\t\tconst trimmed = line.trim();\n\t\tif (!trimmed || trimmed.startsWith(\"#\")) {\n\t\t\tfinalEnvLines.push(line);\n\t\t\tcontinue;\n\t\t}\n\t\tconst eqIdx = trimmed.indexOf(\"=\");\n\t\tif (eqIdx > 0) {\n\t\t\tconst key = trimmed.slice(0, eqIdx);\n\t\t\tquirkedKeys.add(key);\n\t\t\tconst fixedValue = envValues.get(key);\n\t\t\tif (fixedValue !== undefined) {\n\t\t\t\tfinalEnvLines.push(`${key}=${fixedValue}`);\n\t\t\t} else {\n\t\t\t\tfinalEnvLines.push(line);\n\t\t\t}\n\t\t} else {\n\t\t\tfinalEnvLines.push(line);\n\t\t}\n\t}\n\t// Append any new keys introduced by quirks (e.g., must_sync creating a new key)\n\tfor (const [key, value] of envValues) {\n\t\tif (!quirkedKeys.has(key) && !seenKeys.has(key) && !CLAWEXA_MANAGED_ENV_KEYS.has(key)) {\n\t\t\tfinalEnvLines.push(`# Synced by env quirk`);\n\t\t\tfinalEnvLines.push(`${key}=${value}`);\n\t\t\tfinalEnvLines.push(\"\");\n\t\t}\n\t}\n\tconst envFile = finalEnvLines.join(\"\\n\");\n\n\t// 12. Generate skill files\n\tconst skillFiles = generateSkillFiles(addonResolvedOutput);\n\n\t// 13. Build openclaw config patch\n\tconst skillEntries: Record<string, { enabled: boolean }> = {};\n\tlet skillCount = 0;\n\tfor (const svc of deployableServices) {\n\t\tfor (const skill of svc.definition.skills) {\n\t\t\tif (skill.autoInstall) {\n\t\t\t\tskillEntries[skill.skillId] = { enabled: true };\n\t\t\t\tskillCount++;\n\t\t\t}\n\t\t}\n\t}\n\n\t// 14. Build proxy routes\n\tconst proxyRoutes = buildProxyRoutes(deployableServices);\n\n\t// 15. Compose single YAML\n\tconst volumeMap: Record<string, null> = {};\n\tfor (const v of allVolumes) {\n\t\tvolumeMap[v] = null;\n\t}\n\n\tconst composeDoc: Record<string, unknown> = {\n\t\tservices,\n\t};\n\n\tif (Object.keys(volumeMap).length > 0) {\n\t\tcomposeDoc.volumes = volumeMap;\n\t}\n\n\tcomposeDoc.networks = {\n\t\t\"openclaw-network\": {\n\t\t\texternal: true,\n\t\t},\n\t};\n\n\tconst composeOverride = stringify(composeDoc, YAML_OPTIONS);\n\n\t// 16. Return result\n\treturn {\n\t\tcomposeOverride,\n\t\tenvFile,\n\t\tenvVars: envVarGroups,\n\t\tskillFiles,\n\t\topenclawConfigPatch: {\n\t\t\tskills: { entries: skillEntries },\n\t\t},\n\t\tproxyRoutes,\n\t\tmetadata: {\n\t\t\tserviceCount: Object.keys(services).length,\n\t\t\tskillCount,\n\t\t\testimatedMemoryMB: addonResolvedOutput.estimatedMemoryMB,\n\t\t\tresolvedServices: deployableServices.map((s) => s.definition.id),\n\t\t\tskippedServices,\n\t\t\tgeneratedSecretKeys,\n\t\t\tportAssignments: portConflicts.assignments,\n\t\t},\n\t\twarnings,\n\t};\n}\n\n// ── Main: updateAddonStack ───────────────────────────────────────────────────\n\n/**\n * Incrementally updates an existing addon stack by adding or removing services.\n * Preserves existing env values (never overwrites user-customized values).\n *\n * This function never throws.\n */\nexport function updateAddonStack(rawInput: AddonStackUpdateInput): AddonStackUpdateResult {\n\tconst warnings: string[] = [];\n\n\t// 1. Parse & validate\n\tlet input: AddonStackUpdateInput;\n\ttry {\n\t\tinput = AddonStackUpdateInputSchema.parse(rawInput);\n\t} catch (err) {\n\t\treturn emptyUpdateResult(`Invalid input: ${err instanceof Error ? err.message : String(err)}`);\n\t}\n\n\t// 2. Parse existing compose YAML to extract current service list\n\tlet currentServiceIds: string[] = [];\n\ttry {\n\t\tconst existingCompose = parseYaml(input.currentCompose);\n\t\tif (existingCompose?.services && typeof existingCompose.services === \"object\") {\n\t\t\tcurrentServiceIds = Object.keys(existingCompose.services).filter(\n\t\t\t\t(id) => id !== \"postgres-setup\",\n\t\t\t);\n\t\t}\n\t} catch (err) {\n\t\twarnings.push(\n\t\t\t`Failed to parse existing compose YAML: ${err instanceof Error ? err.message : String(err)}`,\n\t\t);\n\t}\n\n\t// 3. Parse existing env into a map\n\tconst existingEnvMap = new Map<string, string>();\n\tfor (const line of input.currentEnv.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\t\tif (!trimmed || trimmed.startsWith(\"#\")) continue;\n\t\tconst eqIdx = trimmed.indexOf(\"=\");\n\t\tif (eqIdx > 0) {\n\t\t\texistingEnvMap.set(trimmed.slice(0, eqIdx), trimmed.slice(eqIdx + 1));\n\t\t}\n\t}\n\n\t// 4. Compute desired service list\n\tconst addSet = new Set(input.addServices);\n\tconst removeSet = new Set(input.removeServices);\n\tconst desiredServiceIds = [\n\t\t...currentServiceIds.filter((id) => !removeSet.has(id)),\n\t\t...input.addServices.filter((id) => !currentServiceIds.includes(id)),\n\t];\n\n\t// 5. Generate the full target state\n\tconst targetResult = generateAddonStack({\n\t\tinstanceId: input.instanceId,\n\t\tservices: desiredServiceIds,\n\t\tskillPacks: [],\n\t\tplatform: input.platform,\n\t\topenclawVersion: input.openclawVersion,\n\t\treservedPorts: input.reservedPorts,\n\t\tgenerateSecrets: input.generateSecrets,\n\t\tcredentials: input.credentials,\n\t\tportOverrides: input.portOverrides,\n\t\taiProviders: input.aiProviders,\n\t\tprebuiltImages: input.prebuiltImages,\n\t});\n\n\t// 6. Merge env: preserve existing values, only add new ones\n\tconst mergedEnvLines: string[] = [];\n\tfor (const line of targetResult.envFile.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\t\tif (!trimmed || trimmed.startsWith(\"#\")) {\n\t\t\tmergedEnvLines.push(line);\n\t\t\tcontinue;\n\t\t}\n\t\tconst eqIdx = trimmed.indexOf(\"=\");\n\t\tif (eqIdx > 0) {\n\t\t\tconst key = trimmed.slice(0, eqIdx);\n\t\t\tif (existingEnvMap.has(key)) {\n\t\t\t\t// Preserve existing value\n\t\t\t\tmergedEnvLines.push(`${key}=${existingEnvMap.get(key)}`);\n\t\t\t} else {\n\t\t\t\tmergedEnvLines.push(line);\n\t\t\t}\n\t\t} else {\n\t\t\tmergedEnvLines.push(line);\n\t\t}\n\t}\n\n\t// 7. Compute diffs\n\tconst currentSet = new Set(currentServiceIds);\n\tconst targetSet = new Set(targetResult.metadata.resolvedServices);\n\tconst added = [...targetSet].filter((id) => !currentSet.has(id));\n\tconst removed = [...currentSet].filter((id) => !targetSet.has(id));\n\tconst unchanged = [...currentSet].filter((id) => targetSet.has(id));\n\n\t// 8. Compute skill diffs\n\tconst newSkillFiles: Record<string, string> = {};\n\tconst removedSkillSlugs: string[] = [];\n\n\t// New skills from added services\n\tfor (const id of added) {\n\t\tconst def = getServiceById(id);\n\t\tif (!def) continue;\n\t\tfor (const skill of def.skills) {\n\t\t\tconst skillPath = Object.keys(targetResult.skillFiles).find(\n\t\t\t\t(path) => path.includes(skill.skillId),\n\t\t\t);\n\t\t\tif (skillPath) {\n\t\t\t\tnewSkillFiles[skillPath] = targetResult.skillFiles[skillPath];\n\t\t\t}\n\t\t}\n\t}\n\n\t// Removed skills from removed services\n\tfor (const id of removed) {\n\t\tconst def = getServiceById(id);\n\t\tif (!def) continue;\n\t\tfor (const skill of def.skills) {\n\t\t\tremovedSkillSlugs.push(skill.skillId);\n\t\t}\n\t}\n\n\t// 9. Proxy route diffs\n\tconst addProxyRoutes = targetResult.proxyRoutes.filter((r) => added.includes(r.serviceId));\n\tconst removeProxyRoutes = removed;\n\n\t// 10. Images to pull for new services\n\tconst imagesToPull: string[] = [];\n\tfor (const id of added) {\n\t\tconst def = getServiceById(id);\n\t\tif (def?.image && def?.imageTag) {\n\t\t\timagesToPull.push(`${def.image}:${def.imageTag}`);\n\t\t} else if (def?.prebuiltImage) {\n\t\t\timagesToPull.push(def.prebuiltImage);\n\t\t}\n\t}\n\n\t// 11. Estimate memory delta\n\tlet memoryDelta = 0;\n\tfor (const id of added) {\n\t\tconst def = getServiceById(id);\n\t\tmemoryDelta += def?.minMemoryMB ?? 128;\n\t}\n\tfor (const id of removed) {\n\t\tconst def = getServiceById(id);\n\t\tmemoryDelta -= def?.minMemoryMB ?? 128;\n\t}\n\n\t// Add existing skills to the config patch\n\tconst addSkillEntries: Record<string, { enabled: boolean }> = {};\n\tfor (const id of added) {\n\t\tconst def = getServiceById(id);\n\t\tif (!def) continue;\n\t\tfor (const skill of def.skills) {\n\t\t\tif (skill.autoInstall) {\n\t\t\t\taddSkillEntries[skill.skillId] = { enabled: true };\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\tcomposeOverride: targetResult.composeOverride,\n\t\tenvFile: mergedEnvLines.join(\"\\n\"),\n\t\tnewSkillFiles,\n\t\tremovedSkillSlugs,\n\t\topenclawConfigPatch: {\n\t\t\tskills: {\n\t\t\t\tadd: addSkillEntries,\n\t\t\t\tremove: removedSkillSlugs,\n\t\t\t},\n\t\t},\n\t\taddProxyRoutes,\n\t\tremoveProxyRoutes,\n\t\timagesToPull,\n\t\trestartRequired: added, // New services need starting, not restarting\n\t\tmetadata: {\n\t\t\tadded,\n\t\t\tremoved,\n\t\t\tunchanged,\n\t\t\testimatedMemoryDelta: memoryDelta,\n\t\t},\n\t\twarnings: [...warnings, ...targetResult.warnings],\n\t};\n}\n\n// ── Empty Result Helpers ─────────────────────────────────────────────────────\n\nfunction emptyResultBase(): AddonStackResult {\n\treturn {\n\t\tcomposeOverride: \"services: {}\\n\",\n\t\tenvFile: \"\",\n\t\tenvVars: [],\n\t\tskillFiles: {},\n\t\topenclawConfigPatch: { skills: { entries: {} } },\n\t\tproxyRoutes: [],\n\t\tmetadata: {\n\t\t\tserviceCount: 0,\n\t\t\tskillCount: 0,\n\t\t\testimatedMemoryMB: 0,\n\t\t\tresolvedServices: [],\n\t\t\tskippedServices: [],\n\t\t\tgeneratedSecretKeys: [],\n\t\t\tportAssignments: {},\n\t\t},\n\t\twarnings: [],\n\t};\n}\n\nfunction emptyResult(warning: string): AddonStackResult {\n\treturn {\n\t\t...emptyResultBase(),\n\t\twarnings: [warning],\n\t};\n}\n\nfunction emptyUpdateResult(warning: string): AddonStackUpdateResult {\n\treturn {\n\t\tcomposeOverride: \"services: {}\\n\",\n\t\tenvFile: \"\",\n\t\tnewSkillFiles: {},\n\t\tremovedSkillSlugs: [],\n\t\topenclawConfigPatch: { skills: { add: {}, remove: [] } },\n\t\taddProxyRoutes: [],\n\t\tremoveProxyRoutes: [],\n\t\timagesToPull: [],\n\t\trestartRequired: [],\n\t\tmetadata: {\n\t\t\tadded: [],\n\t\t\tremoved: [],\n\t\t\tunchanged: [],\n\t\t\testimatedMemoryDelta: 0,\n\t\t},\n\t\twarnings: [warning],\n\t};\n}\n"],"mappings":";;;;;;;;;;;AAyBA,MAAM,oBAAoB,IAAI,IAAI;CACjC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA,CAAC;;AAGF,MAAM,2BAA2B,IAAI,IAAI;CACxC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA,CAAC;;AAKF,SAAS,oBAAoB,YAA4B;AACxD,QAAO,WACL,aAAa,CACb,QAAQ,eAAe,IAAI,CAC3B,QAAQ,YAAY,GAAG,CACvB,QAAQ,UAAU,IAAI,CACtB,MAAM,GAAG,GAAG,IAAI;;;AAInB,SAAS,kBAAkB,OAAuB;AACjD,SAAA,GAAA,YAAA,aAAmB,MAAM,CAAC,SAAS,MAAM;;;AAI1C,SAAS,wBAAwB,OAAuB;AACvD,SAAA,GAAA,YAAA,aAAmB,MAAM,CAAC,SAAS,YAAY;;;;;;;;;;;AAYhD,SAAS,sBACR,KACA,iBACA,iBACW;CACX,MAAM,UAAoB,EAAE;AAC5B,MAAK,MAAM,OAAO,IAAI,aAAa;AAElC,MAAI,CAAC,IAAI,YAAY,CAAC,IAAI,OAAQ;AAElC,MAAI,IAAI,gBAAgB,IAAI,aAAa,SAAS,EAAG;AAErD,MAAI,kBAAkB,IAAI,KAAM;AAIhC,MAAI,mBAAmB,CAAC,IAAI,WAAY;AAExC,UAAQ,KAAK,IAAI,IAAI;;AAEtB,QAAO;;;;;;;AAQR,SAAS,eACR,KACA,WACA,iBACO;AACP,KAAI,CAAC,IAAI,UAAW;AAEpB,MAAK,MAAM,SAAS,IAAI,UACvB,SAAQ,MAAM,OAAd;EACC,KAAK;AACJ,OAAI,MAAM,IAAI,SAAS,eAAe,MAAM,IAAI,UAAU,KAAA,EACzD,WAAU,IAAI,MAAM,KAAK,MAAM,IAAI,MAAM;AAE1C;EAED,KAAK,cAAc;AAClB,OAAI,CAAC,gBAAiB;GACtB,MAAM,UAAU,UAAU,IAAI,MAAM,IAAI,IAAI;GAC5C,MAAM,WAAW,MAAM,IAAI,YAAY;GACvC,MAAM,YAAY,WAAW;AAC7B,OAAI,QAAQ,SAAS;QAChB,MAAM,IAAI,SAAS,eACtB,WAAU,IAAI,MAAM,KAAK,kBAAkB,SAAS,CAAC;aAC3C,MAAM,IAAI,SAAS,qBAC7B,WAAU,IAAI,MAAM,KAAK,wBAAwB,SAAS,CAAC;;AAG7D;;EAED,KAAK;AACJ,OAAI,MAAM,IAAI,SAAS,eAAe,MAAM,IAAI,SAAS;IACxD,MAAM,cAAc,UAAU,IAAI,MAAM,IAAI,IAAI,UAAU,IAAI,MAAM,IAAI,QAAQ;AAChF,QAAI,aAAa;AAChB,eAAU,IAAI,MAAM,KAAK,YAAY;AACrC,eAAU,IAAI,MAAM,IAAI,SAAS,YAAY;;;AAG/C;;;;;;AASJ,SAAS,iBAAiB,UAA2C;CACpE,MAAM,SAAuB,EAAE;AAC/B,MAAK,MAAM,EAAE,YAAY,SAAS,UAAU;EAC3C,MAAM,cAAc,IAAI,MAAM,MAAM,MAAM,EAAE,QAAQ;AACpD,MAAI,CAAC,YAAa;AAElB,SAAO,KAAK;GACX,WAAW,IAAI;GACf,MAAM,IAAI,aAAa,IAAI,IAAI;GAC/B,MAAM,YAAY;GAClB,UAAU;GACV,aAAa;GACb,CAAC;;AAEH,QAAO;;;;;;AAOR,SAAS,yBAAyB,aAAqB,OAAwC;AAC9F,QAAO;EACN;EACA,OAAO;EACP,KAAK;EACL,UAAU,MAAM,YAAY;EAC5B,YAAY;EACZ,iBAAiB,MAAM,mBAAmB;EAC1C,eAAe;EACf,UAAU;EACV,uBAAuB;EACvB;;;;;;AAOF,SAAS,qBACR,eACA,eACA,eAC6F;CAC7F,MAAM,YAAY,IAAI,IAAI,cAAc;CACxC,MAAM,cAAsC,EAAE;CAC9C,MAAM,YAAoD,EAAE;AAE5D,MAAK,MAAM,EAAE,YAAY,SAAS,cACjC,MAAK,MAAM,QAAQ,IAAI,OAAO;AAC7B,MAAI,CAAC,KAAK,QAAS;EAGnB,MAAM,eAAe,gBAAgB,IAAI,MAAM,OAAO,KAAK,KAAK;AAChE,MAAI,cAAc;AACjB,aAAU,IAAI,aAAa;AAC3B,eAAY,GAAG,IAAI,GAAG,GAAG,KAAK,eAAe;AAC7C,OAAI,CAAC,UAAU,IAAI,IAAK,WAAU,IAAI,MAAM,EAAE;AAC9C,aAAU,IAAI,IAAI,OAAO,KAAK,KAAK,IAAI;AACvC;;EAGD,IAAI,eAAe,KAAK;AACxB,MAAI,UAAU,IAAI,aAAa,EAAE;AAEhC,kBAAe,KAAK,OAAO;AAC3B,UAAO,UAAU,IAAI,aAAa,CACjC;AAED,OAAI,CAAC,UAAU,IAAI,IAAK,WAAU,IAAI,MAAM,EAAE;AAC9C,aAAU,IAAI,IAAI,OAAO,KAAK,KAAK,IAAI;;AAExC,YAAU,IAAI,aAAa;AAC3B,cAAY,GAAG,IAAI,GAAG,GAAG,KAAK,eAAe;;AAI/C,QAAO;EAAE;EAAa;EAAW;;;;;;;;;;;AAclC,SAAgB,mBAAmB,UAA6C;CAC/E,MAAM,WAAqB,EAAE;CAC7B,MAAM,kBAAoC,EAAE;CAC5C,MAAM,sBAAgC,EAAE;CAGxC,IAAI;AACJ,KAAI;AACH,UAAQA,eAAAA,sBAAsB,MAAM,SAAS;UACrC,KAAK;AACb,SAAO,YAAY,kBAAkB,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;;CAGzF,MAAM,cAAc,oBAAoB,MAAM,WAAW;CAGzD,MAAM,kBAAkB,MAAM,SAAS,QAAQ,OAAO;AACrD,MAAI,kBAAkB,IAAI,GAAG,EAAE;AAC9B,YAAS,KAAK,YAAY,GAAG,0DAA0D;AACvF,UAAO;;AAER,SAAO;GACN;AAEF,KAAI,gBAAgB,WAAW,EAC9B,QAAO,YAAY,kEAAkE;CAItF,MAAM,kBAA4B,EAAE;AACpC,MAAK,MAAM,MAAM,gBAEhB,KAAI,CADQC,0BAAAA,eAAe,GAAG,CAE7B,iBAAgB,KAAK;EACpB,WAAW;EACX,QAAQ;EACR,SAAS,YAAY,GAAG;EACxB,CAAC;KAEF,iBAAgB,KAAK,GAAG;AAI1B,KAAI,gBAAgB,WAAW,EAC9B,QAAO;EACN,GAAG,iBAAiB;EACpB,UAAU;GACT,GAAG,iBAAiB,CAAC;GACrB;GACA;EACD,UAAU,CAAC,GAAG,UAAU,qCAAqC;EAC7D;CAIF,IAAI;AACJ,KAAI;AACH,aAAWC,iBAAAA,QAAQ;GAClB,UAAU;GACV,YAAY,MAAM;GAClB,aAAa,MAAM;GACnB,UAAU,MAAM,YAAY;GAC5B,CAAC;UACM,KAAK;AACb,SAAO;GACN,GAAG,iBAAiB;GACpB,UAAU;IACT,GAAG,iBAAiB,CAAC;IACrB;IACA;GACD,UAAU,CACT,GAAG,UACH,iCAAiC,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GACjF;GACD;;AAIF,MAAK,MAAM,KAAK,SAAS,SACxB,UAAS,KAAK,EAAE,QAAQ;CAIzB,MAAM,gBAAmC,EAAE;AAC3C,MAAK,MAAM,OAAO,SAAS,UAAU;AACpC,MAAI,kBAAkB,IAAI,IAAI,WAAW,GAAG,CAAE;AAC9C,gBAAc,KAAK,IAAI;;CAIxB,MAAM,qBAAwC,EAAE;AAChD,MAAK,MAAM,OAAO,eAAe;EAChC,MAAM,MAAM,IAAI;AAGhB,MAAI,IAAI,aAAa,IAAI,gBAAgB,CAAC,IAAI;OAEzC,EADa,IAAI,iBAAiB,MAAM,eAAe,IAAI,MAChD;AACd,oBAAgB,KAAK;KACpB,WAAW,IAAI;KACf,QAAQ;KACR,SAAS,YAAY,IAAI,KAAK;KAC9B,CAAC;AACF;;;AAKF,MAAI,IAAI,eAAe,CAAC,MAAM,KAAK;AAClC,mBAAgB,KAAK;IACpB,WAAW,IAAI;IACf,QAAQ;IACR,SAAS,YAAY,IAAI,KAAK;IAC9B,CAAC;AACF;;EAID,MAAM,YAAY,MAAM,YAAY,IAAI;EACxC,MAAM,UAAU,sBAAsB,KAAK,WAAW,MAAM,gBAAgB;AAC5E,MAAI,QAAQ,SAAS,GAAG;AACvB,mBAAgB,KAAK;IACpB,WAAW,IAAI;IACf,QAAQ;IACR,SAAS,YAAY,IAAI,KAAK,0BAA0B,QAAQ,KAAK,KAAK;IAC1E,qBAAqB;IACrB,CAAC;AACF;;AAGD,qBAAmB,KAAK,IAAI;;AAG7B,KAAI,mBAAmB,WAAW,EACjC,QAAO;EACN,GAAG,iBAAiB;EACpB,UAAU;GACT,GAAG,iBAAiB,CAAC;GACrB;GACA;EACD,UAAU,CAAC,GAAG,UAAU,gDAAgD;EACxE;CAIF,MAAM,mBAAmB,CAAC,GAAG,MAAM,cAAc;AACjD,MAAK,MAAM,cAAc,MAAM,kBAAkB;EAChD,MAAM,cAAcD,0BAAAA,eAAe,WAAW;AAC9C,MAAI;QACE,MAAM,QAAQ,YAAY,MAC9B,KAAI,KAAK,QAAS,kBAAiB,KAAK,KAAK,KAAK;;;CAIrD,MAAM,gBAAgB,qBACrB,oBACA,kBACA,MAAM,cACN;CAID,MAAM,sBAAsC;EAC3C,UAAU;EACV,mBAAmB,SAAS;EAC5B,kBAAkB,SAAS;EAC3B,UAAU,SAAS;EACnB,QAAQ,EAAE;EACV,SAAS;EACT,mBAAmB,mBAAmB,QACpC,KAAK,MAAM,OAAO,EAAE,WAAW,eAAe,MAC/C,EACA;EACD,aAAa,MAAM,eAAe,EAAE;EACpC,aAAa,EAAE;EACf;CAGD,MAAM,iBAAiB,yBAAyB,aAAa,MAAM;AACnE,KAAI,OAAO,KAAK,cAAc,UAAU,CAAC,SAAS,EACjD,gBAAe,gBAAgB,cAAc;CAI9C,MAAM,WAAoD,EAAE;CAC5D,MAAM,6BAAa,IAAI,KAAa;CACpC,MAAM,4BAAY,IAAI,KAAqB;AAE3C,MAAK,MAAM,OAAO,oBAAoB;EACrC,MAAM,MAAM,IAAI;AAChB,MAAI;GAEH,IAAI,eAAe;AACnB,OAAI,IAAI,aAAa,IAAI,gBAAgB,CAAC,IAAI,OAAO;IACpD,MAAM,gBAAgB,IAAI,iBAAiB,MAAM,eAAe,IAAI;AACpE,QAAI,eAAe;KAClB,MAAM,CAAC,KAAK,OAAO,cAAc,SAAS,IAAI,GAC3C,cAAc,MAAM,IAAI,GACxB,CAAC,eAAe,SAAS;AAC5B,oBAAe;MACd,GAAG;MACH,OAAO;MACP,UAAU;MACV,WAAW,KAAA;MACX,cAAc,KAAA;MACd;;;GAIH,MAAM,EAAE,OAAO,gBAAgBE,iBAAAA,sBAC9B,cACA,qBACA,gBACA,WACA;AAGD,UAAQ,MAAkC;AAG1C,OAAI,MAAM,YAAY;IACrB,MAAM,OAAO,MAAM;AACnB,SAAK,MAAM,SAAS,OAAO,KAAK,KAAK,CACpC,KAAI,kBAAkB,IAAI,MAAM,CAC/B,QAAO,KAAK;AAGd,QAAI,OAAO,KAAK,KAAK,CAAC,WAAW,EAChC,QAAO,MAAM;;AAIf,YAAS,IAAI,MAAM;AACnB,QAAK,MAAM,KAAK,YAAa,YAAW,IAAI,EAAE;GAG9C,MAAM,YAAY,MAAM,YAAY,IAAI;AACxC,OAAI,WAAW;AACd,SAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,UAAU,CACnD,WAAU,IAAI,KAAK,MAAM;AAK1B,SAAK,MAAM,UAAU,IAAI,YACxB,KACC,UAAU,OAAO,QACjB,OAAO,cAAc,WAAW,KAAK,IACrC,OAAO,cAAc,SAAS,IAAI,EACjC;KACD,MAAM,SAAS,OAAO,aAAa,MAAM,GAAG,GAAG;AAC/C,eAAU,IAAI,QAAQ,UAAU,OAAO,KAAK;;;WAIvC,KAAK;AACb,mBAAgB,KAAK;IACpB,WAAW,IAAI;IACf,QAAQ;IACR,SAAS,kCAAkC,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI;IAC3F,CAAC;AACF,YAAS,KAAK,8BAA8B,IAAI,KAAK,KAAK,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;;;CAO/G,MAAM,SAASC,iCAAAA,kBAAkB,oBAAoB;AACrD,KAAI,OAAO,SAAS,GAAG;EAItB,MAAM,cAAc,CAAC,oDAAoD,WAAW;AAEpF,OAAK,MAAM,OAAO,OACjB,aAAY,KACX,8BAA8B,IAAI,OAAO,eAAe,IAAI,OAAO,QACnE,kDAAkD,IAAI,OAAO,yCAAyC,IAAI,OAAO,0BAA0B,IAAI,eAAe,KAC9J,uBAAuB,IAAI,OAAO,0BAA0B,IAAI,eAAe,KAC/E,sDAAsD,IAAI,OAAO,6CAA6C,IAAI,OAAO,SAAS,IAAI,OAAO,IAC7I,6CAA6C,IAAI,OAAO,MAAM,IAAI,OAAO,gBACzE,iBAAiB,IAAI,OAAO,GAC5B;AAEF,cAAY,KAAK,sCAAsC,gBAAgB;EAEvE,MAAM,QAAgC;GACrC,QAAQ;GACR,QAAQ;GACR,YAAY;GACZ,YAAY;GACZ;AACD,OAAK,MAAM,OAAO,OACjB,OAAM,IAAI,kBAAkB,MAAM,IAAI,eAAe;AAGtD,WAAS,oBAAoB;GAC5B,OAAO;GACP,YAAY,EACX,YAAY,EAAE,WAAW,mBAAmB,EAC5C;GACD,aAAa;GACb,YAAY,CAAC,WAAW,KAAK;GAC7B,SAAS,CAAC,YAAY,KAAK,KAAK,CAAC;GACjC,SAASC,iBAAAA,UAAU,KAAK;GACxB,UAAU,CAAC,mBAAmB;GAC9B;AAGD,OAAK,MAAM,OAAO,QAAQ;GACzB,MAAM,WAAW,SAAS,IAAI;AAC9B,OAAI,UAAU;IACb,MAAM,OAAQ,SAAS,cAAwD,EAAE;AACjF,SAAK,oBAAoB,EAAE,WAAW,kCAAkC;AACxE,aAAS,aAAa;;;;CAMzB,MAAM,WAAqB;EAC1B;EACA;EACA,eAAe,MAAM;EACrB,mCAAkB,IAAI,MAAM,EAAC,aAAa;EAC1C;EACA;EACA;AAGD,KAAI,OAAO,SAAS,GAAG;AACtB,WAAS,KAAK,6EAA6E;AAC3F,OAAK,MAAM,OAAO,QAAQ;GACzB,MAAM,cAAc,MAAM,kBAAkB,kBAAkB,GAAG,GAAG;AACpE,aAAU,IAAI,IAAI,gBAAgB,YAAY;AAC9C,OAAI,YAAa,qBAAoB,KAAK,IAAI,eAAe;AAC7D,YAAS,KAAK,6BAA6B,IAAI,YAAY,QAAQ,IAAI,OAAO,UAAU,IAAI,OAAO,GAAG;AACtG,YAAS,KAAK,GAAG,IAAI,eAAe,GAAG,cAAc;AACrD,YAAS,KAAK,GAAG;;;CAKnB,MAAM,WAAW,IAAI,IAAY,CAAC,GAAG,0BAA0B,GAAG,OAAO,KAAK,MAAM,EAAE,eAAe,CAAC,CAAC;CACvG,MAAM,eAA4C,EAAE;AAEpD,MAAK,MAAM,OAAO,oBAAoB;EACrC,MAAM,MAAM,IAAI;EAChB,MAAM,aAAa,CAAC,GAAG,IAAI,aAAa,GAAG,IAAI,gBAAgB;AAC/D,MAAI,WAAW,WAAW,EAAG;EAE7B,MAAM,YAAyD,EAAE;AAEjE,WAAS,KAAK,QAAQ,IAAI,KAAK,GAAG,IAAI,KAAK,yCAAyC;AAEpF,OAAK,MAAM,UAAU,YAAY;AAChC,OAAI,SAAS,IAAI,OAAO,IAAI,CAAE;AAC9B,YAAS,IAAI,OAAO,IAAI;GAGxB,MAAM,YAAY,MAAM,YAAY,IAAI,MAAM,OAAO;GACrD,IAAI;AAEJ,OAAI,cAAc,KAAA,EACjB,eAAc;YACJ,OAAO,OAEjB,KAAI,OAAO,aAAa,WAAW,KAAK,IAAI,OAAO,aAAa,SAAS,IAAI,EAAE;IAC9E,MAAM,SAAS,OAAO,aAAa,MAAM,GAAG,GAAG;AAC/C,kBAAc,UAAU,IAAI,OAAO,IAAI,OAAO;cACpC,MAAM,iBAAiB;AACjC,kBAAc,kBAAkB,GAAG;AACnC,wBAAoB,KAAK,OAAO,IAAI;SAEpC,eAAc,OAAO;OAGtB,eAAc,OAAO;AAGtB,aAAU,IAAI,OAAO,KAAK,YAAY;AAEtC,YAAS,KAAK,KAAK,OAAO,cAAc;AACxC,YAAS,KAAK,GAAG,OAAO,IAAI,GAAG,cAAc;AAC7C,YAAS,KAAK,GAAG;AAEjB,aAAU,KAAK;IACd,KAAK,OAAO;IACZ,aAAa,OAAO;IACpB,OAAO;IACP,QAAQ,OAAO;IACf,CAAC;;AAGH,MAAI,UAAU,SAAS,EACtB,cAAa,KAAK;GACjB,aAAa,IAAI;GACjB,MAAM;GACN,CAAC;;AAKJ,MAAK,MAAM,OAAO,mBACjB,gBAAe,IAAI,YAAY,WAAW,MAAM,gBAAgB;CAIjE,MAAM,8BAAc,IAAI,KAAa;CACrC,MAAM,gBAA0B,EAAE;AAClC,MAAK,MAAM,QAAQ,UAAU;EAC5B,MAAM,UAAU,KAAK,MAAM;AAC3B,MAAI,CAAC,WAAW,QAAQ,WAAW,IAAI,EAAE;AACxC,iBAAc,KAAK,KAAK;AACxB;;EAED,MAAM,QAAQ,QAAQ,QAAQ,IAAI;AAClC,MAAI,QAAQ,GAAG;GACd,MAAM,MAAM,QAAQ,MAAM,GAAG,MAAM;AACnC,eAAY,IAAI,IAAI;GACpB,MAAM,aAAa,UAAU,IAAI,IAAI;AACrC,OAAI,eAAe,KAAA,EAClB,eAAc,KAAK,GAAG,IAAI,GAAG,aAAa;OAE1C,eAAc,KAAK,KAAK;QAGzB,eAAc,KAAK,KAAK;;AAI1B,MAAK,MAAM,CAAC,KAAK,UAAU,UAC1B,KAAI,CAAC,YAAY,IAAI,IAAI,IAAI,CAAC,SAAS,IAAI,IAAI,IAAI,CAAC,yBAAyB,IAAI,IAAI,EAAE;AACtF,gBAAc,KAAK,wBAAwB;AAC3C,gBAAc,KAAK,GAAG,IAAI,GAAG,QAAQ;AACrC,gBAAc,KAAK,GAAG;;CAGxB,MAAM,UAAU,cAAc,KAAK,KAAK;CAGxC,MAAM,aAAaC,eAAAA,mBAAmB,oBAAoB;CAG1D,MAAM,eAAqD,EAAE;CAC7D,IAAI,aAAa;AACjB,MAAK,MAAM,OAAO,mBACjB,MAAK,MAAM,SAAS,IAAI,WAAW,OAClC,KAAI,MAAM,aAAa;AACtB,eAAa,MAAM,WAAW,EAAE,SAAS,MAAM;AAC/C;;CAMH,MAAM,cAAc,iBAAiB,mBAAmB;CAGxD,MAAM,YAAkC,EAAE;AAC1C,MAAK,MAAM,KAAK,WACf,WAAU,KAAK;CAGhB,MAAM,aAAsC,EAC3C,UACA;AAED,KAAI,OAAO,KAAK,UAAU,CAAC,SAAS,EACnC,YAAW,UAAU;AAGtB,YAAW,WAAW,EACrB,oBAAoB,EACnB,UAAU,MACV,EACD;AAKD,QAAO;EACN,kBAAA,GAAA,KAAA,WAJiC,YAAYC,iBAAAA,aAAa;EAK1D;EACA,SAAS;EACT;EACA,qBAAqB,EACpB,QAAQ,EAAE,SAAS,cAAc,EACjC;EACD;EACA,UAAU;GACT,cAAc,OAAO,KAAK,SAAS,CAAC;GACpC;GACA,mBAAmB,oBAAoB;GACvC,kBAAkB,mBAAmB,KAAK,MAAM,EAAE,WAAW,GAAG;GAChE;GACA;GACA,iBAAiB,cAAc;GAC/B;EACD;EACA;;;;;;;;AAWF,SAAgB,iBAAiB,UAAyD;CACzF,MAAM,WAAqB,EAAE;CAG7B,IAAI;AACJ,KAAI;AACH,UAAQC,eAAAA,4BAA4B,MAAM,SAAS;UAC3C,KAAK;AACb,SAAO,kBAAkB,kBAAkB,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAAG;;CAI/F,IAAI,oBAA8B,EAAE;AACpC,KAAI;EACH,MAAM,mBAAA,GAAA,KAAA,OAA4B,MAAM,eAAe;AACvD,MAAI,iBAAiB,YAAY,OAAO,gBAAgB,aAAa,SACpE,qBAAoB,OAAO,KAAK,gBAAgB,SAAS,CAAC,QACxD,OAAO,OAAO,iBACf;UAEM,KAAK;AACb,WAAS,KACR,0CAA0C,eAAe,QAAQ,IAAI,UAAU,OAAO,IAAI,GAC1F;;CAIF,MAAM,iCAAiB,IAAI,KAAqB;AAChD,MAAK,MAAM,QAAQ,MAAM,WAAW,MAAM,KAAK,EAAE;EAChD,MAAM,UAAU,KAAK,MAAM;AAC3B,MAAI,CAAC,WAAW,QAAQ,WAAW,IAAI,CAAE;EACzC,MAAM,QAAQ,QAAQ,QAAQ,IAAI;AAClC,MAAI,QAAQ,EACX,gBAAe,IAAI,QAAQ,MAAM,GAAG,MAAM,EAAE,QAAQ,MAAM,QAAQ,EAAE,CAAC;;AAKxD,KAAI,IAAI,MAAM,YAAY;CACzC,MAAM,YAAY,IAAI,IAAI,MAAM,eAAe;CAC/C,MAAM,oBAAoB,CACzB,GAAG,kBAAkB,QAAQ,OAAO,CAAC,UAAU,IAAI,GAAG,CAAC,EACvD,GAAG,MAAM,YAAY,QAAQ,OAAO,CAAC,kBAAkB,SAAS,GAAG,CAAC,CACpE;CAGD,MAAM,eAAe,mBAAmB;EACvC,YAAY,MAAM;EAClB,UAAU;EACV,YAAY,EAAE;EACd,UAAU,MAAM;EAChB,iBAAiB,MAAM;EACvB,eAAe,MAAM;EACrB,iBAAiB,MAAM;EACvB,aAAa,MAAM;EACnB,eAAe,MAAM;EACrB,aAAa,MAAM;EACnB,gBAAgB,MAAM;EACtB,CAAC;CAGF,MAAM,iBAA2B,EAAE;AACnC,MAAK,MAAM,QAAQ,aAAa,QAAQ,MAAM,KAAK,EAAE;EACpD,MAAM,UAAU,KAAK,MAAM;AAC3B,MAAI,CAAC,WAAW,QAAQ,WAAW,IAAI,EAAE;AACxC,kBAAe,KAAK,KAAK;AACzB;;EAED,MAAM,QAAQ,QAAQ,QAAQ,IAAI;AAClC,MAAI,QAAQ,GAAG;GACd,MAAM,MAAM,QAAQ,MAAM,GAAG,MAAM;AACnC,OAAI,eAAe,IAAI,IAAI,CAE1B,gBAAe,KAAK,GAAG,IAAI,GAAG,eAAe,IAAI,IAAI,GAAG;OAExD,gBAAe,KAAK,KAAK;QAG1B,gBAAe,KAAK,KAAK;;CAK3B,MAAM,aAAa,IAAI,IAAI,kBAAkB;CAC7C,MAAM,YAAY,IAAI,IAAI,aAAa,SAAS,iBAAiB;CACjE,MAAM,QAAQ,CAAC,GAAG,UAAU,CAAC,QAAQ,OAAO,CAAC,WAAW,IAAI,GAAG,CAAC;CAChE,MAAM,UAAU,CAAC,GAAG,WAAW,CAAC,QAAQ,OAAO,CAAC,UAAU,IAAI,GAAG,CAAC;CAClE,MAAM,YAAY,CAAC,GAAG,WAAW,CAAC,QAAQ,OAAO,UAAU,IAAI,GAAG,CAAC;CAGnE,MAAM,gBAAwC,EAAE;CAChD,MAAM,oBAA8B,EAAE;AAGtC,MAAK,MAAM,MAAM,OAAO;EACvB,MAAM,MAAMP,0BAAAA,eAAe,GAAG;AAC9B,MAAI,CAAC,IAAK;AACV,OAAK,MAAM,SAAS,IAAI,QAAQ;GAC/B,MAAM,YAAY,OAAO,KAAK,aAAa,WAAW,CAAC,MACrD,SAAS,KAAK,SAAS,MAAM,QAAQ,CACtC;AACD,OAAI,UACH,eAAc,aAAa,aAAa,WAAW;;;AAMtD,MAAK,MAAM,MAAM,SAAS;EACzB,MAAM,MAAMA,0BAAAA,eAAe,GAAG;AAC9B,MAAI,CAAC,IAAK;AACV,OAAK,MAAM,SAAS,IAAI,OACvB,mBAAkB,KAAK,MAAM,QAAQ;;CAKvC,MAAM,iBAAiB,aAAa,YAAY,QAAQ,MAAM,MAAM,SAAS,EAAE,UAAU,CAAC;CAC1F,MAAM,oBAAoB;CAG1B,MAAM,eAAyB,EAAE;AACjC,MAAK,MAAM,MAAM,OAAO;EACvB,MAAM,MAAMA,0BAAAA,eAAe,GAAG;AAC9B,MAAI,KAAK,SAAS,KAAK,SACtB,cAAa,KAAK,GAAG,IAAI,MAAM,GAAG,IAAI,WAAW;WACvC,KAAK,cACf,cAAa,KAAK,IAAI,cAAc;;CAKtC,IAAI,cAAc;AAClB,MAAK,MAAM,MAAM,OAAO;EACvB,MAAM,MAAMA,0BAAAA,eAAe,GAAG;AAC9B,iBAAe,KAAK,eAAe;;AAEpC,MAAK,MAAM,MAAM,SAAS;EACzB,MAAM,MAAMA,0BAAAA,eAAe,GAAG;AAC9B,iBAAe,KAAK,eAAe;;CAIpC,MAAM,kBAAwD,EAAE;AAChE,MAAK,MAAM,MAAM,OAAO;EACvB,MAAM,MAAMA,0BAAAA,eAAe,GAAG;AAC9B,MAAI,CAAC,IAAK;AACV,OAAK,MAAM,SAAS,IAAI,OACvB,KAAI,MAAM,YACT,iBAAgB,MAAM,WAAW,EAAE,SAAS,MAAM;;AAKrD,QAAO;EACN,iBAAiB,aAAa;EAC9B,SAAS,eAAe,KAAK,KAAK;EAClC;EACA;EACA,qBAAqB,EACpB,QAAQ;GACP,KAAK;GACL,QAAQ;GACR,EACD;EACD;EACA;EACA;EACA,iBAAiB;EACjB,UAAU;GACT;GACA;GACA;GACA,sBAAsB;GACtB;EACD,UAAU,CAAC,GAAG,UAAU,GAAG,aAAa,SAAS;EACjD;;AAKF,SAAS,kBAAoC;AAC5C,QAAO;EACN,iBAAiB;EACjB,SAAS;EACT,SAAS,EAAE;EACX,YAAY,EAAE;EACd,qBAAqB,EAAE,QAAQ,EAAE,SAAS,EAAE,EAAE,EAAE;EAChD,aAAa,EAAE;EACf,UAAU;GACT,cAAc;GACd,YAAY;GACZ,mBAAmB;GACnB,kBAAkB,EAAE;GACpB,iBAAiB,EAAE;GACnB,qBAAqB,EAAE;GACvB,iBAAiB,EAAE;GACnB;EACD,UAAU,EAAE;EACZ;;AAGF,SAAS,YAAY,SAAmC;AACvD,QAAO;EACN,GAAG,iBAAiB;EACpB,UAAU,CAAC,QAAQ;EACnB;;AAGF,SAAS,kBAAkB,SAAyC;AACnE,QAAO;EACN,iBAAiB;EACjB,SAAS;EACT,eAAe,EAAE;EACjB,mBAAmB,EAAE;EACrB,qBAAqB,EAAE,QAAQ;GAAE,KAAK,EAAE;GAAE,QAAQ,EAAE;GAAE,EAAE;EACxD,gBAAgB,EAAE;EAClB,mBAAmB,EAAE;EACrB,cAAc,EAAE;EAChB,iBAAiB,EAAE;EACnB,UAAU;GACT,OAAO,EAAE;GACT,SAAS,EAAE;GACX,WAAW,EAAE;GACb,sBAAsB;GACtB;EACD,UAAU,CAAC,QAAQ;EACnB"}
|
|
1
|
+
{"version":3,"file":"addon-stack.cjs","names":["AddonStackInputSchema","getServiceById","resolve","buildCompanionService","getDbRequirements","quotedStr","generateSkillFiles","YAML_OPTIONS","AddonStackUpdateInputSchema"],"sources":["../src/addon-stack.ts"],"sourcesContent":["import { randomBytes } from \"node:crypto\";\nimport { stringify } from \"yaml\";\nimport { parse as parseYaml } from \"yaml\";\nimport { buildCompanionService, buildPostgresSetup, quotedStr, YAML_OPTIONS } from \"./composer.js\";\nimport { getDbRequirements } from \"./generators/postgres-init.js\";\nimport { generateSkillFiles } from \"./generators/skills.js\";\nimport { resolve } from \"./resolver.js\";\nimport { AddonStackInputSchema, AddonStackUpdateInputSchema } from \"./schema.js\";\nimport { getServiceById } from \"./services/registry.js\";\nimport type {\n\tAddonStackInput,\n\tAddonStackResult,\n\tAddonStackUpdateInput,\n\tAddonStackUpdateResult,\n\tComposeOptions,\n\tProxyRoute,\n\tResolvedService,\n\tResolverOutput,\n\tServiceDefinition,\n\tSkippedService,\n} from \"./types.js\";\n\n// ── Constants ────────────────────────────────────────────────────────────────\n\n/** Services that Clawexa's cloud-init already provisions (or are mandatory platform services). */\nconst INFRA_SERVICE_IDS = new Set([\n\t\"openclaw-gateway\",\n\t\"openclaw-cli\",\n\t\"redis\",\n\t\"postgresql\",\n\t\"open-webui\",\n\t\"caddy\",\n\t\"traefik\",\n\t\"postgres-setup\",\n\t// Mandatory platform services provisioned by Clawexa cloud-init\n\t\"convex\",\n\t\"convex-dashboard\",\n\t\"mission-control\",\n]);\n\n/** Env keys managed by Clawexa's cloud-init — never include in addon env output. */\nconst CLAWEXA_MANAGED_ENV_KEYS = new Set([\n\t\"COMPOSE_FILE\",\n\t\"COMPOSE_PROFILES\",\n\t\"OPENCLAW_VERSION\",\n\t\"OPENCLAW_GATEWAY_TOKEN\",\n\t\"OPENCLAW_GATEWAY_PORT\",\n\t\"OPENCLAW_BRIDGE_PORT\",\n\t\"OPENCLAW_GATEWAY_BIND\",\n\t\"OPENCLAW_CONFIG_DIR\",\n\t\"OPENCLAW_WORKSPACE_DIR\",\n\t\"REDIS_PASSWORD\",\n\t\"REDIS_HOST\",\n\t\"REDIS_PORT\",\n\t\"POSTGRES_USER\",\n\t\"POSTGRES_PASSWORD\",\n\t\"POSTGRES_DB\",\n\t\"POSTGRES_HOST\",\n\t\"POSTGRES_PORT\",\n]);\n\n// ── Helpers ──────────────────────────────────────────────────────────────────\n\n/** Sanitize instanceId into a valid Docker Compose project name. */\nfunction sanitizeProjectName(instanceId: string): string {\n\treturn instanceId\n\t\t.toLowerCase()\n\t\t.replace(/[^a-z0-9-]/g, \"-\")\n\t\t.replace(/^-+|-+$/g, \"\")\n\t\t.replace(/-{2,}/g, \"-\")\n\t\t.slice(0, 64) || \"addon\";\n}\n\n/** Generate a cryptographically secure hex secret of the given byte length. */\nfunction generateHexSecret(bytes: number): string {\n\treturn randomBytes(bytes).toString(\"hex\");\n}\n\n/** Generate a cryptographically secure base64url secret of the given byte length. */\nfunction generateBase64UrlSecret(bytes: number): string {\n\treturn randomBytes(bytes).toString(\"base64url\");\n}\n\n/**\n * Check if a service requires user-provided credentials that are missing.\n * Returns the list of missing credential keys, or empty if all are satisfied.\n *\n * When `generateSecrets` is true, empty-default secrets are auto-generated\n * (passwords, tokens, etc.) and are NOT considered missing. Only secrets\n * with `validation` regex patterns (indicating specific format like API keys)\n * are flagged as missing when not provided by the user.\n */\nfunction getMissingCredentials(\n\tdef: ServiceDefinition,\n\tuserCredentials: Record<string, string> | undefined,\n\tgenerateSecrets: boolean,\n): string[] {\n\tconst missing: string[] = [];\n\tfor (const env of def.environment) {\n\t\t// Skip if not required or not a secret\n\t\tif (!env.required || !env.secret) continue;\n\t\t// Skip if it has a non-empty default value or is a reference\n\t\tif (env.defaultValue && env.defaultValue.length > 0) continue;\n\t\t// Skip if user provided the credential\n\t\tif (userCredentials?.[env.key]) continue;\n\t\t// When generateSecrets is true, empty secrets are auto-generated\n\t\t// Only flag as missing if the env var has a validation pattern\n\t\t// (indicating it needs a specific format like an API key)\n\t\tif (generateSecrets && !env.validation) continue;\n\n\t\tmissing.push(env.key);\n\t}\n\treturn missing;\n}\n\n/**\n * Apply env quirks (from service definition) to the generated env values.\n * Handles: empty_string_crashes → set fixed value, min_length → generate longer secret,\n * must_sync → ensure two keys have the same value.\n */\nfunction applyEnvQuirks(\n\tdef: ServiceDefinition,\n\tenvValues: Map<string, string>,\n\tgenerateSecrets: boolean,\n): void {\n\tif (!def.envQuirks) return;\n\n\tfor (const quirk of def.envQuirks) {\n\t\tswitch (quirk.issue) {\n\t\t\tcase \"empty_string_crashes\": {\n\t\t\t\tif (quirk.fix.type === \"set_value\" && quirk.fix.value !== undefined) {\n\t\t\t\t\tenvValues.set(quirk.key, quirk.fix.value);\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"min_length\": {\n\t\t\t\tif (!generateSecrets) break;\n\t\t\t\tconst current = envValues.get(quirk.key) || \"\";\n\t\t\t\tconst minBytes = quirk.fix.minBytes || 24;\n\t\t\t\tconst minHexLen = minBytes * 2;\n\t\t\t\tif (current.length < minHexLen) {\n\t\t\t\t\tif (quirk.fix.type === \"generate_hex\") {\n\t\t\t\t\t\tenvValues.set(quirk.key, generateHexSecret(minBytes));\n\t\t\t\t\t} else if (quirk.fix.type === \"generate_base64url\") {\n\t\t\t\t\t\tenvValues.set(quirk.key, generateBase64UrlSecret(minBytes));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tcase \"must_sync\": {\n\t\t\t\tif (quirk.fix.type === \"sync_with\" && quirk.fix.syncKey) {\n\t\t\t\t\tconst sourceValue = envValues.get(quirk.key) || envValues.get(quirk.fix.syncKey);\n\t\t\t\t\tif (sourceValue) {\n\t\t\t\t\t\tenvValues.set(quirk.key, sourceValue);\n\t\t\t\t\t\tenvValues.set(quirk.fix.syncKey, sourceValue);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak;\n\t\t\t}\n\t\t}\n\t}\n}\n\n/**\n * Build proxy routes from resolved addon services.\n */\nfunction buildProxyRoutes(services: ResolvedService[]): ProxyRoute[] {\n\tconst routes: ProxyRoute[] = [];\n\tfor (const { definition: def } of services) {\n\t\tconst exposedPort = def.ports.find((p) => p.exposed);\n\t\tif (!exposedPort) continue;\n\n\t\troutes.push({\n\t\t\tserviceId: def.id,\n\t\t\tpath: def.proxyPath || `/${def.id}`,\n\t\t\tport: exposedPort.container,\n\t\t\tprotocol: \"http\",\n\t\t\tstripPrefix: true,\n\t\t});\n\t}\n\treturn routes;\n}\n\n/**\n * Build a minimal ComposeOptions suitable for addon stack generation.\n * No proxy, no gateway, no hardening by default.\n */\nfunction buildAddonComposeOptions(projectName: string, input: AddonStackInput): ComposeOptions {\n\treturn {\n\t\tprojectName,\n\t\tproxy: \"none\",\n\t\tgpu: false,\n\t\tplatform: input.platform ?? \"linux/amd64\",\n\t\tdeployment: \"clawexa\",\n\t\topenclawVersion: input.openclawVersion ?? \"latest\",\n\t\topenclawImage: \"official\",\n\t\thardened: false, // Clawexa default: no cap_drop/security_opt\n\t\topenclawInstallMethod: \"docker\",\n\t};\n}\n\n/**\n * Resolve port conflicts between addon services and reserved ports.\n * Returns a map of serviceId → { originalPort → assignedPort }.\n */\nfunction resolvePortConflicts(\n\taddonServices: ResolvedService[],\n\treservedPorts: number[],\n\tportOverrides?: Record<string, Record<string, number>>,\n): { assignments: Record<string, number>; overrides: Record<string, Record<string, number>> } {\n\tconst usedPorts = new Set(reservedPorts);\n\tconst assignments: Record<string, number> = {};\n\tconst overrides: Record<string, Record<string, number>> = {};\n\n\tfor (const { definition: def } of addonServices) {\n\t\tfor (const port of def.ports) {\n\t\t\tif (!port.exposed) continue;\n\n\t\t\t// Check for user-specified override\n\t\t\tconst userOverride = portOverrides?.[def.id]?.[String(port.host)];\n\t\t\tif (userOverride) {\n\t\t\t\tusedPorts.add(userOverride);\n\t\t\t\tassignments[`${def.id}:${port.container}`] = userOverride;\n\t\t\t\tif (!overrides[def.id]) overrides[def.id] = {};\n\t\t\t\toverrides[def.id][String(port.host)] = userOverride;\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\tlet assignedPort = port.host;\n\t\t\tif (usedPorts.has(assignedPort)) {\n\t\t\t\t// Auto-reassign to port+1000 range\n\t\t\t\tassignedPort = port.host + 1000;\n\t\t\t\twhile (usedPorts.has(assignedPort)) {\n\t\t\t\t\tassignedPort++;\n\t\t\t\t}\n\t\t\t\tif (!overrides[def.id]) overrides[def.id] = {};\n\t\t\t\toverrides[def.id][String(port.host)] = assignedPort;\n\t\t\t}\n\t\t\tusedPorts.add(assignedPort);\n\t\t\tassignments[`${def.id}:${port.container}`] = assignedPort;\n\t\t}\n\t}\n\n\treturn { assignments, overrides };\n}\n\n// ── Main: generateAddonStack ─────────────────────────────────────────────────\n\n/**\n * Generates a Docker Compose override stack containing only addon services\n * for Clawexa managed instances. Infrastructure services (gateway, redis,\n * postgres, open-webui, caddy) are excluded since Clawexa's cloud-init\n * already provisions them.\n *\n * This function never throws. Errors are reported via `warnings` and\n * `metadata.skippedServices`.\n */\nexport function generateAddonStack(rawInput: AddonStackInput): AddonStackResult {\n\tconst warnings: string[] = [];\n\tconst skippedServices: SkippedService[] = [];\n\tconst generatedSecretKeys: string[] = [];\n\n\t// 1. Parse & validate input\n\tlet input: AddonStackInput;\n\ttry {\n\t\tinput = AddonStackInputSchema.parse(rawInput);\n\t} catch (err) {\n\t\treturn emptyResult(`Invalid input: ${err instanceof Error ? err.message : String(err)}`);\n\t}\n\n\tconst projectName = sanitizeProjectName(input.instanceId);\n\n\t// 2. Filter out infrastructure service IDs from request\n\tconst addonServiceIds = input.services.filter((id) => {\n\t\tif (INFRA_SERVICE_IDS.has(id)) {\n\t\t\twarnings.push(`Service \"${id}\" is managed by Clawexa infrastructure and was excluded.`);\n\t\t\treturn false;\n\t\t}\n\t\treturn true;\n\t});\n\n\tif (addonServiceIds.length === 0) {\n\t\treturn emptyResult(\"No addon services requested (all were infrastructure services).\");\n\t}\n\n\t// 3. Validate all service IDs exist in registry\n\tconst validServiceIds: string[] = [];\n\tfor (const id of addonServiceIds) {\n\t\tconst svc = getServiceById(id);\n\t\tif (!svc) {\n\t\t\tskippedServices.push({\n\t\t\t\tserviceId: id,\n\t\t\t\treason: \"unknown_service\",\n\t\t\t\tdetails: `Service \"${id}\" does not exist in the registry.`,\n\t\t\t});\n\t\t} else {\n\t\t\tvalidServiceIds.push(id);\n\t\t}\n\t}\n\n\tif (validServiceIds.length === 0) {\n\t\treturn {\n\t\t\t...emptyResultBase(),\n\t\t\tmetadata: {\n\t\t\t\t...emptyResultBase().metadata,\n\t\t\t\tskippedServices,\n\t\t\t},\n\t\t\twarnings: [...warnings, \"No valid addon services to deploy.\"],\n\t\t};\n\t}\n\n\t// 4. Resolve dependencies\n\tlet resolved: ResolverOutput;\n\ttry {\n\t\tresolved = resolve({\n\t\t\tservices: validServiceIds,\n\t\t\tskillPacks: input.skillPacks,\n\t\t\taiProviders: input.aiProviders,\n\t\t\tplatform: input.platform ?? \"linux/amd64\",\n\t\t});\n\t} catch (err) {\n\t\treturn {\n\t\t\t...emptyResultBase(),\n\t\t\tmetadata: {\n\t\t\t\t...emptyResultBase().metadata,\n\t\t\t\tskippedServices,\n\t\t\t},\n\t\t\twarnings: [\n\t\t\t\t...warnings,\n\t\t\t\t`Dependency resolution failed: ${err instanceof Error ? err.message : String(err)}`,\n\t\t\t],\n\t\t};\n\t}\n\n\t// Forward resolver warnings\n\tfor (const w of resolved.warnings) {\n\t\twarnings.push(w.message);\n\t}\n\n\t// 5. Filter resolved services: keep only addon services (not infra)\n\tconst addonResolved: ResolvedService[] = [];\n\tfor (const svc of resolved.services) {\n\t\tif (INFRA_SERVICE_IDS.has(svc.definition.id)) continue;\n\t\taddonResolved.push(svc);\n\t}\n\n\t// 6. Check credentials, images, platform, GPU for each addon service\n\tconst deployableServices: ResolvedService[] = [];\n\tfor (const svc of addonResolved) {\n\t\tconst def = svc.definition;\n\n\t\t// Check for git-based services without prebuilt image\n\t\tif (def.gitSource && def.buildContext && !def.image) {\n\t\t\tconst prebuilt = def.prebuiltImage || input.prebuiltImages[def.id];\n\t\t\tif (!prebuilt) {\n\t\t\t\tskippedServices.push({\n\t\t\t\t\tserviceId: def.id,\n\t\t\t\t\treason: \"no_image\",\n\t\t\t\t\tdetails: `Service \"${def.name}\" requires building from source but no pre-built image is available.`,\n\t\t\t\t});\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t}\n\n\t\t// Check GPU requirement — skip if host has no GPU support\n\t\tif (def.gpuRequired && !input.gpu) {\n\t\t\tskippedServices.push({\n\t\t\t\tserviceId: def.id,\n\t\t\t\treason: \"gpu_required\",\n\t\t\t\tdetails: `Service \"${def.name}\" requires a GPU but the host does not have GPU support.`,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\n\t\t// Check user-provided credentials\n\t\tconst userCreds = input.credentials[def.id];\n\t\tconst missing = getMissingCredentials(def, userCreds, input.generateSecrets);\n\t\tif (missing.length > 0) {\n\t\t\tskippedServices.push({\n\t\t\t\tserviceId: def.id,\n\t\t\t\treason: \"missing_credentials\",\n\t\t\t\tdetails: `Service \"${def.name}\" requires credentials: ${missing.join(\", \")}`,\n\t\t\t\trequiredCredentials: missing,\n\t\t\t});\n\t\t\tcontinue;\n\t\t}\n\n\t\tdeployableServices.push(svc);\n\t}\n\n\tif (deployableServices.length === 0) {\n\t\treturn {\n\t\t\t...emptyResultBase(),\n\t\t\tmetadata: {\n\t\t\t\t...emptyResultBase().metadata,\n\t\t\t\tskippedServices,\n\t\t\t},\n\t\t\twarnings: [...warnings, \"No deployable addon services after filtering.\"],\n\t\t};\n\t}\n\n\t// 7. Resolve port conflicts (include ports from existingServices)\n\tconst allReservedPorts = [...input.reservedPorts];\n\tfor (const existingId of input.existingServices) {\n\t\tconst existingDef = getServiceById(existingId);\n\t\tif (existingDef) {\n\t\t\tfor (const port of existingDef.ports) {\n\t\t\t\tif (port.exposed) allReservedPorts.push(port.host);\n\t\t\t}\n\t\t}\n\t}\n\tconst portConflicts = resolvePortConflicts(\n\t\tdeployableServices,\n\t\tallReservedPorts,\n\t\tinput.portOverrides,\n\t);\n\n\t// Build a fake \"full\" resolved output for buildCompanionService\n\t// It needs to see all services to resolve depends_on references\n\tconst addonResolvedOutput: ResolverOutput = {\n\t\tservices: deployableServices,\n\t\taddedDependencies: resolved.addedDependencies,\n\t\tremovedConflicts: resolved.removedConflicts,\n\t\twarnings: resolved.warnings,\n\t\terrors: [],\n\t\tisValid: true,\n\t\testimatedMemoryMB: deployableServices.reduce(\n\t\t\t(sum, s) => sum + (s.definition.minMemoryMB ?? 128),\n\t\t\t0,\n\t\t),\n\t\taiProviders: input.aiProviders ?? [],\n\t\tgsdRuntimes: [],\n\t};\n\n\t// 8. Build compose options (no hardening for Clawexa)\n\tconst composeOptions = buildAddonComposeOptions(projectName, input);\n\tif (Object.keys(portConflicts.overrides).length > 0) {\n\t\tcomposeOptions.portOverrides = portConflicts.overrides;\n\t}\n\n\t// 9. Build per-service entries\n\tconst services: Record<string, Record<string, unknown>> = {};\n\tconst allVolumes = new Set<string>();\n\tconst envValues = new Map<string, string>();\n\n\tfor (const svc of deployableServices) {\n\t\tconst def = svc.definition;\n\t\ttry {\n\t\t\t// Handle prebuilt image substitution for git-based services\n\t\t\tlet effectiveDef = def;\n\t\t\tif (def.gitSource && def.buildContext && !def.image) {\n\t\t\t\tconst prebuiltImage = def.prebuiltImage || input.prebuiltImages[def.id];\n\t\t\t\tif (prebuiltImage) {\n\t\t\t\t\tconst [img, tag] = prebuiltImage.includes(\":\")\n\t\t\t\t\t\t? prebuiltImage.split(\":\")\n\t\t\t\t\t\t: [prebuiltImage, \"latest\"];\n\t\t\t\t\teffectiveDef = {\n\t\t\t\t\t\t...def,\n\t\t\t\t\t\timage: img,\n\t\t\t\t\t\timageTag: tag,\n\t\t\t\t\t\tgitSource: undefined,\n\t\t\t\t\t\tbuildContext: undefined,\n\t\t\t\t\t};\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst { entry, volumeNames } = buildCompanionService(\n\t\t\t\teffectiveDef,\n\t\t\t\taddonResolvedOutput,\n\t\t\t\tcomposeOptions,\n\t\t\t\tallVolumes,\n\t\t\t);\n\n\t\t\t// Remove profiles from the service entry\n\t\t\tdelete (entry as Record<string, unknown>).profiles;\n\n\t\t\t// Remove depends_on references to infrastructure services\n\t\t\tif (entry.depends_on) {\n\t\t\t\tconst deps = entry.depends_on as Record<string, { condition: string }>;\n\t\t\t\tfor (const depId of Object.keys(deps)) {\n\t\t\t\t\tif (INFRA_SERVICE_IDS.has(depId)) {\n\t\t\t\t\t\tdelete deps[depId];\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tif (Object.keys(deps).length === 0) {\n\t\t\t\t\tdelete entry.depends_on;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tservices[def.id] = entry;\n\t\t\tfor (const v of volumeNames) allVolumes.add(v);\n\n\t\t\t// Inject user-provided credentials into env\n\t\t\tconst userCreds = input.credentials[def.id];\n\t\t\tif (userCreds) {\n\t\t\t\tfor (const [key, value] of Object.entries(userCreds)) {\n\t\t\t\t\tenvValues.set(key, value);\n\t\t\t\t}\n\t\t\t\t// Sync referenced keys: if a user provides e.g. DB_POSTGRESDB_PASSWORD\n\t\t\t\t// and the env var's defaultValue is \"${N8N_DB_PASSWORD}\", sync the ref key\n\t\t\t\t// so postgres-setup uses the same password.\n\t\t\t\tfor (const envVar of def.environment) {\n\t\t\t\t\tif (\n\t\t\t\t\t\tuserCreds[envVar.key] &&\n\t\t\t\t\t\tenvVar.defaultValue?.startsWith(\"${\") &&\n\t\t\t\t\t\tenvVar.defaultValue?.endsWith(\"}\")\n\t\t\t\t\t) {\n\t\t\t\t\t\tconst refKey = envVar.defaultValue.slice(2, -1);\n\t\t\t\t\t\tenvValues.set(refKey, userCreds[envVar.key]);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} catch (err) {\n\t\t\tskippedServices.push({\n\t\t\t\tserviceId: def.id,\n\t\t\t\treason: \"resolution_error\",\n\t\t\t\tdetails: `Failed to build compose entry: ${err instanceof Error ? err.message : String(err)}`,\n\t\t\t});\n\t\t\twarnings.push(`Failed to process service \"${def.name}\": ${err instanceof Error ? err.message : String(err)}`);\n\t\t}\n\t}\n\n\t// 10. Build postgres-setup if any addon needs a DB\n\t// We need to check if any of our deployable services require DB setup\n\t// and if postgresql is in the infrastructure (it is for Clawexa)\n\tconst dbReqs = getDbRequirements(addonResolvedOutput);\n\tif (dbReqs.length > 0) {\n\t\t// Build a custom postgres-setup that references the existing PostgreSQL\n\t\t// We can't use buildPostgresSetup directly because it checks for postgresql\n\t\t// in the resolved services. Instead, build it manually.\n\t\tconst scriptLines = [\"echo '=== PostgreSQL database setup (addon) ==='\", \"FAILED=0\"];\n\n\t\tfor (const req of dbReqs) {\n\t\t\tscriptLines.push(\n\t\t\t\t`echo \"Setting up database '${req.dbName}' with user '${req.dbUser}'...\"`,\n\t\t\t\t`psql -c \"SELECT 1 FROM pg_roles WHERE rolname='${req.dbUser}'\" | grep -q 1 || psql -c \"CREATE ROLE ${req.dbUser} WITH LOGIN PASSWORD '$$${req.passwordEnvVar}'\"`,\n\t\t\t\t`psql -c \"ALTER ROLE ${req.dbUser} WITH LOGIN PASSWORD '$$${req.passwordEnvVar}'\"`,\n\t\t\t\t`psql -tc \"SELECT 1 FROM pg_database WHERE datname='${req.dbName}'\" | grep -q 1 || psql -c \"CREATE DATABASE ${req.dbName} OWNER ${req.dbUser}\"`,\n\t\t\t\t`psql -c \"GRANT ALL PRIVILEGES ON DATABASE ${req.dbName} TO ${req.dbUser}\" || FAILED=1`,\n\t\t\t\t`echo \" Done: ${req.dbName}\"`,\n\t\t\t);\n\t\t}\n\t\tscriptLines.push(\"echo '=== All databases ready ==='\", \"exit $$FAILED\");\n\n\t\tconst dbEnv: Record<string, string> = {\n\t\t\tPGHOST: \"postgresql\",\n\t\t\tPGUSER: \"${POSTGRES_USER:-openclaw}\",\n\t\t\tPGDATABASE: \"${POSTGRES_DB:-openclaw}\",\n\t\t\tPGPASSWORD: \"${POSTGRES_PASSWORD}\",\n\t\t};\n\t\tfor (const req of dbReqs) {\n\t\t\tdbEnv[req.passwordEnvVar] = `\\${${req.passwordEnvVar}}`;\n\t\t}\n\n\t\tservices[\"postgres-setup\"] = {\n\t\t\timage: \"postgres:17-alpine\",\n\t\t\tdepends_on: {\n\t\t\t\tpostgresql: { condition: \"service_healthy\" },\n\t\t\t},\n\t\t\tenvironment: dbEnv,\n\t\t\tentrypoint: [\"/bin/sh\", \"-c\"],\n\t\t\tcommand: [scriptLines.join(\"\\n\")],\n\t\t\trestart: quotedStr(\"no\"),\n\t\t\tnetworks: [\"openclaw-network\"],\n\t\t};\n\n\t\t// Update addon services that need DB to depend on postgres-setup\n\t\tfor (const req of dbReqs) {\n\t\t\tconst svcEntry = services[req.serviceId];\n\t\t\tif (svcEntry) {\n\t\t\t\tconst deps = (svcEntry.depends_on as Record<string, { condition: string }>) || {};\n\t\t\t\tdeps[\"postgres-setup\"] = { condition: \"service_completed_successfully\" };\n\t\t\t\tsvcEntry.depends_on = deps;\n\t\t\t}\n\t\t}\n\t}\n\n\t// 11. Generate secrets and env file\n\tconst envLines: string[] = [\n\t\t\"# ═══════════════════════════════════════════════════════════════════════════════\",\n\t\t\"# OpenClaw Addon Stack Environment\",\n\t\t`# Instance: ${input.instanceId}`,\n\t\t`# Generated at ${new Date().toISOString()}`,\n\t\t\"# ═══════════════════════════════════════════════════════════════════════════════\",\n\t\t\"\",\n\t];\n\n\t// DB passwords first\n\tif (dbReqs.length > 0) {\n\t\tenvLines.push(\"# ── Per-Service Database Passwords ──────────────────────────────────────\");\n\t\tfor (const req of dbReqs) {\n\t\t\tconst secretValue = input.generateSecrets ? generateHexSecret(24) : \"\";\n\t\t\tenvValues.set(req.passwordEnvVar, secretValue);\n\t\t\tif (secretValue) generatedSecretKeys.push(req.passwordEnvVar);\n\t\t\tenvLines.push(`# PostgreSQL password for ${req.serviceName} (db: ${req.dbName}, user: ${req.dbUser})`);\n\t\t\tenvLines.push(`${req.passwordEnvVar}=${secretValue}`);\n\t\t\tenvLines.push(\"\");\n\t\t}\n\t}\n\n\t// Per-service env vars\n\tconst seenKeys = new Set<string>([...CLAWEXA_MANAGED_ENV_KEYS, ...dbReqs.map((r) => r.passwordEnvVar)]);\n\tconst envVarGroups: AddonStackResult[\"envVars\"] = [];\n\n\tfor (const svc of deployableServices) {\n\t\tconst def = svc.definition;\n\t\tconst allEnvVars = [...def.environment, ...def.openclawEnvVars];\n\t\tif (allEnvVars.length === 0) continue;\n\n\t\tconst groupVars: AddonStackResult[\"envVars\"][number][\"vars\"] = [];\n\n\t\tenvLines.push(`# ── ${def.icon} ${def.name} ──────────────────────────────────────`);\n\n\t\tfor (const envVar of allEnvVars) {\n\t\t\tif (seenKeys.has(envVar.key)) continue;\n\t\t\tseenKeys.add(envVar.key);\n\n\t\t\t// Check if user provided this credential\n\t\t\tconst userValue = input.credentials[def.id]?.[envVar.key];\n\t\t\tlet actualValue: string;\n\n\t\t\tif (userValue !== undefined) {\n\t\t\t\tactualValue = userValue;\n\t\t\t} else if (envVar.secret) {\n\t\t\t\t// Resolve references like ${N8N_DB_PASSWORD}\n\t\t\t\tif (envVar.defaultValue.startsWith(\"${\") && envVar.defaultValue.endsWith(\"}\")) {\n\t\t\t\t\tconst refKey = envVar.defaultValue.slice(2, -1);\n\t\t\t\t\tactualValue = envValues.get(refKey) || envVar.defaultValue;\n\t\t\t\t} else if (input.generateSecrets) {\n\t\t\t\t\tactualValue = generateHexSecret(24);\n\t\t\t\t\tgeneratedSecretKeys.push(envVar.key);\n\t\t\t\t} else {\n\t\t\t\t\tactualValue = envVar.defaultValue;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tactualValue = envVar.defaultValue;\n\t\t\t}\n\n\t\t\tenvValues.set(envVar.key, actualValue);\n\n\t\t\tenvLines.push(`# ${envVar.description}`);\n\t\t\tenvLines.push(`${envVar.key}=${actualValue}`);\n\t\t\tenvLines.push(\"\");\n\n\t\t\tgroupVars.push({\n\t\t\t\tkey: envVar.key,\n\t\t\t\tdescription: envVar.description,\n\t\t\t\tvalue: actualValue,\n\t\t\t\tsecret: envVar.secret,\n\t\t\t});\n\t\t}\n\n\t\tif (groupVars.length > 0) {\n\t\t\tenvVarGroups.push({\n\t\t\t\tserviceName: def.name,\n\t\t\t\tvars: groupVars,\n\t\t\t});\n\t\t}\n\t}\n\n\t// Apply env quirks after all values are generated\n\tfor (const svc of deployableServices) {\n\t\tapplyEnvQuirks(svc.definition, envValues, input.generateSecrets);\n\t}\n\n\t// Rebuild env lines from envValues (quirks may have modified values or introduced new keys)\n\tconst quirkedKeys = new Set<string>();\n\tconst finalEnvLines: string[] = [];\n\tfor (const line of envLines) {\n\t\tconst trimmed = line.trim();\n\t\tif (!trimmed || trimmed.startsWith(\"#\")) {\n\t\t\tfinalEnvLines.push(line);\n\t\t\tcontinue;\n\t\t}\n\t\tconst eqIdx = trimmed.indexOf(\"=\");\n\t\tif (eqIdx > 0) {\n\t\t\tconst key = trimmed.slice(0, eqIdx);\n\t\t\tquirkedKeys.add(key);\n\t\t\tconst fixedValue = envValues.get(key);\n\t\t\tif (fixedValue !== undefined) {\n\t\t\t\tfinalEnvLines.push(`${key}=${fixedValue}`);\n\t\t\t} else {\n\t\t\t\tfinalEnvLines.push(line);\n\t\t\t}\n\t\t} else {\n\t\t\tfinalEnvLines.push(line);\n\t\t}\n\t}\n\t// Append any new keys introduced by quirks (e.g., must_sync creating a new key)\n\tfor (const [key, value] of envValues) {\n\t\tif (!quirkedKeys.has(key) && !seenKeys.has(key) && !CLAWEXA_MANAGED_ENV_KEYS.has(key)) {\n\t\t\tfinalEnvLines.push(`# Synced by env quirk`);\n\t\t\tfinalEnvLines.push(`${key}=${value}`);\n\t\t\tfinalEnvLines.push(\"\");\n\t\t}\n\t}\n\tconst envFile = finalEnvLines.join(\"\\n\");\n\n\t// 12. Generate skill files\n\tconst skillFiles = generateSkillFiles(addonResolvedOutput);\n\n\t// 13. Build openclaw config patch\n\tconst skillEntries: Record<string, { enabled: boolean }> = {};\n\tlet skillCount = 0;\n\tfor (const svc of deployableServices) {\n\t\tfor (const skill of svc.definition.skills) {\n\t\t\tif (skill.autoInstall) {\n\t\t\t\tskillEntries[skill.skillId] = { enabled: true };\n\t\t\t\tskillCount++;\n\t\t\t}\n\t\t}\n\t}\n\n\t// 14. Build proxy routes\n\tconst proxyRoutes = buildProxyRoutes(deployableServices);\n\n\t// 14b. Build additional files (e.g. sandbox.toml for opensandbox)\n\tconst additionalFiles: Record<string, string> = {};\n\tif (deployableServices.some((s) => s.definition.id === \"opensandbox\")) {\n\t\tadditionalFiles[\"sandbox.toml\"] = [\n\t\t\t\"[server]\",\n\t\t\t'host = \"0.0.0.0\"',\n\t\t\t\"port = 8080\",\n\t\t\t'log_level = \"INFO\"',\n\t\t\t'api_key = \"${OPEN_SANDBOX_API_KEY}\"',\n\t\t\t\"\",\n\t\t\t\"[runtime]\",\n\t\t\t'type = \"docker\"',\n\t\t\t'execd_image = \"opensandbox/execd:v1.0.6\"',\n\t\t\t\"\",\n\t\t\t\"[docker]\",\n\t\t\t\"network_mode = \\\"bridge\\\"\",\n\t\t\t'drop_capabilities = [\"NET_ADMIN\", \"SYS_ADMIN\", \"SYS_PTRACE\", \"MKNOD\", \"NET_RAW\", \"SYS_RAWIO\"]',\n\t\t\t\"no_new_privileges = true\",\n\t\t\t\"pids_limit = 512\",\n\t\t\t\"\",\n\t\t\t\"[secure_runtime]\",\n\t\t\t'type = \"gvisor\"',\n\t\t\t\"\",\n\t\t].join(\"\\n\");\n\t}\n\n\t// 14c. Build pre-pull images list\n\tconst prePullImages: Array<{ image: string; priority: 1 | 2 | 3 }> = [];\n\tif (deployableServices.some((s) => s.definition.id === \"opensandbox\")) {\n\t\tprePullImages.push(\n\t\t\t// Priority 1: always pulled (core + Homespace)\n\t\t\t{ image: \"opensandbox/server:v1.0.6\", priority: 1 },\n\t\t\t{ image: \"opensandbox/execd:v1.0.6\", priority: 1 },\n\t\t\t{ image: \"opensandbox/desktop:latest\", priority: 1 },\n\t\t\t{ image: \"opensandbox/chrome:latest\", priority: 1 },\n\t\t\t// Priority 2: recommended (common languages)\n\t\t\t{ image: \"opensandbox/code-interpreter:python\", priority: 2 },\n\t\t\t{ image: \"opensandbox/code-interpreter:node\", priority: 2 },\n\t\t\t// Priority 3: optional (full multi-lang and IDE)\n\t\t\t{ image: \"opensandbox/code-interpreter:latest\", priority: 3 },\n\t\t\t{ image: \"opensandbox/vscode:latest\", priority: 3 },\n\t\t);\n\t}\n\n\t// 15. Compose single YAML\n\tconst volumeMap: Record<string, null> = {};\n\tfor (const v of allVolumes) {\n\t\tvolumeMap[v] = null;\n\t}\n\n\tconst composeDoc: Record<string, unknown> = {\n\t\tservices,\n\t};\n\n\tif (Object.keys(volumeMap).length > 0) {\n\t\tcomposeDoc.volumes = volumeMap;\n\t}\n\n\tcomposeDoc.networks = {\n\t\t\"openclaw-network\": {\n\t\t\texternal: true,\n\t\t},\n\t};\n\n\tconst composeOverride = stringify(composeDoc, YAML_OPTIONS);\n\n\t// 16. Return result\n\treturn {\n\t\tcomposeOverride,\n\t\tenvFile,\n\t\tenvVars: envVarGroups,\n\t\tskillFiles,\n\t\topenclawConfigPatch: {\n\t\t\tskills: { entries: skillEntries },\n\t\t},\n\t\tproxyRoutes,\n\t\tadditionalFiles,\n\t\tmetadata: {\n\t\t\tserviceCount: Object.keys(services).length,\n\t\t\tskillCount,\n\t\t\testimatedMemoryMB: addonResolvedOutput.estimatedMemoryMB,\n\t\t\tresolvedServices: deployableServices.map((s) => s.definition.id),\n\t\t\tskippedServices,\n\t\t\tgeneratedSecretKeys,\n\t\t\tportAssignments: portConflicts.assignments,\n\t\t\tprePullImages,\n\t\t},\n\t\twarnings,\n\t};\n}\n\n// ── Main: updateAddonStack ───────────────────────────────────────────────────\n\n/**\n * Incrementally updates an existing addon stack by adding or removing services.\n * Preserves existing env values (never overwrites user-customized values).\n *\n * This function never throws.\n */\nexport function updateAddonStack(rawInput: AddonStackUpdateInput): AddonStackUpdateResult {\n\tconst warnings: string[] = [];\n\n\t// 1. Parse & validate\n\tlet input: AddonStackUpdateInput;\n\ttry {\n\t\tinput = AddonStackUpdateInputSchema.parse(rawInput);\n\t} catch (err) {\n\t\treturn emptyUpdateResult(`Invalid input: ${err instanceof Error ? err.message : String(err)}`);\n\t}\n\n\t// 2. Parse existing compose YAML to extract current service list\n\tlet currentServiceIds: string[] = [];\n\ttry {\n\t\tconst existingCompose = parseYaml(input.currentCompose);\n\t\tif (existingCompose?.services && typeof existingCompose.services === \"object\") {\n\t\t\tcurrentServiceIds = Object.keys(existingCompose.services).filter(\n\t\t\t\t(id) => id !== \"postgres-setup\",\n\t\t\t);\n\t\t}\n\t} catch (err) {\n\t\twarnings.push(\n\t\t\t`Failed to parse existing compose YAML: ${err instanceof Error ? err.message : String(err)}`,\n\t\t);\n\t}\n\n\t// 3. Parse existing env into a map\n\tconst existingEnvMap = new Map<string, string>();\n\tfor (const line of input.currentEnv.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\t\tif (!trimmed || trimmed.startsWith(\"#\")) continue;\n\t\tconst eqIdx = trimmed.indexOf(\"=\");\n\t\tif (eqIdx > 0) {\n\t\t\texistingEnvMap.set(trimmed.slice(0, eqIdx), trimmed.slice(eqIdx + 1));\n\t\t}\n\t}\n\n\t// 4. Compute desired service list\n\tconst addSet = new Set(input.addServices);\n\tconst removeSet = new Set(input.removeServices);\n\tconst desiredServiceIds = [\n\t\t...currentServiceIds.filter((id) => !removeSet.has(id)),\n\t\t...input.addServices.filter((id) => !currentServiceIds.includes(id)),\n\t];\n\n\t// 5. Generate the full target state\n\tconst targetResult = generateAddonStack({\n\t\tinstanceId: input.instanceId,\n\t\tservices: desiredServiceIds,\n\t\tskillPacks: [],\n\t\tplatform: input.platform,\n\t\topenclawVersion: input.openclawVersion,\n\t\treservedPorts: input.reservedPorts,\n\t\tgenerateSecrets: input.generateSecrets,\n\t\tcredentials: input.credentials,\n\t\tportOverrides: input.portOverrides,\n\t\taiProviders: input.aiProviders,\n\t\tprebuiltImages: input.prebuiltImages,\n\t});\n\n\t// 6. Merge env: preserve existing values, only add new ones\n\tconst mergedEnvLines: string[] = [];\n\tfor (const line of targetResult.envFile.split(\"\\n\")) {\n\t\tconst trimmed = line.trim();\n\t\tif (!trimmed || trimmed.startsWith(\"#\")) {\n\t\t\tmergedEnvLines.push(line);\n\t\t\tcontinue;\n\t\t}\n\t\tconst eqIdx = trimmed.indexOf(\"=\");\n\t\tif (eqIdx > 0) {\n\t\t\tconst key = trimmed.slice(0, eqIdx);\n\t\t\tif (existingEnvMap.has(key)) {\n\t\t\t\t// Preserve existing value\n\t\t\t\tmergedEnvLines.push(`${key}=${existingEnvMap.get(key)}`);\n\t\t\t} else {\n\t\t\t\tmergedEnvLines.push(line);\n\t\t\t}\n\t\t} else {\n\t\t\tmergedEnvLines.push(line);\n\t\t}\n\t}\n\n\t// 7. Compute diffs\n\tconst currentSet = new Set(currentServiceIds);\n\tconst targetSet = new Set(targetResult.metadata.resolvedServices);\n\tconst added = [...targetSet].filter((id) => !currentSet.has(id));\n\tconst removed = [...currentSet].filter((id) => !targetSet.has(id));\n\tconst unchanged = [...currentSet].filter((id) => targetSet.has(id));\n\n\t// 8. Compute skill diffs\n\tconst newSkillFiles: Record<string, string> = {};\n\tconst removedSkillSlugs: string[] = [];\n\n\t// New skills from added services\n\tfor (const id of added) {\n\t\tconst def = getServiceById(id);\n\t\tif (!def) continue;\n\t\tfor (const skill of def.skills) {\n\t\t\tconst skillPath = Object.keys(targetResult.skillFiles).find(\n\t\t\t\t(path) => path.includes(skill.skillId),\n\t\t\t);\n\t\t\tif (skillPath) {\n\t\t\t\tnewSkillFiles[skillPath] = targetResult.skillFiles[skillPath];\n\t\t\t}\n\t\t}\n\t}\n\n\t// Removed skills from removed services\n\tfor (const id of removed) {\n\t\tconst def = getServiceById(id);\n\t\tif (!def) continue;\n\t\tfor (const skill of def.skills) {\n\t\t\tremovedSkillSlugs.push(skill.skillId);\n\t\t}\n\t}\n\n\t// 9. Proxy route diffs\n\tconst addProxyRoutes = targetResult.proxyRoutes.filter((r) => added.includes(r.serviceId));\n\tconst removeProxyRoutes = removed;\n\n\t// 10. Images to pull for new services\n\tconst imagesToPull: string[] = [];\n\tfor (const id of added) {\n\t\tconst def = getServiceById(id);\n\t\tif (def?.image && def?.imageTag) {\n\t\t\timagesToPull.push(`${def.image}:${def.imageTag}`);\n\t\t} else if (def?.prebuiltImage) {\n\t\t\timagesToPull.push(def.prebuiltImage);\n\t\t}\n\t}\n\n\t// 11. Estimate memory delta\n\tlet memoryDelta = 0;\n\tfor (const id of added) {\n\t\tconst def = getServiceById(id);\n\t\tmemoryDelta += def?.minMemoryMB ?? 128;\n\t}\n\tfor (const id of removed) {\n\t\tconst def = getServiceById(id);\n\t\tmemoryDelta -= def?.minMemoryMB ?? 128;\n\t}\n\n\t// Add existing skills to the config patch\n\tconst addSkillEntries: Record<string, { enabled: boolean }> = {};\n\tfor (const id of added) {\n\t\tconst def = getServiceById(id);\n\t\tif (!def) continue;\n\t\tfor (const skill of def.skills) {\n\t\t\tif (skill.autoInstall) {\n\t\t\t\taddSkillEntries[skill.skillId] = { enabled: true };\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\tcomposeOverride: targetResult.composeOverride,\n\t\tenvFile: mergedEnvLines.join(\"\\n\"),\n\t\tnewSkillFiles,\n\t\tremovedSkillSlugs,\n\t\topenclawConfigPatch: {\n\t\t\tskills: {\n\t\t\t\tadd: addSkillEntries,\n\t\t\t\tremove: removedSkillSlugs,\n\t\t\t},\n\t\t},\n\t\taddProxyRoutes,\n\t\tremoveProxyRoutes,\n\t\timagesToPull,\n\t\trestartRequired: added, // New services need starting, not restarting\n\t\tmetadata: {\n\t\t\tadded,\n\t\t\tremoved,\n\t\t\tunchanged,\n\t\t\testimatedMemoryDelta: memoryDelta,\n\t\t},\n\t\twarnings: [...warnings, ...targetResult.warnings],\n\t};\n}\n\n// ── Empty Result Helpers ─────────────────────────────────────────────────────\n\nfunction emptyResultBase(): AddonStackResult {\n\treturn {\n\t\tcomposeOverride: \"services: {}\\n\",\n\t\tenvFile: \"\",\n\t\tenvVars: [],\n\t\tskillFiles: {},\n\t\topenclawConfigPatch: { skills: { entries: {} } },\n\t\tproxyRoutes: [],\n\t\tadditionalFiles: {},\n\t\tmetadata: {\n\t\t\tserviceCount: 0,\n\t\t\tskillCount: 0,\n\t\t\testimatedMemoryMB: 0,\n\t\t\tresolvedServices: [],\n\t\t\tskippedServices: [],\n\t\t\tgeneratedSecretKeys: [],\n\t\t\tportAssignments: {},\n\t\t\tprePullImages: [],\n\t\t},\n\t\twarnings: [],\n\t};\n}\n\nfunction emptyResult(warning: string): AddonStackResult {\n\treturn {\n\t\t...emptyResultBase(),\n\t\twarnings: [warning],\n\t};\n}\n\nfunction emptyUpdateResult(warning: string): AddonStackUpdateResult {\n\treturn {\n\t\tcomposeOverride: \"services: {}\\n\",\n\t\tenvFile: \"\",\n\t\tnewSkillFiles: {},\n\t\tremovedSkillSlugs: [],\n\t\topenclawConfigPatch: { skills: { add: {}, remove: [] } },\n\t\taddProxyRoutes: [],\n\t\tremoveProxyRoutes: [],\n\t\timagesToPull: [],\n\t\trestartRequired: [],\n\t\tmetadata: {\n\t\t\tadded: [],\n\t\t\tremoved: [],\n\t\t\tunchanged: [],\n\t\t\testimatedMemoryDelta: 0,\n\t\t},\n\t\twarnings: [warning],\n\t};\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"addon-stack.d.cts","names":[],"sources":["../src/addon-stack.ts"],"mappings":";;;;;AAiQA;;;;;;;iBAAgB,kBAAA,CAAmB,QAAA,EAAU,eAAA,GAAkB,gBAAA;;
|
|
1
|
+
{"version":3,"file":"addon-stack.d.cts","names":[],"sources":["../src/addon-stack.ts"],"mappings":";;;;;AAiQA;;;;;;;iBAAgB,kBAAA,CAAmB,QAAA,EAAU,eAAA,GAAkB,gBAAA;;AA8iB/D;;;;;iBAAgB,gBAAA,CAAiB,QAAA,EAAU,qBAAA,GAAwB,sBAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"addon-stack.d.mts","names":[],"sources":["../src/addon-stack.ts"],"mappings":";;;;;;AAiQA;;;;;;iBAAgB,kBAAA,CAAmB,QAAA,EAAU,eAAA,GAAkB,gBAAA;;;
|
|
1
|
+
{"version":3,"file":"addon-stack.d.mts","names":[],"sources":["../src/addon-stack.ts"],"mappings":";;;;;;AAiQA;;;;;;iBAAgB,kBAAA,CAAmB,QAAA,EAAU,eAAA,GAAkB,gBAAA;;;AA8iB/D;;;;iBAAgB,gBAAA,CAAiB,QAAA,EAAU,qBAAA,GAAwB,sBAAA"}
|
package/dist/addon-stack.mjs
CHANGED
|
@@ -469,6 +469,54 @@ function generateAddonStack(rawInput) {
|
|
|
469
469
|
skillCount++;
|
|
470
470
|
}
|
|
471
471
|
const proxyRoutes = buildProxyRoutes(deployableServices);
|
|
472
|
+
const additionalFiles = {};
|
|
473
|
+
if (deployableServices.some((s) => s.definition.id === "opensandbox")) additionalFiles["sandbox.toml"] = [
|
|
474
|
+
"[server]",
|
|
475
|
+
"host = \"0.0.0.0\"",
|
|
476
|
+
"port = 8080",
|
|
477
|
+
"log_level = \"INFO\"",
|
|
478
|
+
"api_key = \"${OPEN_SANDBOX_API_KEY}\"",
|
|
479
|
+
"",
|
|
480
|
+
"[runtime]",
|
|
481
|
+
"type = \"docker\"",
|
|
482
|
+
"execd_image = \"opensandbox/execd:v1.0.6\"",
|
|
483
|
+
"",
|
|
484
|
+
"[docker]",
|
|
485
|
+
"network_mode = \"bridge\"",
|
|
486
|
+
"drop_capabilities = [\"NET_ADMIN\", \"SYS_ADMIN\", \"SYS_PTRACE\", \"MKNOD\", \"NET_RAW\", \"SYS_RAWIO\"]",
|
|
487
|
+
"no_new_privileges = true",
|
|
488
|
+
"pids_limit = 512",
|
|
489
|
+
"",
|
|
490
|
+
"[secure_runtime]",
|
|
491
|
+
"type = \"gvisor\"",
|
|
492
|
+
""
|
|
493
|
+
].join("\n");
|
|
494
|
+
const prePullImages = [];
|
|
495
|
+
if (deployableServices.some((s) => s.definition.id === "opensandbox")) prePullImages.push({
|
|
496
|
+
image: "opensandbox/server:v1.0.6",
|
|
497
|
+
priority: 1
|
|
498
|
+
}, {
|
|
499
|
+
image: "opensandbox/execd:v1.0.6",
|
|
500
|
+
priority: 1
|
|
501
|
+
}, {
|
|
502
|
+
image: "opensandbox/desktop:latest",
|
|
503
|
+
priority: 1
|
|
504
|
+
}, {
|
|
505
|
+
image: "opensandbox/chrome:latest",
|
|
506
|
+
priority: 1
|
|
507
|
+
}, {
|
|
508
|
+
image: "opensandbox/code-interpreter:python",
|
|
509
|
+
priority: 2
|
|
510
|
+
}, {
|
|
511
|
+
image: "opensandbox/code-interpreter:node",
|
|
512
|
+
priority: 2
|
|
513
|
+
}, {
|
|
514
|
+
image: "opensandbox/code-interpreter:latest",
|
|
515
|
+
priority: 3
|
|
516
|
+
}, {
|
|
517
|
+
image: "opensandbox/vscode:latest",
|
|
518
|
+
priority: 3
|
|
519
|
+
});
|
|
472
520
|
const volumeMap = {};
|
|
473
521
|
for (const v of allVolumes) volumeMap[v] = null;
|
|
474
522
|
const composeDoc = { services };
|
|
@@ -481,6 +529,7 @@ function generateAddonStack(rawInput) {
|
|
|
481
529
|
skillFiles,
|
|
482
530
|
openclawConfigPatch: { skills: { entries: skillEntries } },
|
|
483
531
|
proxyRoutes,
|
|
532
|
+
additionalFiles,
|
|
484
533
|
metadata: {
|
|
485
534
|
serviceCount: Object.keys(services).length,
|
|
486
535
|
skillCount,
|
|
@@ -488,7 +537,8 @@ function generateAddonStack(rawInput) {
|
|
|
488
537
|
resolvedServices: deployableServices.map((s) => s.definition.id),
|
|
489
538
|
skippedServices,
|
|
490
539
|
generatedSecretKeys,
|
|
491
|
-
portAssignments: portConflicts.assignments
|
|
540
|
+
portAssignments: portConflicts.assignments,
|
|
541
|
+
prePullImages
|
|
492
542
|
},
|
|
493
543
|
warnings
|
|
494
544
|
};
|
|
@@ -624,6 +674,7 @@ function emptyResultBase() {
|
|
|
624
674
|
skillFiles: {},
|
|
625
675
|
openclawConfigPatch: { skills: { entries: {} } },
|
|
626
676
|
proxyRoutes: [],
|
|
677
|
+
additionalFiles: {},
|
|
627
678
|
metadata: {
|
|
628
679
|
serviceCount: 0,
|
|
629
680
|
skillCount: 0,
|
|
@@ -631,7 +682,8 @@ function emptyResultBase() {
|
|
|
631
682
|
resolvedServices: [],
|
|
632
683
|
skippedServices: [],
|
|
633
684
|
generatedSecretKeys: [],
|
|
634
|
-
portAssignments: {}
|
|
685
|
+
portAssignments: {},
|
|
686
|
+
prePullImages: []
|
|
635
687
|
},
|
|
636
688
|
warnings: []
|
|
637
689
|
};
|