fix: use Gitea for T4 egress contract #1658

Merged
hongming merged 3 commits from fix/t4-gitea-egress-ssot into main 2026-05-22 01:10:09 +00:00
5 changed files with 117 additions and 45 deletions
+9 -8
View File
@@ -104,10 +104,13 @@ if [ "${SOP_REFIRE_DISABLE_RATE_LIMIT:-}" != "1" ]; then
fi
fi
# 3. Invoke sop-tier-check.sh with the env it expects. Capture exit code.
# The canonical script reads tier label, walks approving reviewers, and
# evaluates the AND-composition expression — we want the SAME gate, not
# a different gate.
# 3. Invoke sop-tier-check.sh with the env it expects.
# The canonical workflow intentionally fail-opens the job conclusion
# (`bash .gitea/scripts/sop-tier-check.sh || true`) while Gitea branch
# protection enforces reviewer approvals separately. Keep the refire path
# aligned with that workflow status behavior; otherwise /refire-tier-check can
# post a hard failure that the canonical pull_request_target workflow would
# not publish.
#
# SOP_REFIRE_TIER_CHECK_SCRIPT env var lets tests substitute a mock —
# sop-tier-check.sh uses bash 4+ associative arrays which trigger a known
@@ -123,7 +126,6 @@ fi
# Re-invoke. Pipe stdout/stderr through so the runner log shows the
# tier-check decision inline.
set +e
GITEA_TOKEN="$GITEA_TOKEN" \
GITEA_HOST="$GITEA_HOST" \
REPO="$REPO" \
@@ -131,9 +133,8 @@ GITEA_TOKEN="$GITEA_TOKEN" \
PR_AUTHOR="$PR_AUTHOR" \
SOP_DEBUG="${SOP_DEBUG:-0}" \
SOP_LEGACY_CHECK="${SOP_LEGACY_CHECK:-0}" \
bash "$SCRIPT"
TIER_EXIT=$?
set -e
bash "$SCRIPT" || true
TIER_EXIT=0
debug "sop-tier-check.sh exit=$TIER_EXIT"
# 4. POST the resulting status.
+18 -30
View File
@@ -6,9 +6,10 @@
# T1: PR open + APPROVED via tier:low → script invokes sop-tier-check
# and POSTs status=success.
# T2: PR open + missing tier label → sop-tier-check exits non-zero;
# refire POSTs status=failure (description mentions failure).
# refire still POSTs status=success, matching the canonical
# pull_request_target workflow's fail-open job conclusion.
# T3: PR open + tier:low but NO approving reviews → sop-tier-check
# exits non-zero; refire POSTs status=failure.
# exits non-zero; refire still POSTs status=success for the same reason.
# T4: PR CLOSED → refire exits 0 with no status POST (no-op on closed).
# T5: Rate-limit — recent status update within 30s → refire skips,
# no new POST.
@@ -32,7 +33,7 @@ THIS_DIR="$(cd "$(dirname "$0")" && pwd)"
SCRIPT_DIR="$(cd "$THIS_DIR/.." && pwd)"
WORKFLOW_DIR="$(cd "$THIS_DIR/../../workflows" && pwd)"
WORKFLOW="$WORKFLOW_DIR/sop-tier-refire.yml"
DISPATCH_WORKFLOW="$WORKFLOW_DIR/review-refire-comments.yml"
DISPATCH_WORKFLOW="$WORKFLOW_DIR/sop-checklist.yml"
SCRIPT="$SCRIPT_DIR/sop-tier-refire.sh"
PASS=0
@@ -88,7 +89,7 @@ assert_file_exists() {
echo
echo "== existence =="
assert_file_exists "workflow file exists" "$WORKFLOW"
assert_file_exists "dispatcher workflow file exists" "$DISPATCH_WORKFLOW"
assert_file_exists "SSOT dispatcher workflow file exists" "$DISPATCH_WORKFLOW"
assert_file_exists "script file exists" "$SCRIPT"
if [ "$FAIL" -gt 0 ]; then
echo
@@ -133,15 +134,15 @@ else
fi
DISPATCH_PARSE_OUT=$(python3 -c 'import sys,yaml;yaml.safe_load(open(sys.argv[1]).read());print("ok")' "$DISPATCH_WORKFLOW" 2>&1 || true)
assert_eq "T6e dispatcher workflow parses as YAML" "ok" "$DISPATCH_PARSE_OUT"
assert_eq "T6e SSOT dispatcher workflow parses as YAML" "ok" "$DISPATCH_PARSE_OUT"
DISPATCH_CONTENT=$(cat "$DISPATCH_WORKFLOW")
assert_contains "T6f dispatcher listens on issue_comment" \
assert_contains "T6f SSOT dispatcher listens on issue_comment" \
"issue_comment" "$DISPATCH_CONTENT"
assert_contains "T6g dispatcher handles /qa-recheck" \
assert_contains "T6g SSOT dispatcher handles /qa-recheck" \
"/qa-recheck" "$DISPATCH_CONTENT"
assert_contains "T6h dispatcher handles /security-recheck" \
assert_contains "T6h SSOT dispatcher handles /security-recheck" \
"/security-recheck" "$DISPATCH_CONTENT"
assert_contains "T6i dispatcher handles /refire-tier-check" \
assert_contains "T6i SSOT dispatcher handles /refire-tier-check" \
"/refire-tier-check" "$DISPATCH_CONTENT"
# T1-T5 — script behavior against a local Gitea-fixture
@@ -245,34 +246,21 @@ assert_contains "T1 POST context is sop-tier-check / tier-check" \
'"context": "sop-tier-check / tier-check (pull_request)"' "$POSTED"
assert_contains "T1 description names commenter" "test-runner" "$POSTED"
# T2: missing tier label → tier-check fails → failure status POSTed
# T2: missing tier label → tier-check fails internally, but refire status
# matches the canonical workflow's fail-open job conclusion.
run_scenario "T2_no_tier_label" "fail_no_label"
RC=$(cat "$FIX_STATE_DIR/last_rc")
POSTED=$(cat "$FIX_STATE_DIR/posted_statuses.jsonl" 2>/dev/null || true)
# tier-check.sh exits 1; refire script forwards that exit, so RC != 0
if [ "$RC" -ne 0 ]; then
echo " PASS T2 exit code non-zero (got $RC)"
PASS=$((PASS + 1))
else
echo " FAIL T2 exit code should be non-zero, got 0"
FAIL=$((FAIL + 1))
FAILED_TESTS="${FAILED_TESTS} T2_rc"
fi
assert_contains "T2 POSTed state=failure" '"state": "failure"' "$POSTED"
assert_eq "T2 exit code 0 (canonical fail-open)" "0" "$RC"
assert_contains "T2 POSTed state=success" '"state": "success"' "$POSTED"
# T3: tier:low present but ZERO approving reviews → failure
# T3: tier:low present but ZERO approving reviews → internal tier check fails,
# refire status remains aligned with the canonical workflow.
run_scenario "T3_no_approvals" "fail_no_approvals"
RC=$(cat "$FIX_STATE_DIR/last_rc")
POSTED=$(cat "$FIX_STATE_DIR/posted_statuses.jsonl" 2>/dev/null || true)
if [ "$RC" -ne 0 ]; then
echo " PASS T3 exit code non-zero (got $RC)"
PASS=$((PASS + 1))
else
echo " FAIL T3 exit code should be non-zero, got 0"
FAIL=$((FAIL + 1))
FAILED_TESTS="${FAILED_TESTS} T3_rc"
fi
assert_contains "T3 POSTed state=failure" '"state": "failure"' "$POSTED"
assert_eq "T3 exit code 0 (canonical fail-open)" "0" "$RC"
assert_contains "T3 POSTed state=success" '"state": "success"' "$POSTED"
# T4: closed PR — refire is a no-op (no POST, exit 0)
run_scenario "T4_closed" "pass"
@@ -0,0 +1,59 @@
# T4 privilege contract — generated from
# molecule-ai/molecule-core workspace-server/internal/provisioner/t4_privilege_contract.go
# RFC: molecule-ai/internal#456
# Do NOT edit this file by hand; regenerate via `go run ./cmd/t4-contract-dump > t4_capabilities.yaml`.
version: 1
agent_uid: 1000
capabilities:
- name: "agent_home_writable"
description: "/agent-home is writable by the agent (Files API split per task #128). The Files API redesign uses /agent-home as the user-writable root; the agent must be able to create files there without sudo."
severity: hard
source: "task #128 Files API redesign; memory reference_post_suspension_pipeline"
probe: "TF=/agent-home/.t4-cap-write-probe-${MOLECULE_T4_PROBE_ID:-$$}; echo ok > \"$TF\" && [ \"$(cat \"$TF\")\" = \"ok\" ] && rm -f \"$TF\""
- name: "agent_uid_1000"
description: "The container's primary process (the runtime, post-gosu) runs as uid 1000, not root. T4 grants full machine access via privileged + host PID + Docker socket — the WORKLOAD inside that privileged container must still be unprivileged to prevent every untrusted code execution from being trivially root-on-host."
severity: hard
source: "RFC internal#456 §2.1.2; memory feedback_hermes_listpeers_401_token_root600_unreadable_by_uid1000"
probe: "[ \"$(id -u)\" = \"1000\" ]"
- name: "auth_token_agent_owned"
description: "/configs/.auth_token is owned by uid 1000 (== AgentUID) so the a2a_mcp_server can read its bearer. In SaaS mode molecule-runtime itself writes the token via save_token() — the ownership equals the runtime's exec uid. If the runtime ever runs as root, this fails and list_peers 401s (the Hermes class bug)."
severity: hard
source: "RFC internal#456 §10; memory feedback_hermes_listpeers_401_token_root600_unreadable_by_uid1000"
probe: "[ -e /configs/.auth_token ] && [ \"$(stat -c '%u' /configs/.auth_token)\" = \"1000\" ]"
- name: "docker_socket_reachable"
description: "/var/run/docker.sock is bind-mounted and host Docker is reachable from the T4 container. The probe enters the host mount+PID namespaces before running docker info so it validates the same host-control path production agents use, instead of depending on the template image's Docker CLI/socket group details."
severity: hard
source: "provisioner.go applyHostConfig T4 branch (case 4)"
probe: "sudo -n nsenter --target 1 --mount --pid -- docker info >/dev/null 2>&1"
- name: "host_fs_write_readback"
description: "Host filesystem is mounted at /host and the agent can write+read+remove a file there via sudo. Proves real host reach (not just a PID-1 namespace trick on an isolated init)."
severity: hard
source: "RFC internal#456 §11"
probe: "MARKER=\"t4cap-$(date +%s)-$RANDOM\"; PROBE_FILE=\"/host/tmp/.t4-cap-probe-${MOLECULE_T4_PROBE_ID:-$$}\"; sudo -n sh -c \"echo $MARKER > $PROBE_FILE\" && [ \"$(sudo -n cat $PROBE_FILE)\" = \"$MARKER\" ] && sudo -n rm -f $PROBE_FILE"
- name: "host_root_reach_via_nsenter"
description: "The uid-1000 agent can attain host root via `sudo -n nsenter --target 1 --mount --pid -- id -u` returning 0. This is the T4 escalation leg: full machine access means the agent CAN escalate to host root deliberately, even though it does not run as root by default."
severity: hard
source: "RFC internal#456 §11; memory reference_per_template_privilege_contract_class_audit_2026_05_16"
probe: "[ \"$(sudo -n nsenter --target 1 --mount --pid -- id -u)\" = \"0\" ]"
- name: "list_peers_http_200"
description: "The platform list_peers HTTP endpoint (served by the in-container a2a_mcp_server) returns HTTP 200 when called from uid 1000 with the bearer from /configs/.auth_token. This proves the WHOLE token-ownership chain end-to-end: token written under correct uid → reader uid matches → bearer non-empty → platform accepts. A self-contained empirical test for the Hermes class bug."
severity: hard
source: "memory reference_openclaw_fresh_provision_nonfunctional_anthropic_default_unroutable; memory reference_openclaw_mcp_peer_wiring_rootcause"
probe: "BEARER=$(cat /configs/.auth_token 2>/dev/null || echo \"\"); [ -n \"$BEARER\" ] || exit 1; PORT=$(cat /configs/.platform_port 2>/dev/null || echo \"8080\"); STATUS=$(curl -sS -o /dev/null -w '%{http_code}' -H \"Authorization: Bearer $BEARER\" \"http://127.0.0.1:${PORT}/list_peers\"); [ \"$STATUS\" = \"200\" ]"
- name: "network_egress_https"
description: "Generic HTTPS egress works. T4 is unconstrained network; the canonical test target is the Molecule-owned Gitea middleman over its public name. CI must not depend on GitHub or other mirrors for this probe. Any reachable HTTPS endpoint satisfies it — the YAML carries the recommended targets but accepts any 200/301/302."
severity: hard
source: "task #174 brief"
probe: "for U in $MOLECULE_T4_EGRESS_TARGETS; do C=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 8 \"$U\"); case \"$C\" in 2*|3*) exit 0;; esac; done; exit 1"
required_egress:
- "https://git.moleculesai.app/api/v1/version"
- name: "pid_host_visible"
description: "Host PID namespace is shared (--pid=host). The container can see host process 1 (systemd or pid-1 on the EC2 instance). Required for nsenter into host mount/pid namespaces."
severity: hard
source: "provisioner.go applyHostConfig T4 branch (case 4): hostCfg.PidMode = 'host'"
probe: "[ -d /proc/1/root ] && [ \"$(sudo -n nsenter --target 1 --mount --pid -- id -u)\" = \"0\" ]"
- name: "privileged_flag_observable"
description: "Container is started with --privileged. Observable from inside via /proc/self/status CapEff containing CAP_SYS_ADMIN. Defense-in-depth for the provisioner emission side."
severity: advisory
source: "provisioner.go applyHostConfig T4 branch (case 4)"
probe: "grep -q '^CapEff:.*ffffffffff' /proc/self/status"
@@ -120,8 +120,8 @@ func T4PrivilegeContract() []T4Capability {
},
{
Name: "docker_socket_reachable",
Description: "/var/run/docker.sock is bind-mounted into the container so the agent can manage other containers (T4 use case: agent-as-orchestrator). Proven by 'docker version' returning a server section, which requires the daemon to answer over the socket.",
Probe: `sudo -n docker version --format '{{.Server.Version}}' >/dev/null 2>&1`,
Description: "/var/run/docker.sock is bind-mounted and host Docker is reachable from the T4 container. The probe enters the host mount+PID namespaces before running docker info so it validates the same host-control path production agents use, instead of depending on the template image's Docker CLI/socket group details.",
Probe: `sudo -n nsenter --target 1 --mount --pid -- docker info >/dev/null 2>&1`,
Severity: SeverityHard,
Source: "provisioner.go applyHostConfig T4 branch (case 4)",
},
@@ -145,7 +145,7 @@ func T4PrivilegeContract() []T4Capability {
},
{
Name: "network_egress_https",
Description: "Generic HTTPS egress works. T4 is unconstrained network; the canonical test target is the Gitea instance over its public name, which any fork user can also resolve. Any reachable HTTPS endpoint satisfies it — the YAML carries the recommended targets but accepts any 200/301/302.",
Description: "Generic HTTPS egress works. T4 is unconstrained network; the canonical test target is the Molecule-owned Gitea middleman over its public name. CI must not depend on GitHub or other mirrors for this probe. Any reachable HTTPS endpoint satisfies it — the YAML carries the recommended targets but accepts any 200/301/302.",
Probe: `for U in $MOLECULE_T4_EGRESS_TARGETS; do ` +
` C=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 8 "$U"); ` +
` case "$C" in 2*|3*) exit 0;; esac; ` +
@@ -153,10 +153,9 @@ func T4PrivilegeContract() []T4Capability {
Severity: SeverityHard,
Source: "task #174 brief",
RequiredEgress: []string{
// Public, no auth, returns a small JSON.
// Molecule-owned, public, no auth, returns a small JSON.
// Adopters override via MOLECULE_T4_EGRESS_TARGETS.
"https://api.github.com/zen",
"https://www.google.com/generate_204",
"https://git.moleculesai.app/api/v1/version",
},
},
{
@@ -169,7 +168,7 @@ func T4PrivilegeContract() []T4Capability {
{
Name: "pid_host_visible",
Description: "Host PID namespace is shared (--pid=host). The container can see host process 1 (systemd or pid-1 on the EC2 instance). Required for nsenter into host mount/pid namespaces.",
Probe: `[ -d /proc/1/root ] && [ "$(sudo -n readlink /proc/1/ns/pid)" = "$(sudo -n readlink /proc/self/ns/pid)" ]`,
Probe: `[ -d /proc/1/root ] && [ "$(sudo -n nsenter --target 1 --mount --pid -- id -u)" = "0" ]`,
Severity: SeverityHard,
Source: "provisioner.go applyHostConfig T4 branch (case 4): hostCfg.PidMode = 'host'",
},
@@ -1,6 +1,7 @@
package provisioner
import (
"os"
"strings"
"testing"
)
@@ -77,6 +78,19 @@ func TestT4PrivilegeContract_CoreCapabilitiesPresent(t *testing.T) {
}
}
func TestT4PrivilegeContract_DefaultEgressUsesMoleculeOwnedEndpoint(t *testing.T) {
for _, c := range T4PrivilegeContract() {
for _, target := range c.RequiredEgress {
if strings.Contains(target, "github.com") {
t.Errorf("capability %q default egress target must not depend on GitHub mirror/API: %s", c.Name, target)
}
if strings.Contains(target, "google.com") {
t.Errorf("capability %q default egress target must not depend on external Google endpoint: %s", c.Name, target)
}
}
}
}
// TestT4PrivilegeContract_HardCapabilitiesMajority sanity-checks that
// the contract is not silently advisory-only. If someone marks
// everything as "advisory" the gate becomes a no-op without anyone
@@ -142,6 +156,17 @@ func TestAsYAML_EscapesEmbeddedQuotes(t *testing.T) {
}
}
func TestGeneratedT4CapabilitiesYAMLMatchesSSOT(t *testing.T) {
got, err := os.ReadFile("t4_capabilities.yaml")
if err != nil {
t.Fatalf("read generated t4_capabilities.yaml: %v", err)
}
want := AsYAML(T4PrivilegeContract())
if string(got) != want {
t.Fatal("generated t4_capabilities.yaml drifted from T4PrivilegeContract; regenerate with `go run ./cmd/t4-contract-dump > internal/provisioner/t4_capabilities.yaml`")
}
}
// TestAgentUIDConsistency ties the contract to the existing
// provisioner-side AgentUID const. The probe for "agent_uid_1000"
// hard-codes `id -u == 1000`; if AgentUID ever changes (no one