@marcusrbrown/infra 0.25.0 → 0.25.2

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 (2) hide show
  1. package/dist/cli.js +61 -19
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -6950,7 +6950,7 @@ import { goke } from "goke";
6950
6950
  // package.json
6951
6951
  var package_default = {
6952
6952
  name: "@marcusrbrown/infra",
6953
- version: "0.25.0",
6953
+ version: "0.25.2",
6954
6954
  description: "Infrastructure management CLI \u2014 deploy automation, health checks, and MCP bridge",
6955
6955
  keywords: [
6956
6956
  "infra",
@@ -8390,6 +8390,13 @@ function registerSetupCommand(cli, commandLabel) {
8390
8390
  import { log as log3 } from "@clack/prompts";
8391
8391
  import { z as z6 } from "zod";
8392
8392
 
8393
+ // src/commands/agent/key-layout.ts
8394
+ var KEY_LAYOUT_VERSION = "fro-bot-agent-s3-layout/1";
8395
+ var LEGACY_KEY_LAYOUT_LABELS = new Set(["fro-bot/agent@v0.118.2", "fro-bot/agent@v0.119.2"]);
8396
+ function isKnownKeyLayoutVersion(label) {
8397
+ return label === KEY_LAYOUT_VERSION || LEGACY_KEY_LAYOUT_LABELS.has(label);
8398
+ }
8399
+
8393
8400
  // src/commands/agent/workflow-verify.ts
8394
8401
  var WORKFLOW_PATH = ".github/workflows/fro-bot.yaml";
8395
8402
  var STORAGE_ENVIRONMENT = "fro-bot-storage";
@@ -8405,9 +8412,6 @@ var CONTENT_EVENTS = new Set([
8405
8412
  "discussion",
8406
8413
  "discussion_comment"
8407
8414
  ]);
8408
- var VERIFIED_ACTION_SHA_BY_LAYOUT = {
8409
- "fro-bot/agent@v0.118.2": "77f2bad7d68ac38279cd0fa28f38b26a0cd15dfb"
8410
- };
8411
8415
  var REQUIRED_S3_INPUTS = {
8412
8416
  "s3-backup": undefined,
8413
8417
  "s3-bucket": "FRO_BOT_S3_BUCKET",
@@ -8778,8 +8782,7 @@ function checkStorageAction(jobId, job, manifest, violations) {
8778
8782
  violations.push(`Storage job '${jobId}' must contain a fro-bot/agent action step.`);
8779
8783
  return;
8780
8784
  }
8781
- const verifiedSha = VERIFIED_ACTION_SHA_BY_LAYOUT[manifest.key_layout_version];
8782
- if (!verifiedSha) {
8785
+ if (!isKnownKeyLayoutVersion(manifest.key_layout_version)) {
8783
8786
  violations.push(`Unknown key_layout_version '${manifest.key_layout_version}'; refusing to verify fro-bot/agent.`);
8784
8787
  }
8785
8788
  const firstAgentIndex = steps.findIndex((step) => typeof step.uses === "string" && step.uses.startsWith("fro-bot/agent@"));
@@ -8807,8 +8810,8 @@ function checkStorageAction(jobId, job, manifest, violations) {
8807
8810
  for (const [index, step] of agentSteps.entries()) {
8808
8811
  const uses = String(step.uses);
8809
8812
  const ref = uses.split("@").at(-1) ?? "";
8810
- if (verifiedSha && ref !== verifiedSha) {
8811
- violations.push(`Storage job '${jobId}' action ref '${ref}' is not the verified layout for '${manifest.key_layout_version}'; ` + `storage job must pin fro-bot/agent to the verified SHA for '${manifest.key_layout_version}', not a moveable tag.`);
8813
+ if (!/^[0-9a-f]{40}$/i.test(ref)) {
8814
+ violations.push(`Storage job '${jobId}' fro-bot/agent step ${index + 1} ref '${ref}' is not a full commit SHA; ` + "storage job must pin fro-bot/agent to a full 40-hex commit SHA, not a moveable tag or branch.");
8812
8815
  }
8813
8816
  if (!isMapping(step.with)) {
8814
8817
  violations.push(`Storage job '${jobId}' agent step ${index + 1} is missing its S3 inputs.`);
@@ -8917,7 +8920,7 @@ function formatStorageWorkflowSnippet() {
8917
8920
  with:
8918
8921
  role-to-assume: \${{ vars.FRO_BOT_S3_ROLE_TO_ASSUME }}
8919
8922
  aws-region: \${{ vars.FRO_BOT_S3_REGION }}
8920
- - uses: fro-bot/agent@77f2bad7d68ac38279cd0fa28f38b26a0cd15dfb
8923
+ - uses: fro-bot/agent@83ed683da24111bd1474eb4bda39715ed5799bc2
8921
8924
  with:
8922
8925
  s3-backup: true
8923
8926
  s3-bucket: \${{ vars.FRO_BOT_S3_BUCKET }}
@@ -9138,7 +9141,6 @@ var STORAGE_VARIABLE_NAMES = {
9138
9141
  prefix: "FRO_BOT_S3_PREFIX",
9139
9142
  expectedBucketOwner: "FRO_BOT_S3_EXPECTED_BUCKET_OWNER"
9140
9143
  };
9141
- var KNOWN_KEY_LAYOUT_VERSION = "fro-bot/agent@v0.118.2";
9142
9144
  var manifestSchema = z6.object({
9143
9145
  owner: z6.string().min(1),
9144
9146
  repo: z6.string().min(1),
@@ -9190,8 +9192,8 @@ function assertCanonicalPath(field, value, mustEndWithSlash) {
9190
9192
  }
9191
9193
  }
9192
9194
  function validateManifest(manifest) {
9193
- if (manifest.key_layout_version !== KNOWN_KEY_LAYOUT_VERSION) {
9194
- throw new Error(`Unknown handoff manifest key_layout_version '${manifest.key_layout_version}'; expected ${KNOWN_KEY_LAYOUT_VERSION}.`);
9195
+ if (!isKnownKeyLayoutVersion(manifest.key_layout_version)) {
9196
+ throw new Error(`Unknown handoff manifest key_layout_version '${manifest.key_layout_version}'; expected ${KEY_LAYOUT_VERSION}.`);
9195
9197
  }
9196
9198
  assertRepoFormat(`${manifest.owner}/${manifest.repo}`);
9197
9199
  assertCanonicalPath("s3_prefix", manifest.s3_prefix, false);
@@ -11934,8 +11936,29 @@ function validateGatewayHost(host) {
11934
11936
  return host;
11935
11937
  }
11936
11938
 
11939
+ // src/commands/gateway/volume.ts
11940
+ var GATEWAY_COMPOSE_PROJECT_NAME = "fro-bot";
11941
+ var MITMPROXY_CERTS_VOLUME = `${GATEWAY_COMPOSE_PROJECT_NAME}_mitmproxy-certs`;
11942
+ var VOLUME_EXISTS_SENTINEL = "EXISTS";
11943
+ var VOLUME_MISSING_SENTINEL = "MISSING";
11944
+ function volumeProbeCommand(volume) {
11945
+ return `docker volume inspect ${volume} >/dev/null 2>&1 && echo ${VOLUME_EXISTS_SENTINEL} || echo ${VOLUME_MISSING_SENTINEL}`;
11946
+ }
11947
+ function checkVolumeProbeResult(volume, result) {
11948
+ if (result.exitCode !== 0) {
11949
+ return `Could not verify Docker volume ${volume} on the gateway host (exit ${result.exitCode}): ${result.stderr.trim() || "unknown error"}`;
11950
+ }
11951
+ const sentinel = result.stdout.trim();
11952
+ if (sentinel === VOLUME_EXISTS_SENTINEL) {
11953
+ return;
11954
+ }
11955
+ if (sentinel === VOLUME_MISSING_SENTINEL) {
11956
+ return `Docker volume ${volume} does not exist on the gateway host; refusing to continue (it would be created empty as a side effect).`;
11957
+ }
11958
+ return `Could not verify Docker volume ${volume} on the gateway host: unrecognized probe output`;
11959
+ }
11960
+
11937
11961
  // src/commands/gateway/backup.ts
11938
- var MITMPROXY_CERTS_VOLUME = "mitmproxy-certs";
11939
11962
  var CA_CERT_FILE = "mitmproxy-ca-cert.pem";
11940
11963
  var CA_KEY_FILE = "mitmproxy-ca.pem";
11941
11964
  var DEFAULT_OUTPUT = "apps/gateway/.local/mitmproxy-ca.tar";
@@ -11948,7 +11971,7 @@ async function backupGatewayCa(opts, spawn = defaultSpawn3, printErr) {
11948
11971
  return { ok: false, error: "Only CA backup is currently supported; --no-include-ca is not accepted" };
11949
11972
  }
11950
11973
  validateGatewayHost(opts.host);
11951
- const sshCmd = [
11974
+ const sshCommand = (remote) => [
11952
11975
  "ssh",
11953
11976
  "-o",
11954
11977
  "BatchMode=yes",
@@ -11956,13 +11979,28 @@ async function backupGatewayCa(opts, spawn = defaultSpawn3, printErr) {
11956
11979
  "StrictHostKeyChecking=yes",
11957
11980
  ...buildKnownHostsArgs(),
11958
11981
  `root@${opts.host}`,
11959
- `docker run --rm -v ${MITMPROXY_CERTS_VOLUME}:/src:ro alpine tar -cf - -C /src ${CA_CERT_FILE} ${CA_KEY_FILE}`
11982
+ remote
11960
11983
  ];
11961
11984
  const env = {
11962
11985
  PATH: process.env.PATH ?? "/usr/bin:/bin",
11963
11986
  HOME: process.env.HOME ?? "/tmp",
11964
11987
  ...process.env.SSH_AUTH_SOCK ? { SSH_AUTH_SOCK: process.env.SSH_AUTH_SOCK } : {}
11965
11988
  };
11989
+ const probe = spawn(sshCommand(volumeProbeCommand(MITMPROXY_CERTS_VOLUME)), { env, stdout: "pipe", stderr: "pipe" });
11990
+ const [probeStdout, probeStderr, probeExit] = await Promise.all([
11991
+ new Response(probe.stdout).text(),
11992
+ new Response(probe.stderr).text(),
11993
+ probe.exited
11994
+ ]);
11995
+ const probeError = checkVolumeProbeResult(MITMPROXY_CERTS_VOLUME, {
11996
+ stdout: probeStdout,
11997
+ stderr: probeStderr,
11998
+ exitCode: probeExit
11999
+ });
12000
+ if (probeError !== undefined) {
12001
+ return { ok: false, error: probeError };
12002
+ }
12003
+ const sshCmd = sshCommand(`docker run --rm -v ${MITMPROXY_CERTS_VOLUME}:/src:ro alpine tar -cf - -C /src ${CA_CERT_FILE} ${CA_KEY_FILE}`);
11966
12004
  const child = spawn(sshCmd, { env, stdout: "pipe", stderr: "pipe" });
11967
12005
  const [tarBytes, stderrText, exitCode] = await Promise.all([
11968
12006
  new Response(child.stdout).arrayBuffer(),
@@ -12231,7 +12269,6 @@ function registerGatewayLogs(cli) {
12231
12269
 
12232
12270
  // src/commands/gateway/restore.ts
12233
12271
  import { z as z25 } from "zod";
12234
- var MITMPROXY_CERTS_VOLUME2 = "mitmproxy-certs";
12235
12272
  var CA_CERT_FILE2 = "mitmproxy-ca-cert.pem";
12236
12273
  var CA_KEY_FILE2 = "mitmproxy-ca.pem";
12237
12274
  var COMPOSE_PROJECT_DIR4 = "/opt/gateway/deploy";
@@ -12300,6 +12337,11 @@ async function restoreGatewayCa(opts, spawn = defaultSpawn4) {
12300
12337
  if (archiveError !== undefined) {
12301
12338
  return { ok: false, error: archiveError };
12302
12339
  }
12340
+ const probeResult = await spawnText(spawn, sshCommand(opts.host, volumeProbeCommand(MITMPROXY_CERTS_VOLUME)), env);
12341
+ const probeError = checkVolumeProbeResult(MITMPROXY_CERTS_VOLUME, probeResult);
12342
+ if (probeError !== undefined) {
12343
+ return { ok: false, error: probeError };
12344
+ }
12303
12345
  const mktempResult = await spawnText(spawn, sshCommand(opts.host, "mktemp -t gateway-ca-restore-XXXXXX.tar"), env);
12304
12346
  if (mktempResult.exitCode !== 0) {
12305
12347
  return {
@@ -12336,7 +12378,7 @@ async function restoreGatewayCa(opts, spawn = defaultSpawn4) {
12336
12378
  };
12337
12379
  return primaryError;
12338
12380
  }
12339
- const extractResult = await spawnText(spawn, sshCommand(opts.host, `docker run --rm -v ${MITMPROXY_CERTS_VOLUME2}:/dst -v ${tmpRemote}:/src.tar:ro alpine sh -c 'tar -xf /src.tar -C /dst'`), env);
12381
+ const extractResult = await spawnText(spawn, sshCommand(opts.host, `docker run --rm -v ${MITMPROXY_CERTS_VOLUME}:/dst -v ${tmpRemote}:/src.tar:ro alpine sh -c 'tar -xf /src.tar -C /dst'`), env);
12340
12382
  if (extractResult.exitCode !== 0) {
12341
12383
  primaryError = {
12342
12384
  ok: false,
@@ -12353,8 +12395,8 @@ async function restoreGatewayCa(opts, spawn = defaultSpawn4) {
12353
12395
  return primaryError;
12354
12396
  }
12355
12397
  const [certResult, keyResult] = await Promise.all([
12356
- spawnText(spawn, sshCommand(opts.host, `docker run --rm -v ${MITMPROXY_CERTS_VOLUME2}:/src:ro alpine cat /src/${CA_CERT_FILE2}`), env),
12357
- spawnText(spawn, sshCommand(opts.host, `docker run --rm -v ${MITMPROXY_CERTS_VOLUME2}:/src:ro alpine cat /src/${CA_KEY_FILE2}`), env)
12398
+ spawnText(spawn, sshCommand(opts.host, `docker run --rm -v ${MITMPROXY_CERTS_VOLUME}:/src:ro alpine cat /src/${CA_CERT_FILE2}`), env),
12399
+ spawnText(spawn, sshCommand(opts.host, `docker run --rm -v ${MITMPROXY_CERTS_VOLUME}:/src:ro alpine cat /src/${CA_KEY_FILE2}`), env)
12358
12400
  ]);
12359
12401
  if (certResult.exitCode !== 0 || keyResult.exitCode !== 0) {
12360
12402
  primaryError = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@marcusrbrown/infra",
3
- "version": "0.25.0",
3
+ "version": "0.25.2",
4
4
  "description": "Infrastructure management CLI — deploy automation, health checks, and MCP bridge",
5
5
  "keywords": [
6
6
  "infra",