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:
parent
bfec8cc4d1
commit
b2684231b1
2 changed files with 38 additions and 55 deletions
69
.github/workflows/agent-team-apply-verify.yml
vendored
69
.github/workflows/agent-team-apply-verify.yml
vendored
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
Reference in a new issue