clawup 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. package/README.md +245 -0
  2. package/dist/adapters/api-adapter.d.ts +76 -0
  3. package/dist/adapters/api-adapter.js +250 -0
  4. package/dist/adapters/api-adapter.js.map +1 -0
  5. package/dist/adapters/cli-adapter.d.ts +15 -0
  6. package/dist/adapters/cli-adapter.js +208 -0
  7. package/dist/adapters/cli-adapter.js.map +1 -0
  8. package/dist/adapters/index.d.ts +22 -0
  9. package/dist/adapters/index.js +32 -0
  10. package/dist/adapters/index.js.map +1 -0
  11. package/dist/adapters/types.d.ts +135 -0
  12. package/dist/adapters/types.js +14 -0
  13. package/dist/adapters/types.js.map +1 -0
  14. package/dist/bin.d.ts +8 -0
  15. package/dist/bin.js +221 -0
  16. package/dist/bin.js.map +1 -0
  17. package/dist/commands/config.d.ts +21 -0
  18. package/dist/commands/config.js +323 -0
  19. package/dist/commands/config.js.map +1 -0
  20. package/dist/commands/deploy.d.ts +7 -0
  21. package/dist/commands/deploy.js +13 -0
  22. package/dist/commands/deploy.js.map +1 -0
  23. package/dist/commands/destroy.d.ts +7 -0
  24. package/dist/commands/destroy.js +13 -0
  25. package/dist/commands/destroy.js.map +1 -0
  26. package/dist/commands/init.d.ts +13 -0
  27. package/dist/commands/init.js +698 -0
  28. package/dist/commands/init.js.map +1 -0
  29. package/dist/commands/list.d.ts +8 -0
  30. package/dist/commands/list.js +42 -0
  31. package/dist/commands/list.js.map +1 -0
  32. package/dist/commands/push.d.ts +7 -0
  33. package/dist/commands/push.js +19 -0
  34. package/dist/commands/push.js.map +1 -0
  35. package/dist/commands/redeploy.d.ts +7 -0
  36. package/dist/commands/redeploy.js +13 -0
  37. package/dist/commands/redeploy.js.map +1 -0
  38. package/dist/commands/secrets.d.ts +16 -0
  39. package/dist/commands/secrets.js +169 -0
  40. package/dist/commands/secrets.js.map +1 -0
  41. package/dist/commands/ssh.d.ts +9 -0
  42. package/dist/commands/ssh.js +108 -0
  43. package/dist/commands/ssh.js.map +1 -0
  44. package/dist/commands/status.d.ts +7 -0
  45. package/dist/commands/status.js +13 -0
  46. package/dist/commands/status.js.map +1 -0
  47. package/dist/commands/update.d.ts +7 -0
  48. package/dist/commands/update.js +126 -0
  49. package/dist/commands/update.js.map +1 -0
  50. package/dist/commands/validate.d.ts +7 -0
  51. package/dist/commands/validate.js +13 -0
  52. package/dist/commands/validate.js.map +1 -0
  53. package/dist/commands/webhooks.d.ts +7 -0
  54. package/dist/commands/webhooks.js +13 -0
  55. package/dist/commands/webhooks.js.map +1 -0
  56. package/dist/lib/__tests__/identity.test.d.ts +1 -0
  57. package/dist/lib/__tests__/identity.test.js +186 -0
  58. package/dist/lib/__tests__/identity.test.js.map +1 -0
  59. package/dist/lib/__tests__/validate-agent.test.d.ts +1 -0
  60. package/dist/lib/__tests__/validate-agent.test.js +38 -0
  61. package/dist/lib/__tests__/validate-agent.test.js.map +1 -0
  62. package/dist/lib/config.d.ts +69 -0
  63. package/dist/lib/config.js +218 -0
  64. package/dist/lib/config.js.map +1 -0
  65. package/dist/lib/constants.d.ts +5 -0
  66. package/dist/lib/constants.js +29 -0
  67. package/dist/lib/constants.js.map +1 -0
  68. package/dist/lib/exec.d.ts +24 -0
  69. package/dist/lib/exec.js +63 -0
  70. package/dist/lib/exec.js.map +1 -0
  71. package/dist/lib/prerequisites.d.ts +8 -0
  72. package/dist/lib/prerequisites.js +146 -0
  73. package/dist/lib/prerequisites.js.map +1 -0
  74. package/dist/lib/process.d.ts +18 -0
  75. package/dist/lib/process.js +37 -0
  76. package/dist/lib/process.js.map +1 -0
  77. package/dist/lib/pulumi.d.ts +37 -0
  78. package/dist/lib/pulumi.js +87 -0
  79. package/dist/lib/pulumi.js.map +1 -0
  80. package/dist/lib/tailscale.d.ts +75 -0
  81. package/dist/lib/tailscale.js +251 -0
  82. package/dist/lib/tailscale.js.map +1 -0
  83. package/dist/lib/tool-helpers.d.ts +15 -0
  84. package/dist/lib/tool-helpers.js +35 -0
  85. package/dist/lib/tool-helpers.js.map +1 -0
  86. package/dist/lib/ui.d.ts +26 -0
  87. package/dist/lib/ui.js +86 -0
  88. package/dist/lib/ui.js.map +1 -0
  89. package/dist/lib/update-check.d.ts +8 -0
  90. package/dist/lib/update-check.js +151 -0
  91. package/dist/lib/update-check.js.map +1 -0
  92. package/dist/lib/vendor.d.ts +34 -0
  93. package/dist/lib/vendor.js +128 -0
  94. package/dist/lib/vendor.js.map +1 -0
  95. package/dist/lib/workspace.d.ts +21 -0
  96. package/dist/lib/workspace.js +170 -0
  97. package/dist/lib/workspace.js.map +1 -0
  98. package/dist/tools/deploy.d.ts +16 -0
  99. package/dist/tools/deploy.js +181 -0
  100. package/dist/tools/deploy.js.map +1 -0
  101. package/dist/tools/destroy.d.ts +16 -0
  102. package/dist/tools/destroy.js +119 -0
  103. package/dist/tools/destroy.js.map +1 -0
  104. package/dist/tools/index.d.ts +20 -0
  105. package/dist/tools/index.js +34 -0
  106. package/dist/tools/index.js.map +1 -0
  107. package/dist/tools/push.d.ts +29 -0
  108. package/dist/tools/push.js +341 -0
  109. package/dist/tools/push.js.map +1 -0
  110. package/dist/tools/redeploy.d.ts +17 -0
  111. package/dist/tools/redeploy.js +181 -0
  112. package/dist/tools/redeploy.js.map +1 -0
  113. package/dist/tools/status.d.ts +16 -0
  114. package/dist/tools/status.js +205 -0
  115. package/dist/tools/status.js.map +1 -0
  116. package/dist/tools/validate.d.ts +16 -0
  117. package/dist/tools/validate.js +219 -0
  118. package/dist/tools/validate.js.map +1 -0
  119. package/dist/tools/webhooks.d.ts +17 -0
  120. package/dist/tools/webhooks.js +181 -0
  121. package/dist/tools/webhooks.js.map +1 -0
  122. package/dist/types.d.ts +6 -0
  123. package/dist/types.js +10 -0
  124. package/dist/types.js.map +1 -0
  125. package/infra/Pulumi.yaml +6 -0
  126. package/infra/dist/components/cloud-init.js +412 -0
  127. package/infra/dist/components/config-generator.js +254 -0
  128. package/infra/dist/components/hetzner-agent.js +162 -0
  129. package/infra/dist/components/index.js +18 -0
  130. package/infra/dist/components/openclaw-agent.js +287 -0
  131. package/infra/dist/components/shared.js +132 -0
  132. package/infra/dist/components/types.js +6 -0
  133. package/infra/dist/index.js +387 -0
  134. package/infra/dist/shared-vpc.js +167 -0
  135. package/infra/node_modules/@clawup/core/dist/__tests__/schemas.test.d.ts +2 -0
  136. package/infra/node_modules/@clawup/core/dist/__tests__/schemas.test.d.ts.map +1 -0
  137. package/infra/node_modules/@clawup/core/dist/__tests__/schemas.test.js +124 -0
  138. package/infra/node_modules/@clawup/core/dist/__tests__/schemas.test.js.map +1 -0
  139. package/infra/node_modules/@clawup/core/dist/coding-agent-registry.d.ts +32 -0
  140. package/infra/node_modules/@clawup/core/dist/coding-agent-registry.d.ts.map +1 -0
  141. package/infra/node_modules/@clawup/core/dist/coding-agent-registry.js +56 -0
  142. package/infra/node_modules/@clawup/core/dist/coding-agent-registry.js.map +1 -0
  143. package/infra/node_modules/@clawup/core/dist/constants.d.ts +137 -0
  144. package/infra/node_modules/@clawup/core/dist/constants.d.ts.map +1 -0
  145. package/infra/node_modules/@clawup/core/dist/constants.js +314 -0
  146. package/infra/node_modules/@clawup/core/dist/constants.js.map +1 -0
  147. package/infra/node_modules/@clawup/core/dist/dep-registry.d.ts +25 -0
  148. package/infra/node_modules/@clawup/core/dist/dep-registry.d.ts.map +1 -0
  149. package/infra/node_modules/@clawup/core/dist/dep-registry.js +46 -0
  150. package/infra/node_modules/@clawup/core/dist/dep-registry.js.map +1 -0
  151. package/infra/node_modules/@clawup/core/dist/deps.d.ts +18 -0
  152. package/infra/node_modules/@clawup/core/dist/deps.d.ts.map +1 -0
  153. package/infra/node_modules/@clawup/core/dist/deps.js +39 -0
  154. package/infra/node_modules/@clawup/core/dist/deps.js.map +1 -0
  155. package/infra/node_modules/@clawup/core/dist/identity.d.ts +20 -0
  156. package/infra/node_modules/@clawup/core/dist/identity.d.ts.map +1 -0
  157. package/infra/node_modules/@clawup/core/dist/identity.js +217 -0
  158. package/infra/node_modules/@clawup/core/dist/identity.js.map +1 -0
  159. package/infra/node_modules/@clawup/core/dist/index.d.ts +18 -0
  160. package/infra/node_modules/@clawup/core/dist/index.d.ts.map +1 -0
  161. package/infra/node_modules/@clawup/core/dist/index.js +52 -0
  162. package/infra/node_modules/@clawup/core/dist/index.js.map +1 -0
  163. package/infra/node_modules/@clawup/core/dist/plugin-registry.d.ts +13 -0
  164. package/infra/node_modules/@clawup/core/dist/plugin-registry.d.ts.map +1 -0
  165. package/infra/node_modules/@clawup/core/dist/plugin-registry.js +24 -0
  166. package/infra/node_modules/@clawup/core/dist/plugin-registry.js.map +1 -0
  167. package/infra/node_modules/@clawup/core/dist/schemas/identity.d.ts +74 -0
  168. package/infra/node_modules/@clawup/core/dist/schemas/identity.d.ts.map +1 -0
  169. package/infra/node_modules/@clawup/core/dist/schemas/identity.js +45 -0
  170. package/infra/node_modules/@clawup/core/dist/schemas/identity.js.map +1 -0
  171. package/infra/node_modules/@clawup/core/dist/schemas/index.d.ts +6 -0
  172. package/infra/node_modules/@clawup/core/dist/schemas/index.d.ts.map +1 -0
  173. package/infra/node_modules/@clawup/core/dist/schemas/index.js +13 -0
  174. package/infra/node_modules/@clawup/core/dist/schemas/index.js.map +1 -0
  175. package/infra/node_modules/@clawup/core/dist/schemas/manifest.d.ts +159 -0
  176. package/infra/node_modules/@clawup/core/dist/schemas/manifest.d.ts.map +1 -0
  177. package/infra/node_modules/@clawup/core/dist/schemas/manifest.js +54 -0
  178. package/infra/node_modules/@clawup/core/dist/schemas/manifest.js.map +1 -0
  179. package/infra/node_modules/@clawup/core/dist/skills.d.ts +30 -0
  180. package/infra/node_modules/@clawup/core/dist/skills.d.ts.map +1 -0
  181. package/infra/node_modules/@clawup/core/dist/skills.js +52 -0
  182. package/infra/node_modules/@clawup/core/dist/skills.js.map +1 -0
  183. package/infra/node_modules/@clawup/core/dist/types.d.ts +59 -0
  184. package/infra/node_modules/@clawup/core/dist/types.d.ts.map +1 -0
  185. package/infra/node_modules/@clawup/core/dist/types.js +30 -0
  186. package/infra/node_modules/@clawup/core/dist/types.js.map +1 -0
  187. package/infra/node_modules/@clawup/core/package.json +46 -0
  188. package/infra/package.json +12 -0
  189. package/package.json +43 -0
  190. package/scripts/postinstall.mjs +395 -0
@@ -0,0 +1,387 @@
1
+ "use strict";
2
+ /**
3
+ * Clawup - Data-Driven Multi-Agent Pulumi Stack
4
+ *
5
+ * Reads clawup.yaml manifest to dynamically deploy OpenClaw agents.
6
+ * The manifest is created by `clawup init` and serves as the single
7
+ * source of truth for the agent fleet configuration.
8
+ *
9
+ * All agents share a single VPC for cost optimization.
10
+ * Each agent loads workspace files from identity repos.
11
+ * Secrets are pulled from Pulumi config (set by CLI or ESC).
12
+ * Plugin configs are loaded from ~/.clawup/configs/<stack>/plugins/.
13
+ */
14
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
15
+ if (k2 === undefined) k2 = k;
16
+ var desc = Object.getOwnPropertyDescriptor(m, k);
17
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
18
+ desc = { enumerable: true, get: function() { return m[k]; } };
19
+ }
20
+ Object.defineProperty(o, k2, desc);
21
+ }) : (function(o, m, k, k2) {
22
+ if (k2 === undefined) k2 = k;
23
+ o[k2] = m[k];
24
+ }));
25
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
26
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
27
+ }) : function(o, v) {
28
+ o["default"] = v;
29
+ });
30
+ var __importStar = (this && this.__importStar) || (function () {
31
+ var ownKeys = function(o) {
32
+ ownKeys = Object.getOwnPropertyNames || function (o) {
33
+ var ar = [];
34
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
35
+ return ar;
36
+ };
37
+ return ownKeys(o);
38
+ };
39
+ return function (mod) {
40
+ if (mod && mod.__esModule) return mod;
41
+ var result = {};
42
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
43
+ __setModuleDefault(result, mod);
44
+ return result;
45
+ };
46
+ })();
47
+ var __importDefault = (this && this.__importDefault) || function (mod) {
48
+ return (mod && mod.__esModule) ? mod : { "default": mod };
49
+ };
50
+ Object.defineProperty(exports, "__esModule", { value: true });
51
+ const pulumi = __importStar(require("@pulumi/pulumi"));
52
+ const aws = __importStar(require("@pulumi/aws"));
53
+ const fs = __importStar(require("fs"));
54
+ const path = __importStar(require("path"));
55
+ const yaml_1 = __importDefault(require("yaml"));
56
+ const components_1 = require("./components");
57
+ const shared_vpc_1 = require("./shared-vpc");
58
+ const core_1 = require("@clawup/core");
59
+ const identity_1 = require("@clawup/core/identity");
60
+ const os = __importStar(require("os"));
61
+ // -----------------------------------------------------------------------------
62
+ // Configuration from Pulumi Config / ESC
63
+ // -----------------------------------------------------------------------------
64
+ const config = new pulumi.Config();
65
+ const anthropicApiKey = config.requireSecret("anthropicApiKey");
66
+ const tailscaleAuthKey = config.requireSecret("tailscaleAuthKey");
67
+ const tailnetDnsName = config.require("tailnetDnsName");
68
+ const instanceType = config.get("instanceType") ?? "t3.medium";
69
+ const ownerName = config.get("ownerName") ?? "Boss";
70
+ const timezone = config.get("timezone") ?? "PST (America/Los_Angeles)";
71
+ const workingHours = config.get("workingHours") ?? "9am-6pm";
72
+ const userNotes = config.get("userNotes") ?? "No additional notes provided yet.";
73
+ // Identity cache directory
74
+ const identityCacheDir = path.join(os.homedir(), ".clawup", "identity-cache");
75
+ /**
76
+ * Process template placeholders in workspace files
77
+ */
78
+ function processTemplates(files, variables) {
79
+ const processed = {};
80
+ for (const [filename, content] of Object.entries(files)) {
81
+ let processedContent = content;
82
+ for (const [key, value] of Object.entries(variables)) {
83
+ processedContent = processedContent.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
84
+ }
85
+ processed[filename] = processedContent;
86
+ }
87
+ return processed;
88
+ }
89
+ // -----------------------------------------------------------------------------
90
+ // Load Manifest (YAML)
91
+ // -----------------------------------------------------------------------------
92
+ // Pulumi sets cwd to the project root (where Pulumi.yaml lives)
93
+ const manifestPath = path.join(process.cwd(), "clawup.yaml");
94
+ if (!fs.existsSync(manifestPath)) {
95
+ throw new Error("clawup.yaml not found. Run `clawup init` to create it.");
96
+ }
97
+ // Cast as partial — old manifests may omit `provider` (defaults to "aws" below)
98
+ const manifest = yaml_1.default.parse(fs.readFileSync(manifestPath, "utf-8"));
99
+ // Load plugin configs from ~/.clawup/configs/<stackName>/plugins/
100
+ const pluginConfigsDir = path.join(os.homedir(), ".clawup", "configs", manifest.stackName, "plugins");
101
+ const pluginConfigs = {};
102
+ if (fs.existsSync(pluginConfigsDir)) {
103
+ for (const file of fs.readdirSync(pluginConfigsDir)) {
104
+ if (file.endsWith(".yaml")) {
105
+ const pluginName = file.replace(/\.yaml$/, "");
106
+ try {
107
+ const raw = fs.readFileSync(path.join(pluginConfigsDir, file), "utf-8");
108
+ pluginConfigs[pluginName] = yaml_1.default.parse(raw);
109
+ }
110
+ catch (err) {
111
+ pulumi.log.warn(`Failed to load plugin config '${pluginName}': ${err instanceof Error ? err.message : String(err)}`);
112
+ }
113
+ }
114
+ }
115
+ }
116
+ // Default provider to AWS for backwards compatibility with existing manifests
117
+ const provider = manifest.provider ?? "aws";
118
+ // Validate provider
119
+ if (provider !== "aws" && provider !== "hetzner") {
120
+ throw new Error(`Unsupported provider: ${provider}. Supported providers are: aws, hetzner`);
121
+ }
122
+ // -----------------------------------------------------------------------------
123
+ // Resource Tags (AWS) / Labels (Hetzner)
124
+ // -----------------------------------------------------------------------------
125
+ const baseTags = {
126
+ Project: "clawup",
127
+ Environment: pulumi.getStack(),
128
+ ManagedBy: "pulumi",
129
+ };
130
+ // -----------------------------------------------------------------------------
131
+ // Provider-specific infrastructure
132
+ // -----------------------------------------------------------------------------
133
+ let sharedVpc;
134
+ if (provider === "aws") {
135
+ // -------------------------------------------------------------------------
136
+ // Dynamic AZ Selection - Find an AZ that supports all instance types
137
+ // -------------------------------------------------------------------------
138
+ // Collect all instance types we'll need
139
+ const instanceTypes = [
140
+ instanceType, // default from config
141
+ ...manifest.agents.map(a => a.instanceType).filter(Boolean)
142
+ ];
143
+ const uniqueInstanceTypes = [...new Set(instanceTypes)];
144
+ // Query AWS to find which AZs support our instance types
145
+ const availabilityZone = pulumi
146
+ .all(uniqueInstanceTypes.map((instanceType) => aws.ec2.getInstanceTypeOfferings({
147
+ filters: [
148
+ {
149
+ name: "instance-type",
150
+ values: [instanceType],
151
+ },
152
+ ],
153
+ locationType: "availability-zone",
154
+ })))
155
+ .apply((offeringsResults) => {
156
+ // Build a set of AZs for each instance type
157
+ const azSets = offeringsResults.map((result) => new Set(result.locations));
158
+ // Find intersection - AZs that support ALL instance types
159
+ const intersection = azSets[0];
160
+ for (let i = 1; i < azSets.length; i++) {
161
+ for (const az of intersection) {
162
+ if (!azSets[i].has(az)) {
163
+ intersection.delete(az);
164
+ }
165
+ }
166
+ }
167
+ // Pick the first available AZ alphabetically for consistency
168
+ const availableAzs = Array.from(intersection).sort();
169
+ if (availableAzs.length === 0) {
170
+ throw new Error(`No availability zone found that supports all instance types: ${uniqueInstanceTypes.join(", ")}`);
171
+ }
172
+ return availableAzs[0];
173
+ });
174
+ // -------------------------------------------------------------------------
175
+ // Shared VPC (cost optimization - all agents share one VPC)
176
+ // -------------------------------------------------------------------------
177
+ sharedVpc = new shared_vpc_1.SharedVpc("clawup", {
178
+ availabilityZone: availabilityZone,
179
+ tags: baseTags,
180
+ });
181
+ // VPC outputs
182
+ module.exports["vpcId"] = sharedVpc.vpcId;
183
+ module.exports["subnetId"] = sharedVpc.subnetId;
184
+ module.exports["securityGroupId"] = sharedVpc.securityGroupId;
185
+ module.exports["selectedAvailabilityZone"] = availabilityZone;
186
+ }
187
+ // Hetzner reads hcloud:token automatically from Pulumi config — no explicit provider needed
188
+ // -----------------------------------------------------------------------------
189
+ // Helper: Build PluginInstallConfig[] for an agent
190
+ // -----------------------------------------------------------------------------
191
+ function buildPluginsForAgent(agent, identityDefaults, identityPlugins) {
192
+ const plugins = [];
193
+ const pluginSecrets = {};
194
+ let enableFunnel = false;
195
+ const pluginList = identityPlugins ?? [];
196
+ for (const pluginName of pluginList) {
197
+ let agentSection;
198
+ if (agent.plugins && agent.plugins[pluginName]) {
199
+ // New format: inline plugin config on the agent definition
200
+ agentSection = agent.plugins[pluginName];
201
+ }
202
+ else {
203
+ // Backward compat: fall back to file-based plugin config
204
+ const pluginCfg = pluginConfigs[pluginName];
205
+ const userConfig = pluginCfg?.agents?.[agent.role] ?? {};
206
+ const identityConfig = identityDefaults?.[pluginName] ?? {};
207
+ agentSection = { ...identityConfig, ...userConfig };
208
+ }
209
+ const registryEntry = core_1.PLUGIN_REGISTRY[pluginName];
210
+ const secretMapping = registryEntry?.secretEnvVars ?? {};
211
+ plugins.push({
212
+ name: pluginName,
213
+ config: agentSection,
214
+ secretEnvVars: Object.keys(secretMapping).length > 0 ? secretMapping : undefined,
215
+ installable: registryEntry?.installable ?? true,
216
+ });
217
+ // Collect secret outputs from Pulumi config
218
+ for (const [, envVar] of Object.entries(secretMapping)) {
219
+ if (!pluginSecrets[envVar]) {
220
+ // Derive Pulumi config key from role + env var pattern
221
+ // e.g., LINEAR_API_KEY → <role>LinearApiKey, SLACK_BOT_TOKEN → <role>SlackBotToken
222
+ const secret = config.getSecret(`${agent.role}${envVarToConfigKey(envVar)}`);
223
+ if (secret) {
224
+ pluginSecrets[envVar] = secret;
225
+ }
226
+ }
227
+ }
228
+ // Enable funnel if the plugin needs webhooks
229
+ if (registryEntry?.needsFunnel) {
230
+ enableFunnel = true;
231
+ }
232
+ }
233
+ return { plugins, pluginSecrets, enableFunnel };
234
+ }
235
+ /**
236
+ * Convert an env var name to a Pulumi config key suffix.
237
+ * e.g., LINEAR_API_KEY → LinearApiKey, LINEAR_WEBHOOK_SECRET → LinearWebhookSecret
238
+ */
239
+ function envVarToConfigKey(envVar) {
240
+ // Strip the common prefix (e.g., "LINEAR_") and convert to camelCase
241
+ // LINEAR_API_KEY → LinearApiKey
242
+ return envVar
243
+ .toLowerCase()
244
+ .split("_")
245
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
246
+ .join("");
247
+ }
248
+ // -----------------------------------------------------------------------------
249
+ // Dynamic Agent Deployments
250
+ // -----------------------------------------------------------------------------
251
+ const agentOutputs = {};
252
+ /**
253
+ * Build the base agent args shared by all providers.
254
+ * Provider-specific fields (VPC, location, tags/labels) are added by the caller.
255
+ */
256
+ function buildBaseAgentArgs(agent) {
257
+ const templateVars = {
258
+ OWNER_NAME: ownerName,
259
+ TIMEZONE: timezone,
260
+ WORKING_HOURS: workingHours,
261
+ USER_NOTES: userNotes,
262
+ ...(manifest.templateVars ?? {}),
263
+ };
264
+ // Fetch identity (always required)
265
+ const identity = (0, identity_1.fetchIdentitySync)(agent.identity, identityCacheDir);
266
+ // Identity files are the workspace files
267
+ const workspaceFiles = processTemplates(identity.files, templateVars);
268
+ // Pull defaults from identity manifest
269
+ const agentEmoji = identity.manifest.emoji ?? "";
270
+ const agentDisplayName = agent.displayName || identity.manifest.displayName;
271
+ const agentVolumeSize = agent.volumeSize ?? identity.manifest.volumeSize ?? 30;
272
+ // Extract public (clawhub) skills from identity manifest
273
+ const { public: publicSkills } = (0, core_1.classifySkills)(identity.manifest.skills);
274
+ const clawhubSkillSlugs = publicSkills.map((s) => s.slug);
275
+ // Build plugin configs for this agent (always from identity)
276
+ const { plugins, pluginSecrets, enableFunnel } = buildPluginsForAgent(agent, identity.manifest.pluginDefaults, identity.manifest.plugins);
277
+ // Resolve model/codingAgent from identity
278
+ const agentModel = identity.manifest.model ?? "anthropic/claude-opus-4-6";
279
+ const agentBackupModel = identity.manifest.backupModel;
280
+ const agentCodingAgent = identity.manifest.codingAgent ?? "claude-code";
281
+ // Resolve deps from identity
282
+ const depNames = identity.manifest.deps ?? [];
283
+ const resolvedDeps = (0, core_1.resolveDeps)(depNames);
284
+ const depEntries = resolvedDeps.map(d => ({
285
+ name: d.name,
286
+ installScript: d.entry.installScript,
287
+ postInstallScript: d.entry.postInstallScript,
288
+ secrets: Object.fromEntries(Object.entries(d.entry.secrets).map(([k, v]) => [k, { envVar: v.envVar }])),
289
+ }));
290
+ // Collect dep secrets from Pulumi config (scope-aware)
291
+ const depSecretDefs = (0, core_1.collectDepSecrets)(resolvedDeps);
292
+ const depSecrets = {};
293
+ for (const def of depSecretDefs) {
294
+ const secret = def.scope === "agent"
295
+ ? config.getSecret(`${agent.role}${def.configKeySuffix}`)
296
+ : config.getSecret(`${def.configKeySuffix.charAt(0).toLowerCase()}${def.configKeySuffix.slice(1)}`);
297
+ if (secret) {
298
+ depSecrets[def.envVar] = secret;
299
+ }
300
+ }
301
+ return {
302
+ baseArgs: {
303
+ anthropicApiKey,
304
+ tailscaleAuthKey,
305
+ tailnetDnsName,
306
+ model: agentModel,
307
+ backupModel: agentBackupModel,
308
+ codingAgent: agentCodingAgent,
309
+ workspaceFiles,
310
+ envVars: {
311
+ AGENT_ROLE: agent.role,
312
+ AGENT_NAME: agentDisplayName,
313
+ AGENT_EMOJI: agentEmoji,
314
+ ...agent.envVars,
315
+ },
316
+ plugins,
317
+ pluginSecrets,
318
+ enableFunnel,
319
+ clawhubSkills: clawhubSkillSlugs,
320
+ deps: depEntries,
321
+ depSecrets,
322
+ },
323
+ agentDisplayName,
324
+ agentVolumeSize,
325
+ };
326
+ }
327
+ for (const agent of manifest.agents) {
328
+ const { baseArgs, agentVolumeSize } = buildBaseAgentArgs(agent);
329
+ if (provider === "aws") {
330
+ const agentResource = new components_1.OpenClawAgent(agent.name, {
331
+ ...baseArgs,
332
+ instanceType: agent.instanceType ?? instanceType,
333
+ volumeSize: agentVolumeSize ?? 30,
334
+ vpcId: sharedVpc.vpcId,
335
+ subnetId: sharedVpc.subnetId,
336
+ securityGroupId: sharedVpc.securityGroupId,
337
+ tags: {
338
+ ...baseTags,
339
+ AgentRole: agent.role,
340
+ AgentName: agent.displayName,
341
+ },
342
+ });
343
+ agentOutputs[agent.role] = {
344
+ tailscaleUrl: agentResource.tailscaleUrl,
345
+ gatewayToken: agentResource.gatewayToken,
346
+ instanceId: agentResource.instanceId,
347
+ publicIp: agentResource.publicIp,
348
+ sshPrivateKey: agentResource.sshPrivateKey,
349
+ };
350
+ }
351
+ else {
352
+ const agentResource = new components_1.HetznerOpenClawAgent(agent.name, {
353
+ ...baseArgs,
354
+ serverType: agent.instanceType ?? instanceType,
355
+ location: manifest.region,
356
+ labels: {
357
+ ...baseTags,
358
+ AgentRole: agent.role,
359
+ AgentName: agent.displayName,
360
+ },
361
+ });
362
+ agentOutputs[agent.role] = {
363
+ tailscaleUrl: agentResource.tailscaleUrl,
364
+ gatewayToken: agentResource.gatewayToken,
365
+ instanceId: agentResource.serverId,
366
+ publicIp: agentResource.publicIp,
367
+ sshPrivateKey: agentResource.sshPrivateKey,
368
+ };
369
+ }
370
+ }
371
+ // -----------------------------------------------------------------------------
372
+ // Dynamic Stack Outputs
373
+ // -----------------------------------------------------------------------------
374
+ for (const [role, outputs] of Object.entries(agentOutputs)) {
375
+ module.exports[`${role}TailscaleUrl`] = pulumi.secret(outputs.tailscaleUrl);
376
+ module.exports[`${role}GatewayToken`] = pulumi.secret(outputs.gatewayToken);
377
+ module.exports[`${role}InstanceId`] = outputs.instanceId;
378
+ module.exports[`${role}PublicIp`] = outputs.publicIp;
379
+ module.exports[`${role}SshPrivateKey`] = pulumi.secret(outputs.sshPrivateKey);
380
+ // Webhook URL for plugins that need it (derived from Tailscale Funnel public URL)
381
+ module.exports[`${role}WebhookUrl`] = outputs.tailscaleUrl.apply((url) => {
382
+ // Extract base URL (remove query params like ?token=...) and append webhook path
383
+ const baseUrl = url.split("?")[0].replace(/\/$/, "");
384
+ return `${baseUrl}/hooks/linear`;
385
+ });
386
+ }
387
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,167 @@
1
+ "use strict";
2
+ /**
3
+ * Shared VPC Component for Multi-Agent Deployments
4
+ *
5
+ * Creates a single VPC with subnet, internet gateway, and security group
6
+ * that can be shared across multiple OpenClaw agent instances for cost optimization.
7
+ */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
41
+ Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.SharedVpc = void 0;
43
+ const pulumi = __importStar(require("@pulumi/pulumi"));
44
+ const aws = __importStar(require("@pulumi/aws"));
45
+ /**
46
+ * SharedVpc ComponentResource
47
+ *
48
+ * Creates shared networking infrastructure for multiple OpenClaw agents:
49
+ * - VPC with DNS support
50
+ * - Public subnet with auto-assign public IP
51
+ * - Internet gateway for outbound access
52
+ * - Route table with internet route
53
+ * - Security group allowing SSH and all outbound traffic
54
+ */
55
+ class SharedVpc extends pulumi.ComponentResource {
56
+ /** VPC ID */
57
+ vpcId;
58
+ /** Subnet ID */
59
+ subnetId;
60
+ /** Security Group ID */
61
+ securityGroupId;
62
+ /** Internet Gateway ID */
63
+ internetGatewayId;
64
+ constructor(name, args = {}, opts) {
65
+ super("clawup:aws:SharedVpc", name, {}, opts);
66
+ const defaultResourceOptions = { parent: this };
67
+ const cidrBlock = args.cidrBlock ?? "10.0.0.0/16";
68
+ const subnetCidrBlock = args.subnetCidrBlock ?? "10.0.1.0/24";
69
+ const availabilityZone = args.availabilityZone ?? "us-east-1a";
70
+ const baseTags = args.tags ?? {};
71
+ // Create VPC
72
+ const vpc = new aws.ec2.Vpc(`${name}-vpc`, {
73
+ cidrBlock: cidrBlock,
74
+ enableDnsHostnames: true,
75
+ enableDnsSupport: true,
76
+ tags: pulumi.output(baseTags).apply((tags) => ({
77
+ ...tags,
78
+ Name: `${name}-vpc`,
79
+ })),
80
+ }, defaultResourceOptions);
81
+ // Create Internet Gateway
82
+ const internetGateway = new aws.ec2.InternetGateway(`${name}-igw`, {
83
+ vpcId: vpc.id,
84
+ tags: pulumi.output(baseTags).apply((tags) => ({
85
+ ...tags,
86
+ Name: `${name}-igw`,
87
+ })),
88
+ }, defaultResourceOptions);
89
+ // Create public subnet
90
+ const subnet = new aws.ec2.Subnet(`${name}-subnet`, {
91
+ vpcId: vpc.id,
92
+ cidrBlock: subnetCidrBlock,
93
+ availabilityZone: availabilityZone,
94
+ mapPublicIpOnLaunch: true,
95
+ tags: pulumi.output(baseTags).apply((tags) => ({
96
+ ...tags,
97
+ Name: `${name}-subnet`,
98
+ })),
99
+ }, defaultResourceOptions);
100
+ // Create route table with internet route
101
+ const routeTable = new aws.ec2.RouteTable(`${name}-rt`, {
102
+ vpcId: vpc.id,
103
+ routes: [
104
+ {
105
+ cidrBlock: "0.0.0.0/0",
106
+ gatewayId: internetGateway.id,
107
+ },
108
+ ],
109
+ tags: pulumi.output(baseTags).apply((tags) => ({
110
+ ...tags,
111
+ Name: `${name}-rt`,
112
+ })),
113
+ }, defaultResourceOptions);
114
+ // Associate route table with subnet
115
+ new aws.ec2.RouteTableAssociation(`${name}-rta`, {
116
+ subnetId: subnet.id,
117
+ routeTableId: routeTable.id,
118
+ }, defaultResourceOptions);
119
+ // Create security group
120
+ const securityGroup = new aws.ec2.SecurityGroup(`${name}-sg`, {
121
+ vpcId: vpc.id,
122
+ description: `Shared security group for ${name} agent fleet`,
123
+ // SSH is disabled by default — Tailscale is the primary access method.
124
+ // Pass allowedSshCidrs to enable SSH from specific IPs as a fallback.
125
+ ingress: pulumi
126
+ .output(args.allowedSshCidrs ?? [])
127
+ .apply((cidrs) => cidrs.length > 0
128
+ ? [
129
+ {
130
+ description: "SSH access (restricted)",
131
+ fromPort: 22,
132
+ toPort: 22,
133
+ protocol: "tcp",
134
+ cidrBlocks: cidrs,
135
+ },
136
+ ]
137
+ : []),
138
+ egress: [
139
+ {
140
+ description: "All outbound traffic",
141
+ fromPort: 0,
142
+ toPort: 0,
143
+ protocol: "-1",
144
+ cidrBlocks: ["0.0.0.0/0"],
145
+ },
146
+ ],
147
+ tags: pulumi.output(baseTags).apply((tags) => ({
148
+ ...tags,
149
+ Name: `${name}-sg`,
150
+ })),
151
+ }, defaultResourceOptions);
152
+ // Set outputs
153
+ this.vpcId = vpc.id;
154
+ this.subnetId = subnet.id;
155
+ this.securityGroupId = securityGroup.id;
156
+ this.internetGatewayId = internetGateway.id;
157
+ // Register outputs
158
+ this.registerOutputs({
159
+ vpcId: this.vpcId,
160
+ subnetId: this.subnetId,
161
+ securityGroupId: this.securityGroupId,
162
+ internetGatewayId: this.internetGatewayId,
163
+ });
164
+ }
165
+ }
166
+ exports.SharedVpc = SharedVpc;
167
+ //# sourceMappingURL=shared-vpc.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=schemas.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schemas.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/schemas.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,124 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const vitest_1 = require("vitest");
4
+ const schemas_1 = require("../schemas");
5
+ (0, vitest_1.describe)("AgentDefinitionSchema", () => {
6
+ const validAgent = {
7
+ name: "agent-pm",
8
+ displayName: "Juno",
9
+ role: "pm",
10
+ identity: "https://github.com/org/identities#pm",
11
+ volumeSize: 30,
12
+ };
13
+ (0, vitest_1.it)("accepts a valid agent definition", () => {
14
+ (0, vitest_1.expect)(() => schemas_1.AgentDefinitionSchema.parse(validAgent)).not.toThrow();
15
+ });
16
+ (0, vitest_1.it)("accepts agent with optional fields", () => {
17
+ const result = schemas_1.AgentDefinitionSchema.parse({
18
+ ...validAgent,
19
+ identityVersion: "v1.0",
20
+ instanceType: "t3.large",
21
+ envVars: { FOO: "bar" },
22
+ plugins: { "openclaw-linear": { agentId: "agent-pm" } },
23
+ });
24
+ (0, vitest_1.expect)(result.plugins).toEqual({ "openclaw-linear": { agentId: "agent-pm" } });
25
+ });
26
+ (0, vitest_1.it)("rejects empty object with descriptive errors", () => {
27
+ const result = schemas_1.AgentDefinitionSchema.safeParse({});
28
+ (0, vitest_1.expect)(result.success).toBe(false);
29
+ if (!result.success) {
30
+ const paths = result.error.issues.map((i) => i.path[0]);
31
+ (0, vitest_1.expect)(paths).toContain("name");
32
+ (0, vitest_1.expect)(paths).toContain("displayName");
33
+ (0, vitest_1.expect)(paths).toContain("role");
34
+ (0, vitest_1.expect)(paths).toContain("identity");
35
+ (0, vitest_1.expect)(paths).toContain("volumeSize");
36
+ }
37
+ });
38
+ (0, vitest_1.it)("rejects non-positive volumeSize", () => {
39
+ const result = schemas_1.AgentDefinitionSchema.safeParse({ ...validAgent, volumeSize: 0 });
40
+ (0, vitest_1.expect)(result.success).toBe(false);
41
+ });
42
+ (0, vitest_1.it)("rejects empty name", () => {
43
+ const result = schemas_1.AgentDefinitionSchema.safeParse({ ...validAgent, name: "" });
44
+ (0, vitest_1.expect)(result.success).toBe(false);
45
+ });
46
+ });
47
+ (0, vitest_1.describe)("ClawupManifestSchema", () => {
48
+ const validManifest = {
49
+ stackName: "dev",
50
+ provider: "aws",
51
+ region: "us-east-1",
52
+ instanceType: "t3.medium",
53
+ ownerName: "Boss",
54
+ agents: [
55
+ {
56
+ name: "agent-pm",
57
+ displayName: "Juno",
58
+ role: "pm",
59
+ identity: "https://github.com/org/identities#pm",
60
+ volumeSize: 30,
61
+ },
62
+ ],
63
+ };
64
+ (0, vitest_1.it)("accepts a valid manifest", () => {
65
+ (0, vitest_1.expect)(() => schemas_1.ClawupManifestSchema.parse(validManifest)).not.toThrow();
66
+ });
67
+ (0, vitest_1.it)("rejects manifest with no agents", () => {
68
+ const result = schemas_1.ClawupManifestSchema.safeParse({ ...validManifest, agents: [] });
69
+ (0, vitest_1.expect)(result.success).toBe(false);
70
+ });
71
+ (0, vitest_1.it)("rejects invalid provider", () => {
72
+ const result = schemas_1.ClawupManifestSchema.safeParse({ ...validManifest, provider: "gcp" });
73
+ (0, vitest_1.expect)(result.success).toBe(false);
74
+ });
75
+ });
76
+ (0, vitest_1.describe)("IdentityManifestSchema", () => {
77
+ const validIdentity = {
78
+ name: "juno",
79
+ displayName: "Juno",
80
+ role: "pm",
81
+ emoji: "clipboard",
82
+ description: "Product manager agent",
83
+ volumeSize: 30,
84
+ skills: ["pm-queue-handler"],
85
+ templateVars: ["OWNER_NAME"],
86
+ };
87
+ (0, vitest_1.it)("accepts a valid identity manifest", () => {
88
+ (0, vitest_1.expect)(() => schemas_1.IdentityManifestSchema.parse(validIdentity)).not.toThrow();
89
+ });
90
+ (0, vitest_1.it)("rejects identity missing required fields", () => {
91
+ const result = schemas_1.IdentityManifestSchema.safeParse({ name: "only-name" });
92
+ (0, vitest_1.expect)(result.success).toBe(false);
93
+ if (!result.success) {
94
+ const paths = result.error.issues.map((i) => i.path[0]);
95
+ (0, vitest_1.expect)(paths).toContain("role");
96
+ (0, vitest_1.expect)(paths).toContain("volumeSize");
97
+ (0, vitest_1.expect)(paths).toContain("skills");
98
+ }
99
+ });
100
+ (0, vitest_1.it)("rejects non-positive volumeSize", () => {
101
+ const result = schemas_1.IdentityManifestSchema.safeParse({ ...validIdentity, volumeSize: 0 });
102
+ (0, vitest_1.expect)(result.success).toBe(false);
103
+ });
104
+ (0, vitest_1.it)("validates plugins as array of non-empty strings", () => {
105
+ const result = schemas_1.IdentityManifestSchema.safeParse({
106
+ ...validIdentity,
107
+ plugins: ["openclaw-linear", ""],
108
+ });
109
+ (0, vitest_1.expect)(result.success).toBe(false);
110
+ });
111
+ (0, vitest_1.it)("accepts optional fields", () => {
112
+ const result = schemas_1.IdentityManifestSchema.parse({
113
+ ...validIdentity,
114
+ model: "anthropic/claude-opus-4-6",
115
+ backupModel: "anthropic/claude-sonnet-4-5",
116
+ codingAgent: "claude-code",
117
+ deps: ["gh"],
118
+ pluginDefaults: { "openclaw-linear": { key: "value" } },
119
+ });
120
+ (0, vitest_1.expect)(result.model).toBe("anthropic/claude-opus-4-6");
121
+ (0, vitest_1.expect)(result.deps).toEqual(["gh"]);
122
+ });
123
+ });
124
+ //# sourceMappingURL=schemas.test.js.map