Compare commits

..

8 Commits

Author SHA1 Message Date
claude-ceo-assistant 3e491c673b chore(ci): adopt .runtime-version push-mode cascade signal
CI / Adapter unit tests (pull_request) Successful in 21s
CI / validate (push) Successful in 11m50s
CI / validate (pull_request) Successful in 11m38s
CI / Adapter unit tests (push) Successful in 20s
Secret scan / Scan diff for credential-shaped strings (pull_request) Successful in 6s
Background: post-2026-05-06 SCM is Gitea, not GitHub. Gitea 1.22.6 has
no repository_dispatch / workflow_dispatch trigger API (empirically
verified across 6 candidate paths in molecule-core#20 issuecomment-913).
The molecule-core/publish-runtime.yml cascade therefore cannot fire
templates via curl-dispatch — pivots to push-mode instead.

This PR is the consumer side of that pivot:

- `.runtime-version` file at repo root — single line, plain version
  string. Currently 0.1.129 (latest published as of 2026-05-07).
  publish-runtime overwrites this on each cascade.

- publish-image.yml gains a `resolve-version` job that reads the file
  and forwards the value to the reusable build workflow as the
  third-priority source in the resolution chain:
    1. client_payload.runtime_version (forward-compat with future
       GitHub-style dispatch if Gitea ever adds it)
    2. inputs.runtime_version (manual workflow_dispatch override)
    3. .runtime-version file (push-mode cascade — the new path)
    4. '' (Dockerfile requirements.txt default)

No behavioural change for PRs / manual dispatches; only fills in the
on-push case where previously the version was empty.

Sequencing context: this PR (and 8 sibling PRs to the other template
repos) MUST land before molecule-core#20 v2 is merged — otherwise the
first cascade push would trigger an on-push rebuild that pins the OLD
requirements.txt floor instead of the freshly-published version.

Refs molecule-core#14, molecule-core#20, molecule-core/issues/20.
2026-05-07 03:03:02 -07:00
security-auditor 91e5010888 ci: re-trigger after orchestrator restarted runners 1-8
Secret scan / Scan diff for credential-shaped strings (push) Successful in 9s
CI / Adapter unit tests (push) Successful in 50s
CI / validate (push) Successful in 12m11s
Per saved memory feedback_runner_config_partial_deploy: orchestrator
identified that runners 1-8 last restarted before AGENT_TOOLSDIRECTORY
+ RUNNER_TOOL_CACHE were added; cycle 7 retrigger landed ~50% on stale
runners. Orchestrator restarted 1-8 at ~09:37; this empty commit
re-triggers CI on the now-consistent runner pool.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 02:40:53 -07:00
security-auditor b91f1ab694 fix(ci): inline secret-scan body, drop cross-repo uses: of private molecule-core
Secret scan / Scan diff for credential-shaped strings (push) Successful in 7s
CI / Adapter unit tests (push) Failing after 16s
CI / validate (push) Failing after 18s
The 3-line wrapper at .github/workflows/secret-scan.yml referenced
`uses: molecule-ai/molecule-core/.github/workflows/secret-scan.yml@staging`.
molecule-core is private; act_runner clones cross-repo reusable
workflows anonymously, so the resolve fails at 0s with no logs.

Same root cause + same fix that molecule-controlplane already shipped
(see its secret-scan.yml comment block lines 10-22). Inlining keeps
the gate functional until Gitea is upgraded or the canonical scanner
moves to a public repo. When either lands, this file reverts to the
3-line wrapper.

Refs: internal#46 Phase 3 Class 2.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 02:29:04 -07:00
security-auditor cd68aae474 ci: re-trigger after runner-config v2 (AGENT_TOOLSDIRECTORY etc.)
Secret scan / secret-scan (push) Failing after 0s
CI / Adapter unit tests (push) Failing after 15s
CI / validate (push) Failing after 18s
Empty commit to re-run CI against the act_runner config that landed
in /opt/molecule/runners/config.yaml (cycle ~58 internal#46 Phase 3).
No source change. CI now runs setup-python with /tmp/hostedtoolcache,
which works (verified in cycle 6 task 1022 log, careful-bash#2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 02:27:50 -07:00
claude-ceo-assistant f549d0e4f3 Merge pull request 'docs(install): migrate git clone URL to git.moleculesai.app (#37)' (#1) from fix/install-path-gitea into main
Secret scan / secret-scan (push) Failing after 0s
CI / validate (push) Failing after 11s
CI / Adapter unit tests (push) Successful in 18s
2026-05-07 09:24:04 +00:00
claude-ceo-assistant 09c95308fd Merge pull request 'fix(ci): lowercase 'molecule-ai/' in cross-repo workflow refs' (#2) from fix/lowercase-org-slug into main
Secret scan / secret-scan (push) Failing after 0s
CI / Adapter unit tests (push) Failing after 17s
CI / validate (push) Failing after 23s
2026-05-07 08:59:12 +00:00
security-auditor fb450b0758 fix(ci): lowercase 'molecule-ai/' in cross-repo workflow refs
CI / validate (pull_request) Failing after 0s
Secret scan / secret-scan (pull_request) Failing after 0s
CI / validate (push) Failing after 0s
CI / Adapter unit tests (push) Failing after 13s
CI / Adapter unit tests (pull_request) Failing after 13s
Gitea is case-sensitive on owner slugs; canonical is lowercase
`molecule-ai/...`. Mixed-case `Molecule-AI/...` refs fail-at-0s
when the runner tries to resolve the cross-repo workflow / checkout.

Same fix as molecule-controlplane#12. Mechanical case-correction;
no behavior change beyond making CI resolve again.

Refs: internal#46

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 00:59:45 -07:00
documentation-specialist e28c2d0fd7 docs(install): migrate git clone URL to git.moleculesai.app (#37)
CI / Adapter unit tests (push) Failing after 10s
CI / Adapter unit tests (pull_request) Failing after 10s
CI / validate (pull_request) Failing after 0s
Secret scan / secret-scan (pull_request) Failing after 0s
CI / validate (push) Failing after 0s
One anonymous git-clone ref in runbooks/local-dev-setup.md:27.
Public repo, no auth-shape change.

Refs: molecule-ai/internal#37, molecule-ai/internal#38
2026-05-07 00:31:16 -07:00
6 changed files with 235 additions and 228 deletions
+1 -1
View File
@@ -2,7 +2,7 @@ name: CI
on: [push, pull_request]
jobs:
validate:
uses: Molecule-AI/molecule-ci/.github/workflows/validate-workspace-template.yml@main
uses: molecule-ai/molecule-ci/.github/workflows/validate-workspace-template.yml@main
tests:
name: Adapter unit tests
+41 -8
View File
@@ -32,14 +32,47 @@ permissions:
packages: write
jobs:
# The `.runtime-version` file is the push-mode cascade signal post-
# 2026-05-06: when molecule-core/publish-runtime.yml ships a new
# version to PyPI, it does NOT call repository_dispatch (Gitea 1.22.6
# has no such endpoint — empirically verified molecule-core#20).
# Instead it git-pushes an updated `.runtime-version` to each template,
# which trips this workflow's `on: push: branches: [main]` trigger.
# This job reads that file and forwards the version to the reusable
# build workflow so the Dockerfile pip-installs the exact published
# version, not whatever requirements.txt currently bounds.
resolve-version:
runs-on: ubuntu-latest
timeout-minutes: 2
outputs:
version: ${{ steps.read.outputs.version }}
steps:
- uses: actions/checkout@v4
- id: read
run: |
if [ -f .runtime-version ]; then
v="$(head -n1 .runtime-version | tr -d '[:space:]')"
echo "version=$v" >> "$GITHUB_OUTPUT"
echo "resolved runtime version: $v"
else
echo "no .runtime-version file present — falling through to Dockerfile default"
fi
publish:
uses: Molecule-AI/molecule-ci/.github/workflows/publish-template-image.yml@main
needs: resolve-version
uses: molecule-ai/molecule-ci/.github/workflows/publish-template-image.yml@main
secrets: inherit
with:
# When the cascade fires, client_payload.runtime_version is the
# exact version PyPI just published. Forwarded to the reusable
# workflow as a docker --build-arg so the cache key changes
# per-version and pip install resolves freshly.
# On other events (push to main / manual without input), this is
# empty and the Dockerfile's default (requirements.txt pin) applies.
runtime_version: ${{ github.event.client_payload.runtime_version || inputs.runtime_version || '' }}
# Resolution chain (highest priority first):
# 1. client_payload.runtime_version — legacy GitHub
# repository_dispatch path (will return if Gitea ever adds
# the dispatch API; left in place for forward-compat).
# 2. inputs.runtime_version manual workflow_dispatch run from
# the Actions UI for ad-hoc rebuilds against a specific
# version.
# 3. needs.resolve-version.outputs.version — the
# `.runtime-version` file in this repo, written by
# molecule-core/publish-runtime.yml's push-mode cascade.
# 4. '' — fall through to the Dockerfile default
# (requirements.txt pin).
runtime_version: ${{ github.event.client_payload.runtime_version || inputs.runtime_version || needs.resolve-version.outputs.version || '' }}
+191 -12
View File
@@ -1,22 +1,201 @@
name: Secret scan
# Calls the canonical reusable workflow in molecule-core. Defense
# against the #2090-class leak (a hosted-agent commit slipping a
# credential-shaped string into a PR). Pattern set lives in
# molecule-core so we do not maintain a parallel copy here.
# Hard CI gate. Refuses any PR / push whose diff additions contain a
# recognisable credential. Defense-in-depth for the #2090-class incident
# (2026-04-24): GitHub's hosted Copilot Coding Agent leaked a ghs_*
# installation token into tenant-proxy/package.json via `npm init`
# slurping the URL from a token-embedded origin remote. We can't fix
# upstream's clone hygiene, so we gate here.
#
# Pinned to @staging because that is the active default branch on the
# upstream repo (main lags behind via the staging-promotion workflow).
# Updates ride along automatically as the upstream regex set evolves.
# Inlined copy from molecule-ai/molecule-core/.github/workflows/secret-scan.yml.
# Cross-repo workflow_call to a private repo doesn't fully work on Gitea 1.22.6
# (workflow file fails parse-time at 0s with no logs); inline keeps the gate
# functional until Gitea is upgraded or the canonical scanner moves to a public
# repo. When that lands, this file reverts to the 3-line wrapper:
#
# jobs:
# secret-scan:
# uses: Molecule-AI/molecule-core/.github/workflows/secret-scan.yml@staging
#
# Pin to @staging not @main — staging is the active default branch,
# main lags via the staging-promotion workflow. Updates ride along
# automatically on the next consumer workflow run.
#
# Same regex set as the runtime's bundled pre-commit hook
# (molecule-ai-workspace-runtime: molecule_runtime/scripts/pre-commit-checks.sh).
# Keep the two sides aligned when adding patterns.
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches: [main, staging, master]
merge_group:
types: [checks_requested]
branches: [main, staging]
jobs:
secret-scan:
uses: Molecule-AI/molecule-core/.github/workflows/secret-scan.yml@staging
scan:
name: Scan diff for credential-shaped strings
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 2 # need previous commit to diff against on push events
# For pull_request events the diff base may be many commits behind
# HEAD and absent from the shallow clone. Fetch it explicitly.
- name: Fetch PR base SHA (pull_request events only)
if: github.event_name == 'pull_request'
run: git fetch --depth=1 origin ${{ github.event.pull_request.base.sha }}
# For merge_group events the queue's pre-merge ref is a commit on
# `gh-readonly-queue/...` whose parent is the queue's base_sha.
# That parent isn't part of the queue branch's shallow clone, so
# we fetch it explicitly. Without this the diff falls through to
# "no BASE → scan entire tree" mode and false-positives on legit
# test fixtures (e.g. canvas/src/lib/validation/__tests__/secret-formats.test.ts).
- name: Refuse if credential-shaped strings appear in diff additions
env:
# Plumb event-specific SHAs through env so the script doesn't
# need conditional `${{ ... }}` interpolation per event type.
# github.event.before/after only exist on push events;
# merge_group has its own base_sha/head_sha; pull_request has
# pull_request.base.sha / pull_request.head.sha.
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PUSH_BEFORE: ${{ github.event.before }}
PUSH_AFTER: ${{ github.event.after }}
run: |
# Pattern set covers GitHub family (the actual #2090 vector),
# Anthropic / OpenAI / Slack / AWS. Anchored on prefixes with low
# false-positive rates against agent-generated content. Mirror of
# molecule-ai-workspace-runtime/molecule_runtime/scripts/pre-commit-checks.sh
# — keep aligned.
SECRET_PATTERNS=(
'ghp_[A-Za-z0-9]{36,}' # GitHub PAT (classic)
'ghs_[A-Za-z0-9]{36,}' # GitHub App installation token
'gho_[A-Za-z0-9]{36,}' # GitHub OAuth user-to-server
'ghu_[A-Za-z0-9]{36,}' # GitHub OAuth user
'ghr_[A-Za-z0-9]{36,}' # GitHub OAuth refresh
'github_pat_[A-Za-z0-9_]{82,}' # GitHub fine-grained PAT
'sk-ant-[A-Za-z0-9_-]{40,}' # Anthropic API key
'sk-proj-[A-Za-z0-9_-]{40,}' # OpenAI project key
'sk-svcacct-[A-Za-z0-9_-]{40,}' # OpenAI service-account key
'sk-cp-[A-Za-z0-9_-]{60,}' # MiniMax API key (F1088 vector — caught only after the fact)
'xox[baprs]-[A-Za-z0-9-]{20,}' # Slack tokens
'AKIA[0-9A-Z]{16}' # AWS access key ID
'ASIA[0-9A-Z]{16}' # AWS STS temp access key ID
)
# Determine the diff base. Each event type stores its SHAs in
# a different place — see the env block above.
case "${{ github.event_name }}" in
pull_request)
BASE="$PR_BASE_SHA"
HEAD="$PR_HEAD_SHA"
;;
*)
BASE="$PUSH_BEFORE"
HEAD="$PUSH_AFTER"
;;
esac
# On push events with shallow clones, BASE may be present in
# the event payload but absent from the local object DB
# (fetch-depth=2 doesn't always reach the previous commit
# across true merges). Try fetching it on demand. If the
# fetch fails — e.g. the SHA was force-overwritten — we fall
# through to the empty-BASE branch below, which scans the
# entire tree as if every file were new. Correct, just slow.
if [ -n "$BASE" ] && ! echo "$BASE" | grep -qE '^0+$'; then
if ! git cat-file -e "$BASE" 2>/dev/null; then
git fetch --depth=1 origin "$BASE" 2>/dev/null || true
fi
fi
# Files added or modified in this change.
if [ -z "$BASE" ] || echo "$BASE" | grep -qE '^0+$' || ! git cat-file -e "$BASE" 2>/dev/null; then
# New branch / no previous SHA / BASE unreachable — check the
# entire tree as added content. Slower, but correct on first
# push.
CHANGED=$(git ls-tree -r --name-only HEAD)
DIFF_RANGE=""
else
CHANGED=$(git diff --name-only --diff-filter=AM "$BASE" "$HEAD")
DIFF_RANGE="$BASE $HEAD"
fi
if [ -z "$CHANGED" ]; then
echo "No changed files to inspect."
exit 0
fi
# Self-exclude: this workflow file legitimately contains the
# pattern strings as regex literals. Without an exclude it would
# block its own merge.
SELF=".github/workflows/secret-scan.yml"
OFFENDING=""
# `while IFS= read -r` (not `for f in $CHANGED`) so filenames
# containing whitespace don't word-split silently — a path
# with a space would otherwise produce two iterations on
# tokens that aren't real filenames, breaking the
# self-exclude + diff lookup.
while IFS= read -r f; do
[ -z "$f" ] && continue
[ "$f" = "$SELF" ] && continue
if [ -n "$DIFF_RANGE" ]; then
ADDED=$(git diff --no-color --unified=0 "$BASE" "$HEAD" -- "$f" 2>/dev/null | grep -E '^\+[^+]' || true)
else
# No diff range (new branch first push) — scan the full file
# contents as if every line were new.
ADDED=$(cat "$f" 2>/dev/null || true)
fi
[ -z "$ADDED" ] && continue
for pattern in "${SECRET_PATTERNS[@]}"; do
if echo "$ADDED" | grep -qE "$pattern"; then
OFFENDING="${OFFENDING}${f} (matched: ${pattern})\n"
break
fi
done
done <<< "$CHANGED"
if [ -n "$OFFENDING" ]; then
echo "::error::Credential-shaped strings detected in diff additions:"
# `printf '%b' "$OFFENDING"` interprets backslash escapes
# (the literal `\n` we appended above becomes a newline)
# WITHOUT treating OFFENDING as a format string. Plain
# `printf "$OFFENDING"` is a format-string sink: a filename
# containing `%` would be interpreted as a conversion
# specifier, corrupting the error message (or printing
# `%(missing)` artifacts).
printf '%b' "$OFFENDING"
echo ""
echo "The actual matched values are NOT echoed here, deliberately —"
echo "round-tripping a leaked credential into CI logs widens the blast"
echo "radius (logs are searchable + retained)."
echo ""
echo "Recovery:"
echo " 1. Remove the secret from the file. Replace with an env var"
echo " reference (e.g. \${{ secrets.GITHUB_TOKEN }} in workflows,"
echo " process.env.X in code)."
echo " 2. If the credential was already pushed (this PR's commit"
echo " history reaches a public ref), treat it as compromised —"
echo " ROTATE it immediately, do not just remove it. The token"
echo " remains valid in git history forever and may be in any"
echo " log/cache that consumed this branch."
echo " 3. Force-push the cleaned commit (or stack a revert) and"
echo " re-run CI."
echo ""
echo "If the match is a false positive (test fixture, docs example,"
echo "or this workflow's own regex literals): use a clearly-fake"
echo "placeholder like ghs_EXAMPLE_DO_NOT_USE that doesn't satisfy"
echo "the length suffix, OR add the file path to the SELF exclude"
echo "list in this workflow with a short reason."
echo ""
echo "Mirror of the regex set lives in the runtime's bundled"
echo "pre-commit hook (molecule-ai-workspace-runtime:"
echo "molecule_runtime/scripts/pre-commit-checks.sh) — keep aligned."
exit 1
fi
echo "✓ No credential-shaped strings in this change."
+1
View File
@@ -0,0 +1 @@
0.1.129
+1 -1
View File
@@ -24,7 +24,7 @@ common problems.
## Step 1 — Clone the Repository
```bash
git clone https://github.com/Molecule-AI/molecule-ai-workspace-template-claude-code.git
git clone https://git.moleculesai.app/molecule-ai/molecule-ai-workspace-template-claude-code.git
cd molecule-ai-workspace-template-claude-code
```
-206
View File
@@ -1,206 +0,0 @@
r"""Tests for entrypoint.sh's log_boot_context() shell function.
The Python-side audit (test_adapter_logging.py) pins what `_audit_auth_env_presence`
in adapter.py emits. But the shell function fires FIRST — twice, even (once
pre-gosu as root, once post-gosu as agent). When the adapter never runs at
all because the SDK import fails, the entrypoint emission is the operator's
ONLY visibility into the boot env. So this contract needs its own test.
The cross-file gate `test_audit_env_list_matches_entrypoint_sh` proves the
NAME LIST matches; this file proves the SHELL CODE actually emits the
right lines for those names. Without this, a typo in the for-loop body
(e.g. `eval "val=\$$var"` → `val=$var`, which would print the literal
name not its value) silently breaks the audit.
Strategy: extract the `log_boot_context()` function body from entrypoint.sh
and run it in a fresh subprocess with controlled env. Asserts on stdout.
We never source entrypoint.sh wholesale because it would chown /workspace
and exec molecule-runtime — neither is appropriate in a test sandbox.
"""
from __future__ import annotations
import os
import re
import subprocess
from pathlib import Path
import pytest
TEMPLATE_DIR = Path(__file__).resolve().parent.parent
ENTRYPOINT = TEMPLATE_DIR / "entrypoint.sh"
def _extract_function() -> str:
"""Pull just the log_boot_context() function definition out of entrypoint.sh.
Returns the literal function definition (`log_boot_context() { ... }`) as
a string, suitable for `sh -c "<func>; log_boot_context"`. Bails with a
clear message if the function can't be located — that itself is a
regression worth a loud test failure.
"""
text = ENTRYPOINT.read_text()
# `log_boot_context() {` on its own line, then everything up to the
# matching closing `}` at column 0. The function is small and shape-stable;
# we don't try to be a full shell parser.
match = re.search(r"^log_boot_context\(\)\s*\{.*?^\}\s*$", text, re.DOTALL | re.MULTILINE)
if not match:
pytest.fail("Could not locate log_boot_context() in entrypoint.sh")
return match.group(0)
def _run_function(env: dict[str, str]) -> str:
"""Run log_boot_context() in a fresh /bin/sh with the given env. Returns stdout."""
func = _extract_function()
script = f"{func}\nlog_boot_context\n"
# Empty base env so PATH lookups (`id`, `hostname`, `date`, `ls`) still work
# but no inherited auth vars leak into the test. We restore PATH explicitly.
safe_env = {"PATH": os.environ.get("PATH", "/usr/bin:/bin")}
safe_env.update(env)
result = subprocess.run(
["/bin/sh", "-c", script],
env=safe_env,
capture_output=True,
text=True,
timeout=10,
check=False,
)
assert result.returncode == 0, (
f"log_boot_context exited rc={result.returncode}\n"
f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}"
)
return result.stdout
# Audit names — kept in lockstep with adapter.py's _AUTH_ENV_AUDIT and the
# entrypoint.sh for-loop. test_audit_env_list_matches_entrypoint_sh and
# test_loop_var_list_matches_audit (below) gate any drift across the three
# locations.
_AUDIT_NAMES = (
"CLAUDE_CODE_OAUTH_TOKEN",
"ANTHROPIC_API_KEY",
"ANTHROPIC_AUTH_TOKEN",
"ANTHROPIC_BASE_URL",
"MINIMAX_API_KEY",
"GLM_API_KEY",
"KIMI_API_KEY",
"DEEPSEEK_API_KEY",
)
def test_emits_set_for_present_env():
"""A set var must produce `env NAME=set` — proves the eval-deref works."""
out = _run_function({"MINIMAX_API_KEY": "secret-MUST-NOT-LEAK"})
assert "env MINIMAX_API_KEY=set" in out
def test_emits_unset_for_absent_env():
"""An unset var must produce `env NAME=unset` — proves the empty-string branch."""
out = _run_function({})
for name in _AUDIT_NAMES:
assert f"env {name}=unset" in out, (
f"missing `env {name}=unset` line — for-loop body may be miscoded"
)
def test_never_leaks_value():
"""The audit prints NAMES, not VALUES. Regression here = secret leak.
Same threat model as the Python-side test: an operator-visible boot log
that contains the actual key would defeat the whole point of the audit
(the audit exists so we can answer 'is the key present' WITHOUT exposing
the key). A `eval "val=\\$$var"` typo collapsing to `echo $var` would
trip this test.
"""
secret = "sk-FAKE-MUST-NEVER-APPEAR-IN-BOOT-LOG"
out = _run_function({
"MINIMAX_API_KEY": secret,
"CLAUDE_CODE_OAUTH_TOKEN": secret,
"ANTHROPIC_BASE_URL": "https://api.example.com",
})
assert secret not in out, f"boot-context log leaked the env VALUE:\n{out}"
# ANTHROPIC_BASE_URL is the most-likely-to-be-logged-by-mistake field
# because operators sometimes WANT to see it; pin that it's still
# name-only.
assert "https://api.example.com" not in out
def test_emits_workspace_id_and_platform_url():
"""WORKSPACE_ID and PLATFORM_URL appear by VALUE — these are not secrets.
They're the operator-visible identifiers a support engineer needs to
correlate logs with platform records. Pinning the field shape so a
later refactor doesn't accidentally redact them.
"""
out = _run_function({
"WORKSPACE_ID": "ws-test-1234",
"PLATFORM_URL": "https://test.example.com",
})
assert "workspace_id=ws-test-1234" in out
assert "platform_url=https://test.example.com" in out
def test_emits_unset_marker_when_workspace_id_missing():
"""Missing WORKSPACE_ID falls back to the literal `<unset>` placeholder.
A support engineer reading the boot log must be able to distinguish
'WORKSPACE_ID was empty string' from 'WORKSPACE_ID was never injected
by the platform'. The shell `${VAR:-<unset>}` default handles that.
"""
out = _run_function({})
assert "workspace_id=<unset>" in out
assert "platform_url=<unset>" in out
def test_emits_uid_and_gid():
"""uid/gid line is critical — answers 'did the privilege drop happen?'
The two-emission pattern (pre-gosu as root, post-gosu as agent) only
works as a diagnostic if uid/gid is in every emission. Pin the field
shape; we don't pin the literal value because CI runs vary.
"""
out = _run_function({})
assert re.search(r"uid=\d+\s+gid=\d+", out), (
f"missing or malformed uid/gid line:\n{out}"
)
def test_emits_boot_marker():
"""Each emission starts with the dated `entrypoint boot` banner.
Operators grep for this to count restarts in a crash loop.
"""
out = _run_function({})
# Format: "----- entrypoint boot 2026-05-02T12:34:56Z -----"
assert re.search(
r"-----\s+entrypoint boot \d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z\s+-----",
out,
), f"missing boot banner:\n{out}"
def test_loop_var_list_matches_audit():
"""The for-loop's literal NAME list must match _AUDIT_NAMES (this file).
Companion to test_audit_env_list_matches_entrypoint_sh in
test_adapter_logging.py: that test cross-checks adapter.py vs
entrypoint.sh; this one cross-checks entrypoint.sh vs the test
fixture above. If a maintainer adds a vendor to entrypoint.sh
without updating the audit name tuple in this file, the existing
`test_emits_unset_for_absent_env` would still pass (because all
audited names also appear in the loop), but the maintainer would
have a false sense of coverage. This test catches that.
"""
text = ENTRYPOINT.read_text()
loop_line = next(
(line for line in text.splitlines()
if "for var in" in line and "CLAUDE_CODE_OAUTH_TOKEN" in line),
None,
)
assert loop_line, "entrypoint.sh missing the auth-env for-loop"
names_in_shell = tuple(
loop_line.split("for var in", 1)[1].split(";", 1)[0].split()
)
assert set(names_in_shell) == set(_AUDIT_NAMES), (
f"_AUDIT_NAMES in this file ({set(_AUDIT_NAMES)}) and the for-loop "
f"in entrypoint.sh ({set(names_in_shell)}) disagree — update the "
"test fixture or the shell loop to bring them back in sync."
)