@clawops/cli 2.2.1 → 2.3.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 (44) hide show
  1. package/README.md +48 -3
  2. package/dist/{apply-F5SQ2ZOS.js → apply-ODV3NOJJ.js} +9 -9
  3. package/dist/{automation-NBYQ6MFZ.js → automation-VDKLZMJI.js} +1 -1
  4. package/dist/{backup-flows-NDP5JTR3.js → backup-flows-THPX2D7S.js} +1 -1
  5. package/dist/{bootstrap-32TZXVDF.js → bootstrap-L3P5E4D7.js} +2 -2
  6. package/dist/{chunk-PASAVVNS.js → chunk-47CGHUVT.js} +1 -1
  7. package/dist/{chunk-E6BRQK67.js → chunk-4SEFKZRL.js} +1 -1
  8. package/dist/{chunk-PCSX6Y3U.js → chunk-66CUJFXK.js} +2 -2
  9. package/dist/{chunk-55UMO37O.js → chunk-CHPMOE4T.js} +1 -1
  10. package/dist/{chunk-Y3N63PDS.js → chunk-EGGXYZC6.js} +60 -59
  11. package/dist/{chunk-755X5OGS.js → chunk-FGY262WZ.js} +4 -1
  12. package/dist/{chunk-LV7UO5CG.js → chunk-G2ROJXYJ.js} +2 -2
  13. package/dist/{chunk-GFIR2IIJ.js → chunk-GYMAC4MW.js} +47 -28
  14. package/dist/{chunk-T6OCPTTR.js → chunk-K7OWMI63.js} +14 -8
  15. package/dist/{chunk-OUQVKIIA.js → chunk-PNRFFYOA.js} +14 -14
  16. package/dist/{chunk-AX65JMDZ.js → chunk-PRBDJSWN.js} +12 -9
  17. package/dist/{chunk-4MHDW2NB.js → chunk-QRF4OWG4.js} +1 -1
  18. package/dist/{chunk-AVERNLUY.js → chunk-SQPHNVCF.js} +6 -6
  19. package/dist/{chunk-RDUKRIRC.js → chunk-U3NRKBDW.js} +1 -1
  20. package/dist/cli.js +111 -72
  21. package/dist/{context-EHSLWKSH.js → context-4KTM4DKH.js} +5 -5
  22. package/dist/{flows-6B2I6TRW.js → flows-6YEJGHCK.js} +7 -7
  23. package/dist/{generate-4JWTMOLQ.js → generate-OJ5UWTLV.js} +9 -7
  24. package/dist/{harden-UHAOVQMZ.js → harden-7O553IOK.js} +5 -5
  25. package/dist/{init-HOPXDNWY.js → init-RVQ72GZ5.js} +2 -2
  26. package/dist/{mcp-wire-7MGEAMZH.js → mcp-wire-HSGZFOG5.js} +21 -21
  27. package/dist/{migrate-flow-24XA4GZL.js → migrate-flow-4XQUUFMH.js} +2 -2
  28. package/dist/{package-ESYPCIES.js → package-UOWSHBWP.js} +1 -1
  29. package/dist/policy-4FFK7REN.js +42 -0
  30. package/dist/{remote-config-OZYBMBSK.js → remote-config-V2WXIXIF.js} +1 -1
  31. package/dist/{server-Y2EDRUDL.js → server-YH2JWZ2Q.js} +21 -21
  32. package/dist/setup-policy-OK5PGWP7.js +90 -0
  33. package/dist/{stack-config-ICQ5YHUL.js → stack-config-3E7KWFPI.js} +2 -2
  34. package/dist/{stack-delete-NTIGKWUF.js → stack-delete-SNPMLWMD.js} +2 -2
  35. package/dist/{state-AKZP2S4T.js → state-KG6K5QZC.js} +2 -2
  36. package/dist/{store-YOLQYXMB.js → store-BTQOOIJH.js} +1 -1
  37. package/dist/{tailscale-cutover-V42HO5UM.js → tailscale-cutover-XTMGDM5H.js} +2 -2
  38. package/dist/{validate-DZE6LFEB.js → validate-CRXX7OYF.js} +1 -1
  39. package/package.json +1 -1
  40. package/spec/errors.yaml +5 -3
  41. package/spec/mcp-tools.yaml +4 -3
  42. package/dist/{chunk-UAJJEDCJ.js → chunk-AGSNTIYZ.js} +0 -0
  43. package/dist/{chunk-YPVWF7IA.js → chunk-HSGKK3RF.js} +3 -3
  44. package/dist/{ready-IJEUBFVX.js → ready-4H3NNEYQ.js} +3 -3
package/README.md CHANGED
@@ -262,7 +262,7 @@ clawops down --yes # Destroy local-provider stack
262
262
 
263
263
  | Command | Description |
264
264
  |---|---|
265
- | `setup` | First-run wizard: guided LLM, integrations, and deploy-plan generation |
265
+ | `setup` | First-run wizard: guided LLM, integrations, spending limits, and deploy-plan generation |
266
266
  | `init` | Register a stack in `~/.clawops/config.json` without provisioning. Additive, existing stacks are kept; `--force` is needed only to overwrite one |
267
267
  | `up` | Provision or update stack (`--dry-run` for preview, `--gateway-port` for a non-default port) |
268
268
  | `down` | Destroy local-provider stack (requires `--yes`; `--dry-run` shows current outputs) |
@@ -397,7 +397,7 @@ Do not bind to a non-loopback address without additional authentication controls
397
397
  | `clawops_harden` | cli | Apply hardening modules; join or leave a tailnet (elicits confirmation) |
398
398
  | `clawops_init` | cli | Register a stack and write `~/.clawops/config.json` (no cloud resources) |
399
399
  | `clawops_workflow_deploy_app` | workflow | End-to-end deploy: plan → confirm → apply → status |
400
- | `clawops_workflow_recover` | workflow | Diagnostic workflow for an unhealthy stack |
400
+ | `clawops_workflow_recover` | workflow | Diagnose an unhealthy stack: status, failed health checks, recent logs, and which tool fixes what |
401
401
  | `clawops_task_status` | cli | Poll a long-running task |
402
402
 
403
403
  Tools in the `read` toolset are also available in `--read-only` mode; the table's Toolset column shows the primary toolset. All other toolsets require full mode.
@@ -442,6 +442,24 @@ Config lives at `~/.clawops/config.json` (override with `$CLAWOPS_HOME`).
442
442
  | Azure | `AZURE_CLIENT_ID` / `AZURE_CLIENT_SECRET` or `az login` |
443
443
  | Local | SSH host + key configured in `stacks[name].localOpts` |
444
444
 
445
+ ### Spending limits
446
+
447
+ An optional `policy` block limits what clawops will deploy from this machine, whichever tool asks:
448
+
449
+ ```json
450
+ "policy": {
451
+ "allowedInstanceTypes": { "aws": ["micro", "small"] },
452
+ "maxStacks": 3
453
+ }
454
+ ```
455
+
456
+ `plan`, `apply` and `up` refuse a server size outside `allowedInstanceTypes` for that provider
457
+ (a clawops size or the provider's own type; `small` and `t3.small` are the same on AWS), and
458
+ `init` refuses a new stack beyond `maxStacks`. Every MCP tool that deploys is held to the same
459
+ limits, in the same words, and no tool can change them. `clawops setup` asks whether to set
460
+ them, then offers only the sizes you allowed. The limit lives here rather than in the plan
461
+ because a plan is what an agent writes.
462
+
445
463
  ---
446
464
 
447
465
  ## Known limitations
@@ -576,7 +594,7 @@ pnpm dev doctor # verify toolchain
576
594
  ```bash
577
595
  pnpm dev # run CLI from src/ via tsx
578
596
  pnpm build # tsup → dist/
579
- pnpm test # vitest (2091 tests, ~13s)
597
+ pnpm test # vitest (2115 tests, ~13s)
580
598
  pnpm test:changed # vitest --changed (fast edit loop)
581
599
  pnpm test:integration # Docker-based SSH integration tests
582
600
  pnpm test:e2e:local # builds, then bootstrap, the host-touching MCP tools and a 1.x migration, in systemd containers
@@ -635,6 +653,33 @@ Use `pnpm changeset` to record a release note before merging a `feat` or `fix`.
635
653
 
636
654
  ---
637
655
 
656
+ ## What's new in 2.3
657
+
658
+ Spending limits that an agent cannot lift, and a run of fixes from the backlog.
659
+
660
+ ### Spending limits
661
+
662
+ - An optional `policy` block in `~/.clawops/config.json` limits the server sizes clawops may
663
+ deploy on each provider and the number of deployments it may manage. `plan`, `apply` and `up`
664
+ refuse a size outside the list; `init` refuses a stack beyond the cap.
665
+ - Every MCP tool that deploys is held to the same limits, in the same words, and no tool can
666
+ change them. Confirmation is something an agent can answer for itself; this is not.
667
+ - `clawops setup` asks whether to set them, then offers only the sizes you allowed. See
668
+ [Spending limits](#spending-limits).
669
+
670
+ ### Fixes
671
+
672
+ - **The setup wizard's plan asked the cloud for a machine called `small`.** It wrote the clawops
673
+ size where the provider's own type belongs. It now maps it, as `clawops plan` does.
674
+ - **`clawops_workflow_recover` runs the stack's health checks, and recommends only real tools.** It
675
+ ran two of the four steps it described and finished by suggesting `clawops refresh`, which does
676
+ not exist.
677
+ - **The MCP audit log redacts credential arguments.** `GOOGLE_APPLICATION_CREDENTIALS` was named
678
+ sensitive in `spec/errors.yaml` and logged in the clear. A test now fails when the spec and the
679
+ code disagree on an error class, its exit code, or a redaction pattern (#185).
680
+
681
+ ---
682
+
638
683
  ## What's new in 2.2
639
684
 
640
685
  Recovery is a flag, agents can reach every command they need, and SSH failures say what failed.
@@ -1,27 +1,27 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  applyPlan
4
- } from "./chunk-AX65JMDZ.js";
4
+ } from "./chunk-PRBDJSWN.js";
5
5
  import "./chunk-BOPSG2LI.js";
6
- import "./chunk-UAJJEDCJ.js";
7
- import "./chunk-PASAVVNS.js";
8
- import "./chunk-55UMO37O.js";
6
+ import "./chunk-CHPMOE4T.js";
9
7
  import "./chunk-AEEMRFZV.js";
10
- import "./chunk-AVERNLUY.js";
8
+ import "./chunk-AGSNTIYZ.js";
9
+ import "./chunk-47CGHUVT.js";
10
+ import "./chunk-SQPHNVCF.js";
11
11
  import "./chunk-6ZFIFDBJ.js";
12
12
  import "./chunk-WPZIJONN.js";
13
- import "./chunk-YPVWF7IA.js";
14
13
  import "./chunk-JW5A7E3X.js";
15
- import "./chunk-VUFMZEIM.js";
14
+ import "./chunk-HSGKK3RF.js";
16
15
  import "./chunk-N5ZYUTZS.js";
17
16
  import "./chunk-VOEJYRIK.js";
18
17
  import "./chunk-EI7CQQ7D.js";
19
18
  import "./chunk-FEK7VJAY.js";
20
19
  import "./chunk-RHOWYIWT.js";
21
20
  import "./chunk-N4WZLWJ4.js";
22
- import "./chunk-RDUKRIRC.js";
23
- import "./chunk-GFIR2IIJ.js";
21
+ import "./chunk-U3NRKBDW.js";
22
+ import "./chunk-GYMAC4MW.js";
24
23
  import "./chunk-KGXPLI7W.js";
24
+ import "./chunk-VUFMZEIM.js";
25
25
  export {
26
26
  applyPlan
27
27
  };
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-V25QRZRB.js";
8
8
  import {
9
9
  getConfigDir
10
- } from "./chunk-GFIR2IIJ.js";
10
+ } from "./chunk-GYMAC4MW.js";
11
11
  import "./chunk-KGXPLI7W.js";
12
12
 
13
13
  // src/pulumi/automation.ts
@@ -10,7 +10,7 @@ import {
10
10
  defaultBackupFilename,
11
11
  nothingChanged,
12
12
  stageRestore
13
- } from "./chunk-PCSX6Y3U.js";
13
+ } from "./chunk-66CUJFXK.js";
14
14
  import "./chunk-WPZIJONN.js";
15
15
  import "./chunk-N5ZYUTZS.js";
16
16
  import "./chunk-VOEJYRIK.js";
@@ -14,8 +14,8 @@ import {
14
14
  import "./chunk-EI7CQQ7D.js";
15
15
  import {
16
16
  writeLocalState
17
- } from "./chunk-RDUKRIRC.js";
18
- import "./chunk-GFIR2IIJ.js";
17
+ } from "./chunk-U3NRKBDW.js";
18
+ import "./chunk-GYMAC4MW.js";
19
19
  import {
20
20
  ProviderError
21
21
  } from "./chunk-KGXPLI7W.js";
@@ -44,7 +44,7 @@ async function validatePlanConfig(plan) {
44
44
  ]);
45
45
  const { loadVersionSpec } = await import("./versions-W5LRQCAJ.js");
46
46
  const spec = loadVersionSpec(yaml);
47
- const { deepMerge } = await import("./remote-config-OZYBMBSK.js");
47
+ const { deepMerge } = await import("./remote-config-V2WXIXIF.js");
48
48
  const merged = deepMerge(
49
49
  { gateway: { mode: "local" } },
50
50
  overlay
@@ -136,7 +136,7 @@ var SYNTHESISED_CONFIG = JSON.stringify({
136
136
  channels: {}
137
137
  });
138
138
  async function prepareMigration(args) {
139
- const { buildContext } = await import("./context-EHSLWKSH.js");
139
+ const { buildContext } = await import("./context-4KTM4DKH.js");
140
140
  const { guardOpenclawVersion, defaultOpenclawVersion } = await import("./version-guard-XVYW5WFJ.js");
141
141
  const version = args.openclawVersion ? await guardOpenclawVersion(args.openclawVersion) : await defaultOpenclawVersion();
142
142
  const ctx = buildContext({ stack: args.stack });
@@ -230,8 +230,8 @@ async function stageRestore(session, opts) {
230
230
  async function activateStaged(session, staged, opts = {}) {
231
231
  const { signal } = opts;
232
232
  const hostExec = (command) => execPrivileged(session, command, signal);
233
- const { restartGateway } = await import("./remote-config-OZYBMBSK.js");
234
- const { waitForGateway } = await import("./ready-IJEUBFVX.js");
233
+ const { restartGateway } = await import("./remote-config-V2WXIXIF.js");
234
+ const { waitForGateway } = await import("./ready-4H3NNEYQ.js");
235
235
  opts.onPhase?.("activating");
236
236
  const chown = await hostExec(`chown -R ${CONTAINER_UID}:${CONTAINER_UID} ${staged.statePath}`);
237
237
  if (chown.code !== 0) {
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-VOEJYRIK.js";
8
8
  import {
9
9
  getConfig
10
- } from "./chunk-GFIR2IIJ.js";
10
+ } from "./chunk-GYMAC4MW.js";
11
11
  import {
12
12
  UsageError
13
13
  } from "./chunk-KGXPLI7W.js";
@@ -1,9 +1,13 @@
1
1
  #!/usr/bin/env node
2
+ import {
3
+ DEFAULT_AGENT_LOG_LIMIT,
4
+ readAgentActivity
5
+ } from "./chunk-27335SIH.js";
2
6
  import {
3
7
  migrationQuestion,
4
8
  prepareMigration,
5
9
  runMigration
6
- } from "./chunk-E6BRQK67.js";
10
+ } from "./chunk-4SEFKZRL.js";
7
11
  import {
8
12
  ACTIVATED,
9
13
  ARCHIVE_IS_A_CREDENTIAL,
@@ -15,43 +19,39 @@ import {
15
19
  defaultBackupFilename,
16
20
  nothingChanged,
17
21
  stageRestore
18
- } from "./chunk-PCSX6Y3U.js";
22
+ } from "./chunk-66CUJFXK.js";
19
23
  import {
20
24
  checkStackDelete,
21
25
  deleteStackFromConfig,
22
26
  forgetNotice
23
- } from "./chunk-LV7UO5CG.js";
27
+ } from "./chunk-G2ROJXYJ.js";
24
28
  import {
25
29
  applyPlan
26
- } from "./chunk-AX65JMDZ.js";
30
+ } from "./chunk-PRBDJSWN.js";
27
31
  import {
28
32
  resolveConn
29
33
  } from "./chunk-SX75L2T3.js";
30
34
  import {
31
- DEFAULT_AGENT_LOG_LIMIT,
32
- readAgentActivity
33
- } from "./chunk-27335SIH.js";
34
- import {
35
- generatePlan
36
- } from "./chunk-T6OCPTTR.js";
35
+ acquireSession,
36
+ drainPool
37
+ } from "./chunk-TYNMOYVM.js";
37
38
  import {
38
39
  extractBaseOutputs
39
40
  } from "./chunk-3QJBNAHW.js";
41
+ import {
42
+ generatePlan
43
+ } from "./chunk-K7OWMI63.js";
40
44
  import {
41
45
  atomicWriteConfig,
42
46
  readRemoteConfig,
43
47
  restartGateway
44
- } from "./chunk-UAJJEDCJ.js";
48
+ } from "./chunk-AGSNTIYZ.js";
45
49
  import {
46
50
  validatePlan
47
- } from "./chunk-PASAVVNS.js";
51
+ } from "./chunk-47CGHUVT.js";
48
52
  import {
49
53
  buildContext
50
- } from "./chunk-AVERNLUY.js";
51
- import {
52
- acquireSession,
53
- drainPool
54
- } from "./chunk-TYNMOYVM.js";
54
+ } from "./chunk-SQPHNVCF.js";
55
55
  import {
56
56
  AUDIT_CLEAN_MESSAGE,
57
57
  AUDIT_FIX_HINT,
@@ -69,7 +69,7 @@ import {
69
69
  resolveUpdateVersion,
70
70
  runDiagnostics,
71
71
  updateGateway
72
- } from "./chunk-OUQVKIIA.js";
72
+ } from "./chunk-PNRFFYOA.js";
73
73
  import {
74
74
  GATEWAY_LOGS_PROBE,
75
75
  chooseLogSource,
@@ -82,7 +82,7 @@ import {
82
82
  import {
83
83
  getConfig,
84
84
  getConfigDir
85
- } from "./chunk-GFIR2IIJ.js";
85
+ } from "./chunk-GYMAC4MW.js";
86
86
  import {
87
87
  StateError,
88
88
  UsageError
@@ -105,7 +105,7 @@ function getSessionId() {
105
105
  if (!_sessionId) _sessionId = randomUUID();
106
106
  return _sessionId;
107
107
  }
108
- var SENSITIVE_KEYS = ["token", "secret", "key", "password", "connectionstring", "authorization"];
108
+ var SENSITIVE_KEYS = ["token", "secret", "key", "password", "connectionstring", "authorization", "credential"];
109
109
  var EXEMPT_KEYS = ["keyname", "keypath", "privateKeyPath", "knownHostsPath"];
110
110
  var ARN_RE = /arn:aws:[a-z0-9\-*]+:[a-z0-9\-]*:[0-9]*:[^\s,'"]+/gi;
111
111
  function sanitize(args) {
@@ -565,9 +565,10 @@ var clawops_workflow_deploy_appAnnotations = {
565
565
  var clawops_workflow_recoverSchema = z.object({
566
566
  stackName: z.string().optional().describe("Which stack to diagnose. Omitted = the default stack in ~/.clawops/config.json")
567
567
  });
568
- var clawops_workflow_recoverDescription = `Diagnostic workflow for an unhealthy stack. Internally: status check \u2192
569
- gateway logs \u2192 agent logs \u2192 systemd service status \u2192 produces a structured
570
- diagnostic report with suggested remediation.
568
+ var clawops_workflow_recoverDescription = `Diagnostic workflow for an unhealthy stack. Internally: what is deployed
569
+ (clawops_status) \u2192 the stack's failed health checks (clawops_doctor) \u2192 the
570
+ gateway's last 50 log lines (clawops_logs_tail) \u2192 a report with next steps that
571
+ name the tool for each kind of failure. Changes nothing.
571
572
 
572
573
  Use when: the user reports any "not working" symptom and you don't know
573
574
  where to start. Best entry point for troubleshooting.
@@ -1155,7 +1156,7 @@ async function handleUp(input, server) {
1155
1156
  );
1156
1157
  }
1157
1158
  const { localOpts } = stackConfig;
1158
- const { localBootstrap } = await import("./bootstrap-32TZXVDF.js");
1159
+ const { localBootstrap } = await import("./bootstrap-L3P5E4D7.js");
1159
1160
  const ac = new AbortController();
1160
1161
  const state = await localBootstrap({
1161
1162
  host: localOpts.host,
@@ -1178,8 +1179,8 @@ async function handleUp(input, server) {
1178
1179
  const progressToken = void 0;
1179
1180
  const emit = makeProgressEmitter(server, progressToken);
1180
1181
  startTask(taskId, `clawops_up stack=${ctx.stackName}`);
1181
- const { generatePlan: generatePlan2 } = await import("./generate-4JWTMOLQ.js");
1182
- const { applyPlan: applyPlan2 } = await import("./apply-F5SQ2ZOS.js");
1182
+ const { generatePlan: generatePlan2 } = await import("./generate-OJ5UWTLV.js");
1183
+ const { applyPlan: applyPlan2 } = await import("./apply-ODV3NOJJ.js");
1183
1184
  const { resolveNetworkFlags } = await import("./network-args-PYJRJBYO.js");
1184
1185
  const { detectEgressIp } = await import("./firewall-5VKZ4WBH.js");
1185
1186
  const network = await resolveNetworkFlags(
@@ -1294,7 +1295,7 @@ async function handlePlan(input, _server) {
1294
1295
  let network = resolved;
1295
1296
  if (input.privateOnly) {
1296
1297
  const { privateOnlyNetwork, assertTailnetReachable } = await import("./private-only-DIA26I2E.js");
1297
- const { probeSsh } = await import("./tailscale-cutover-V42HO5UM.js");
1298
+ const { probeSsh } = await import("./tailscale-cutover-XTMGDM5H.js");
1298
1299
  network = privateOnlyNetwork(flags, resolved, ctx.config.stacks[stackName]?.tailscale, stackName);
1299
1300
  await assertTailnetReachable(ctx, (conn) => probeSsh(conn));
1300
1301
  }
@@ -1331,7 +1332,7 @@ async function handleHarden(input, server) {
1331
1332
  "tailscale and tailscaleRevert do the opposite of each other; pass one, not both."
1332
1333
  );
1333
1334
  }
1334
- const { getConfig: getConfig2 } = await import("./store-YOLQYXMB.js");
1335
+ const { getConfig: getConfig2 } = await import("./store-BTQOOIJH.js");
1335
1336
  const config = getConfig2();
1336
1337
  if (!config) {
1337
1338
  return errText(
@@ -1350,12 +1351,12 @@ async function handleHarden(input, server) {
1350
1351
  return okText(confirmation.reason);
1351
1352
  }
1352
1353
  }
1353
- const { revertTailnet, cutOverToTailnet, connectionFor } = await import("./flows-6B2I6TRW.js");
1354
+ const { revertTailnet, cutOverToTailnet, connectionFor } = await import("./flows-6YEJGHCK.js");
1354
1355
  if (input.tailscaleRevert) {
1355
1356
  const outcome = await revertTailnet(ctx, config);
1356
1357
  return outcome.ok ? okText(outcome.message) : errText(outcome.reason);
1357
1358
  }
1358
- const { MODULE_CATALOG, resolveModules, runHardening, formatHardenSummary, makeTailscaleModule } = await import("./harden-UHAOVQMZ.js");
1359
+ const { MODULE_CATALOG, resolveModules, runHardening, formatHardenSummary, makeTailscaleModule } = await import("./harden-7O553IOK.js");
1359
1360
  const forStack = makeTailscaleModule(ctx.stackName);
1360
1361
  const resolved = resolveModules(MODULE_CATALOG, input.options, ctx.adapter.name).map(
1361
1362
  (m) => m.id === "tailscale" ? forStack : m
@@ -1399,7 +1400,7 @@ async function handleHarden(input, server) {
1399
1400
 
1400
1401
  // src/mcp/tools/cli/init.ts
1401
1402
  async function handleInit(input, _server) {
1402
- const { initStack } = await import("./init-HOPXDNWY.js");
1403
+ const { initStack } = await import("./init-RVQ72GZ5.js");
1403
1404
  const result = await initStack({
1404
1405
  provider: input.provider,
1405
1406
  stackName: input.stackName,
@@ -1865,39 +1866,39 @@ Changes: ${diffLine}. This will provision cloud resources and may incur costs.`,
1865
1866
  }
1866
1867
 
1867
1868
  // src/mcp/tools/workflow/recover.ts
1868
- async function handleWorkflowRecover(input, server) {
1869
- const parts = [`## Diagnostic Report \u2014 stack: ${input.stackName ?? "default"}
1870
- `];
1871
- try {
1872
- const statusResult = await handleStatus({ stackName: input.stackName }, server);
1873
- const statusText = statusResult.content.filter((c) => c.type === "text").map((c) => c.text).join("");
1874
- parts.push(`### Status
1875
- \`\`\`json
1876
- ${statusText}
1877
- \`\`\``);
1878
- } catch (err) {
1879
- parts.push(`### Status
1880
- Error: ${err instanceof Error ? err.message : String(err)}`);
1881
- }
1869
+ function textOf(result) {
1870
+ return result.content.filter((c) => c.type === "text").map((c) => c.text).join("");
1871
+ }
1872
+ async function section(title, fence, run) {
1882
1873
  try {
1883
- const logsResult = await handleLogsTail({ stackName: input.stackName, tailLines: 50 }, server);
1884
- const logsText = logsResult.content.filter((c) => c.type === "text").map((c) => c.text).join("");
1885
- parts.push(`### Recent Logs (last 50 lines)
1886
- \`\`\`
1887
- ${logsText}
1888
- \`\`\``);
1874
+ const result = await run();
1875
+ return result.isError ? `### ${title}
1876
+ Error: ${textOf(result)}` : `### ${title}
1877
+ \`\`\`${fence}
1878
+ ${textOf(result)}
1879
+ \`\`\``;
1889
1880
  } catch (err) {
1890
- parts.push(`### Recent Logs
1891
- Error: ${err instanceof Error ? err.message : String(err)}`);
1881
+ return `### ${title}
1882
+ Error: ${err instanceof Error ? err.message : String(err)}`;
1892
1883
  }
1884
+ }
1885
+ async function handleWorkflowRecover(input, server) {
1886
+ const stack = input.stackName ?? "default";
1887
+ const parts = [`## Diagnostic Report \u2014 stack: ${stack}
1888
+ `];
1889
+ parts.push(await section("Status", "json", () => handleStatus({ stackName: input.stackName }, server)));
1890
+ parts.push(await section("Failed health checks", "json", () => handleDoctor({ stackName: input.stackName, failuresOnly: true }, server)));
1891
+ parts.push(await section("Recent Logs (last 50 lines)", "", () => handleLogsTail({ stackName: input.stackName, tailLines: 50 }, server)));
1893
1892
  parts.push(
1894
1893
  `### Next Steps
1895
- - If gateway is not running: try \`clawops_gateway_restart\`
1896
- - If bootstrap failed: run \`clawops_up\` again (idempotent)
1897
- - If stack is not deployed: run \`clawops_workflow_deploy_app\`
1898
- - For Pulumi drift: run \`clawops refresh\` from the CLI`
1894
+ - Gateway not running, or not answering: \`clawops_gateway_restart\`
1895
+ - Gateway fails on its current version: \`clawops_gateway_update\` to a version that worked, or \`clawops_backup_restore\` with \`activate\` if the state is at fault
1896
+ - Host not set up, or set up only partly: \`clawops_up\` again (it is idempotent)
1897
+ - Infrastructure differs from what was deployed: \`clawops_plan\`, review, then \`clawops_apply\`
1898
+ - Nothing deployed yet: \`clawops_workflow_deploy_app\``
1899
1899
  );
1900
- return { content: [{ type: "text", text: parts.join("\n\n") }] };
1900
+ const { content } = trimForMcp(parts.join("\n\n"), stack);
1901
+ return { content: [{ type: "text", text: content }] };
1901
1902
  }
1902
1903
 
1903
1904
  // src/mcp/tools/registry.ts
@@ -2232,7 +2233,7 @@ function bearerFrom(header) {
2232
2233
  return match?.[1]?.trim();
2233
2234
  }
2234
2235
  async function serveMcp(opts) {
2235
- const { version } = await import("./package-ESYPCIES.js");
2236
+ const { version } = await import("./package-UOWSHBWP.js");
2236
2237
  const makeServer = () => {
2237
2238
  const server = new McpServer({ name: "clawops", version });
2238
2239
  registerTools(server, opts);
@@ -3,7 +3,7 @@ import {
3
3
  getConfig,
4
4
  getConfigDir,
5
5
  setConfig
6
- } from "./chunk-GFIR2IIJ.js";
6
+ } from "./chunk-GYMAC4MW.js";
7
7
 
8
8
  // src/config/init.ts
9
9
  import { spawnSync } from "child_process";
@@ -109,6 +109,9 @@ async function initStack(opts) {
109
109
  reason: `Stack "${stackName}" already exists in ${path.join(getConfigDir(), "config.json")} (${existing.stacks[stackName].stateUrl}). Pass force to overwrite its settings, or use a different stack name to add another one.`
110
110
  };
111
111
  }
112
+ const { stackCapRefusal } = await import("./policy-4FFK7REN.js");
113
+ const overCap = stackCapRefusal(existing, stackName);
114
+ if (overCap) return { ok: false, reason: overCap };
112
115
  const configDir = getConfigDir();
113
116
  mkdirSync(configDir, { recursive: true });
114
117
  const keyPath = opts.keyPath ?? path.join(configDir, "id_ed25519");
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  requireConfig,
4
4
  setConfig
5
- } from "./chunk-GFIR2IIJ.js";
5
+ } from "./chunk-GYMAC4MW.js";
6
6
 
7
7
  // src/config/stack-delete.ts
8
8
  function forgetNotice(name) {
@@ -47,7 +47,7 @@ async function checkStackDelete(name, opts) {
47
47
  return { ok: true, name, isDefault, warnings };
48
48
  }
49
49
  async function isStackDeployed(name) {
50
- const { buildContext } = await import("./context-EHSLWKSH.js");
50
+ const { buildContext } = await import("./context-4KTM4DKH.js");
51
51
  const ctx = buildContext({ stack: name });
52
52
  if (ctx.adapter.name === "local") return !!ctx.localState;
53
53
  const stack = await ctx.getStack();
@@ -10,46 +10,64 @@ import { randomUUID } from "crypto";
10
10
  import os from "os";
11
11
  import path from "path";
12
12
  import process from "process";
13
+ import { z as z2 } from "zod";
14
+
15
+ // src/config/policy-schema.ts
13
16
  import { z } from "zod";
14
- var CredentialsRefSchema = z.object({
15
- source: z.enum(["env", "cli-profile", "file", "instance-metadata"]),
16
- envVars: z.array(z.string()).optional(),
17
- profileName: z.string().optional()
17
+ var PolicySchema = z.object({
18
+ /**
19
+ * Per provider, the instance types clawops may deploy. A clawops size (`micro`, `small`,
20
+ * `medium`, `large`) or a provider-native type; sizes are compared after the provider maps
21
+ * them, so `small` and `t3.small` are the same entry on AWS. A provider with no entry is
22
+ * unrestricted.
23
+ */
24
+ allowedInstanceTypes: z.record(z.array(z.string()).min(1)).optional(),
25
+ /** How many stacks this config may hold. Absent = no cap. */
26
+ maxStacks: z.number().int().positive().optional()
27
+ });
28
+
29
+ // src/config/store.ts
30
+ var CredentialsRefSchema = z2.object({
31
+ source: z2.enum(["env", "cli-profile", "file", "instance-metadata"]),
32
+ envVars: z2.array(z2.string()).optional(),
33
+ profileName: z2.string().optional()
18
34
  });
19
- var LocalOptsSchema = z.object({
20
- host: z.string(),
21
- sshUser: z.string(),
22
- sshPort: z.number(),
23
- sshKeyPath: z.string()
35
+ var LocalOptsSchema = z2.object({
36
+ host: z2.string(),
37
+ sshUser: z2.string(),
38
+ sshPort: z2.number(),
39
+ sshKeyPath: z2.string()
24
40
  });
25
- var TailscaleOverrideSchema = z.object({
26
- ip: z.string(),
27
- hostname: z.string().optional(),
41
+ var TailscaleOverrideSchema = z2.object({
42
+ ip: z2.string(),
43
+ hostname: z2.string().optional(),
28
44
  /** When the address was verified, ISO 8601. */
29
- verifiedAt: z.string(),
45
+ verifiedAt: z2.string(),
30
46
  /** Set once --private-only has closed the public ports, so revert knows to reopen them. */
31
- privateOnly: z.boolean().optional()
47
+ privateOnly: z2.boolean().optional()
32
48
  });
33
- var StackConfigSchema = z.object({
34
- provider: z.string(),
35
- stateUrl: z.string(),
36
- region: z.string().optional(),
49
+ var StackConfigSchema = z2.object({
50
+ provider: z2.string(),
51
+ stateUrl: z2.string(),
52
+ region: z2.string().optional(),
37
53
  credentialsRef: CredentialsRefSchema,
38
54
  localOpts: LocalOptsSchema.optional(),
39
55
  tailscale: TailscaleOverrideSchema.optional()
40
56
  });
41
- var ClawopsConfigSchema = z.object({
42
- version: z.literal(1),
43
- defaults: z.object({
44
- stack: z.string(),
45
- provider: z.string()
57
+ var ClawopsConfigSchema = z2.object({
58
+ version: z2.literal(1),
59
+ defaults: z2.object({
60
+ stack: z2.string(),
61
+ provider: z2.string()
46
62
  }),
47
- stacks: z.record(StackConfigSchema),
48
- ssh: z.object({
49
- keyPath: z.string(),
50
- knownHostsPath: z.string()
63
+ stacks: z2.record(StackConfigSchema),
64
+ ssh: z2.object({
65
+ keyPath: z2.string(),
66
+ knownHostsPath: z2.string()
51
67
  }),
52
- mcp: z.object({ auditLogPath: z.string() }).optional()
68
+ mcp: z2.object({ auditLogPath: z2.string() }).optional(),
69
+ /** The operator's spending policy; see ./policy.ts. */
70
+ policy: PolicySchema.optional()
53
71
  });
54
72
  function getConfigDir() {
55
73
  return process.env["CLAWOPS_HOME"] ?? path.join(os.homedir(), ".clawops");
@@ -92,6 +110,7 @@ function setConfig(config) {
92
110
  }
93
111
 
94
112
  export {
113
+ PolicySchema,
95
114
  getConfigDir,
96
115
  getConfigPath,
97
116
  getConfig,
@@ -4,14 +4,14 @@ import {
4
4
  } from "./chunk-POYS3EVQ.js";
5
5
  import {
6
6
  validatePlan
7
- } from "./chunk-PASAVVNS.js";
7
+ } from "./chunk-47CGHUVT.js";
8
8
  import {
9
9
  buildContext,
10
10
  loadAdapterModule
11
- } from "./chunk-AVERNLUY.js";
11
+ } from "./chunk-SQPHNVCF.js";
12
12
  import {
13
13
  getConfig
14
- } from "./chunk-GFIR2IIJ.js";
14
+ } from "./chunk-GYMAC4MW.js";
15
15
  import {
16
16
  StateError,
17
17
  UsageError
@@ -62,9 +62,12 @@ async function resolveInstanceType(intent) {
62
62
  `[clawops] note: "${requested}" is not a clawops size (${INSTANCE_ALIASES.join(", ")}), so it is passed to ${intent.provider} as written.
63
63
  `
64
64
  );
65
- return requested;
66
65
  }
67
- const adapter = await loadAdapterModule(intent.provider);
66
+ return nativeInstanceType(intent.provider, requested);
67
+ }
68
+ async function nativeInstanceType(provider, requested) {
69
+ if (!isInstanceAlias(requested)) return requested;
70
+ const adapter = await loadAdapterModule(provider);
68
71
  return adapter.normalizeInstanceType(requested);
69
72
  }
70
73
  var parseDiffForTest = parseDiff;
@@ -74,9 +77,11 @@ async function generatePlan(intent, _opts) {
74
77
  "plan/apply is not supported for the local provider. Use `clawops up` directly."
75
78
  );
76
79
  }
77
- const { version } = await import("./package-ESYPCIES.js");
80
+ const { version } = await import("./package-UOWSHBWP.js");
78
81
  const config = getConfig();
79
82
  const instanceType = await resolveInstanceType(intent);
83
+ const { enforceInstanceType } = await import("./policy-4FFK7REN.js");
84
+ await enforceInstanceType(config, intent.provider, instanceType);
80
85
  const { guardOpenclawVersion, defaultOpenclawVersion } = await import("./version-guard-XVYW5WFJ.js");
81
86
  const openclawVersion = await guardOpenclawVersion(
82
87
  intent.openclawVersion ?? await defaultOpenclawVersion()
@@ -126,7 +131,7 @@ A plan is only as good as the state it was computed against, so this is not a pl
126
131
  );
127
132
  }
128
133
  try {
129
- const { writeStackConfig } = await import("./stack-config-ICQ5YHUL.js");
134
+ const { writeStackConfig } = await import("./stack-config-3E7KWFPI.js");
130
135
  await writeStackConfig(stack, plan);
131
136
  const outputLines = [];
132
137
  const preview = await stack.preview({
@@ -154,7 +159,7 @@ A plan is only as good as the state it was computed against, so this is not a pl
154
159
  throw new Error(`Generated plan failed schema validation:
155
160
  ${validation.errors.join("\n")}`);
156
161
  }
157
- const { validatePlanConfig, validatePlanNetwork } = await import("./validate-DZE6LFEB.js");
162
+ const { validatePlanConfig, validatePlanNetwork } = await import("./validate-CRXX7OYF.js");
158
163
  const cfgCheck = await validatePlanConfig(plan);
159
164
  for (const w of cfgCheck.warnings) process.stderr.write(`[clawops] warning: ${w}
160
165
  `);
@@ -186,6 +191,7 @@ function expandHome(p) {
186
191
  export {
187
192
  INSTANCE_ALIASES,
188
193
  isInstanceAlias,
194
+ nativeInstanceType,
189
195
  parseDiffForTest,
190
196
  generatePlan,
191
197
  planId