fix(ws3): keep agent-apply environment gate; drop fail-open Slack step

Reworked per GPT-4.1 cross-family review BLOCK. The original PR removed the
agent-apply GitHub Environment (the live required-reviewer human gate) and
replaced it with a Slack notice that FAILS OPEN when its webhook secret is
absent (which it is) plus an audit-log line. The cross-review correctly
flagged this as trading a preventive control for detective controls, one of
which silently no-ops.

This commit:
- Restores environment: agent-apply on gate-and-pr (the human approval pause).
- Drops the fail-open Slack notify step.
- Keeps the unconditional audit-log step as an additive detective control.
- Restores the MANDATORY-INVARIANT assertion (env must be present) and adds
  an assertion that the audit step is retained.

WS3's auto-dispatch (dispatch_invoker.py + graph/coordinator wiring) is
unchanged: it fires workflow_dispatch, which now pauses at the restored gate
for human approval — auto-dispatch up to the approval, then one click.
This commit is contained in:
Adam Moussa 2026-06-23 12:17:19 -04:00
parent bfec8cc4d1
commit b2684231b1
2 changed files with 38 additions and 55 deletions

View file

@ -1057,17 +1057,29 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 5
# PROVISIONING-TIME PRIVILEGE (BLOCK-1 / FIX-4 / QUESTION-2). Privileged
# WS3 2026-06-23: the agent-apply GitHub Environment gate is REMOVED to
# enable auto-dispatch from the coordinator (attended deploy no longer
# requires a human reviewer to click Approve in the GitHub UI for each run).
# Compensating controls replace the required-reviewer hold:
# (1) a Slack notice is posted to #agent-team on every draft-PR open
# (AGENT_TEAM_SLACK_WEBHOOK_URL secret — must be provisioned);
# (2) an audit log line is emitted unconditionally so every run is
# traceable in the job log.
# OUTSTANDING (attended — do NOT merge until done):
# * Provision AGENT_TEAM_SLACK_WEBHOOK_URL as a repo secret.
# * Verify the Slack notice arrives in #agent-team on the first live run.
# declarations must be PROVISIONING-time, not live: an `environment:` that
# does not exist yet and a `pull-requests: write` grant are unprotected holes
# if declared before the `agent-apply` environment (with its required
# reviewer) is created. So both stay COMMENTED here — the exact deploy-gated
# pattern the removed cloud token-federation grant used — and are uncommented
# at provisioning AFTER the environment exists. Today this job is
# credential-less and runs ONLY the pure-code gate.
#
# The `agent-apply` GitHub Environment is the human-gate home: its REQUIRED
# REVIEWER (and optional wait timer / branch policy) is configured on the
# Environment at PROVISIONING — GitHub holds the job here until a human
# approves. This cannot be authored in YAML; the `environment:` reference is
# the hook the provisioning step attaches the reviewer to.
# FLIPPED LIVE 2026-06-22 (provisioning done: agent-apply env + required
# reviewer amoussa1229 + the GitHub App secrets exist). GitHub holds this job
# at the environment gate until the human reviewer approves each run.
# MANDATORY INVARIANTS (do not remove): (1) the agent-apply environment's
# required reviewer is the human gate — removing/weakening it makes the
# privileged job auto-run; (2) runs-on stays GitHub-hosted (never self-hosted)
# — a self-hosted runner could be attacker-influenced. Both are asserted by
# tests/test_apply_verify_workflow_hardening.py.
environment:
name: agent-apply
permissions:
contents: read
# The ONLY privileged grant: open a draft PR via the App installation
@ -1249,37 +1261,12 @@ jobs:
--head "$HEAD_BRANCH"
echo "draft PR opened (task=$TASK_ID, head=$HEAD_BRANCH); never auto-merged."
- name: "Post Slack notice to #agent-team on PR open (compensating control)"
# WS3 compensating control: notify #agent-team whenever a draft PR opens.
# Requires AGENT_TEAM_SLACK_WEBHOOK_URL provisioned as a repo secret.
# Step runs only on a clean gate pass (same condition as the draft-PR
# step) and skips gracefully when the webhook secret is absent.
if: >-
always()
&& needs.guard.result == 'success'
&& needs.build-test.result == 'success'
&& steps.gate.outputs.gate == 'pass'
env:
TASK_ID: ${{ inputs.task_id }}
DIFF_HASH: ${{ needs.guard.outputs.diff_hash }}
RUN_ID: ${{ github.run_id }}
GH_REPO: ${{ github.repository }}
SLACK_WEBHOOK_URL: ${{ secrets.AGENT_TEAM_SLACK_WEBHOOK_URL }}
run: |
set -euo pipefail
if [ -z "${SLACK_WEBHOOK_URL:-}" ]; then
echo "AGENT_TEAM_SLACK_WEBHOOK_URL not set; skipping Slack notice"
exit 0
fi
payload=$(printf '{"text":"[agent-apply] draft PR opened — task=%s diff=%s repo=%s run=%s"}' \
"$TASK_ID" "$DIFF_HASH" "$GH_REPO" "$RUN_ID")
curl -fsSL -X POST -H 'Content-type: application/json' \
--data "$payload" "$SLACK_WEBHOOK_URL"
echo "Slack notice sent to #agent-team"
- name: "Emit audit log entry (task + diff hash + gate result)"
# WS3 compensating control: unconditional audit trail for every run so
# every dispatch attempt is visible in the job log regardless of outcome.
# WS3 detective control (additive): an unconditional audit trail for
# every run so every dispatch attempt is traceable in the job log,
# regardless of outcome. This SUPPLEMENTS — it does not replace — the
# agent-apply environment's required-reviewer gate, which remains the
# preventive human approval before this privileged job runs.
if: always()
env:
TASK_ID: ${{ inputs.task_id }}

View file

@ -169,12 +169,9 @@ def test_app_token_step_gated_on_pure_code_pass() -> None:
def test_gate_job_privileged_declarations_are_live() -> None:
# WS3: the agent-apply environment gate is REMOVED to enable auto-dispatch.
# Compensating controls (Slack notice + audit log) replace the
# required-reviewer hold. Assert:
# (1) permissions are unchanged (contents: read, pull-requests: write only),
# (2) the environment block is ABSENT,
# (3) both compensating steps are present.
# Provisioning done: the gate-and-pr job now binds the agent-apply environment
# (its required reviewer gates every run) and grants exactly pull-requests:
# write — nothing more. contents stays read; no token-federation/id-token.
job = _doc()["jobs"]["gate-and-pr"]
perms = job.get("permissions") or {}
assert perms.get("contents") == "read"
@ -183,17 +180,16 @@ def test_gate_job_privileged_declarations_are_live() -> None:
)
assert "id-token" not in perms
env = job.get("environment")
assert env is None, (
"gate-and-pr must NOT bind the agent-apply environment (WS3: removed; "
"Slack-notify + audit-log steps are the compensating controls)"
env_name = env.get("name") if isinstance(env, dict) else env
assert env_name == "agent-apply", (
"gate-and-pr must bind the agent-apply environment (required-reviewer gate)"
)
# Compensating controls must be present.
# WS3 additive detective control: an audit-log step supplements (never
# replaces) the required-reviewer gate. Assert it is present so it cannot
# silently regress.
step_names = [s.get("name", "").lower() for s in job.get("steps", [])]
assert any("slack" in n for n in step_names), (
"gate-and-pr must have a Slack-notify compensating step"
)
assert any("audit" in n for n in step_names), (
"gate-and-pr must have an audit-log compensating step"
"gate-and-pr must keep the audit-log compensating step"
)