HOMELAB-2578: fix(forgejo): resolve landing credential at runtime #82
No reviewers
Labels
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Homelab/pi-extensions!82
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "plane/HOMELAB-2578-runtime-landing-credential"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
forgejo_merge_prcould only write with a landing credential the process already held at startup (FORGEJO_LANDING_TOKEN_FILEorFORGEJO_LANDING_TOKEN). An orchestrator session that started without either could never acquire one, so the measured failure stood: onHomelab/ghostty-in-the-shell#126— formal review 1650, all seven required contexts green — the merge authenticated asaaronand the live merge whitelist["pi-landing"]answered 405.The merge leg now resolves the sanctioned path at merge time:
resolveLandingCredentialAtRuntimereadskv/pi/forgejo-landingthrough the sanctioned CLI (bao kv get -format=json <path>, or the helper named byFORGEJO_LANDING_OPENBAO_CLI). argv carries a path, never a token; the child env is an allowlist (PATH/HOME/namespaces +BAO_ADDR/VAULT_ADDR), so the CLI takes its login material from its own$HOME/.vault-token(0600 file). stdout is parsed and never echoed; a failure quotes only one scrubbed stderr line.materialiseLandingTokenas a 0600 file inside a 0700 directory, the modes are read back from disk, and the merge credential is resolved from that file. The directory is removed in afinallyon every exit path (success, identity refusal, merge-write failure), with a process-exit drain as a last resort.assertMergeIdentity(GET /user) runs immediately before the write, andmergeAsLandingtakes the token from that proof. No raw-API merge path was added.FORGEJO_API_KEY(the GITS-359 substitution).GET .../branch_protections) in the refusal.Red first
forgejo/tests/landing-runtime-credential.test.ts, driving the realforgejo_merge_prhandler over the route-table fake plus a fake OpenBao CLI (0700 script in a 0700 temp dir that answers the KV read the waybao kv get -format=jsondoes, with a sentinel proving whether the runtime path ran). No network, no live OpenBao, no real credential.At
9747346: 9 of 11 failedGreen
At this head, rebased onto
origin/maine2b6f6e:npx tsc --noEmit— cleannpx vitest run— 15 files / 406 tests passed (baseline at9747346: 14 / 372)GET /userimmediately before it, 0600/0700 read back from disk, cleanup on success / identity refusal / merge-write failure, no OpenBao read when the lock refuses, OpenBao failure and missing field and un-runnable CLI all refuse with zero mutating requests, wrong identity refuses namingaaron+["pi-landing"], ambiguous field name configurable, explicit token file still wins and never invokes the helper, and the token never appears in the result.No live credential was read by this worker or by any test: the shared
tests/fake.tsinstalls an inert OpenBao helper at import, so no suite can reachkv/pi/forgejo-landing.Files
forgejo/api.ts,forgejo/index.ts,forgejo/types.ts,forgejo/tests/fake.ts(additive: branch-protections route,mergeHttp, inert OpenBao helper),forgejo/tests/landing-runtime-credential.test.ts(new).docs/forgejo.mdis not touched (collides with #81),forgejo/landing.tsis not touched,verdict-cli.ts/verdict.tsare not touched,~/.pi/agent/extensionsis not touched.Deployment note (owner action, not code)
The runtime read needs an OpenBao address. This host's pi processes carry
FORGEJO_API_KEY/FORGEJO_BASE_URLand noBAO_ADDR/VAULT_ADDR(checked by env key names only), so on this box the read will refuse until the address (a non-secret URL) or a sanctioned helper is available to the session — eitherBAO_ADDRin the pi host's unit env, orFORGEJO_LANDING_OPENBAO_CLIpointing at the existing sanctioned mint+read helper. The refusal names exactly that. The 405 class the ticket reports is fixed at the tool level; the deployment wiring is a one-line, non-secret change outside this repo's allowed files.Scope
HOMELAB-2518 / HOMELAB-2578 only. No merge, no verdict, no review, no Done by this worker. GITS untouched (read-only).
Non-verdict worker note (HOMELAB-2578 / HOMELAB-2518) — head
05e8b728519c9bdda5b5f4450faf4a8991e0776aFollow-up on the review finding: resolving
kv/pi/forgejo-landingat merge time still refused on this host, because a running session carries noBAO_ADDR/VAULT_ADDRand there is no~/.bao_address, so the sanctionedbaoCLI had no endpoint.Change (runtime files only):
forgejo/api.ts—DEFAULT_LANDING_OPENBAO_ADDR = https://openbao.aaron.reynoza.orgplusresolveLandingOpenBaoAddress(env)with precedenceFORGEJO_LANDING_OPENBAO_ADDR→BAO_ADDR→VAULT_ADDR→ default (never empty);runOpenBaoClinow always setsBAO_ADDR/VAULT_ADDRin the allowlisted child env. Refusals and the reported origin name the endpoint (OpenBao <path> at <endpoint>).forgejo/index.ts— refusal text and merge-tool guidance name the address env and its default.forgejo/tests/landing-runtime-credential.test.ts— 4 new cases.Red-first: the new fake CLI refuses when the child env carries no
BAO_ADDR(this host's shape). Before the fix, 2 of 20 failed: the merge refused (OpenBao kv/pi/forgejo-landing could not be read … no OpenBao address is configured), zero mutating requests, andFORGEJO_LANDING_OPENBAO_ADDRwas ignored.Green: targeted 20/20; full
forgejosuite 15 files / 410 tests;tsc --noEmitclean; CI run #196forgejojob success on this head (~/tmp/h2578-addr-red.log, /tmp/h2578-addr-full.log).Unchanged: the address is a server URL, not a credential — no new credential required, the CLI still authenticates from its own
$HOME/.vault-token, nothing new in argv, no secret printed, 0600/0700 staging and the brandedGET /userproof before the write are intact, refusal never degrades toFORGEJO_API_KEY, landing lock untouched. Tests use a local/bin/shfake only: no live OpenBao read, no network. No docs/verdict/landing-lock edits. Not merged, no verdict posted.Addendum: CI run #196 on
05e8b728519c9bdda5b5f4450faf4a8991e0776ais now pass — all 14 jobs green (incl.forgejo1m24s andpi-swarm-suite5m41s). Still non-verdict; not merged.Verdict: APPROVED
model: omniroute/deepseek-flash
head:
05e8b72851Independent general review of the exact head
05e8b728519c9bdda5b5f4450faf4a8991e0776a("default the landing OpenBao address" on top of
49ac697, basee2b6f6e). Re-fetched beforethe verdict; the head had not moved. CI run #196 on this sha: 14/14 jobs
success. Re-run locallyon the same tree:
npx tsc --noEmitclean,npx vitest run→ 15 files / 410 tests passed. Noblocker found; the findings below are documentation, design-note, receipt-honesty and coverage
items.
What I verified myself (not taken from the worker log)
Fail-closed is structural, not incidental.
resolveLandingCredentialAtRuntimereturns atstep 1 unless the answer is
configured-token, and step 2 either refuses or returnssource: "openbao". There is no reachable return ofconfigured-tokenon the merge leg, andindex.tsno longer importsresolveLandingCredentialat all — the substitution that producedGITS-359 is removed rather than merely discouraged.
No secret in argv or in the child environment.
execFile(cli, ["kv","get","-format=json", path])— argv carries a path. The child env is an explicit allowlist (
PATH,HOME,BAO_NAMESPACE,VAULT_NAMESPACE) plusBAO_ADDR/VAULT_ADDR, which are URLs.execFileis used with noshell: true, so the env-configurable path and CLI are not an injection surface. stdout is parsedand never echoed; the only stderr text that can escape is one line passed through
scrubSecretShapes(any[A-Za-z0-9_\-+/=]{32,}run collapses to<redacted>, then 300 chars).No production
console.*/JSON.stringifyof credential material appears in the diff.The 0600/0700 contract is read back from disk.
materialiseLandingTokendoesmkdtempSync→chmodSync(dir, 0o700)→writeFileSync(…, {mode: 0o600})→chmodSync(file, 0o600)(theexplicit chmod defeats umask) →
statSyncon both, and on any mismatch removes the directory andrefuses rather than use it.
dispose()is flag-guarded and idempotent, with aprocess.once("exit")drain as a last resort.
Ordering is correct and least-privilege.
assertLandingLockruns before any credentialresolution, so a PR that cannot land never causes the sealed secret to be read — proven by a
sentinel in the new suite, not by inspection alone. The identity proof (
GET /user) sits insidetry, immediately beforemergeAsLanding, and carries the same branded proof/config, so the writeuses exactly the credential that was just proved.
finally { credential.dispose?.() }coverssuccess, identity refusal and merge-write failure; all three are asserted against the real
filesystem.
The refusal evidence is honest.
listBranchProtectionsis a read-onlyGET …/branch_protections; non-2xx throws anddescribeMergeWhitelistconverts that into "could notbe read", never into "no whitelist". A disabled whitelist is reported as DISABLED. The
identity refusal names the authenticated login, the requirement, and the live list.
Tests use fakes only.
tests/fake.tsinstalls an inert/bin/shhelper at import (0700 scriptin a 0700
mkdtempdir) throughFORGEJO_LANDING_OPENBAO_CLI, so no suite in this package canreach a live
kv/pi/forgejo-landingor the network. The fixture token is a lowercase placeholder,not credential-shaped. I performed no live OpenBao read and used no landing credential.
Findings (non-blocking)
F1 — the PR body is stale after the default-address commit. The "Deployment note (owner
action, not code)" section still says the runtime read "will refuse until the address … is
available to the session", and the Green section cites 406 tests with 16 in the new file. At this
head a sanctioned default endpoint exists (
DEFAULT_LANDING_OPENBAO_ADDR), so that read no longerrefuses for want of an address, and the suite is actually 410 tests / 20 in the new file. The code
and the tool
description/promptGuidelinesare correct and current; only the prose lags.Updating the body stops the next reader from planning work that is already done.
F2 — the default endpoint redirects ambient CLI login material (design note).
runOpenBaoClisets
BAO_ADDR/VAULT_ADDRunconditionally and forwardsHOME, so the CLI presents its own$HOME/.vault-tokento the resolved address. On a host that holds a vault token for a differentcluster and sets no
BAO_ADDR, that token is presented to the hardcoded host. Here the endpoint isthe sanctioned one and this is the commit's explicit intent, and all three override variables are
covered by tests, so this does not block — but it deserves a sentence in the docs when
docs/forgejo.mdis next touched (currently excluded because PR #81 owns it). Related: theallowlist does not forward
HTTPS_PROXY/NO_PROXY/BAO_CACERT/BAO_SKIP_VERIFY, so a hostbehind a proxy or with a private CA fails closed and must supply a wrapper through
FORGEJO_LANDING_OPENBAO_CLI. Failing closed is the right direction.F3 —
details.runtimeCredential.removedasserts a fact it did not observe. The handler setsremoved: truebefore thefinallyruns, andremoveRuntimeDirswallows a failedrmSync. Themode fields are genuinely read back, but
removedis a prediction — the only receipt field in thischange not derived from a read-back, in a package whose other receipts (merged sha, comment
identity) are all observed. Reporting an
existsSyncresult after disposal, or dropping the field,would close the gap. Minor: the control itself is sound and removal is separately asserted by three
tests.
F4 — coverage gap for an unreadable runtime identity. The suite covers a runtime credential
that resolves to a mismatched login (
aaron,ld-reviewer) but not one whoseGET /userisunreadable (non-2xx). The code path refuses correctly —
assertMergeIdentityreturnsok: falsewith "the merge identity could not be read …" before any write, and that branch isexercised in
merge-identity.test.ts— so this is coverage, not a defect.Scope
Diff is confined to
forgejo/api.ts,forgejo/index.ts,forgejo/types.ts,forgejo/tests/fake.tsand the new
forgejo/tests/landing-runtime-credential.test.ts. Nodocs/forgejo.md, nolanding.ts, noverdict*.ts, nopackage.jsonchange, no new dependency, nothing under~/.pi/agent/extensions. No raw-API merge path was introduced and the landing lock is untouched.Verdict: APPROVED
model: omniroute/k3.1-pro
head:
05e8b72851tier: premium
Independent premium review of exact head
05e8b728519c9bdda5b5f4450faf4a8991e0776a. Re-fetched immediately before submission and unchanged. CI run #196 on this SHA is green in 14/14 jobs. On the clean exact-head worktree I independently rannpx tsc --noEmit(clean) andnpx vitest run(15 files / 410 tests passed, including 20 runtime-credential tests), using only the route-table Forgejo fake and local fake OpenBao CLI.Verified directly: runtime resolution cannot return the configured operator credential for a merge; OpenBao/identity/staging failures refuse without operator fallback. Address precedence is
FORGEJO_LANDING_OPENBAO_ADDR->BAO_ADDR->VAULT_ADDR-> the sanctioned default.execFilecarries only the KV path in argv, uses no shell, and gives the child an allowlisted environment with no credential value. The runtime token is staged 0600 inside 0700, modes are read back from disk, and the credential is re-read from that file. The landing lock precedes secret resolution; runtimeGET /userimmediately precedes the branded merge write, which uses the same proved credential.finallydisposes on success, identity refusal, and merge failure, with normal-path removal asserted by tests.No blocker found. Four non-blocking findings:
F1 — stale PR prose. The PR body still says no default OpenBao address exists and reports 406 total / 16 new tests. This head has the default endpoint and verifies at 410 total / 20 new tests. Code and tool guidance are current; only the description lags.
F2 — ambient CLI authority and omitted connection variables. With no override, forwarding
HOMEwhile forcing the default address presents ambient$HOME/.vault-tokenmaterial to that host, even if the token originated for another cluster. This is intended and overrideable in this Homelab deployment, but should be documented. Proxy/CA variables are omitted, so hosts needing them fail closed and require a sanctioned wrapper.F3 — cleanup receipt is not read back.
details.runtimeCredential.removedis settruebeforefinallyruns.removeRuntimeDirswallows removal errors and drops the directory from the live set even on failure, so a failed removal would be misreported and not retried by the exit drain. Normal-path cleanup is test-proven; this is a failure-path receipt/retention defect.F4 — unreadable runtime identity coverage. The new suite covers wrong login but not non-2xx runtime
GET /user. The shared identity branch is directly tested with 403 and zero mutations inmerge-identity.test.ts, and inspection confirms the runtime path uses it; add the runtime-specific regression case for completeness.Evidence:
/home/aaron/repos/homelab/working-files/homelab-2518/HOMELAB-2578-review-kimi-final.md.Verdict: APPROVED
model: omniroute/deepseek-flash
head:
05e8b72851Fresh panel seat on
Homelab/pi-extensionsPR #82, exact head05e8b72. I re-fetched the branch head before writing this (it has not moved from05e8b728519c9bdda5b5f4450faf4a8991e0776a), worked from a clean worktree of that exact commit, and derived everything below from the tree rather than from the PR body, the worker log, or any earlier review on this PR. The earlier reviews are not cited, edited, or reused as this verdict.What the diff contains: five files, +1233/−79 —
forgejo/api.ts,forgejo/index.ts,forgejo/types.ts,forgejo/tests/fake.ts, and a newforgejo/tests/landing-runtime-credential.test.ts.docs/forgejo.md,forgejo/landing.ts,verdict*.ts,package.json, and~/.pi/agent/extensionsare all untouched, andgit diff --checkis clean.Independent verification on that exact head:
npx tsc --noEmitexits 0 with no diagnostics;npx vitest runreports 15 files / 410 tests passed, with 20 tests in the new file. CI run #196 on05e8b72851shows 14/14 jobssuccess, includingforgejo,forgejo-ci, andpr-title.The property the ticket exists to guarantee holds by construction, not by convention:
resolveLandingCredentialAtRuntimereturns early unless the answer isconfigured-token, and after that point every branch either returnssource: "openbao"or a refusal, so there is no reachable path that hands the merge write the shared operator credential.resolveLandingCredentialis no longer imported byindex.tsat all (only the runtime variant is), which means the substitution GITS-359 described was removed rather than left available beside the new path.No secret reaches argv or a child environment: the only child process in the production diff is a single
execFile(cli, ["kv", "get", "-format=json", path], …)whose fourth argv slot is a KV path, and the child env is an explicit allowlist ofPATH,HOME,BAO_NAMESPACE,VAULT_NAMESPACEplusBAO_ADDR/VAULT_ADDR, which are server URLs. The secret exists only as a local variable after stdout is parsed. There is noshell: trueanywhere inapi.tsorindex.ts, so the configurable path and CLI are not an injection surface, and a grep of the two production files finds noconsole.*,process.stdout, orprocess.stderrwrite at all.Failure reasons are scrubbed by construction:
scrubSecretShapescollapses any 32-plus-character token-shaped run to<redacted>and truncates to 300 characters, and it is applied both to the quoted CLI stderr line and to the final refusal.parseOpenBaoPayloaderrors carry the path only.The staging control is honest about being a control:
materialiseLandingTokendoesmkdtempSync→chmodSync(dir, 0o700)→writeFileSync(…, {mode: 0o600})→chmodSync(file, 0o600)(the explicit chmod defeats umask) →statSyncon both, and on any mode mismatch it removes the directory and refuses instead of using it. The merge credential is then re-resolved throughresolveLandingCredentialagainst that staged file, so the token that authorises the write is the one read back off disk.Ordering is least-privilege:
assertLandingLockis awaited beforeresolveLandingCredentialAtRuntime, so a PR that cannot land never causes the sealed secret to be read, and the new suite proves it with a sentinel file the fake CLI touches when it runs.assertMergeIdentity(GET /user) sits inside the cleanuptry, immediately before the brandedmergeAsLandingwrite, using the same credential — the new suite asserts theGET /userindex is strictly before the merge POST index and that the merge POST carries the OpenBao token rather than the operator token.Cleanup is on every exit path: the staged credential is disposed in a
finally, so success, wrong identity, and merge-write failure all remove theforgejo-landing-*directory, and each of those three outcomes is asserted against a sandboxedTMPDIR. Disposal is idempotent and aprocess.once("exit")drain covers a throw past thefinally.The refusal evidence is read-only and honest:
listBranchProtectionsis aGET repos/{owner}/{repo}/branch_protections, anddescribeMergeWhitelistreports an unreadable list as "could not be read" rather than as an empty allow-list, and a disabled whitelist as DISABLED. The test suite drives a route-table fetch fake and a local 0700/bin/shfake CLI, andtests/fake.tsinstalls an inert helper at import, so no suite in this package can reach the livekv/pi/forgejo-landingor the network; I read no live credential and read no live secret.I confirm the four findings carried forward are real and non-blocking and I add no blocker of my own: F1 (the PR body still describes the pre-
05e8b72world with no default address and reports 406/16 rather than the actual 410/20), F2 (the default address means an ambient$HOME/.vault-tokenis presented to the sanctioned host, and proxy/CA variables are not forwarded, so affected hosts fail closed), F3 (details.runtimeCredential.removed: trueis a prediction set before thefinally, andremoveRuntimeDiralso drops the directory from the exit-drain set even whenrmSyncfailed, so a failed removal would be misreported and not retried), and F4 (no runtime-specific regression test for a non-2xxGET /user, though that branch is covered inmerge-identity.test.ts). All four are behaviours I traced in the code, not claims I took on trust.No blocker open.
Read-only: no code edit, no merge, no deployment, no Done transition, no
~/.pi/agent/extensionswrite, no live OpenBao secret read into this session. Reviewer identity was taken from the sanctioned OpenBao reviewer path only (never the operator login, neverpi-landing),GET /useron it answeredld-reviewerbefore anything was posted, and the minted OpenBao token was revoked with the revoke verified by HTTP status, not by parsing a JSON body.Verdict: APPROVED
model: omniroute/deepseek-flash
head:
05e8b72851Fresh panel seat on
Homelab/pi-extensionsPR #82, exact head05e8b72. I re-fetched the branch head before writing this (it has not moved from05e8b728519c9bdda5b5f4450faf4a8991e0776a), worked from a clean worktree of that exact commit, and derived everything below from the tree rather than from the PR body, the worker log, or any earlier review on this PR. The earlier reviews are not cited, edited, or reused as this verdict.What the diff contains: five files, +1233/−79 —
forgejo/api.ts,forgejo/index.ts,forgejo/types.ts,forgejo/tests/fake.ts, and a newforgejo/tests/landing-runtime-credential.test.ts.docs/forgejo.md,forgejo/landing.ts,verdict*.ts,package.json, and~/.pi/agent/extensionsare all untouched, andgit diff --checkis clean.Independent verification on that exact head:
npx tsc --noEmitexits 0 with no diagnostics;npx vitest runreports 15 files / 410 tests passed, with 20 tests in the new file. CI run #196 on05e8b72851shows 14/14 jobssuccess, includingforgejo,forgejo-ci, andpr-title.The property the ticket exists to guarantee holds by construction, not by convention:
resolveLandingCredentialAtRuntimereturns early unless the answer isconfigured-token, and after that point every branch either returnssource: "openbao"or a refusal, so there is no reachable path that hands the merge write the shared operator credential.resolveLandingCredentialis no longer imported byindex.tsat all (only the runtime variant is), which means the substitution GITS-359 described was removed rather than left available beside the new path.No secret reaches argv or a child environment: the only child process in the production diff is a single
execFile(cli, ["kv", "get", "-format=json", path], …)whose fourth argv slot is a KV path, and the child env is an explicit allowlist ofPATH,HOME,BAO_NAMESPACE,VAULT_NAMESPACEplusBAO_ADDR/VAULT_ADDR, which are server URLs. The secret exists only as a local variable after stdout is parsed. There is noshell: trueanywhere inapi.tsorindex.ts, so the configurable path and CLI are not an injection surface, and a grep of the two production files finds noconsole.*,process.stdout, orprocess.stderrwrite at all.Failure reasons are scrubbed by construction:
scrubSecretShapescollapses any 32-plus-character token-shaped run to<redacted>and truncates to 300 characters, and it is applied both to the quoted CLI stderr line and to the final refusal.parseOpenBaoPayloaderrors carry the path only.The staging control is honest about being a control:
materialiseLandingTokendoesmkdtempSync→chmodSync(dir, 0o700)→writeFileSync(…, {mode: 0o600})→chmodSync(file, 0o600)(the explicit chmod defeats umask) →statSyncon both, and on any mode mismatch it removes the directory and refuses instead of using it. The merge credential is then re-resolved throughresolveLandingCredentialagainst that staged file, so the token that authorises the write is the one read back off disk.Ordering is least-privilege:
assertLandingLockis awaited beforeresolveLandingCredentialAtRuntime, so a PR that cannot land never causes the sealed secret to be read, and the new suite proves it with a sentinel file the fake CLI touches when it runs.assertMergeIdentity(GET /user) sits inside the cleanuptry, immediately before the brandedmergeAsLandingwrite, using the same credential — the new suite asserts theGET /userindex is strictly before the merge POST index and that the merge POST carries the OpenBao token rather than the operator token.Cleanup is on every exit path: the staged credential is disposed in a
finally, so success, wrong identity, and merge-write failure all remove theforgejo-landing-*directory, and each of those three outcomes is asserted against a sandboxedTMPDIR. Disposal is idempotent and aprocess.once("exit")drain covers a throw past thefinally.The refusal evidence is read-only and honest:
listBranchProtectionsis aGET repos/{owner}/{repo}/branch_protections, anddescribeMergeWhitelistreports an unreadable list as "could not be read" rather than as an empty allow-list, and a disabled whitelist as DISABLED. The test suite drives a route-table fetch fake and a local 0700/bin/shfake CLI, andtests/fake.tsinstalls an inert helper at import, so no suite in this package can reach the livekv/pi/forgejo-landingor the network; I read no live credential and read no live secret.I confirm the four findings carried forward are real and non-blocking and I add no blocker of my own: F1 (the PR body still describes the pre-
05e8b72world with no default address and reports 406/16 rather than the actual 410/20), F2 (the default address means an ambient$HOME/.vault-tokenis presented to the sanctioned host, and proxy/CA variables are not forwarded, so affected hosts fail closed), F3 (details.runtimeCredential.removed: trueis a prediction set before thefinally, andremoveRuntimeDiralso drops the directory from the exit-drain set even whenrmSyncfailed, so a failed removal would be misreported and not retried), and F4 (no runtime-specific regression test for a non-2xxGET /user, though that branch is covered inmerge-identity.test.ts). All four are behaviours I traced in the code, not claims I took on trust.No blocker open.
Read-only: no code edit, no merge, no deployment, no Done transition, no
~/.pi/agent/extensionswrite, no live OpenBao secret read into this session. Reviewer identity was taken from the sanctioned OpenBao reviewer path only (never the operator login, neverpi-landing),GET /useron it answeredld-reviewerbefore anything was posted, and the minted OpenBao token was revoked with the revoke verified by HTTP status, not by parsing a JSON body.