docs(reimbursement-payment-batch-deployment): 记录受控部署验收

这个提交包含在:
2026-08-05 10:52:54 +08:00
父节点 566c37679a
当前提交 e14400ce1b
共修改 3 个文件,包含 133 行新增和 22 行删除
@@ -19,6 +19,103 @@ operations require explicit approval and evidence.
- Do not execute real payments, import bank statements, connect payment
providers or write ERP without a separate approved Trellis task.
## Scenario: Existing Deployment SSH Identity Disambiguation
### 1. Scope / Trigger
- Trigger: `DEPLOY_SERVER` / `DEPLOY_REFRESH_SERVER` are not injected and local
SSH configuration exposes more than one alias that can read the frozen
deployment contract.
- Purpose: select the existing least-privilege deployment account without
guessing from alias names or accidentally publishing as unrestricted root.
### 2. Signatures
- Contract reader: `deployment_contract_load <ssh-target> <systemd-unit> <assoc-array>`.
- Aggregate preflight:
`DEPLOY_SERVER=<target> DEPLOY_REFRESH_SERVER=<target> bash deploy/trialrun/check-existing-deployment.sh`.
- Refresh probe: `ssh <refresh-target> probe` must return exactly
`[deployment-contract-refresh][PASS] probe`.
- Deployment entry: `bash deploy/trialrun/deploy-existing.sh` with the two
resolved targets kept only in process environment.
### 3. Contracts
- Never choose a target from its alias name, apparent hostname or ordering in
SSH config, and never print candidate targets while probing.
- A normal deployment candidate must be non-root, own the contracted
`REMOTE_DIR`, `REMOTE_ENV_FILE` and synchronized source files, and have
Docker access. These checks use numeric UID equality on the remote host.
- An unrestricted root alias is not a valid normal deployment target even if
it can read the same contract and pass the aggregate checks. Publishing as
root can change synchronized-file ownership and break the intended
least-privilege path.
- Multiple aliases may point to the same machine and contract. Select a target
automatically only when exactly one candidate satisfies the full non-root
ownership contract; otherwise stop for explicit local configuration
guidance.
- The refresh identity is separate and valid only when the exact forced-command
`probe` response matches. Do not reuse an unrestricted root login as the
refresh identity.
- After deployment, assert the source files still belong to the selected
deployment UID before recording success.
### 4. Validation & Error Matrix
- No contract-readable candidate -> stop before remote mutation.
- Exactly one non-root owner candidate + one exact refresh probe -> continue to
13/13 aggregate preflight.
- Several contract-readable aliases, but only one non-root owner -> use the
owner candidate; do not treat alias count alone as ambiguity.
- Several non-root owner candidates -> stop and request local SSH guidance;
do not choose by config order or connection speed.
- Root candidate passes 13/13 -> exclude it from normal deployment selection.
- Post-deploy ownership differs from the selected UID -> fail closeout and
investigate; do not report the release as clean.
### 5. Good/Base/Bad Cases
- Good: two aliases reach the same host and contract; the directory-owning
non-root account is uniquely selected and ownership remains unchanged.
- Base: one normal deployment identity and one forced-command refresh identity
are injected explicitly; no discovery is needed.
- Bad: use an unrestricted root alias because it also returns 13/13 PASS.
- Bad: select the first matching `Host` entry or expose aliases, usernames,
paths or SSH configuration in task evidence.
### 6. Tests Required
- Confirm the selected normal identity has a non-zero UID and numerically owns
the contracted repository, environment file and a synchronized source file.
- Confirm Docker access succeeds without sudo and the aggregate preflight is
exactly `PASS=13 FAIL=0`.
- Confirm the refresh identity returns the exact probe token and the normal
identity is not substituted for it.
- After release, compare the synchronized source owner UID with the selected
identity UID and require equality.
- Persist only PASS/FAIL counts and the selection rationale; run the sensitive
content gate before commit.
### 7. Wrong vs Correct
#### Wrong
```bash
# Both aliases connect, so choose the first one (which may be root).
DEPLOY_SERVER="${candidates[0]}" bash deploy/trialrun/deploy-existing.sh
```
#### Correct
```bash
# Resolve exactly one non-root candidate that owns the frozen contract paths,
# then run the aggregate gate before the controlled deployment wrapper.
test "${#owner_candidates[@]}" -eq 1
DEPLOY_SERVER="${owner_candidates[0]}" \
DEPLOY_REFRESH_SERVER="$forced_refresh_target" \
bash deploy/trialrun/check-existing-deployment.sh
```
## Scenario: Deployment Env Contract
### 1. Scope / Trigger