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
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 5
|
timeout-minutes: 5
|
||||||
# PROVISIONING-TIME PRIVILEGE (BLOCK-1 / FIX-4 / QUESTION-2). Privileged
|
# PROVISIONING-TIME PRIVILEGE (BLOCK-1 / FIX-4 / QUESTION-2). Privileged
|
||||||
# WS3 2026-06-23: the agent-apply GitHub Environment gate is REMOVED to
|
# declarations must be PROVISIONING-time, not live: an `environment:` that
|
||||||
# enable auto-dispatch from the coordinator (attended deploy no longer
|
# does not exist yet and a `pull-requests: write` grant are unprotected holes
|
||||||
# requires a human reviewer to click Approve in the GitHub UI for each run).
|
# if declared before the `agent-apply` environment (with its required
|
||||||
# Compensating controls replace the required-reviewer hold:
|
# reviewer) is created. So both stay COMMENTED here — the exact deploy-gated
|
||||||
# (1) a Slack notice is posted to #agent-team on every draft-PR open
|
# pattern the removed cloud token-federation grant used — and are uncommented
|
||||||
# (AGENT_TEAM_SLACK_WEBHOOK_URL secret — must be provisioned);
|
# at provisioning AFTER the environment exists. Today this job is
|
||||||
# (2) an audit log line is emitted unconditionally so every run is
|
# credential-less and runs ONLY the pure-code gate.
|
||||||
# traceable in the job log.
|
#
|
||||||
# OUTSTANDING (attended — do NOT merge until done):
|
# The `agent-apply` GitHub Environment is the human-gate home: its REQUIRED
|
||||||
# * Provision AGENT_TEAM_SLACK_WEBHOOK_URL as a repo secret.
|
# REVIEWER (and optional wait timer / branch policy) is configured on the
|
||||||
# * Verify the Slack notice arrives in #agent-team on the first live run.
|
# 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:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
# The ONLY privileged grant: open a draft PR via the App installation
|
# The ONLY privileged grant: open a draft PR via the App installation
|
||||||
|
|
@ -1249,37 +1261,12 @@ jobs:
|
||||||
--head "$HEAD_BRANCH"
|
--head "$HEAD_BRANCH"
|
||||||
echo "draft PR opened (task=$TASK_ID, head=$HEAD_BRANCH); never auto-merged."
|
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)"
|
- name: "Emit audit log entry (task + diff hash + gate result)"
|
||||||
# WS3 compensating control: unconditional audit trail for every run so
|
# WS3 detective control (additive): an unconditional audit trail for
|
||||||
# every dispatch attempt is visible in the job log regardless of outcome.
|
# 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()
|
if: always()
|
||||||
env:
|
env:
|
||||||
TASK_ID: ${{ inputs.task_id }}
|
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:
|
def test_gate_job_privileged_declarations_are_live() -> None:
|
||||||
# WS3: the agent-apply environment gate is REMOVED to enable auto-dispatch.
|
# Provisioning done: the gate-and-pr job now binds the agent-apply environment
|
||||||
# Compensating controls (Slack notice + audit log) replace the
|
# (its required reviewer gates every run) and grants exactly pull-requests:
|
||||||
# required-reviewer hold. Assert:
|
# write — nothing more. contents stays read; no token-federation/id-token.
|
||||||
# (1) permissions are unchanged (contents: read, pull-requests: write only),
|
|
||||||
# (2) the environment block is ABSENT,
|
|
||||||
# (3) both compensating steps are present.
|
|
||||||
job = _doc()["jobs"]["gate-and-pr"]
|
job = _doc()["jobs"]["gate-and-pr"]
|
||||||
perms = job.get("permissions") or {}
|
perms = job.get("permissions") or {}
|
||||||
assert perms.get("contents") == "read"
|
assert perms.get("contents") == "read"
|
||||||
|
|
@ -183,17 +180,16 @@ def test_gate_job_privileged_declarations_are_live() -> None:
|
||||||
)
|
)
|
||||||
assert "id-token" not in perms
|
assert "id-token" not in perms
|
||||||
env = job.get("environment")
|
env = job.get("environment")
|
||||||
assert env is None, (
|
env_name = env.get("name") if isinstance(env, dict) else env
|
||||||
"gate-and-pr must NOT bind the agent-apply environment (WS3: removed; "
|
assert env_name == "agent-apply", (
|
||||||
"Slack-notify + audit-log steps are the compensating controls)"
|
"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", [])]
|
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), (
|
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