Files
tech-log-frontend/docs/operations/ci-quality-gates.md
T

382 lines
26 KiB
Markdown

# CI quality-gate orchestration
`config/ci/gates.json` is the executable registry for all 26 gates and the sole
workflow model. `.gitea/workflows/quality-gates.yml` is generated from that
strict v2 contract; it is not an independent configuration authority. Update
the registry, run `corepack pnpm generate:ci-workflow`, and commit the generated
bytes together. `corepack pnpm check:ci-workflow` rejects missing, stale, CRLF,
or otherwise byte-different output. `corepack pnpm check:ci` runs artifact-schema
checking, the typed contract checker, and this workflow drift check. The Gitea
adapter runs each gate as an independent matrix check with full fan-out and no
soft-fail wiring.
Contract loading validates every registered command entry against the
authoritative root package-script graph and lifecycle/install policy before the
runner enters its execution callback. Foreign cwd/workspace script dispatch,
argument-sensitive dispatchers, and ineffective lifecycle suppression therefore
fail preflight even when the later `check:ci` command would never run.
For npm, that root-only boundary also parses options following an explicit
`run`/`run-script` target or implicit `test`/`start`/`stop`/`restart` target:
`--workspace`, `-w`, `--workspaces`, and `--prefix` are foreign manifest scope
and are rejected before execution. The first literal `--` ends npm option
parsing, so later tokens are ordinary script arguments. pnpm and Yarn differ:
their options following the selected script name are forwarded to that script;
their cwd/workspace selectors are rejected only where the manager consumes
them before script selection.
The npm graph includes existing root-manifest `pre<script>` and `post<script>`
hooks around every explicit `run`/`run-script` dependency and implicit
`start`/`stop`/`restart`/`test` dependency. Hooks are omitted only when ordered
npm options establish an unambiguous effective `--ignore-scripts` value before
the first literal `--`; bare and explicitly true forms omit hooks, while false,
negative, contradictory, malformed, and post-delimiter forms keep traversal or
fail closed. Case-insensitive `npm_config_workspace`, `npm_config_workspaces`,
`npm_config_prefix`, `npm_config_userconfig`, and `npm_config_globalconfig`
assignments are rejected whenever the tokenized command invokes npm. Direct
assignments, static paths whose basename is `env`, exact `command`/`exec`
prefix chains, prior exports, and `set -a` assignments share one prefix grammar.
Only modeled non-scope `env -i`, `env -u`, `env --unset`, and `env --` forms are
allowed; cwd-changing or unknown options fail closed. Dynamic assignment names,
unmodeled environment mutation, npm `--userconfig`/`--globalconfig`, and
unquoted pathname expansion before the npm argument delimiter are rejected.
The gate runner checks the same inherited environment names before loading the
contract or entering the execution callback, so they cannot reach a child gate
process.
The dependency graph is:
```text
MERGE_READY
-> RELEASE_READY
-> PROD_PROMOTION_READY
-> FIELD_SLO_READY
DOCUMENTATION_READY (off-chain)
```
`check:architecture`는 dependency-cruiser 결과와 별도로 Babel parser/Node
resolver 기반 `staticImportGraph``artifacts/quality/dependency-report.json`
기록한다. TS/TSX의 static, dynamic, type, CommonJS 및 JSDoc import를 검사하고,
로컬 source import에는 실제 TypeScript 확장자를 요구한다. 실행 source나 로컬
specifier에 `.js/.jsx/.mjs/.cjs`가 있거나 unresolved dependency, parse failure,
error-severity layer violation, cycle 또는 지원하지 않는 architecture rule
shape가 하나라도 있으면 gate는 실패한다. resolver, unresolved import, layer
edge와 cycle regression fixture도 같은 명령에서 실행된다.
Gitea workflow는 run ID/attempt, checkout SHA와 ref에서 build/release
식별자를 만들고 최소 `contents: read` 권한만 요청한다. Gate runner는 `HEAD`
full commit ID와 commit timestamp를 한 번 읽어 `SOURCE_DATE_EPOCH`를 유도한
뒤 workflow의 SHA와 실제 checkout이 일치하는지 확인한다. `CI=true`에서 이
식별자 중 하나라도 없거나 불일치하면 build step 전에 fail-closed하며, build와
release manifest도 같은 build ID, commit SHA, release ID와 timestamp를 가져야
한다.
Release tier의 `immutable_build``dist`의 hidden `.vite` entries, raw
`pnpm-lock.yaml`, build/module inventory와 local release/security evidence를 한
번 생성해 run ID/attempt로 구분된 archive로 게시한다. Raw lock bytes의 SHA-256은
dependency inventory와 candidate manifest 양쪽의 `lockfileSha256`과 같아야 한다.
Producer는 게시 전에 manifest가 선언한 exact member set과 각 byte digest를
검증하고 archive SHA-256을 job output으로 고정한다. Vulnerability/provenance
provider job은 같은 archive를 각각 내려받아 output SHA-256과 exact member set을
확인한 뒤 checkout과 분리된 root에만 추출하고, `CANDIDATE_LOCKFILE_PATH`
`CANDIDATE_DIST_SHA256`를 외부 command에 전달한다. Provider command 실행 후에는
archive와 추출된 candidate를 다시 검증하고, strict provider JSON과 candidate
digest가 일치할 때만 report를 업로드한다. Promotion은 같은 archive와 두 report를
별도 경로로 내려받고 SHA/member 검증을 마친 뒤 격리된 root에 추출하여 local
evidence를 read-only로 다시 계산하고 Ed25519 signature/digest를 확인한다.
Promotion job에는 build/rebuild command가 없으며 검증한 archive 자체를 변경 없이
그대로 승격한다. Local evidence 관계 검증에 필요한 policy와 verifier source도
archive member로 고정되며, checkout 밖의 격리된 cwd에서도 archive path와 기대
digest만으로 candidate-internal check를 재계산하고 archived secret-scan
policy/rule/SARIF/zero-finding/digest 관계를 검증한다. Archive에 없는 checkout
source를 다시 scan했다고 주장하지 않는다.
Provider baseline은 Gitea 1.26.4 이상과 Gitea Runner 1.0.0 이상이다. Linux
runner에는 실행 가능한 `/usr/bin/bwrap`, `/usr/bin/prlimit`,
`/usr/bin/systemd-run`, `/usr/bin/systemctl`, `bwrap --size` 지원, active user
bus, systemd user manager 254 이상, unified cgroup v2와 delegated
memory/pids/CPU controller가 모두 필요하다. 실행 파일, user manager/version,
trust/archive/path 같은 host-side preflight 실패는 raw report 생성 전에 차단된다.
`bwrap --size` 수용 여부와 실제 delegated controller/limit 값은 owned raw inode를
만든 뒤 scope 안에서만 확정할 수 있으며, 이 단계의 실패는 해당 inode를
identity-bound cleanup하고 fail closed한다.
각 provider는 고유한 collected user scope에서 실행된다. Supervisor는 실행 전에
실제 cgroup membership과 `memory.max=1073741824`, `memory.swap.max=0`,
`pids.max=64`, `cpu.max="100000 100000"`을 확인한다. 내부 process에는 core 0,
file-size 8,388,607 bytes, open FD 64, CPU 1,200초 상한도 적용된다.
같은 UID 전체에 합산되는 `RLIMIT_NPROC`로 provider별 32개를 보장한다고 주장하지
않으며 aggregate PID authority는 cgroup `TasksMax=64`다.
Bubblewrap는 network namespace를 분리해 완전 offline으로 실행하고 workspace,
verified candidate, `.git` mask, `/tmp`, `/etc`, `/proc`, `/dev`를 read-only로
유지한다. Archive 검증·추출과 sandbox/trust preflight가 끝난 뒤 supervisor가
생성하고 inode를 고정한 정확한 raw report 파일 하나만 read-write bind된다.
주변 `untrusted` directory 전체는 writable이 아니다. 실행 전 또는 provider
실패 시, 그리고 성공적으로 sealed evidence를 게시한 뒤에도 supervisor가 소유한
inode만 atomic quarantine을 거쳐 제거하므로 빈 stale report 없이 재시도할 수 있다.
Provider command와 provider-prefixed environment는 bounded length-prefixed bwrap vector로
`systemd-run` stdin에 전달되어 supervisor/systemd/bwrap wrapper argv나 unit
metadata에 노출되지 않는다. 단, 최종 provider executable의 일반 argv는 같은
UID의 process inspection에 보일 수 있으므로 command 문자열과 인자에 token,
password, private-key material을 넣으면 안 된다. Credential은 반드시 해당 종류의
`VULNERABILITY_PROVIDER_*` 또는 `PROVENANCE_PROVIDER_*` environment로만 전달하고
`*_COMMAND`에는 넣지 않는다. 정상 provider 종료까지 같은 stdin을 parent-liveness
pipe로 열어 두며 supervisor hard death의 EOF를 받은 in-scope wrapper는 provider
process group 전체를 종료하고 dev/inode가 일치하는 raw report만 정리한다.
별도의 trusted guardian child는 provider scope 밖에서 filesystem transaction 전체를
소유한다. Client는 spawn 전에 canonical raw/evidence directory를
`O_DIRECTORY|O_NOFOLLOW`로 열고 identity를 확인한 뒤 provider kind와 nonce에서
canonical raw/final 및 nonce-private raw-staging/sealed-temp exact leaf를 확정한다.
Client가 두 private file을 `O_RDWR|O_CREAT|O_EXCL|O_NOFOLLOW` mode `0600`으로
미리 할당하고 dev/inode를 기록한다. Directory descriptor는 guardian fd 3/4,
private file descriptor는 fd 5/6으로 상속되며 guardian argv에는 Node와 helper
경로만 있다. 최초 canonical v2 frame에는 provider kind, absolute deadline,
32-byte nonce만 전달한다.
Guardian bootstrap은 procfs link를 읽기 전에 fd 5/6을 fstat한다. Procfs pathname은
canonical directory의 exact direct-child grammar를 만족하고 descriptor-relative
lstat이 이미 확보한 fd identity/type/mode/size/link count와 일치할 때만 해당
identity의 cleanup alias가 된다. Guard frame 검증 뒤 raw staging을 fixed raw leaf에
no-overwrite hard link하고 두 alias의 link count 2를 확인한 다음 private alias를
제거하고 raw directory를 sync한다. Canonical raw가 같은 identity와 link count 1로
남은 뒤에만 READY를 응답한다. READY 전 종료 시 client는 pre-spawn raw identity로
raw staging/canonical을, sealed identity로 temp/final을 각각 확인해 일치하는 alias만
정리한다. 현재 canonical pathname을 새 ownership identity로 승격하지 않으므로 외부
file과 concurrent same-kind winner를 보존하면서 같은 workspace를 즉시 재시도할 수
있다. Supervisor는 READY identity와 canonical target도 정확히 확인한다.
검증된 JSON bytes는 pinned temp inode에만 기록하고 `0400` 적용과 file sync를 마친다.
Guardian은 authenticated publish의 size/hash/identity를 재검증하고 같은 directory에서
no-replace `link(temp, final)`, temp unlink, directory sync를 수행한 뒤 PUBLISHED를
응답한다. `GITHUB_OUTPUT` append 이후 commit은 raw inode를 제거하고
`commitPending`으로만 전이하며, 깨끗한 control EOF까지 확인해야 PASS가 된다. 그 전의
EOF, deadline, 잘못된 frame/nonce, commit 뒤 추가 byte 또는 guardian 조기 종료는
identity가 일치하는 raw/temp/final을 모두 정리하고 fail closed한다. Provider wall
timeout 30분에 post-processing 10분을 더한 guardian lease 상한은 40분이다.
Client가 private allocation 뒤 guardian spawn 전에 hard stop되면 empty mode-`0600`
nonce-private leaf만 남을 수 있다. 기록되지 않은 pathname은 ownership 근거가 아니므로
자동 sweeping하지 않는다. 이 private leaf는 fixed raw/final name을 점유하지 않아
same-kind retry를 막지 않는다.
`GITHUB_OUTPUT`은 runner가 소유한 regular file이라는 실행기 계약을 전제로 한다.
Sealing/output I/O의 OS-level cancellation을 보장하지 않는다. Provider scope가 이미
수집된 뒤 guardian이 종료되면 scope-active latch는 늦은 kill을 시작하지 않고 lifecycle
error만 기록한다. 이후 publish/commit의 awaited failure가 identity가 고정된
raw/temp/final fallback을 모두 정리하고 fail closed한다.
Provider stdout/stderr는 credential을 포함할 수 있는 untrusted bytes이므로 CI log로
재전송하거나 보관하지 않고, byte 수만 합산해 1 MiB 상한을 적용한다. Guardian의
stderr/control fd가 닫혀 진단 출력이 `EPIPE`/`EBADF`가 되어도 cleanup 뒤 nonzero
종료는 생략되지 않는다. Provider wall-clock 상한은 30분이다. Wall
timeout, aggregate output 초과, parent-liveness loss, 실행 중 guardian loss는 provider process group을
명시적으로 SIGKILL한다. 일반 command 실패와 FD/CPU RLIMIT 종료는 실제 exit/signal로
systemd completion을 거치며, 모든 경로에서 wrapper 종료와 systemd unit/cgroup
collection을 확인한다. Adapter는 사전에 배치된 offline data와 supervisor candidate binding만
읽어 정확한 report inode에 기록해야 한다. Scope/cgroup limit drift, residual unit,
workspace·host home/toolcache·sealed evidence 접근 의존성은 모두 blocking failure다.
Workflow가 실행하는 action은 `scripts/contracts/ci-gates.ts`의 단일 typed,
runtime-frozen registry에서만 resolve된다. `uses:`에는 repository 별칭, tag,
branch 또는 short SHA를 허용하지 않고 다음 absolute upstream URL과 full 40-hex
commit만 사용한다.
- `https://github.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5`
(`v4.3.1`)
- `https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020`
(`v4.4.0`)
- `https://github.com/ChristopherHX/gitea-upload-artifact@81f940d004763f986ba3582c007fd842dd5cb0d7`
(patched `v4` branch)
- `https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7`
(patched `v4` branch)
Gitea 1.22 공식 Actions 문서는 artifact v4 호환 경로로 이
`ChristopherHX` patched upload/download fork를 권고한다. 이 권고는 실제
설치 환경의 호환성을 증명하지 않으므로, 지원 baseline은 계속 Gitea 1.26.4+
및 Gitea Runner 1.0.0+이며 action 변경은 registry, semantic tests, snapshot과
generated workflow를 함께 갱신해야 한다.
이 workflow를 required check로 전환하기 전에 staging instance에서
`permissions`, `gitea.run_attempt`, 위 digest-pinned patched artifact action을
포함한 한 번의 실제 end-to-end provider smoke를 반드시 통과시켜야 한다.
mock/local generation 결과는 required-check enablement evidence가 아니다. 모든 setup step은
`node-version-file: .nvmrc`를 사용하므로 CI Node.js 버전은 `.nvmrc`의 exact
pin과 같다. CI contract는 `.nvmrc`가 full semantic version인지, 모든 job이 이
파일을 사용하는지 함께 검사한다. `ubuntu-latest` runner label은
관리자가 임의 환경에 매핑할 수 있으므로 provenance로 사용하지 않는다. 대신
그 label을 immutable container image에 매핑하고 동일한 image digest를
repository variable `RUNNER_IMAGE_DIGEST`에 설정한다. Workflow는 이 값을
runtime environment의 `CI_RUNNER_IMAGE`로 전달하며, 값이 비어 있으면 모든
gate가 실행 전에 실패한다. Gitea repository variable 이름에는 `CI` prefix를
사용할 수 없으므로 두 이름을 의도적으로 구분한다.
Pull requests and `develop` pushes evaluate merge readiness. Version tags
evaluate merge then release readiness. Production and field evaluation require
an explicit workflow dispatch. The field tier cannot pass until the 28-day
sample threshold decision is recorded. Documentation readiness consumes the
canonical project-note evidence in which both scoped diagrams already received
100/100 `PASS_SCOPED`; the repo ledger preserves the evidence scope and
canonical digests.
All jobs upload the shared `artifacts/` tree even after failure. Numeric
retention remains an organization/provider decision; the workflow intentionally
does not invent `retention-days`. The relative minimums are recorded in the
registry: merge evidence through the PR decision, coherent release evidence
through the next release promotion, drill evidence through the next production
promotion, and field evidence through aggregation.
Runbook jobs write to the stable path
`artifacts/runbooks/<runbook-id>/record.json`; the dynamic release identity is
stored inside the record. This keeps gate evidence lookup independent from
slashes or other provider-specific characters in `RELEASE_ID`, while each
workflow artifact remains scoped to its own run.
Browser-backed merge gates install and execute the pinned Chromium, Firefox,
and WebKit engines. This makes route behavior, reflow, native dialog semantics,
theme persistence, and automated accessibility a cross-engine contract rather
than a Chromium-only smoke check.
`FE-GATE-005`의 unit suite에는 client cache/storage의 deterministic contract가
포함된다. `storage-registry.test.ts`는 key별 codec, schema/TTL, 16,384-byte
상한, quota/security/corrupt cleanup과 memory fallback을,
`cross-tab-invalidation.test.ts`는 2,048-byte exact wire, topic/release epoch,
self/duplicate/out-of-order/gap, BroadcastChannel → localStorage → local-only
degrade와 cleanup을 검증한다. `tanstack-cache-coordinator.test.ts`는 topic을
local namespace로만 resolve하고 query key/data를 wire에 보내지 않으며 mutation
중 remote hint를 coalesce하는지 검증한다. 결과는 기존
`artifacts/tests/unit.xml`과 coverage evidence에 포함된다.
`FE-GATE-006`과 runtime adapter unit은
`QueryInvalidationProvider`/coordinator가 실제 production provider tree와
bootstrap에 존재하고 browser capability getter가 실패해도 boot가
`DEGRADED_LOCAL_ONLY`로 계속되는지 확인한다.
`FE-GATE-008``test:browser-capabilities` suite는 File/Blob, native file
input, IndexedDB, OPFS, Cache Storage와 StorageManager reference runtime을 Vite
dev origin의 실제 browser API에 연결한다. API가 없는 engine에서는 skip하지 않고
adapter의 명시적인 `UNSUPPORTED` fallback을 검증한다. 생성한 database, OPFS
namespace와 owned cache는 각 test가 자신이 만든 opaque namespace만 정리한다.
`FE-GATE-010`은 application/domain/presentation에서 raw browser storage와
picker global 접근을 막고, 선택되지 않은 reference runtime을 bootstrap이나
installed feature가 import하는 것도 거절한다. catalog의
`referenceRuntime.sourceRoots`와 conformance script가 실제로 존재해야 하며,
`productionComposition`은 project capability 결정 전까지 `false`다. 같은
gate의 realtime source/fixture 검사는 native SSE/WebSocket/Web Push API가
소유 adapter 밖으로 새는 것, presentation timer owner, 미선택 runtime의
bootstrap/installed-feature 조합을 차단한다. optional recipe source gate는
uncomposed runtime 전체를 tree-shaking 없이 합성해 catalog의 gzip 예산도
blocking으로 검증한다.
`FE-GATE-020`은 reference runtime source·전용 test·catalog metadata를 제거한
임시 repository에서도 base typecheck, architecture, test, build가 통과하는지
검증해 skeleton의 선택성을 유지한다. browser file/storage와 realtime runtime은
각각 독립 removal fixture와 JUnit evidence를 가지며, realtime fixture는 공통
event authority, SSE, WebSocket, bounded Polling, Web Push source와 공개 export,
전용 boundary script를 제거한 뒤 base gate를 다시 실행한다.
같은 `FE-GATE-010``FE-REG-QUERY` governance는 installed query마다
namespace, serialization/identity, invalidation topic, version,
`crossContext: "invalidate-only"``persistence: "disabled"`를 요구한다.
따라서 cross-tab invalidation은 조립됐지만 query persistence와 기존 IndexedDB
reference runtime은 계속 미조립이다.
현재 `FE-GATE-008` browser-capability suite에는 실제 두 page의
BroadcastChannel/localStorage fallback scenario가 아직 없다. unit fake 통과를
native multi-tab promotion evidence로 간주하지 않는다. 이 gap은
[Client cache and browser storage platform](../architecture/client-cache-and-storage.md)의
완료 기준에 미완료로 남아 있다. 현재 장애 분류와 안전한 local-only 복구 절차는
[Client cache and Web Storage recovery](./client-cache-and-storage-recovery.md)를
따른다.
Repository variables required by higher tiers:
- `RUNNER_IMAGE_DIGEST` for the immutable job-container image digest used by the
`ubuntu-latest` runner label; the workflow exposes it to gates as
`CI_RUNNER_IMAGE` (required by every tier)
- `HOSTING_BASE_URL` for live header verification
- `FIELD_WEB_VITALS_INPUT` for the privacy-approved field sample document
- `MIN_ELIGIBLE_SAMPLES` after the baseline decision
- `VULNERABILITY_PROVIDER_COMMAND` and `PROVENANCE_PROVIDER_COMMAND` for the
independently operated scanner and attestor commands
- `VULNERABILITY_PUBLIC_KEY_PATH`, `VULNERABILITY_KEY_ID`,
`PROVENANCE_PUBLIC_KEY_PATH`, and `PROVENANCE_KEY_ID` for separately managed
trusted Ed25519 verification material
두 provider role은 서로 다른 key ID뿐 아니라 canonical DER-SPKI public-key bytes도
사용해야 한다. 동일 key를 서로 다른 ID로 재등록한 구성도 finalizer가 거절한다.
If any external provider command, report, trust path, or key ID is absent,
promotion remains unavailable with `FAIL_UNVERIFIED`; there is no local
generator/restore fallback.
Each provider command must atomically emit strict provider evidence v2 from
the supervisor bindings for evidence type, issued/expires timestamps, run
ID/attempt, `PROVIDER_INVOCATION_NONCE`, source identity, candidate digests,
key ID, and DER-SPKI key fingerprint. Promotion receives the two supervisor
job outputs as `VULNERABILITY_INVOCATION_NONCE` and
`PROVENANCE_INVOCATION_NONCE`; do not replace them with constants or values
parsed from provider reports.
Promotion verification/staging step과 promoted-release upload action 사이에는
어떤 step도 둘 수 없고 upload에는 `if: always()`를 사용할 수 없다. 이 인접성은
실패한 검증의 publication을 막지만 staging path를 upload action에
cryptographically hand off하지는 않는다. 따라서 promotion runner는 trusted,
exclusive, single-tenant여야 하고 staging step 종료부터 바로 다음 upload action이
파일을 여는 시점까지 같은 UID로 실행되는 provider command나 다른 process가
살아남지 않는다는 운영 가정을 만족해야 한다. 업로드된 artifact의 downstream
consumer도 artifact service나 transfer action을 신뢰 경계 밖으로 보고, candidate
manifest와 signed provider evidence에 바인딩된 digest를 다운로드 후 다시
검증해야 한다. 현재 producer-side adjacency 자체는 consumer-side digest
revalidation을 대신하지 않는다.
Promotion job에는 job-level `if`가 없다. 기본 `needs` 성공 의미론으로 immutable
build, vulnerability provider, provenance provider 세 job이 모두 성공해야 하며,
`always()``cancelled()`로 cancellation을 덮어쓰지 않는다. Bare `always()`
step cleanup에만 사용된다. 다만 cancellation 시 cleanup 실행 여부는 workflow
정적 타입이나 단위 테스트로 증명하지 않았으며 runner/native smoke에서 확인해야
하는 신뢰 경계다.
Finalizer output은 `RUNNER_TEMP` 아래 random private directory이며 exact-five
upload는 `${{ steps.finalize.outputs.staging_root }}` 아래 다음 다섯 canonical
pathname만 사용한다: `release-candidate.tar.gz`, `vulnerability-report.json`,
`provenance-attestation.json`, `provider-verification.json`,
`promotion-verification.json`. 바로 다음
`always()` cleanup은 staging path/token, runner-temp device/inode와 staging-leaf
device/inode 여섯 output을 모두 요구한다. cleanup은 pin한 leaf descriptor에서
exact-five name만 unlink하고 non-recursive `rmdir`만 사용하므로 교체된 directory나
canary tree를 recursive 삭제하지 않는다. stable `.release/promoted-staging`
directory를 만들거나 재사용하지 않는다. exact five는 captured archive 한 개,
captured report 두 개와 process 안에서 생성한 provider/promotion verification v3
두 개이며 promotion
record는 provider record, local assessment, report hashes와 run/source/candidate/
nonces/key identities/trust-policy hash 및 signed `secretScanAttestation`을 함께
bind한다. 이 attestation은 PASS와 local-assessment/source-set/policy/SARIF/
scan-input digest를 포함한다. Supervisor/finalizer는 captured archive에서 기대
tuple을 유도해 exact equality를 확인하지만, 실제로 같은 source-set 전체를
독립 스캔하고 forged empty SARIF에 서명하지 않을 책임은 trusted vulnerability
provider에 있다. Staging은 restrictive
umask와 무관하게 directory `0700`, file `0400`을 강제하고, 모든 write 뒤 live
time으로 exact-five signature/freshness를 다시 확인한 뒤에만 output을 공개한다.
Descriptor-relative 정리는 ancestor/leaf 교체와 symlink를 fail-closed로 처리하지만
upload action의 same-UID pathname reopen 또는 atomic
`renameat2` handoff를 보장하지 않는다. staging Gitea smoke/native adapter 확인
전에는 그 경계를 닫았다고 보고하지 않는다. 실제 smoke는 exact-five
upload-download와 success, validation failure, upload failure, cancellation 각각의
cleanup을 관찰해야 한다. 현재 repository에는 native uploader나 `renameat2`
보장이 없다.
또한 portable Node의 `mkdir`와 최초 pathname `lstat`는 atomic하지 않다. 구현은
mkdir 직후 metadata를 저장하고 이후 `O_DIRECTORY|O_NOFOLLOW` descriptor의
device/inode와 비교한 뒤에만 permission을 바꾸지만, 최초 lstat보다 앞서 성공한
malicious same-UID 교체는 native/privilege 경계로 남는다. 따라서 `RUNNER_TEMP`
private `0700` ancestor와 exclusive single-tenant runner가 필수다.
실패 cleanup도 created device/inode와 opened descriptor가 일치한 뒤에만 활성화된다.
불일치 descriptor는 close만 수행하며 현재 visible replacement pathname은 unlink나
`rmdir`하지 않는다. 공격자가 original directory를 다른 이름이나 parent 밖으로
이동한 경우 portable Node parent scan으로 안전하게 회수할 수 없으므로, 공격자를
배제한 trusted runner/native cleanup 또는 격리된 test fixture가 잔여 directory를
후처리해야 한다.
Branch protection must mark each `FE-GATE-* / <name>` check required for its
declared tier. This repository cannot configure server-side protection by
committing a file.