Files
bot-bottle/bot_bottle/manifest_schema.py
T
didericis-codex 72e35a1343
test / integration-docker (pull_request) Successful in 13s
tracker-policy-pr / check-pr (pull_request) Successful in 11s
test / unit (pull_request) Successful in 37s
test / stage-firecracker-inputs (pull_request) Successful in 2s
lint / lint (push) Successful in 2m44s
test / build-infra (pull_request) Successful in 3m38s
test / integration-firecracker (pull_request) Successful in 2m7s
test / coverage (pull_request) Failing after 1m59s
test / publish-infra (pull_request) Has been skipped
feat(macos): spike rootless Docker inside bottles
Add an opt-in docker_access path that layers rootless Docker tooling onto the selected agent image, starts the daemon only after registration, and retains the existing outer network and capability boundary. Include fail-closed bootstrap checks plus a live-Mac Compose/security acceptance test.\n\nRefs #392.
2026-07-21 05:53:54 +00:00

81 lines
2.6 KiB
Python

"""Internal manifest schema policy helpers."""
from __future__ import annotations
import re
from pathlib import Path
# Filename-as-key uses kebab-case ASCII. The first character is a
# letter so we don't conflict with hidden files / Markdown special
# names (`.md`, `_template.md`, etc.). Filenames that fail this
# pattern are skipped with a warning rather than crashing the load.
_FILENAME_RX = re.compile(r"^[a-z][a-z0-9-]*$")
# Frontmatter keys we accept on each entity. Anything not in these
# sets dies with a "did you mean" pointer: typos should not silently
# ghost into an empty config.
BOTTLE_KEYS = frozenset(
{
"env", "extends", "agent_provider", "git-gate", "egress", "supervise",
"docker_access",
}
)
AGENT_KEYS_REQUIRED: frozenset[str] = frozenset()
AGENT_KEYS_OPTIONAL = frozenset({"bottle", "skills", "git-gate"})
# Claude Code subagent fields bot-bottle ignores at launch but does
# not reject. This lets the same file double as
# `~/.claude/agents/*.md` without modification.
CLAUDE_CODE_AGENT_PASSTHROUGH_KEYS = frozenset({
"name", "description", "model", "color", "memory",
})
AGENT_KEYS = (
AGENT_KEYS_REQUIRED | AGENT_KEYS_OPTIONAL | CLAUDE_CODE_AGENT_PASSTHROUGH_KEYS
)
AGENT_MODEL_KEYS = AGENT_KEYS | frozenset({"prompt"})
def is_valid_entity_name(name: str) -> bool:
"""True if `name` fits the kebab-case `[a-z][a-z0-9-]*` convention
shared by bottle/agent filenames and skill names. Names that satisfy
this are also safe to interpolate into a host/guest path segment."""
return bool(_FILENAME_RX.match(name))
def entity_name_from_path(path: Path) -> str | None:
"""Return the entity name implied by the filename, or None if the
filename does not fit the [a-z][a-z0-9-]* convention."""
if path.suffix != ".md":
return None
stem = path.stem
if not is_valid_entity_name(stem):
return None
return stem
def validate_bottle_frontmatter_keys(path: Path, keys: object) -> None:
_validate_frontmatter_keys("bottle", path, keys, BOTTLE_KEYS)
def validate_agent_frontmatter_keys(path: Path, keys: object) -> None:
_validate_frontmatter_keys("agent", path, keys, AGENT_KEYS)
def _validate_frontmatter_keys(
kind: str,
path: Path,
keys: object,
allowed_keys: frozenset[str],
) -> None:
from .manifest_util import ManifestError
key_set = set(keys) # type: ignore
unknown = key_set - allowed_keys # type: ignore
if unknown:
allowed = ", ".join(sorted(allowed_keys))
raise ManifestError(
f"{kind} file {path}: unknown frontmatter key(s) "
f"{sorted(unknown)}; allowed keys are {allowed}." # type: ignore
)