Compare commits

..

2 Commits

Author SHA1 Message Date
didericis a73aba935f fix(firecracker): read only BOT_BOTTLE_INFRA_ARTIFACT_TOKEN for the artifact
lint / lint (push) Successful in 2m19s
test / unit (pull_request) Successful in 1m21s
test / integration (pull_request) Successful in 24s
test / coverage (pull_request) Successful in 1m24s
Drop the fallback to the general-purpose BOT_BOTTLE_CLAUDE_GITEA_TOKEN so
the artifact pull/publish uses a dedicated, package-scoped token that can
be granted (or revoked) independently of the general Gitea token.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoEZHDjv84ChoZbozQERhJ
2026-07-16 23:31:17 -04:00
didericis 948504bde2 feat(firecracker): pull the infra rootfs as a prebuilt artifact (PRD 0069 Stage 2)
lint / lint (push) Successful in 2m25s
test / unit (pull_request) Successful in 1m22s
test / integration (pull_request) Successful in 26s
test / coverage (pull_request) Successful in 1m23s
Stage 2 of the docker-free Firecracker backend (#348): stop building the
fixed infra image on the launch host. The infra VM's rootfs is host- and
bottle-agnostic (authorized_keys + guest IP ride the kernel cmdline, not the
rootfs), so it's built once off-host and published as a versioned, ready-to-
boot ext4; the launch host downloads + verifies + boots it — no Docker, no
image tooling, just HTTP + gunzip.

- infra_artifact.py: version = content hash of the rootfs inputs (the shipped
  bot_bottle package + the three Dockerfiles + the init), so a launch host
  pulls the artifact matching its code and a content change can't silently
  boot a stale rootfs. Pull + sha256-verify (fail-closed) + gunzip from a
  Gitea generic package; base/owner/token configurable, default this Gitea.
- infra_vm.ensure_built/boot default to the pull path; BOT_BOTTLE_INFRA_BUILD=
  local keeps the docker build-from-source path for iterating on Dockerfiles.
- publish_infra.py: the off-host half — builds the images with Docker, mke2fs
  the rootfs (with buildah slack), gzips, and PUTs it to the generic package.

Rollout note: default=pull means a launch 404s until an artifact is published;
until the Gitea packages endpoint is enabled + an artifact published, use
BOT_BOTTLE_INFRA_BUILD=local. Freeze/migrate's remaining docker use is a
separate PR.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UoEZHDjv84ChoZbozQERhJ
2026-07-16 23:21:17 -04:00
3 changed files with 3 additions and 16 deletions
-10
View File
@@ -31,7 +31,6 @@ from __future__ import annotations
import dataclasses import dataclasses
import json import json
import secrets import secrets
import socket
import string import string
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
@@ -88,14 +87,6 @@ def bottle_identity(agent_name: str) -> str:
return f"{slug}-{suffix}" return f"{slug}-{suffix}"
def globalize_slug(slug: str) -> str:
"""Return a globally-unique slug qualified with the current hostname.
Assumes slug is a value returned from mint_slug. Use wherever a slug
must be unique across hosts (e.g. deploy-key titles)."""
return f"{socket.gethostname()}-{slug}"
@dataclass(frozen=True) @dataclass(frozen=True)
class BottleMetadata: class BottleMetadata:
"""Persistent record of how a bottle was launched, written at """Persistent record of how a bottle was launched, written at
@@ -349,7 +340,6 @@ __all__ = [
"BottleMetadata", "BottleMetadata",
"agent_state_dir", "agent_state_dir",
"bottle_identity", "bottle_identity",
"globalize_slug",
"bottle_state_dir", "bottle_state_dir",
"cleanup_state", "cleanup_state",
"clear_preserve_marker", "clear_preserve_marker",
+1 -2
View File
@@ -13,7 +13,6 @@ import dataclasses
from pathlib import Path from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from .bottle_state import globalize_slug
from .errors import MissingEnvVarError from .errors import MissingEnvVarError
from .log import info from .log import info
from .manifest import ManifestBottle, ManifestGitEntry from .manifest import ManifestBottle, ManifestGitEntry
@@ -47,7 +46,7 @@ def _provision_dynamic_key(
owner_repo = entry.UpstreamPath owner_repo = entry.UpstreamPath
if owner_repo.endswith(".git"): if owner_repo.endswith(".git"):
owner_repo = owner_repo[:-4] owner_repo = owner_repo[:-4]
title = f"bot-bottle:{globalize_slug(slug)}:{entry.Name}" title = f"bot-bottle:{slug}:{entry.Name}"
info(f"provisioning deploy key for git-gate.repos[{entry.Name!r}]") info(f"provisioning deploy key for git-gate.repos[{entry.Name!r}]")
key_id, private_key_bytes = provisioner.create(owner_repo, title) key_id, private_key_bytes = provisioner.create(owner_repo, title)
+2 -4
View File
@@ -6,7 +6,6 @@ Covers the pure `git_gate_render_gitconfig` renderer and the dynamic
from __future__ import annotations from __future__ import annotations
import socket
import tempfile import tempfile
import types import types
import unittest import unittest
@@ -127,9 +126,8 @@ class TestProvisionDynamicKey(unittest.TestCase):
self.assertEqual(b"PRIVATE-KEY-BYTES", key_file.read_bytes()) self.assertEqual(b"PRIVATE-KEY-BYTES", key_file.read_bytes())
id_file = Path(d) / "repo-deploy-key-id" id_file = Path(d) / "repo-deploy-key-id"
self.assertEqual("kid123", id_file.read_text()) self.assertEqual("kid123", id_file.read_text())
# owner_repo had .git stripped; title carries globalize_slug(slug) + name # owner_repo had .git stripped; title carries slug + name
hostname = socket.gethostname() self.assertEqual([("o/r", "bot-bottle:myslug:repo")], fake.created)
self.assertEqual([("o/r", f"bot-bottle:{hostname}-myslug:repo")], fake.created)
def test_missing_token_raises(self) -> None: def test_missing_token_raises(self) -> None:
with tempfile.TemporaryDirectory() as d, \ with tempfile.TemporaryDirectory() as d, \