@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.
Files changed (89) hide show
  1. package/dist/addon-stack.cjs +55 -3
  2. package/dist/addon-stack.cjs.map +1 -1
  3. package/dist/addon-stack.d.cts.map +1 -1
  4. package/dist/addon-stack.d.mts.map +1 -1
  5. package/dist/addon-stack.mjs +54 -2
  6. package/dist/addon-stack.mjs.map +1 -1
  7. package/dist/addon-stack.test.cjs +113 -1
  8. package/dist/addon-stack.test.cjs.map +1 -1
  9. package/dist/addon-stack.test.mjs +112 -0
  10. package/dist/addon-stack.test.mjs.map +1 -1
  11. package/dist/compose-validation.test.cjs +1 -1
  12. package/dist/composer.cjs +1 -1
  13. package/dist/composer.test.cjs +1 -1
  14. package/dist/deployers/strip-host-ports.cjs +1 -1
  15. package/dist/generate.cjs +1 -1
  16. package/dist/generate.test.cjs +1 -1
  17. package/dist/generators/env.cjs +1 -1
  18. package/dist/generators/postgres-init.cjs +5 -0
  19. package/dist/generators/postgres-init.cjs.map +1 -1
  20. package/dist/generators/postgres-init.d.cts.map +1 -1
  21. package/dist/generators/postgres-init.d.mts.map +1 -1
  22. package/dist/generators/postgres-init.mjs +5 -0
  23. package/dist/generators/postgres-init.mjs.map +1 -1
  24. package/dist/generators/skills.cjs +1 -1
  25. package/dist/generators/skills.d.cts.map +1 -1
  26. package/dist/generators/skills.d.mts.map +1 -1
  27. package/dist/generators/skills.mjs +141 -0
  28. package/dist/generators/skills.mjs.map +1 -1
  29. package/dist/index.cjs +1 -1
  30. package/dist/index.d.cts +1 -1
  31. package/dist/index.d.mts +1 -1
  32. package/dist/presets/presets.test.cjs +1 -1
  33. package/dist/{schema-CKBRu-Rt.d.cts → schema-BQnZrcw8.d.cts} +6 -1
  34. package/dist/{schema-CKBRu-Rt.d.cts.map → schema-BQnZrcw8.d.cts.map} +1 -1
  35. package/dist/{schema-Dn-_Jpb6.d.mts → schema-SBpL0bdI.d.mts} +6 -1
  36. package/dist/{schema-Dn-_Jpb6.d.mts.map → schema-SBpL0bdI.d.mts.map} +1 -1
  37. package/dist/schema.cjs +11 -2
  38. package/dist/schema.cjs.map +1 -1
  39. package/dist/schema.d.cts +1 -1
  40. package/dist/schema.d.mts +1 -1
  41. package/dist/schema.mjs +10 -1
  42. package/dist/schema.mjs.map +1 -1
  43. package/dist/services/definitions/burnlink.cjs +142 -0
  44. package/dist/services/definitions/burnlink.cjs.map +1 -0
  45. package/dist/services/definitions/burnlink.d.cts +7 -0
  46. package/dist/services/definitions/burnlink.d.cts.map +1 -0
  47. package/dist/services/definitions/burnlink.d.mts +7 -0
  48. package/dist/services/definitions/burnlink.d.mts.map +1 -0
  49. package/dist/services/definitions/burnlink.mjs +141 -0
  50. package/dist/services/definitions/burnlink.mjs.map +1 -0
  51. package/dist/services/definitions/hindsight.cjs +130 -0
  52. package/dist/services/definitions/hindsight.cjs.map +1 -0
  53. package/dist/services/definitions/hindsight.d.cts +7 -0
  54. package/dist/services/definitions/hindsight.d.cts.map +1 -0
  55. package/dist/services/definitions/hindsight.d.mts +7 -0
  56. package/dist/services/definitions/hindsight.d.mts.map +1 -0
  57. package/dist/services/definitions/hindsight.mjs +129 -0
  58. package/dist/services/definitions/hindsight.mjs.map +1 -0
  59. package/dist/services/definitions/index.cjs +9 -0
  60. package/dist/services/definitions/index.cjs.map +1 -1
  61. package/dist/services/definitions/index.d.cts +4 -1
  62. package/dist/services/definitions/index.d.cts.map +1 -1
  63. package/dist/services/definitions/index.d.mts +4 -1
  64. package/dist/services/definitions/index.d.mts.map +1 -1
  65. package/dist/services/definitions/index.mjs +7 -1
  66. package/dist/services/definitions/index.mjs.map +1 -1
  67. package/dist/services/definitions/opensandbox.cjs +149 -0
  68. package/dist/services/definitions/opensandbox.cjs.map +1 -0
  69. package/dist/services/definitions/opensandbox.d.cts +7 -0
  70. package/dist/services/definitions/opensandbox.d.cts.map +1 -0
  71. package/dist/services/definitions/opensandbox.d.mts +7 -0
  72. package/dist/services/definitions/opensandbox.d.mts.map +1 -0
  73. package/dist/services/definitions/opensandbox.mjs +148 -0
  74. package/dist/services/definitions/opensandbox.mjs.map +1 -0
  75. package/dist/{skills-BlzpHmpH.cjs → skills-BSF7iNa4.cjs} +142 -1
  76. package/dist/{skills-BlzpHmpH.cjs.map → skills-BSF7iNa4.cjs.map} +1 -1
  77. package/dist/types.d.cts +1 -1
  78. package/dist/types.d.mts +1 -1
  79. package/dist/validator.cjs +1 -1
  80. package/package.json +1 -1
  81. package/src/addon-stack.test.ts +158 -0
  82. package/src/addon-stack.ts +48 -0
  83. package/src/generators/postgres-init.ts +2 -0
  84. package/src/generators/skills.ts +142 -0
  85. package/src/schema.ts +7 -0
  86. package/src/services/definitions/burnlink.ts +142 -0
  87. package/src/services/definitions/hindsight.ts +131 -0
  88. package/src/services/definitions/index.ts +10 -0
  89. package/src/services/definitions/opensandbox.ts +156 -0
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_skills = require("./skills-BlzpHmpH.cjs");
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
  };
@@ -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":"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"}
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;;AAggB/D;;;;;iBAAgB,gBAAA,CAAiB,QAAA,EAAU,qBAAA,GAAwB,sBAAA"}
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;;;AAggB/D;;;;iBAAgB,gBAAA,CAAiB,QAAA,EAAU,qBAAA,GAAwB,sBAAA"}
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"}
@@ -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
  };