refactor: adapter 구현중..

This commit is contained in:
DongHyeonka
2026-08-13 16:02:21 +09:00
parent 30ceac23c1
commit 4dc033cf33
72 changed files with 13370 additions and 1549 deletions
@@ -0,0 +1,222 @@
# Provider Evidence Guardian Transaction Design
## Goal
Make one guardian process own the provider evidence filesystem transaction from
raw creation through sealed publication. A supervisor or provider hard death
must leave neither canonical raw evidence nor a guardian-owned sealed temp/final
inode, and the same workspace must be immediately retryable. Only the complete
authenticated `publish -> PUBLISHED -> commit -> EOF` sequence preserves the
canonical sealed artifact.
## Chosen Ownership Boundary
The guardian owns filesystem identity and publication. The supervisor retains
archive, trust, schema, signature, and evidence validation. This avoids two
unsafe alternatives:
- Keeping raw-only guardianship would leave the sealed rename-to-supervisor-death
cleanup gap.
- Moving evidence validation into the guardian would duplicate security policy
and make the helper unnecessarily privileged and complex.
The client opens the canonical `provider-evidence/untrusted` and
`provider-evidence` directories with `O_DIRECTORY|O_NOFOLLOW` before spawning
the guardian. Those identity-pinned directory descriptors are inherited as fd 3
and fd 4; they are never encoded in argv or the environment. The canonical raw
and final leaves are fixed by provider kind. Before spawn, the client exclusively
creates a nonce-private raw staging inode and the nonce-private sealed temp
inode, records both identities, and inherits their open descriptors as fd 5 and
fd 6. The guardian validates each inherited descriptor against its
descriptor-relative pathname, then publishes raw staging to the fixed raw leaf
with a no-replace hard link. Startup recovery authority is therefore an inode
identity acquired before spawn, never an identity discovered later from an
expected pathname. Both processes perform transaction I/O through
`/proc/self/fd/<fd>/<leaf>` so pathname substitution cannot redirect creation or
recovery into another directory. No raw path, sealed path, identity, provider
command, or credential is an argv value. The initial request contains only the
version, kind, random control nonce, and absolute deadline.
## Transaction Invariants
1. Before a complete valid guard frame, the guardian has not published a
canonical filesystem object. The client may have allocated only zero-byte,
mode `0600`, nonce-private raw staging and sealed temp inodes whose identities
it already holds. EOF with no frame or a partial frame removes both allocations.
2. Before spawning, the client validates that both pinned descriptors name the
expected canonical directories; computes fixed raw/final leaves and
nonce-private raw-staging/sealed-temp leaves; and creates the private leaves
with `O_CREAT|O_EXCL|O_NOFOLLOW`, mode `0600`, size zero, and link count one.
It retains both handles and inherits them as fd 5/fd 6 in addition to directory
fd 3/fd 4.
3. At bootstrap, the guardian fstats fd 5/fd 6, reads only their
`/proc/self/fd/5|6` link targets, and accepts each basename only when it is a
direct child of the canonical fd 3/fd 4 directory and matches the exact
provider-kind/32-lowercase-hex private-leaf grammar. It then requires
descriptor-relative lstat of that basename to match the already-fstat fd
identity, type, mode, size, and link count. This binds a deletion alias to an
inherited identity; it never promotes a pathname-discovered identity to
ownership. The two basenames must encode the same kind and nonce prefix.
4. After guard validation, the guardian verifies that the received kind/nonce
derives those exact bootstrapped private leaves. It verifies fd 5/fd 6 remain
regular zero-byte single-link `0600` files and exactly match the derived
private pathnames. It
uses `link(raw staging, canonical raw)` without replacement, verifies both
names have the inherited raw identity and link count two, unlinks the private
raw name, fsyncs the raw directory, and verifies the canonical raw link count
is one. READY is emitted only after this authority transfer succeeds.
5. READY is authenticated by the request nonce and returns raw dev/inode plus
sealed temp leaf/dev/inode. The supervisor starts the provider only after it
validates this exact bounded response with constant-time nonce equality.
6. The supervisor writes only schema-validated sealed bytes to the temp inode.
It opens with `O_NOFOLLOW`, checks dev/inode before and after writing, applies
mode `0400`, writes the complete bounded bytes, fsyncs, and closes.
7. Publish metadata contains the nonce, sealed dev/inode, byte length, and
SHA-256. The guardian checks the held descriptor and temp pathname identity,
regular-file type, link count, exact mode/size/hash, and canonical final-path
absence.
8. Publication uses atomic no-replace `link(temp, final)`, then unlinks temp and
fsyncs the parent directory. If death occurs between link and unlink, both
names refer to the same pinned inode and both are cleanup candidates.
9. PUBLISHED is authenticated and is emitted only after final pathname identity
and directory durability are verified.
10. Commit is legal only after PUBLISHED. It removes the pinned raw inode and
enters `commitPending`; it does not exit. EOF with no pending bytes is the
sole success terminal and preserves only the sealed final inode.
11. Any data after commit, including a separate later chunk, is a protocol error.
EOF/abort/deadline/protocol failure before the success terminal cleans raw,
temp, and final only when each path still names the guardian-owned identity.
12. If the guardian dies before READY is accepted, the client attempts cleanup
of raw staging, canonical raw, sealed temp, and sealed final aliases using
only the two identities recorded before spawn. A current pathname is never
opened and promoted to an owned identity. A competing canary or same-kind
transaction therefore survives every startup failure.
13. Cleanup attempts every owned target and reports cleanup failures together
with the primary failure using `AggregateError` at the supervisor boundary.
Client fd 3-fd 6 handles and guardian fd 5/fd 6 duplicates are closed on
every success and failure branch; close errors join the same aggregate rather
than skipping remaining cleanup.
Client-side exclusive private allocation is the startup ownership token. The
guardian accepts that token only after inherited-fd, descriptor-relative
pathname, type, mode, size, and link-count checks. Every cleanup identity is
recorded at allocation or authenticated READY; pathname discovery never creates
authority. Creation, validation, link, unlink, chmod, fstat, close, publish,
sync, and cleanup failures all fail closed.
## Bounded Authenticated Protocol
Every control or acknowledgement message is a four-byte big-endian length plus
canonical JSON with an exact ordered field set, strict UTF-8, no NUL, and a
total payload bound. Unknown, duplicate, reordered, oversized, truncated, or
trailing fields are rejected.
The state sequence is:
```text
guard -> READY(raw identity, sealed temp identity)
-> publish(size, sha256, sealed identity)
-> PUBLISHED(sealed identity)
-> commit
-> EOF success
```
All messages carry the same 32-byte random nonce. READY and PUBLISHED are
validated with `timingSafeEqual`; publish and commit are authenticated the same
way. Commit merely changes state, so a byte delivered in a later chunk before
EOF remains observable and causes fail-closed cleanup.
The maximum initial lease is the provider wall timeout plus a fixed ten-minute
post-processing allowance. The provider timeout remains bounded at 30 minutes,
so the guardian maximum is 40 minutes. Near-provider-timeout tests must show
that valid publication still has post-processing time, while an expired lease
cleans all owned objects.
## Supervisor and Scope Exit Ownership
The lease exposes raw/temp/final identities, `publish(bytes)`, `commit()`,
`abort()`, and a non-rejecting premature-exit promise. The client knows all
possible leaves and both startup identities before spawn and retains its pinned
directory and private-file handles until the lease terminates. Before READY it
cleans only aliases that still match those recorded identities. After READY it
checks the guardian response against the same identities and fallback-cleans
raw, temp, and final by identity if the guardian dies.
Provider waiting owns an explicit `scopeActive` latch. A guardian exit starts
whole-scope kill and collection only while that latch is true. Once the scope
completion path has collected the unit, the callback records a lifecycle error
but cannot start an unawaited kill. Publication and terminal commit observe the
guardian exit through their normal awaited failure path and clean sealed state.
Provider stdout and stderr are untrusted secret-bearing byte streams. The
supervisor counts and bounds them for resource enforcement but never forwards
their raw bytes into supervisor/CI stdout or stderr, on either success or
failure. Functional provider assertions use signed evidence or a non-log side
channel. Sealing/output I/O is allowed to settle; the design does not claim
OS-level cancellation. `GITHUB_OUTPUT` is a runner-owned regular file. After
output append succeeds, commit makes the guardian remove raw and EOF completes
the transaction.
Guardian diagnostics are best-effort only. A closed stderr or control descriptor
must not turn a fail-closed branch into a resolved operation or exit zero:
diagnostic and fd-close failures are absorbed after cleanup, and a nonzero exit
or requested fatal signal is issued unconditionally.
## Failure and Recovery
- No/partial guard EOF: no canonical raw or sealed object is published. The
guardian removes both nonce-private allocations through aliases that bootstrap
already bound to inherited fd identities, without needing kind/nonce from a
complete control frame.
- A competing canonical raw canary or another same-kind attempt causes
no-replace link failure. The loser removes only its private identities and
never removes the winner or canary.
- Guardian death after linking raw but before READY: the client uses its
pre-recorded raw identity to clean both private and canonical aliases and its
pre-recorded sealed identity for temp/final aliases, then retries the same
workspace immediately.
- Parent death after creation but before READY: stdout/control pipe failure or
EOF makes the still-running guardian clean both owned objects.
- Guardian death after READY: the supervisor knows raw and sealed identities and
cleans raw, temp, and final fallbacks.
- Supervisor death after PUBLISHED: guardian EOF cleans raw and the published
final inode, including the link/unlink intermediate state.
- Publish or commit race: serialized guardian state completes the current file
operation, then applies EOF/protocol failure cleanup; success requires clean
EOF after commitPending.
- Cleanup failure: remaining targets are still attempted and every error is
preserved; PASS is impossible.
There is one bounded crash window before spawn: if the client itself is killed
after private allocation but before the guardian is created, zero-byte `0600`
nonce-private leaves can remain. They contain no provider or credential bytes
and cannot occupy the fixed canonical raw/final names, so they do not block an
immediate same-kind retry. Automatic pathname sweeping is intentionally omitted
because an unproven stale pathname is not deletion authority.
After each observable managed-process failure, tests require canonical raw,
private staging/temp, canonical final, guardian, and provider cgroup residual
counts to be zero before retrying the same workspace successfully. The
documented pre-spawn client hard-death window is the sole residual exception.
## Verification
Real-process tests cover no/partial frames, a competing raw canary, same-kind
concurrency, guardian `SIGKILL` after raw link but before READY followed by
same-workspace retry, parent death around READY, valid
READY identities, EOF/deadline cleanup, publish/PUBLISHED, post-scope guardian
death, supervisor death after publication, commit trailing bytes in a later
chunk, closed-stderr fail-closed termination, near-timeout publication, and no
residual guardian/files. A provider that successfully prints a supplied
credential is verified not to expose it through supervisor stdout/stderr. Live fixtures
also specify active-scope guardian kill, detached-child external marker/raw
append suppression, cgroup collection, and same-workspace retry. Live
systemd/bwrap execution remains explicitly unverified when the approval limit
prevents running it.
The external-canary regression waits for a test guardian spawn marker before
creating the fixed raw file, proving that the initial absence check has already
completed. The fixed raw bytes and dev/inode must remain unchanged after startup
rejection. The pre-READY link regression watches only the fixed raw basename,
kills the exact direct child on that link event, and requires identity-bound
cleanup plus an immediate same-workspace retry.