Split out of #228 / PR #237, where the same report-vs-read asymmetry was found and closed twice — first for the age identity, then for the age vault. This is the third instance, one level up, and it is the config file itself.
Measured
Binary built at 38a3803, a 0600 FIFO at ~/.agentsync/agentsync.toml, neutral cwd:
| command |
result |
check |
rc=1, refuses cleanly: agentsync home … has an agentsync.toml that is not a regular file |
secret list |
rc=124 — hangs, zero output |
status |
rc=124 — hangs, zero output |
doctor |
rc=124 — hangs after printing its header |
Verbatim the symptom PR #237 closed for the two age paths.
Why
probeSourceInit (internal/cli/sourceinit.go) gates the shape of agentsync.toml and is run by check. The surfaces that actually read the file do not go through it:
loadSecretsConfig — internal/cli/secrets.go
source.loadConfig — internal/source/loader.go
Both reach for os.ReadFile, and os.ReadFile blocks in the open on a FIFO with no writer, so no error path below it ever runs. A directory or socket fails rather than hangs; the FIFO is the sharp shape.
Note this is not a regression from PR #237 — at base e452f7d check hung too. That PR fixed the report surface and left the reading surfaces as they were.
Why it was deferred rather than fixed in PR #237
Scope: the read sites include internal/source, which that PR does not otherwise touch, and its subject is the [secrets] block. The known-exception note in internal/cli/sourceinit_guard_internal_test.go has been updated to record the measured gap and to retract the reason it previously gave for declining — that folding the check in "would mean stat-ing a file it is about to read". Both secrets.CheckIdentityPermissions and secrets.openVault now do exactly that, deliberately, because a stat is the only way to learn a path's shape without an open that may never return. The remaining objection is scope, not principle.
Suggested fix
Apply the shape rule at the point of the read, in the shape PR #237 settled on: prefer a helper that returns the data or the handle over a separate Check* predicate the callers must remember to call first, so the bytes cannot be obtained without passing the gate. secrets.openVault / secrets.ReadVault (internal/secrets/age.go) are the precedent. A stat failure should keep falling through to the read so a genuinely missing agentsync.toml still gets its existing "run agentsync init" message rather than a shape error.
Test note
Nothing currently tests sourceInitConfigNotFile, and there is no Mkfifo anywhere in internal/cli's tests. A regression test here must be timeout-bounded — this class of bug hangs rather than fails, so a plain call wedges CI with no diagnostic. See TestVaultShapeGate (internal/secrets/age_fifo_unix_test.go) and TestWriteSecretsVerifiedSurvivesNonRegularVault (internal/cli/secrets_vault_shape_unix_internal_test.go) for the pattern.
Split out of #228 / PR #237, where the same report-vs-read asymmetry was found and closed twice — first for the age identity, then for the age vault. This is the third instance, one level up, and it is the config file itself.
Measured
Binary built at
38a3803, a0600FIFO at~/.agentsync/agentsync.toml, neutral cwd:checkagentsync home … has an agentsync.toml that is not a regular filesecret liststatusdoctorVerbatim the symptom PR #237 closed for the two age paths.
Why
probeSourceInit(internal/cli/sourceinit.go) gates the shape ofagentsync.tomland is run bycheck. The surfaces that actually read the file do not go through it:loadSecretsConfig—internal/cli/secrets.gosource.loadConfig—internal/source/loader.goBoth reach for
os.ReadFile, andos.ReadFileblocks in the open on a FIFO with no writer, so no error path below it ever runs. A directory or socket fails rather than hangs; the FIFO is the sharp shape.Note this is not a regression from PR #237 — at base
e452f7dcheckhung too. That PR fixed the report surface and left the reading surfaces as they were.Why it was deferred rather than fixed in PR #237
Scope: the read sites include
internal/source, which that PR does not otherwise touch, and its subject is the[secrets]block. The known-exception note ininternal/cli/sourceinit_guard_internal_test.gohas been updated to record the measured gap and to retract the reason it previously gave for declining — that folding the check in "would mean stat-ing a file it is about to read". Bothsecrets.CheckIdentityPermissionsandsecrets.openVaultnow do exactly that, deliberately, because a stat is the only way to learn a path's shape without an open that may never return. The remaining objection is scope, not principle.Suggested fix
Apply the shape rule at the point of the read, in the shape PR #237 settled on: prefer a helper that returns the data or the handle over a separate
Check*predicate the callers must remember to call first, so the bytes cannot be obtained without passing the gate.secrets.openVault/secrets.ReadVault(internal/secrets/age.go) are the precedent. A stat failure should keep falling through to the read so a genuinely missingagentsync.tomlstill gets its existing "runagentsync init" message rather than a shape error.Test note
Nothing currently tests
sourceInitConfigNotFile, and there is noMkfifoanywhere ininternal/cli's tests. A regression test here must be timeout-bounded — this class of bug hangs rather than fails, so a plain call wedges CI with no diagnostic. SeeTestVaultShapeGate(internal/secrets/age_fifo_unix_test.go) andTestWriteSecretsVerifiedSurvivesNonRegularVault(internal/cli/secrets_vault_shape_unix_internal_test.go) for the pattern.