Documentation
¶
Overview ¶
Package secret holds the on-disk primitives the credential store is built from. The ground rule across the package is that every path that decides whether a write may happen is reached exactly once: locks are taken on inodes that are never unlinked, directories that hold them are opened without following symbolic links, and any error that is not plain contention fails closed, because the alternative is two processes rotating one refresh chain.
Index ¶
- Constants
- Variables
- func AuditLogPath(paths *config.Paths) string
- func BusyRefusal(holderAlive bool, stoppedPIDs []int32) error
- func CreateDirUnder(anchor, dir string) (int, error)
- func CurrentAccount() string
- func Digest8(digest string) (string, bool)
- func EnsureLockedMemoryBudget() error
- func ForEachAuditLineAt(dir int, name, shown string, maxLineBytes int, visit func(line string)) (bool, error)
- func ForeignServiceName(sha8 string) string
- func HeldLocksDir(paths *config.Paths) string
- func ListStrayAdoptedTmp(nsDir string) ([]string, error)
- func ListStrayTmp(nsDir string) ([]string, error)
- func LockRefusal(err error) error
- func OpenAuditLog(paths *config.Paths, shown string) (*os.File, error)
- func OpenAuditLogAt(dir int, name, shown string) (*os.File, error)
- func OpenDirUnder(anchor, dir string) (int, error)
- func OpenNamespaceDir(paths *config.Paths, nsDir string) (int, error)
- func Purge()
- func ReadAuditLogAt(dir int, name, shown string) (string, bool, error)
- func RemoveAdoptedFile(paths *config.Paths, nsDir string) (bool, error)
- func RemoveCredentialsFile(paths *config.Paths, nsDir string) (bool, error)
- func RemoveDirUnder(anchor, path string) error
- func RemoveDirUnderRoot(paths *config.Paths, path string) error
- func RemoveNamespace(paths *config.Paths, nsDir string) error
- func ResolvePendingWith(dir int, shown string, spec *PendingSpec, foreignTakenOver bool, ...) (PendingDecision, *PendingWrite, error)
- func SampleHolderAcrossInterval(ctx context.Context, at LockSlot, interval time.Duration, clock Clock, ...) bool
- func WriteAuditLine(file *os.File, shown, line string) error
- type AcquireError
- type Acquisition
- type AuditEntry
- type AuditEvent
- type AuditID
- type AuditLogState
- type AuditLogStateKind
- type AuditTail
- type BreakAttempt
- type BreakDecision
- type BreakDraft
- type BreakOutcome
- type BreakReason
- type BudgetExceededError
- type CleanupRegistry
- type Clock
- type CompromisedError
- type ConfigHold
- type ConfigLockError
- type ConfigLockErrorKind
- type ConfigOutcome
- type ConfigReason
- type ConfigWriteRecord
- type Digests
- type DisabledReader
- type FileSnapshot
- func CommitStaged(paths *config.Paths, s *StagedAdoption) (FileSnapshot, error)
- func Snapshot(path string) (*FileSnapshot, error)
- func SnapshotFollowing(path string) (*FileSnapshot, error)
- func WriteAdopted(ctx context.Context, paths *config.Paths, nsDir string, blobJSON *Secret) (FileSnapshot, error)
- type ForeignActivity
- type ForeignActivityKind
- type HeldLockFile
- type HeldLockRecord
- type HeldLocks
- func (h *HeldLocks) DriftCheck() error
- func (h *HeldLocks) HoldElapsed() time.Duration
- func (h *HeldLocks) Paths() []string
- func (h *HeldLocks) RecordPath() string
- func (h *HeldLocks) Release()
- func (h *HeldLocks) Slot(path string) (LockSlot, bool)
- func (h *HeldLocks) StoreDir() string
- func (h *HeldLocks) Tree() Tree
- func (h *HeldLocks) WriteAdmission() error
- type HolderEvidence
- type HolderSightings
- type HolderUnreadableError
- type IncomingIdentity
- type KeychainError
- type KeychainState
- type KeychainStatus
- type KeychainStdinLine
- func (l *KeychainStdinLine) Account() string
- func (l *KeychainStdinLine) Format(state fmt.State, _ rune)
- func (l *KeychainStdinLine) GoString() string
- func (l *KeychainStdinLine) Len() int
- func (l *KeychainStdinLine) LogValue() slog.Value
- func (KeychainStdinLine) MarshalJSON() ([]byte, error)
- func (KeychainStdinLine) MarshalJSONTo(encoder *jsontext.Encoder) error
- func (l *KeychainStdinLine) Service() string
- func (l *KeychainStdinLine) String() string
- func (l *KeychainStdinLine) WriteTo(sink io.Writer) (int64, error)
- type KeychainTargetMismatchError
- type KeychainWriteOutcome
- type KeychainWriter
- type LineTooLongError
- type LiveStoreEnv
- type Location
- type LocationKind
- type LockAnchor
- type LockBody
- type LockBreakRecord
- type LockFS
- type LockGuard
- type LockIOError
- type LockProfile
- type LockSample
- type LockSlot
- type LockSubject
- type LockUnavailableError
- type NotEmptyError
- type NotRegularError
- type Outcome
- type OutcomeKind
- type OutsideRootError
- type OwnedMeta
- type PendingCredential
- type PendingDecision
- type PendingDecisionKind
- type PendingDiscardReason
- type PendingSpec
- type PendingWrite
- type ProcHolders
- type ReadOutcome
- type Reader
- type RealFS
- type RefusedSymlinkError
- type Seams
- type Secret
- func (s *Secret) AppendPlaintextTo(dst []byte) ([]byte, error)
- func (Secret) AppendText(dst []byte) ([]byte, error)
- func (s *Secret) Clone() *Secret
- func (s *Secret) Digest() (string, error)
- func (s *Secret) Digest8() (string, error)
- func (s *Secret) Equal(other *Secret) (bool, error)
- func (Secret) Format(state fmt.State, _ rune)
- func (Secret) GoString() string
- func (s *Secret) Len() int
- func (Secret) LogValue() slog.Value
- func (Secret) MarshalJSON() ([]byte, error)
- func (Secret) MarshalJSONTo(encoder *jsontext.Encoder) error
- func (Secret) MarshalText() ([]byte, error)
- func (Secret) String() string
- func (s *Secret) WithPlaintext(fn func([]byte) error) error
- type SecretFile
- func (f *SecretFile) Park(ctx context.Context, doc []byte, spec PendingSpec) (WriteOutcome, error)
- func (f *SecretFile) Read(limit int64) (ReadOutcome, error)
- func (f *SecretFile) ReadSecret(limit int64) (*Secret, *FileSnapshot, error)
- func (f *SecretFile) ReadStrict(limit int64) (ReadOutcome, error)
- func (f *SecretFile) Remove() (bool, error)
- func (f *SecretFile) Write(ctx context.Context, doc []byte, pending *PendingSpec, stop StopPolicy) (WriteOutcome, error)
- func (f *SecretFile) WriteWithFaults(ctx context.Context, doc []byte, pending *PendingSpec, stop StopPolicy, ...) (WriteOutcome, error)
- type ServiceEntry
- type StagedAdoption
- type StderrClass
- type StopPolicy
- type SymlinkRefusedError
- type Target
- type TooLargeError
- type Tree
- type UndoKind
- type Undoable
- type UnquotableError
- type UnreachableError
- type UnreadableAuditLine
- type UnrecognizedEvent
- type WriteCancelledError
- type WriteDirection
- type WriteEvent
- type WriteFaultNames
- type WriteOutcome
- type WriteRequest
- type WriteWindowClosedError
- type WrongTreeError
Constants ¶
const ( // FaultLockStale makes a fresh lock eligible for the break rule. FaultLockStale = fault.LockStale // FaultLockContended makes the primary lock's first mkdir report a // holder. FaultLockContended = fault.LockContended // FaultLockResumeAfterSampleB touches the sampled lock in exactly // the window the third sample exists to close. FaultLockResumeAfterSampleB = fault.LockResumeAfterSampleB // FaultSwapLockLeak makes a release leave the directories and the // record behind, the way a crashed hold would. FaultSwapLockLeak = fault.SwapLockLeak )
The injected fault names the testing build's fault seam may carry. Production code never reads an environment variable here: the hook is injected through Seams.
const AdoptedFile = ".credentials.adopted.json"
AdoptedFile is where a hot-swap parks the credential it displaced.
Deliberately not CredentialsFile: the vendor's composed store falls through to that name on a read failure or a throttle — not only on an absent item — so a displaced credential parked there would be served to the peer session whenever its keychain read hiccuped, silently undoing the swap the user asked for. The session reads exactly one file name, so any other name in the same directory is invisible to it. It is adopt-only: nothing composes it, ReadCredentials does not look at it, and it is never replayed into CredentialsFile the way PendingFile is.
const AuditLogFile = "keychain-writes.jsonl"
AuditLogFile is the append-only audit log's file name, under the namespace root.
A line in this file is the only evidence a crashed swap leaves behind: a keychain write has no rename to undo, and a lock artefact is a directory the kernel releases nothing on death. It is not a place secrets live: an entry carries digest prefixes — eight hex digits of a SHA-256 — and the append refuses anything else, so a caller that passed a whole digest or a token is a failed write rather than a leak.
const ClaudeProcessName = "claude"
ClaudeProcessName is the exact kernel command name the holder check sweeps for.
const ClockSkewTolerance = 1 * time.Second
ClockSkewTolerance is how far the wall clock and the monotonic clock may disagree across the sampling interval before a break is abandoned.
It catches a step in either direction: a backward jump makes the two deltas differ by their sum, and a forward jump inflates both ages and would otherwise pass undiagnosed.
const ConfigHoldBudget = 1200 * time.Millisecond
ConfigHoldBudget is the configuration lock's hold budget.
A different derivation from a different peer behaviour: a session inside its first 30 s abandons its own acquire ladder after 1500 ms and then writes the whole configuration document unlocked, which would erase a swap. 1200 ms leaves 300 ms under that floor.
const ContentionFloor = 7500 * time.Millisecond
ContentionFloor is the least total wait the contention schedule tops itself up to before deciding whether a holder is beating.
const ContentionRoundBase = 1 * time.Second
ContentionRoundBase is the fixed part of one contention round.
const ContentionRoundJitter = 1 * time.Second
ContentionRoundJitter is the span of the random part of one contention round, so each round waits base plus a uniform draw below this.
const ContentionRounds = 5
ContentionRounds is how many rounds the contention schedule waits for a held primary lock before it decides.
const CredentialsFile = ".credentials.json"
CredentialsFile is the credential file's name, chosen to match the shape a Claude Code session reads so the directory can serve one.
const DigestPrefixLen = 8
DigestPrefixLen is how many hex digits of a digest an entry may carry.
const DumpTimeout = 10 * time.Second
DumpTimeout is the budget for dump-keychain, which walks every item.
const HeldLocksDirName = "held-locks"
HeldLocksDirName is where the records live, under the namespace root.
const HoldBudget = 3000 * time.Millisecond
HoldBudget is the longest a credential-store hold may last, from the first mkdir to the last rmdir.
Derived, not chosen: the peer's scope-expansion helper gives up on its own refresh after 4000 ms at the floor, so the budget is 4000 ms minus 1000 ms of margin.
const KeychainClassUnsupported = errs.KeychainClass("unsupported on this platform")
KeychainClassUnsupported is the class carried by a platform with no keychain transport. The errs vocabulary carries it verbatim rather than naming it, because no data flow branches on it.
const KeychainLineLimit = 4032
KeychainLineLimit is the longest line `security -i` accepts, counting the trailing newline.
The blob's author compares the whole line, newline included, against this bound and falls back to putting the payload in argv when it is longer. agentctl has no such fallback: a line one byte over the limit is refused and nothing is spawned, because an argv fallback is what would put a refresh token into the process listing.
const KeychainVerifyTimeout = 800 * time.Millisecond
KeychainVerifyTimeout bounds the under-lock read before a keychain write.
const KeychainWriteTimeout = 1200 * time.Millisecond
KeychainWriteTimeout bounds the child while the caller holds the peer locks.
const LegacyLockSuffix = ".lock"
LegacyLockSuffix is the legacy lock's suffix: the peer creates `<resolved store dir>.lock` beside the store directory rather than inside it.
const LegacyStorageWriteArtefact = ".storage-write"
LegacyStorageWriteArtefact is an old artefact of this store's own earlier layout — never a peer mutex, and never a removal target.
const LiveKeychainService = "Claude Code-credentials"
LiveKeychainService is the keychain service name Claude Code stores the live credentials under; a namespaced item appends `-<sha8>`.
const LockCreateAttempts = lockfile.CreateAttempts
LockCreateAttempts is how many times opening a lock file re-looks before giving up while another process is creating the same lock file.
const MaxCredentialsBytes int64 = 1 << 20
MaxCredentialsBytes is the largest credential file that will be read.
const MaxHeldLockRecordBytes = 4096
MaxHeldLockRecordBytes is the largest record that will be parsed.
A record names one store and at most three directories, so anything larger is not one — and a report must not be turned into an unbounded read by a file somebody dropped in this directory.
const MaxMetaBytes int64 = 4096
MaxMetaBytes is the largest pending-metadata file that will be read.
const MaxRestarts = 3
MaxRestarts is how many times an EEXIST may send an acquisition back to the lock-free probe before it reports the store busy.
const NamespaceLockRetry = lockfile.RetryInterval
NamespaceLockRetry is how often a blocked acquisition retries.
const NamespaceLockWait = 5 * time.Second
NamespaceLockWait is the default wait for the interactive commands, which have no pass deadline of their own.
const PeerHeartbeat = 5 * time.Second
PeerHeartbeat is how often a live Claude Code holder refreshes a lock directory's modification time.
No heartbeat runs on this side: a hold bounded by HoldBudget can never reach the heartbeat period, so one could only ever be dead code pretending to be a safety net. The number still governs StaleSampleInterval's margin, which is why it stays a named constant.
const PendingFile = ".credentials.json.pending"
PendingFile is where credentials wait when a rename failed.
const PendingMetaFile = ".pending.meta"
PendingMetaFile records what the pending credentials were derived from.
const ReadTimeout = 2 * time.Second
ReadTimeout is the budget for show-keychain-info and find-generic-password. A keychain prompt nobody answers must not hang the process, so every call is bounded and an overrun is killed.
const RefreshLockName = ".oauth_refresh.lock"
RefreshLockName is Claude Code's primary refresh lock, inside the store directory.
const StaleSampleInterval = 12 * time.Second
StaleSampleInterval is how long the break rule waits between its first and second samples of a stale lock's modification time.
It must stay greater than 2 × PeerHeartbeat: 12 s is 2.4 heartbeat periods, which is the margin that makes a single heartbeat landing anywhere in the window fail the modification-time comparison. Never reduce it toward 10 s.
const StorageWriteLockName = ".storage-write.lock"
StorageWriteLockName is Claude Code's storage mutex directory: the caller supplies the base name and the locking library appends `.lock`.
Variables ¶
var ( // ErrKeychainLocked matches a read refused by a locked keychain. ErrKeychainLocked = &KeychainError{Class: errs.KeychainLocked, Transient: true} // all, including a security(1) that could not be started. ErrKeychainUnavailable = &KeychainError{Class: errs.KeychainUnavailable, Transient: true} // ErrKeychainTimeout matches a security(1) child that exceeded its // budget and was killed. ErrKeychainTimeout = &KeychainError{Class: errs.KeychainTimeout, Transient: true} // ErrItemNotFound matches a readable keychain that holds no item under // the requested service name — the normal "needs login" answer. ErrItemNotFound = &KeychainError{Class: errs.KeychainNotFound} // ErrKeychainUnsupported matches a platform with no keychain transport. ErrKeychainUnsupported = &KeychainError{Class: KeychainClassUnsupported} )
The classified keychain failures callers branch on, as errors.Is targets. Matching compares the class only, so a wrapped failure carrying extra detail still matches its named value.
var ErrLockBusy = lockfile.ErrLockBusy
ErrLockBusy reports that somebody else holds the lock and the deadline passed while waiting. The caller must re-read before deciding anything: losing the race usually means the winner just refreshed what this process wanted to write.
var ErrLockExists = errors.New("the lock directory already exists")
ErrLockExists reports that somebody else holds a lock: the mkdir found the directory already there.
var ErrLockGone = errors.New("the lock directory is not there")
ErrLockGone reports that a lock artefact is not there: the rmdir or stat found nothing, or a mkdir found its parent missing.
Functions ¶
func AuditLogPath ¶
AuditLogPath is where the log lives for one store.
func BusyRefusal ¶
BusyRefusal maps a busy store onto the exit vocabulary: the locks are held and were not broken.
func CreateDirUnder ¶
CreateDirUnder is OpenDirUnder, but creating the directory when it is not there.
The anchor itself is never created: every component below it is, so a walk that still ends early can only mean the anchor is absent — and creating that by path is the step this function exists to replace. The caller closes the descriptor.
func CurrentAccount ¶
func CurrentAccount() string
CurrentAccount is the account attribute the vendor tooling stores its items under: $USER, with LOGNAME as the fallback. An empty account still produces a well-formed find-generic-password call, which simply finds nothing.
func Digest8 ¶
Digest8 takes the first DigestPrefixLen hex digits of a digest for an entry. ok is false when the digest is shorter than that, or when its prefix is not lowercase hex — which means the caller has something other than a digest in its hand and should not be writing it to a file either way.
func EnsureLockedMemoryBudget ¶
func EnsureLockedMemoryBudget() error
EnsureLockedMemoryBudget rejects a soft RLIMIT_MEMLOCK below the startup budget, before any secret is sealed or opened. Unlimited limits pass. A low limit returns an errs.ConfigError (fatal exit status 1), because a memguard allocation failure can deadlock while purging rather than return an error. Failure to read the limit returns an errs.IOError. This check does not raise limits or reserve memory; callers must still bound concurrent opens and account for payloads larger than one page.
func ForEachAuditLineAt ¶
func ForEachAuditLineAt(dir int, name, shown string, maxLineBytes int, visit func(line string)) (bool, error)
ForEachAuditLineAt hands the caller one line at a time instead of the whole file, reporting whether a log was there at all.
A caller that must consult every line — which provider state a refused login left behind, a thousand writes back — would otherwise turn an append-only log into a whole-file allocation that grows with the user's history; this reads through a buffer, so the memory it needs is the bound plus the buffer, whatever the log's size. A line longer than maxLineBytes, and a line that is not UTF-8, is skipped rather than refused: neither is a line this store wrote, and a reader asked to find this store's own entries must not be stopped by somebody else's. The bound is the caller's, because only the caller knows how long its own entries are.
func ForeignServiceName ¶
ForeignServiceName returns the keychain service name a namespace's credentials migrate into, by the eight hex digits of its suffix.
One spelling for the two readers that must agree: the detection below decides which listed entries are this namespace's, and the unlisted status mode names the items to ask about — two formats of one shape would be a listing that silently matched nothing.
func HeldLocksDir ¶
HeldLocksDir returns the directory holding one store's held-lock records.
func ListStrayAdoptedTmp ¶
ListStrayAdoptedTmp lists leftover `.credentials.adopted.json.tmp.<8 hex>` files: a crashed adoption, handled by the same callers that handle the credential writer's strays.
func ListStrayTmp ¶
ListStrayTmp lists leftover `.credentials.json.tmp.<8 hex>` files. A stray one is a crashed write and it holds token material at rest, so doctor reports them and login and removal clean them up.
func LockRefusal ¶
LockRefusal maps a lock failure onto the exit vocabulary.
A compromised hold — drifted or unreadable held modification time, or a hold past its budget — is the lettered refusal; an unreachable store is fatal configuration; a closed write window discards; a cancellation passes through for the signal exit; and every other acquisition failure carries the refusal code with its own words.
func OpenAuditLog ¶
OpenAuditLog opens the log for one append, refusing a link at its name and a mode that is not 0600.
The log lives in the directory the held-lock records are kept in, so whoever could plant that name could plant this one too — and this file is the only durable evidence a broken lock leaves. The directory is resolved once, one no-follow component at a time from the configuration directory down, and the log opened relative to the descriptor that walk produced. The mode is checked on the open descriptor, not on the name, so there is no window between the check and the write; it is refused, never repaired. A live-store swap gates on this descriptor rather than on AuditLogStateOf: a report leaves the whole width between the look and the write to whoever can plant a name here, a held descriptor leaves none.
func OpenAuditLogAt ¶
OpenAuditLogAt is OpenAuditLog's open, relative to a directory the caller's own no-follow walk produced, for a log with any name. Every rule is the same, because it is that function's body; another provider's log reaches its own directory and opens its own name through here. name must be one plain path component; shown is the log as the user would recognise it, used only in the error sentences.
func OpenDirUnder ¶
OpenDirUnder opens one existing directory below anchor without ever following a symbolic link, for a caller that then operates relative to the descriptor. Resolving the way to a directory once — and then operating relative to what the walk produced — is what stops a link planted at a component from redirecting the whole protocol. The caller closes the descriptor.
func OpenNamespaceDir ¶
OpenNamespaceDir opens a namespace directory without ever following a symbolic link, creating missing components at 0700.
The walk starts at the namespace root — after that directory and the configuration directory above it have been created — and descends one component at a time. The returned descriptor is what every subsequent operation on the namespace is performed relative to, so no later operation re-resolves a path that could have changed underneath it. The caller closes it.
func Purge ¶
func Purge()
Purge destroys every open plaintext buffer and the session key, so no previously created Secret can be decrypted again. It is idempotent and safe to call more than once after all plaintext users have stopped. Callers must stop and join those users before purging: destroying a buffer while its callback is running can fault on the callback's next access.
It must run on every process exit path — the normal return from the command tree, the error return, and the signal path before the deferred exit — because locked plaintext pages are otherwise released to the kernel unwiped. This package deliberately never calls memguard.CatchInterrupt or memguard.CatchSignal: both call signal.Reset, which would silently remove the program's own handlers, and both terminate with their own exit status. The program's signal handler owns signal disposition and calls Purge itself. Code review must keep those two calls out of the whole module; the test for this package asserts the default signal disposition survives importing it.
func ReadAuditLogAt ¶
ReadAuditLogAt reads a log's whole text relative to a directory the caller's own no-follow walk produced. The reader refuses what the writer refuses — an undo acts on what a tail returns, so a log somebody else could redirect would be an undo somebody else could direct — but it does not apply the mode rule: a log at the wrong mode is one doctor must still be able to report, and reading it discloses nothing its mode has not already disclosed. present is false when nothing is there.
func RemoveAdoptedFile ¶
RemoveAdoptedFile removes the redundant adopted copy after a confirmed live undo, returning whether the file existed. The caller must hold the namespace lock and verify that its own store still holds the same credential.
func RemoveCredentialsFile ¶
RemoveCredentialsFile removes the namespace's plaintext CredentialsFile, if it is there, returning whether one was removed.
The one caller is a swap whose store had not migrated: once the keychain item demonstrably holds the incoming credential this name is a second copy of the displaced one rather than its home, and the vendor's composed read would hand it to the peer session on nothing worse than a keychain hiccup. The directory is reached through the no-follow walk and the name unlinked relative to that descriptor.
func RemoveDirUnder ¶
RemoveDirUnder is RemoveDirUnderRoot with the no-follow walk anchored elsewhere, for the one removal outside the namespace root: a lock directory a crashed process left in the store it was holding, where the anchor is that store directory's parent, leaving both components the record cannot vouch for to be walked and refused here.
Nothing recurses. AT_REMOVEDIR fails on a directory with anything inside it, and that is reported: a lock directory holding a file is not the empty artefact a lapsed lock leaves. It also refuses anything that is not a directory, so a regular file or a symbolic link at the final component is reported rather than deleted.
func RemoveDirUnderRoot ¶
RemoveDirUnderRoot removes one empty directory under the namespace root, resolving the way to it without ever following a symbolic link.
The lexical containment check compares spellings and cannot see a link planted at a component: a path can spell a location under the root while naming a live store in the user's home directory. So the parent is walked down from the root one no-follow component at a time and the artefact removed relative to the descriptor that walk produced.
func RemoveNamespace ¶
RemoveNamespace removes a namespace's files and then its directories.
The lock file is untouched: it lives outside the namespace, is never unlinked, and removing it would break flock's inode semantics for whoever is waiting on it. Deleting through a symbolic link is the same escape as writing through one, so a link anywhere along the chain is refused.
func ResolvePendingWith ¶
func ResolvePendingWith(dir int, shown string, spec *PendingSpec, foreignTakenOver bool, cred PendingCredential) (PendingDecision, *PendingWrite, error)
ResolvePendingWith applies the pending decision table to one namespace directory.
The table, in check order — validity first, then foreign activity, then the digest comparison:
| on disk | decision | |--------------------------------------------------------|-------------------------| | no pending file (a lone meta is removed) | nothing pending | | pending or meta unreadable, a link, big, or unparsable | discarded, invalid | | the namespace was taken over by somebody else | discarded, taken over | | target present, digests equal the meta's derived ones | replayed | | target present, digests differ | discarded, file changed | | target absent, meta has no derived digests | replayed, first write | | target absent, meta has derived digests | discarded, file removed |
dir is a descriptor the caller's no-follow walk produced, and every operation is relative to it. shown is that directory as the user would recognise it, used only in error sentences. foreignTakenOver is the caller's own judgement of whether somebody else now manages the namespace; how it is reached differs by vendor, and the table only needs the answer. Errors are returned only for failures that leave the namespace in an unknown state — an unreadable target, or a replay rename that failed; every decidable outcome, including a corrupt or hostile pending file, is a PendingDecision.
func SampleHolderAcrossInterval ¶
func SampleHolderAcrossInterval(ctx context.Context, at LockSlot, interval time.Duration, clock Clock, fs LockFS) bool
SampleHolderAcrossInterval is the two-sample variant of the stale proof, for a consumer that reports and removes through its own permit: two stats one interval apart, and only an unchanged modification time across the whole interval reads as "nothing is holding this". A changed time, a vanished artefact, an unreadable stat and a cancelled wait all read as held, because none of them proves absence.
func WriteAuditLine ¶
WriteAuditLine lands one line with one write and one fsync, because the point of the record is to survive the crash that happens next. Exported so another provider's log appends its own entry type through the same one-write-one-flush rule rather than a copy of it.
Types ¶
type AcquireError ¶
type AcquireError struct {
// Err is why the acquisition failed.
Err error
// BreakRecord is the single break this acquisition completed before
// failing, if any. The caller owes it to the audit log before it
// maps the error to anything.
BreakRecord *BreakDraft
}
AcquireError is an acquisition failure, carrying the one break it may already have performed.
The break rule removes a peer's lock directory at its third sample and hands back a draft record; every way out of the acquisition after that moment owes that draft to the audit log. The draft is carried rather than dropped so no failure path can remove another process's lock with nothing durable to say so.
func (*AcquireError) Error ¶
func (e *AcquireError) Error() string
Error implements the error interface.
func (*AcquireError) Unwrap ¶
func (e *AcquireError) Unwrap() error
Unwrap exposes the underlying failure for errors.Is and errors.As.
type Acquisition ¶
type Acquisition struct {
// Held is the live hold, or nil when the store is busy.
Held *HeldLocks
// HolderAlive says whether the primary lock's modification time
// moved during the contention schedule, for a busy answer.
HolderAlive bool
// StoppedPIDs is the stopped `claude` process ids, for the terminal
// message only — never recorded.
StoppedPIDs []int32
// BreakRecord is the single break this acquisition attempted, if
// any.
BreakRecord *BreakDraft
}
Acquisition is what an acquisition came back with: the hold, or a busy answer, and the one break it may have performed either way.
The break record travels back to the caller instead of being appended here, so that no code between the rule's last sample and its removal can grow an I/O call; the caller appends it through the audit log.
func AcquirePeerLocks ¶
func AcquirePeerLocks(ctx context.Context, subject LockSubject, paths *config.Paths, live *LiveStoreEnv, seams *Seams) (*Acquisition, error)
AcquirePeerLocks takes all three credential-store locks, or reports the store busy.
In order, and each wait with nothing held:
- With nothing held, stat all three; for any that exists and is stale by its own profile, run the full break rule — including its sampling wait. At most one break per acquisition, so the protocol cannot loop against a peer that recreates a lock.
- With nothing held, wait out a primary that is present and not stale on the peer's own contention schedule. If it is released during the schedule the acquisition proceeds; if it is still there, the store is busy.
- Write the held-lock record before the first mkdir.
- Mkdir the three in the peer's nesting. A holder found at any position releases everything already taken, clears the record, and restarts the stale checks — never a wait or a sampling window while holding anything. At most MaxRestarts restarts.
Every failure is an *AcquireError whose break record, if any, the caller owes to the audit log before mapping the error to anything.
func AcquirePeerLocksWith ¶
func AcquirePeerLocksWith(ctx context.Context, anchor *LockAnchor, paths *config.Paths, seams *Seams) (*Acquisition, error)
AcquirePeerLocksWith is AcquirePeerLocks over an already-opened anchor. It owns the anchor: every return path either hands it to the hold or gives its descriptors back.
func (*Acquisition) Busy ¶
func (a *Acquisition) Busy() bool
Busy reports that the locks were not taken.
type AuditEntry ¶
type AuditEntry struct {
// TS is when this process wrote the entry, RFC 3339 in UTC. Kept as
// the string the line carries, so a round trip is byte-stable.
TS string
// MonotonicMS is milliseconds on this process's monotonic clock, for
// the reason [auditProcessStart] gives.
MonotonicMS uint64
// PID is this process's own id — never a holder's.
PID uint32
// Event is what happened.
Event AuditEvent
}
AuditEntry is one line of the log. The provenance members — the timestamp, the monotonic reading, the writing process — come first and belong to this process, so the pid can never be read as the pid of somebody else's lock holder.
func NewAuditEntry ¶
func NewAuditEntry(event AuditEvent) AuditEntry
NewAuditEntry stamps an event with now, this process's monotonic reading, and this process's id.
func (AuditEntry) ID ¶
func (e AuditEntry) ID() AuditID
ID is this entry's identity: its timestamp and the process that wrote it.
type AuditEvent ¶
type AuditEvent interface {
// contains filtered or unexported methods
}
AuditEvent is one kind of event the log records. The set is closed: outside packages construct the exported event types but cannot add one, so every reader's dispatch stays complete.
type AuditID ¶
type AuditID struct {
// TS is the entry's timestamp.
TS string
// PID is the process that wrote it.
PID uint32
}
AuditID names one entry, so a command can print it and a later doctor or undo can find the same line.
func AuditAppend ¶
AuditAppend appends one entry and returns its identity.
The file is created 0600 if it is absent, opened O_APPEND through the same no-follow walk every store mutation uses, written with a single write and fsynced before this returns — because the whole point of the record is to survive the crash that happens next. A refused entry — a digest field that is not exactly eight lowercase hex digits, a backup that is not a bare file name — never creates the log on its way to being refused; so does a refused log: a link at its name or on the way to it, something that is not a regular file, or a mode that is not 0600, which is refused rather than repaired because a chmod would erase the evidence that somebody else can read this machine's swap history.
func AuditAppendThrough ¶
AuditAppendThrough appends one entry through a descriptor the caller is already holding.
A live swap gates on OpenAuditLog before it writes anything — a held descriptor, not a report — and keeps it across the write, so the entry that records the write is appended through the same file the gate proved appendable. Nothing between the two can redirect the log: a planted link or a chmod after the open changes the name, and this writes to the descriptor. Serialization and the digest checks are AuditAppend's own, shared rather than repeated. shown is for the error sentences only; nothing is resolved through it.
type AuditLogState ¶
type AuditLogState struct {
// Kind classifies what is there.
Kind AuditLogStateKind
// Mode carries the permission bits for [AuditLogWrongMode].
Mode fs.FileMode
// Why carries the refusal for [AuditLogRefused].
Why string
}
AuditLogState is the log's state seen through the same walk the append writes through. The vocabulary exists so the refusal and the report cannot drift: the append hands AuditLogState.Note's sentence to its caller, and doctor prints the same sentence. A wrong mode is a state, never a repair: the file belongs to whoever set it that way, and saying so is worth more than quietly making it look right.
func AuditLogStateOf ¶
func AuditLogStateOf(paths *config.Paths) AuditLogState
AuditLogStateOf is what is at the audit log's name, for doctor's report. It reads through the same walk the append uses, so what it reports is what the append would meet rather than what a second path resolution would find. A store with no namespace root at all has never appended.
func (AuditLogState) IsAppendable ¶
func (s AuditLogState) IsAppendable() bool
IsAppendable reports whether the append will write to what is at that name.
func (AuditLogState) Note ¶
func (s AuditLogState) Note() string
Note is the sentence doctor prints, and the reason a refused append carries.
type AuditLogStateKind ¶
type AuditLogStateKind int
AuditLogStateKind is what is at the log's name.
const ( // AuditLogAbsent means nothing is there: a store that has never // appended. AuditLogAbsent AuditLogStateKind = iota + 1 // AuditLogPresent means a regular file at mode 0600 — the one shape // the append writes to. AuditLogPresent // AuditLogWrongMode means a regular file whose permission bits are // something else. AuditLogWrongMode // AuditLogRefused means a symbolic link at the log's name, a link on // the way to it, or something that is not a regular file at all. AuditLogRefused )
type AuditTail ¶
type AuditTail struct {
// Entries are the entries that parsed, oldest first.
Entries []AuditEntry
// Unreadable names the lines in the window that did not.
Unreadable []UnreadableAuditLine
}
AuditTail is what one tail read found: the entries it could parse, and the lines it could not. Two lists rather than one failure, because the caller's job is to report this file and one damaged line must not take the report down with it: the append is one write plus an fsync, so a process killed between them leaves a truncated final line, which is by construction inside the last n — failing the whole read for it would break doctor and undo exactly after the crash they are the recovery for.
func TailAuditLog ¶
TailAuditLog returns the last n entries, oldest first, plus the lines in that window that could not be read.
An absent log is no entries rather than an error: a store that has never written a keychain item has nothing to explain. A line carrying unknown members is not unreadable — a log written by a later build still reads — and neither is a line whose event is a kind this build does not know: it reads as UnrecognizedEvent rather than blocking an undo that refuses on unreadable lines. The read goes through the same no-follow walk the append writes through.
type BreakAttempt ¶
type BreakAttempt struct {
// Decision says what happened.
Decision BreakDecision
// Reason says why the break was abandoned, for
// [DecisionAbandoned].
Reason BreakReason
// FailureMessage says what failed, for [DecisionFailed].
FailureMessage string
// Record is the draft to complete and append, or nil when there is
// nothing to record: a lock that vanished, a cancelled wait, or a
// failed operation.
Record *BreakDraft
}
BreakAttempt is a break attempt's decision and the draft record for it.
func ResolveStale ¶
func ResolveStale(ctx context.Context, subject LockSubject, at LockSlot, profile *LockProfile, seams *Seams) BreakAttempt
ResolveStale decides whether one lock directory may be removed, and removes it.
The only code in the module that removes a directory it did not create, so the predicate is written out rather than left to a reader, and it runs only with nothing held. All conditions must hold in order and any failure abandons the break:
- The first stat succeeds — sample A — and the lock is at least profile.Stale old.
- Holder evidence: any same-user `claude` in a stopped state abandons the break; unprovable peer visibility refuses it. Evidence that could not be gathered continues on modification times alone — it is never read as "no stopped holder".
- Wait StaleSampleInterval, cancellably.
- Sample B: the stat succeeds and the modification time equals A's exactly — nanoseconds, never a tolerance.
- The lock is still at least profile.Stale old, and the wall and monotonic clocks agree to within ClockSkewTolerance. The clock check comes before the age check because a wall clock that stepped backwards makes the second age smaller: an age test placed first would call a clock jump "too young", the wrong diagnosis.
- Sample C, immediately before the removal, with no I/O of any kind in between — no log line, no record append.
Then the removal. Whether the lock is recreated afterwards is the acquisition's business; a recreation there is "retaken" and ends the acquisition. Known and accepted: a heartbeat landing just after sample C is unobservable to any sampling rule; the window is two system calls wide.
type BreakDecision ¶
type BreakDecision int
BreakDecision is what the break rule decided.
const ( // DecisionBroken means the directory was removed. DecisionBroken BreakDecision = iota // DecisionAbandoned means it was left alone, for the attempt's // reason. DecisionAbandoned // DecisionCancelled means the sampling wait was cancelled. Nothing // was sampled further and nothing was removed, so there is nothing // to record. DecisionCancelled // DecisionFailed means a filesystem operation failed. Nothing was // removed. DecisionFailed )
type BreakDraft ¶
type BreakDraft struct {
// Path is the lock directory, as the hold spells it.
Path string
// StoreDir is the store directory it guards.
StoreDir string
// Tree is which tree that store is in.
Tree Tree
// SampleA is the first sample.
SampleA LockSample
// SampleB is the sample one interval later, when the rule got that
// far.
SampleB *LockSample
// SampleC is the sample taken immediately before the removal.
SampleC *LockSample
// IntervalWallMS is how much wall-clock time passed between A and B.
IntervalWallMS uint64
// IntervalMonotonicMS is how much monotonic time passed between the
// same two samples. The two are compared, which is what catches a
// clock step in either direction.
IntervalMonotonicMS uint64
// Evidence is what the holder check found.
Evidence HolderEvidence
// Outcome says whether the directory was removed.
Outcome BreakOutcome
// Reason says why not, when it was not — and retaken when it was
// removed and immediately taken by somebody else. Empty for a clean
// break.
Reason BreakReason
}
BreakDraft is everything the break rule can observe about one break, waiting for the two fields only its caller knows.
The rule fills the members it can see; the swap supplies the service and target, which it alone knows; and Complete is the only way to get from one to the other, so no field can arrive at the log empty because a caller forgot it. The draft is returned rather than appended, which is what keeps any logging call out of the gap between the last sample and the removal.
func (*BreakDraft) Complete ¶
func (d *BreakDraft) Complete(service string, target Target) *LockBreakRecord
Complete fills in the caller's own two fields and hands back the record to append to the audit log.
The service is the keychain item the hold was for and the target says which item that is; both belong to the caller because the rule is given a lock artefact and nothing else.
type BreakOutcome ¶
type BreakOutcome string
BreakOutcome says whether a stale artefact was removed.
const ( // OutcomeBroken means the directory was removed. OutcomeBroken BreakOutcome = "broken" // OutcomeAbandoned means the rule refused and the directory was left // exactly as it was. OutcomeAbandoned BreakOutcome = "abandoned" )
type BreakReason ¶
type BreakReason string
BreakReason says why a break did not happen — or, for ReasonRetaken, what happened to the artefact after one did.
Every member is a reason not to have broken a lock, which is why a clean break carries no reason at all. There is deliberately no word meaning "it was stale", because staleness is the precondition of the whole rule rather than an outcome of it.
const ( // ReasonHeartbeatObserved means a sample's modification time // differed from the one before it: somebody is alive in there. ReasonHeartbeatObserved BreakReason = "heartbeat_observed" // ReasonTooYoung means the artefact was not yet stale. ReasonTooYoung BreakReason = "too_young" // ReasonVanished means the artefact disappeared between samples. ReasonVanished BreakReason = "vanished" // ReasonClockJump means the wall clock and the monotonic clock // disagreed by more than the tolerance. ReasonClockJump BreakReason = "clock_jump" // ReasonRetaken means the artefact was back after the removal: a // peer re-took it, and there is no second break. ReasonRetaken BreakReason = "retaken" // ReasonHolderStopped means a same-user `claude` process is stopped, // so the break was abandoned whether or not that process is the // holder. ReasonHolderStopped BreakReason = "holder_stopped" // ReasonHolderUnreadable means peer visibility could not be // established, so removal is unsupported. ReasonHolderUnreadable BreakReason = "holder_unreadable" )
type BudgetExceededError ¶
type BudgetExceededError struct {
// Elapsed is how long the hold had lasted when it was noticed.
Elapsed time.Duration
// Budget is the profile's budget.
Budget time.Duration
}
BudgetExceededError reports that a hold outlived its profile's budget, which would make the peer's own refresh give up and fail.
func (*BudgetExceededError) Error ¶
func (e *BudgetExceededError) Error() string
Error implements the error interface.
type CleanupRegistry ¶
type CleanupRegistry interface {
// Register adds a release to run on an emergency exit and returns
// its withdrawal, which reports whether the release was still
// registered — false means the emergency path already ran it.
Register(release func()) (unregister func() bool)
}
CleanupRegistry is where a hold registers its emergency release, so a signal exit still gives the peer's locks back. Injected rather than imported: the signal runtime owns the real registry and wires it in.
type Clock ¶
type Clock interface {
// Wall returns the wall clock, which can step in either direction.
// The reading carries no monotonic component, so arithmetic on it
// reflects wall time alone.
Wall() time.Time
// Monotonic returns the monotonic clock as the time since an
// arbitrary fixed origin. It cannot step.
Monotonic() time.Duration
// Sleep waits howLong or until ctx is done, and returns ctx.Err()
// when cancellation ended the wait early.
Sleep(ctx context.Context, howLong time.Duration) error
// Jitter returns a uniform draw in [0, span), and zero for a span
// that is not positive.
Jitter(span time.Duration) time.Duration
}
Clock is where the lock protocol's time comes from.
Injectable because the break rule's honesty rests on a 12-second wait and a comparison between two clocks, and a test that could not move either would have to take 12 s per case or assert something weaker than the rule. The jitter draw lives here too, so a test can pin every wait the contention schedules make.
type CompromisedError ¶
type CompromisedError struct {
// Path is the lock directory that moved.
Path string
}
CompromisedError reports that a lock held by this process had its modification time moved under it, so the protocol has already been violated and nothing may be written.
func (*CompromisedError) Error ¶
func (e *CompromisedError) Error() string
Error implements the error interface.
type ConfigHold ¶
type ConfigHold struct {
// contains filtered or unexported fields
}
ConfigHold is a configuration lock beside the literal configuration path. Release must be called when the caller finishes. The caller checks Elapsed against ConfigHoldBudget across its whole read/write sequence; DriftCheck checks only the lock's modification time.
func AcquireConfigLock ¶
AcquireConfigLock takes one lock beside configPath, not its symlink target. A held lock gets three waits of 200, 400 and 800 milliseconds, each with jitter below that rung. Stale locks are reported and never removed.
func TryConfigLock ¶
TryConfigLock makes exactly one nonblocking mkdir attempt. It never sleeps or retries a vanished lock; callers can read without the lock on refusal.
func (*ConfigHold) DriftCheck ¶
func (h *ConfigHold) DriftCheck() error
DriftCheck refuses an unreadable or changed modification time.
func (*ConfigHold) Elapsed ¶
func (h *ConfigHold) Elapsed() time.Duration
Elapsed returns the monotonic time since the successful mkdir.
func (*ConfigHold) Release ¶
func (h *ConfigHold) Release()
Release unlinks any tracked temporary file and gives back the lock. It is idempotent and never removes a lock emergency cleanup already released.
func (*ConfigHold) Shown ¶
func (h *ConfigHold) Shown() string
Shown returns the lock's literal path, for messages only.
func (*ConfigHold) TrackTemp ¶
func (h *ConfigHold) TrackTemp(dir int, name string) error
TrackTemp registers a temporary file before its creation. It duplicates dir so cleanup remains valid if the caller closes its descriptor. name must be one plain component. The caller retains ownership of dir even on failure.
func (*ConfigHold) UntrackTemp ¶
func (h *ConfigHold) UntrackTemp()
UntrackTemp withdraws a temporary file once it was renamed or removed.
type ConfigLockError ¶
type ConfigLockError struct {
Kind ConfigLockErrorKind
Age time.Duration
Path string
Message string
Err error
}
ConfigLockError is why a configuration lock could not be taken or trusted.
func (*ConfigLockError) Error ¶
func (e *ConfigLockError) Error() string
Error implements the error interface.
func (*ConfigLockError) Unwrap ¶
func (e *ConfigLockError) Unwrap() error
Unwrap exposes the cancellation or filesystem error.
type ConfigLockErrorKind ¶
type ConfigLockErrorKind uint8
ConfigLockErrorKind classifies a configuration lock refusal.
const ( // ConfigLockBusy means the lock remained held through every attempt. ConfigLockBusy ConfigLockErrorKind = iota + 1 // ConfigLockStale means the lock is stale and was left untouched. ConfigLockStale // ConfigLockCancelled means cancellation interrupted a wait. ConfigLockCancelled // ConfigLockCompromised means a held lock's modification time changed. ConfigLockCompromised // ConfigLockUnreachable means the literal parent could not be opened. ConfigLockUnreachable // ConfigLockIO means a directory operation failed without contention. ConfigLockIO )
type ConfigOutcome ¶
type ConfigOutcome string
ConfigOutcome is how one config step of a live pass ended.
const ( // ConfigApplied means the file was rewritten. ConfigApplied ConfigOutcome = "applied" // ConfigSkipped means nothing was there to rewrite, or the lock could // not be taken. ConfigSkipped ConfigOutcome = "skipped" // ConfigRefused means the file could not be rewritten safely, decided // before anything was written. ConfigRefused ConfigOutcome = "refused" // ConfigAborted means a check under the lock failed and nothing was // renamed. ConfigAborted ConfigOutcome = "aborted" // ConfigFailed means a write or a rename failed. ConfigFailed ConfigOutcome = "failed" // ConfigNotAttempted means the step deliberately did not run. ConfigNotAttempted ConfigOutcome = "not_attempted" // ConfigUnrecognized is a word a later build writes; read-only. ConfigUnrecognized ConfigOutcome = "unrecognized" )
type ConfigReason ¶
type ConfigReason string
ConfigReason is why a config step did not apply.
const ( // ConfigReasonAbsent: there is no configuration file. ConfigReasonAbsent ConfigReason = "absent" // ConfigReasonUnreadable: not a regular file, too large, or unreadable. ConfigReasonUnreadable ConfigReason = "unreadable" // ConfigReasonUnparseable: not JSON. ConfigReasonUnparseable ConfigReason = "unparseable" // ConfigReasonNotAnObject: the top level is not an object. ConfigReasonNotAnObject ConfigReason = "not_an_object" // ConfigReasonNotReproducible: re-serialising did not reproduce its bytes. ConfigReasonNotReproducible ConfigReason = "not_reproducible" // ConfigReasonBackupUnwritable: its backup could not be written first. ConfigReasonBackupUnwritable ConfigReason = "backup_unwritable" // ConfigReasonLockBusy: a session held the configuration lock throughout. ConfigReasonLockBusy ConfigReason = "lock_busy" // ConfigReasonLockStale: the configuration lock was stale; it is never broken. ConfigReasonLockStale ConfigReason = "lock_stale" // ConfigReasonCancelled: the run was cancelled while waiting for the lock. ConfigReasonCancelled ConfigReason = "cancelled" // ConfigReasonChangedUnderLock: the file changed while the lock was held. ConfigReasonChangedUnderLock ConfigReason = "changed_under_lock" // ConfigReasonCompromised: the held lock was broken underneath us. ConfigReasonCompromised ConfigReason = "compromised" // ConfigReasonBudget: a term could not finish inside the lock's budget. ConfigReasonBudget ConfigReason = "budget" // ConfigReasonIO: a write or a rename failed. ConfigReasonIO ConfigReason = "io" ConfigReasonProfileUnavailable ConfigReason = "profile_unavailable" // ConfigReasonSwapUnknown: the swap's own outcome is unknown. ConfigReasonSwapUnknown ConfigReason = "swap_unknown" // ConfigReasonAlreadyCurrent: a catch-up found the file already naming // the live item's account. ConfigReasonAlreadyCurrent ConfigReason = "already_current" // ConfigReasonDeclined: a catch-up's rewrite was not confirmed. Never // written: a declined catch-up appends no line. ConfigReasonDeclined ConfigReason = "declined" // ConfigReasonAuditRefused: a catch-up met an audit log that is // refused. Never written: there is no log to write it to. ConfigReasonAuditRefused ConfigReason = "audit_refused" // ConfigReasonUnrecognized is a word a later build writes; read-only. ConfigReasonUnrecognized ConfigReason = "unrecognized" )
type ConfigWriteRecord ¶
type ConfigWriteRecord struct {
// After is the identity of the write entry this pass appended, as
// [AuditID.String] renders it; nil on a catch-up, which follows no
// write of its own.
After *string `json:"after"`
// Outcome says how the step ended.
Outcome ConfigOutcome `json:"outcome"`
// Reason says why, when it did not apply.
Reason *ConfigReason `json:"reason"`
// Account is the account written into the configuration, by id alone.
Account *IncomingIdentity `json:"account"`
// FromSHA8 is the first eight hex digits of the file's digest as it
// was read.
FromSHA8 *string `json:"from_sha8"`
// ToSHA8 is the same for the file as it was written; only on applied.
ToSHA8 *string `json:"to_sha8"`
// Backup is the backup's file name, under the peer's backups
// directory.
Backup *string `json:"backup"`
// HoldMS is how long the configuration lock was held, in whole
// milliseconds, when one was taken.
HoldMS *uint64 `json:"hold_ms"`
}
ConfigWriteRecord is what one config step of a live pass did: the rewrite that follows an applied live write, or the record that it was not attempted. One line per step even when nothing was written, so the log says why a file was left as it was. Ids and digest prefixes only: the account is a pair of ids, never an email, and the backup is a file name, never a path — the append refuses an entry that carries anything else.
type Digests ¶
type Digests struct {
// AccessSHA256 is sha256(access_token), lowercase hex.
AccessSHA256 string
// RefreshSHA256 is sha256(refresh_token), lowercase hex; empty when the
// credential carries no refresh token.
RefreshSHA256 string
}
Digests are fingerprints of the token material, safe to write to disk and to compare. They answer "is this the same credential?" without holding the credential: folding a keychain entry into the live row, and deciding whether a pending credential still applies to the file it was derived from.
type DisabledReader ¶
type DisabledReader struct{}
DisabledReader answers "there is no keychain here" to everything. It is what a tagged build falls back to when no stand-in is wired, so a test that has not decided to talk to a keychain cannot reach the developer's own by accident.
func (DisabledReader) ListServices ¶
func (DisabledReader) ListServices(context.Context, string) ([]ServiceEntry, error)
ListServices lists nothing.
func (DisabledReader) Preflight ¶
func (DisabledReader) Preflight(context.Context) KeychainStatus
Preflight reports the keychain as unavailable, with the reason naming the deliberate switch-off rather than a failure.
type FileSnapshot ¶
type FileSnapshot struct {
// Dev is the device number.
Dev uint64
// Ino is the inode number.
Ino uint64
// Size is the size in bytes.
Size int64
// MtimeNS is the modification time in nanoseconds since the epoch.
// Nanoseconds since 1970 fit in an int64 until the year 2262.
MtimeNS int64
}
FileSnapshot is enough of a file's identity to notice it changed underneath us. Size and mtime alone would miss a same-size rewrite inside one mtime granularity; the device and inode catch a replacement.
func CommitStaged ¶
func CommitStaged(paths *config.Paths, s *StagedAdoption) (FileSnapshot, error)
CommitStaged replaces AdoptedFile with a staged credential and consumes the staging even on failure. There is intentionally no cancellation check: the caller commits only once the keychain may hold the credential being parked. A fresh no-follow walk must still reach the staged directory. Rename and cleanup remain relative to the retained descriptor, never a resolved path.
func Snapshot ¶
func Snapshot(path string) (*FileSnapshot, error)
Snapshot lstats a path.
It returns nil when nothing is there, and an error when the path is a symbolic link: every caller is about to decide whether to trust or replace the file, and a link is a decision to refuse, not to follow.
func SnapshotFollowing ¶
func SnapshotFollowing(path string) (*FileSnapshot, error)
SnapshotFollowing is Snapshot for a file this store only ever reads and never writes, such as the vendor's own live configuration: symbolic links are followed, because that file is commonly one, and refusing it would blind the live row rather than protect anything.
func WriteAdopted ¶
func WriteAdopted(ctx context.Context, paths *config.Paths, nsDir string, blobJSON *Secret) (FileSnapshot, error)
WriteAdopted atomically parks a displaced credential under AdoptedFile. There is no pending fallback: an adopted copy must never be replayed into the credential name a session reads. Errors leave the old copy untouched, except for a snapshot failure after a successful rename.
type ForeignActivity ¶
type ForeignActivity struct {
// Kind says what was found.
Kind ForeignActivityKind
// LockName is the artefact's file name, for [ForeignClaudeLock].
LockName string
// LockAgeMS is how long ago the artefact was last touched, for
// [ForeignClaudeLock]. Holders heartbeat every 5 s and self-lapse
// after 60 s, so the age is what tells a live session from a crashed
// one — though the caller refuses either way.
LockAgeMS uint64
// Service is the keychain service name that was found, for
// [ForeignMigratedToKeychain].
Service string
}
ForeignActivity is what detection found in one namespace.
func DetectForeignActivity ¶
func DetectForeignActivity(ctx context.Context, nsDir string, owned *OwnedMeta, listing []ServiceEntry, reader Reader) ForeignActivity
DetectForeignActivity looks for signs that something other than this store owns a namespace.
The keychain is only consulted when listing — the attribute listing from this pass — actually contains the service name. That keeps a namespace with no migration from issuing a keychain read at all, and it keeps the common case to zero extra subprocesses.
type ForeignActivityKind ¶
type ForeignActivityKind int
ForeignActivityKind says what somebody else is doing with a namespace.
const ( // ForeignNone means nothing: the caller may proceed. ForeignNone ForeignActivityKind = iota // ForeignClaudeLock means a Claude Code lock artefact is present. ForeignClaudeLock // ForeignMigratedToKeychain means a keychain item exists for this // namespace: a session has migrated the credentials out of the file // and this store must stop writing it. ForeignMigratedToKeychain )
type HeldLockFile ¶
type HeldLockFile struct {
// File is the record file itself.
File string
// Record is what it said.
Record HeldLockRecord
}
HeldLockFile is one record and the file it was read from.
func ReadAllHeldLockRecords ¶
func ReadAllHeldLockRecords(paths *config.Paths) []HeldLockFile
ReadAllHeldLockRecords returns every readable record in this store, ordered by file name.
Anything that is not a readable, parseable record is skipped rather than raised: a report that refuses to render because one file in a directory is malformed cannot diagnose the machine it was run on. The read itself refuses symbolic links and stops at MaxHeldLockRecordBytes, and the directory is reached by a no-follow walk from the namespace root, because these records are the only thing that lets stale-lock removal act outside that root: a listing somebody else could redirect would be a permit somebody else could redirect. A directory that is simply not there reads as no records — that is every machine that has never held one of these locks.
type HeldLockRecord ¶
type HeldLockRecord struct {
// WriterPID is the process that took the locks.
WriterPID uint32 `json:"agctl_pid"`
// WriterStartTime is when that process started, so a recycled
// process id cannot pass for the one that wrote the record. Nil when
// it could not be read, and nil in a record written by a build that
// predates the field.
WriterStartTime *string `json:"agctl_start_time"`
// Tree is which tree the locks are in.
Tree Tree `json:"tree"`
// StoreDir is the credential store directory being locked.
StoreDir string `json:"store_dir"`
// Paths is every directory that was created, in the order they were
// taken.
Paths []string `json:"paths"`
// TakenAt is when they were taken, RFC 3339.
TakenAt string `json:"taken_at"`
}
HeldLockRecord is one held-lock record, as it is written before the first mkdir and as the diagnostics read it back.
One type serves both sides so the reader and the writer cannot disagree about a field name or a tree spelling. The JSON keys are the store format's fixed spelling: records written by either manager binary sharing the store must keep reading each other.
func (*HeldLockRecord) Anchor ¶
func (r *HeldLockRecord) Anchor() (string, bool)
Anchor returns where a no-follow walk to one of these paths may start, or false when the store directory has no parent.
The store directory's parent, not the store directory: the legacy lock sits beside the store rather than inside it, so an anchor at the store itself could not reach it. Everything below the anchor — the store directory included — is still walked one no-follow component at a time.
func (*HeldLockRecord) Attests ¶
func (r *HeldLockRecord) Attests(path string) bool
Attests reports whether this record vouches for path.
Exact paths, compared whole: this is the check that decides whether a stale-lock removal may leave the namespace root, so a prefix or a parent is not enough.
func (*HeldLockRecord) WriterIsGone ¶
func (r *HeldLockRecord) WriterIsGone(ctx context.Context) bool
WriterIsGone reports whether the process that wrote this record is gone, so the directories it names are a leak rather than a live hold.
There are two ways to be gone, and the second is why the start time is recorded at all: the process id no longer names a live process (a collected exit included), or it does and the process's start identity differs from the recorded one, which means the kernel handed the id to somebody else. An unknown identity on either side is not evidence of anything: reading it as one would turn every record written by an older build into a permitted removal.
type HeldLocks ¶
type HeldLocks struct {
// contains filtered or unexported fields
}
HeldLocks is a live hold of the three credential-store locks.
Released in reverse order by HeldLocks.Release, and — when a cleanup registry was injected — by the emergency path a signal exit runs, with the same reverse-order removals through the same descriptors.
func (*HeldLocks) DriftCheck ¶
DriftCheck is the last check before a write: one stat of each held directory, immediately before the caller commits.
Any modification time differing from the value recorded at its mkdir means a third party has touched a lock this process holds — the protocol has been violated and nothing may be written. An unreadable modification time on either side is the same refusal, not an equality: an unreadable reading is not evidence that nothing moved. The budget is checked in the same place, because this is the last moment at which abandoning still costs nothing.
func (*HeldLocks) HoldElapsed ¶
HoldElapsed returns how long the hold has lasted, from the first mkdir, measured on the same clock that stamped it — mixing an injected clock with the real one would make this the one number a test could not check.
func (*HeldLocks) RecordPath ¶
RecordPath returns the held-lock record's path, so diagnostics and tests can name it.
func (*HeldLocks) Release ¶
func (h *HeldLocks) Release()
Release gives everything back in reverse order and clears the record. Safe to call twice. A hold acquired under the injected leak fault releases nothing, leaving the directories and the record exactly as a crashed hold would.
func (*HeldLocks) Slot ¶
Slot returns the slot for one held artefact by its recorded path, so a caller can sample a lock it holds; false when the hold does not carry that path.
func (*HeldLocks) WriteAdmission ¶
WriteAdmission is the decision a writer asks before starting: a write may begin only while what remains of the hold budget still covers a whole configuration-lock hold, so a write that starts can also finish inside the budget.
type HolderEvidence ¶
type HolderEvidence string
HolderEvidence is what the holder check could see.
Four values, and no value names a pid or claims a store was identified — the vocabulary itself is the assertion, so a later edit cannot reintroduce attribution through the log. Partial evidence — any pid whose state could not be read — is EvidenceNone, never EvidenceNoStoppedClaude.
const ( // EvidenceStoppedClaudePresent means some same-user `claude` process // is stopped or traced. EvidenceStoppedClaudePresent HolderEvidence = "stopped_claude_present" // EvidenceNoStoppedClaude means every same-user `claude` process was // readable and none was stopped. EvidenceNoStoppedClaude HolderEvidence = "no_stopped_claude" // EvidenceUnreadable means peer visibility is unproved; modification // times cannot authorize a removal. EvidenceUnreadable HolderEvidence = "unreadable" // EvidenceNone means the check could not be made, or could not be // made completely. EvidenceNone HolderEvidence = "none" )
type HolderSightings ¶
type HolderSightings interface {
// StoppedClaudePresent answers the one question the break rule
// asks, at the moment the rule asks it.
StoppedClaudePresent(ctx context.Context) HolderEvidence
// StoppedPIDs returns the stopped process ids, for the terminal's
// busy message only — never for a record. A bare busy is a dead end
// for a user whose suspended pane is blocking every break, so the
// terminal names the pids and disclaims attribution in the same
// sentence.
StoppedPIDs(ctx context.Context) []int32
}
HolderSightings is whether any same-user `claude` process is stopped.
type HolderUnreadableError ¶
type HolderUnreadableError struct{}
HolderUnreadableError reports that visibility of store-sharing peers is unproved, so no stale directory may be removed.
func (*HolderUnreadableError) Error ¶
func (*HolderUnreadableError) Error() string
Error implements the error interface.
type IncomingIdentity ¶
type IncomingIdentity struct {
// AccountUUID is the account id.
AccountUUID string `json:"account_uuid"`
// OrganizationUUID is the organization id, when the registry knows
// one; nil for a record still carrying the unknown-organization
// placeholder.
OrganizationUUID *string `json:"organization_uuid"`
}
IncomingIdentity is the account a live write installed in the item, by id alone — never a token and never an email address.
type KeychainError ¶
type KeychainError struct {
// Class names the failure the way the user sees it. The named errs
// classes cover the branches the data flow takes; any other classified
// stderr travels verbatim as its own class value, so widening the
// classifier does not change this type.
Class errs.KeychainClass
// Detail carries the trimmed stderr, the spawn failure, or the exceeded
// budget, phrased for a diagnostic line. It never contains item payload
// bytes: the payload travels on stdout and is sealed into a [Secret]
// before any error is built.
Detail string
// Transient reports whether a later attempt might succeed. A locked
// keychain gets unlocked and a timeout is a busy machine; a missing
// binary and an absent platform transport will not fix themselves. A
// transient keychain failure is never answered by falling back to the
// credential file, because the two stores can hold different accounts'
// credentials and two readers of one refresh chain is how it dies.
Transient bool
}
KeychainError reports one failed read through security(1), classified.
func (*KeychainError) Error ¶
func (e *KeychainError) Error() string
Error names the classified failure, with the diagnostic detail appended when one was captured.
func (*KeychainError) Is ¶
func (e *KeychainError) Is(target error) bool
Is reports whether target names the same failure class, which is what makes the named values above work with errors.Is regardless of detail.
func (*KeychainError) Unwrap ¶
func (e *KeychainError) Unwrap() error
Unwrap exposes the errs vocabulary, so errors.As reaches an errs.KeychainError and main maps the failure onto a partial exit rather than a fatal one.
type KeychainState ¶
type KeychainState int
KeychainState is what the keychain preflight found.
const ( // KeychainStateUnsupported means this platform does not provide the // keychain transport. It is the zero value so an unset status refuses // rather than passing for readable. KeychainStateUnsupported KeychainState = iota // KeychainStateUnlocked means the keychain is readable. KeychainStateUnlocked // KeychainStateLocked means the keychain is present but locked; the // user must unlock it. KeychainStateLocked KeychainStateUnavailable // KeychainStateTimeout means security(1) did not answer inside its // budget. KeychainStateTimeout )
func (KeychainState) String ¶
func (s KeychainState) String() string
String names the state for diagnostics.
type KeychainStatus ¶
type KeychainStatus struct {
// State is what the preflight found.
State KeychainState
// Reason says why the keychain is unreachable; it is set only for
// [KeychainStateUnavailable].
Reason string
}
KeychainStatus is the keychain preflight's answer: the state, plus the reason security(1) gave when the keychain is unavailable.
type KeychainStdinLine ¶
type KeychainStdinLine struct {
// contains filtered or unexported fields
}
KeychainStdinLine is one keychain update line, built and ready for `security -i`'s standard input.
Three things make this a type rather than a bare secret:
- It can only be built by NewKeychainStdinLine; the line field is unexported and there is no other constructor, so the write transport cannot be handed a bare token or a hand-assembled line in place of one that went through the shape and length rules.
- It records who it was built for. The account and service are not secrets; carrying them is what lets the write transport refuse a line built for a different item than the target it was asked to write.
- It is written without being read back. KeychainStdinLine.WriteTo hands the bytes to the sink from inside the locked buffer, so no caller ever holds the plaintext line.
func NewKeychainStdinLine ¶
func NewKeychainStdinLine(account, service string, blob []byte) (*KeychainStdinLine, error)
NewKeychainStdinLine builds the keychain update line that stores blob — a serialized credential document — under the named item, ready for `security -i`'s standard input.
account is the item's acct attribute and service is the target item's service name. Both are recorded in the returned value so the write transport can refuse a line built for a different item than the one it was asked to write.
The payload is blob's lowercase hex, so the caller keeps ownership of the plaintext document: blob is read, never retained, and wiping it afterwards stays the caller's job.
A finished line — trailing newline included — over KeychainLineLimit returns LineTooLongError, whose exit classification is the lettered refusal for an over-long credential line.
func (*KeychainStdinLine) Account ¶
func (l *KeychainStdinLine) Account() string
Account returns the item's acct attribute this line was built for.
func (*KeychainStdinLine) Format ¶
func (l *KeychainStdinLine) Format(state fmt.State, _ rune)
Format prints the same redacted form as String for every verb, width, precision and flag.
func (*KeychainStdinLine) GoString ¶
func (l *KeychainStdinLine) GoString() string
GoString prints the same redacted form as String.
func (*KeychainStdinLine) Len ¶
func (l *KeychainStdinLine) Len() int
Len returns the line's length in bytes, trailing newline included: what the KeychainLineLimit rule is measured against, and what the write transport re-checks before it spawns anything.
func (*KeychainStdinLine) LogValue ¶
func (l *KeychainStdinLine) LogValue() slog.Value
LogValue prints the same redacted form as String.
func (KeychainStdinLine) MarshalJSON ¶
func (KeychainStdinLine) MarshalJSON() ([]byte, error)
MarshalJSON returns a redacted JSON string.
func (KeychainStdinLine) MarshalJSONTo ¶
func (KeychainStdinLine) MarshalJSONTo(encoder *jsontext.Encoder) error
MarshalJSONTo writes a redacted JSON string through the streaming hook.
func (*KeychainStdinLine) Service ¶
func (l *KeychainStdinLine) Service() string
Service returns the keychain service name this line was built for.
func (*KeychainStdinLine) String ¶
func (l *KeychainStdinLine) String() string
String prints the item the line names and its length, never the line.
type KeychainTargetMismatchError ¶
KeychainTargetMismatchError reports a line built for a different item. The request is refused before a child is started.
func (*KeychainTargetMismatchError) Error ¶
func (e *KeychainTargetMismatchError) Error() string
Error names the mismatched account or service, never the credential.
type KeychainWriteOutcome ¶
type KeychainWriteOutcome string
KeychainWriteOutcome is how a keychain write ended.
const ( // WriteApplied means the item was re-read after the locks were // released and holds the incoming credential. WriteApplied KeychainWriteOutcome = "applied" // WriteUnknown means the write returned but the verifying re-read did // not agree — which includes a legitimate peer write landing in the // gap. It means "re-run status", not "failed". WriteUnknown KeychainWriteOutcome = "unknown" // WriteFailed means the write itself failed and the item was left as // it was. WriteFailed KeychainWriteOutcome = "failed" // WriteDiscarded means the refresh was performed and never written: a // refusal after the POST threw away a credential the server had // already minted. Recorded rather than silent, because the item still // holds the old refresh token, which the server has usually rotated // away, so the next pass may report needs-login for reasons this pass // created. WriteDiscarded KeychainWriteOutcome = "discarded" )
type KeychainWriter ¶
type KeychainWriter struct {
// contains filtered or unexported fields
}
KeychainWriter updates items through a bounded, stdin-only child process. Callers must derive the service from an owned namespace or the validated live environment and hold the corresponding peer locks before calling Write. The zero value refuses every write.
func NewKeychainWriter ¶
func NewKeychainWriter() *KeychainWriter
NewKeychainWriter selects the fixed production binary or the tagged test seam. Construction does not start a process. A testing build without a stand-in refuses writes rather than falling back to the user's keychain.
func (*KeychainWriter) Write ¶
func (w *KeychainWriter) Write(ctx context.Context, service string, line *KeychainStdinLine) error
Write updates service in place with line, creating the item when absent. Account, service and length are checked before spawning. It does not acquire locks, derive write authority, retry failures or remove an existing item. Transport failures return a classified KeychainError; a timeout leaves the write outcome unknown. Plaintext is exposed only while writing the stdin pipe.
type LineTooLongError ¶
type LineTooLongError struct {
// Len is the line's length in bytes, trailing newline included.
Len int
}
LineTooLongError reports a keychain line over KeychainLineLimit. Nothing was spawned and the keychain was not touched.
func (*LineTooLongError) Error ¶
func (e *LineTooLongError) Error() string
Error states the length and the limit it exceeded.
func (*LineTooLongError) Unwrap ¶
func (e *LineTooLongError) Unwrap() error
Unwrap classifies the failure as the lettered refusal whose exit status scripts branch on.
type LiveStoreEnv ¶
type LiveStoreEnv struct {
// SecureStorageDir is the raw value of the environment variable
// that points Claude Code at a namespaced store. A non-empty value
// means this environment names a namespace rather than the live
// store, so the live tree is refused; an empty value is not set and
// refuses nothing.
SecureStorageDir string
// NamedStoreDir is the live store directory the environment names.
NamedStoreDir string
// SecureStorageEnvName is the variable's name, for the refusal
// message alone.
SecureStorageEnvName string
}
LiveStoreEnv is what a live-tree hold needs to know about the process environment, supplied by the caller because this package sits below the provider that reads the environment.
type Location ¶
type Location struct {
// Kind picks the store. The zero value names no store at all, and every
// read against it classifies as transient, because failing closed beats
// inventing a place to read from.
Kind LocationKind
// Service is the keychain service name; meaningful only when Kind is
// [LocationKeychain].
Service string
// Path is the credential file path; meaningful only when Kind is
// [LocationFile].
Path string
}
Location names where exactly one account's credential lives.
A location is one store, never a chain of them. The vendor tooling this program coexists with composes its stores as "keychain, and if that does not work, the file", which is right for a tool that must start a session somehow. This program does not: the two stores can hold different accounts' credentials, so falling through would show one account's usage under another's row and — worse — decide a namespace was free to write while a session owned it. Location.Classify therefore has no outcome that names another store.
func CredentialFile ¶
CredentialFile returns the location of one credential file.
func KeychainItem ¶
KeychainItem returns the location of one keychain item.
func (Location) Classify ¶
Classify maps the error of one read against this location onto the outcome the caller branches on. It is pure: it inspects only its arguments, and it never performs or suggests a read of any other store.
The rule, by store:
| store | absent means | locked | anything else | |----------|-------------------------|--------|---------------| | keychain | no item (ErrItemNotFound) | locked | transient | | file | no file (fs.ErrNotExist) | — | transient |
A keychain failure classifying as transient — a timeout, an unreachable keychain, a spawn failure — leaves the credential file unconsulted: a keychain that is merely slow still holds the authoritative credentials, and reading the file instead is how two writers of one refresh chain happen.
type LocationKind ¶
type LocationKind int
LocationKind picks the one store a credential may be read from.
const ( // LocationKeychain is a macOS keychain item, named by its service. LocationKeychain LocationKind = iota + 1 // LocationFile is a credential file this program's store owns. LocationFile )
type LockAnchor ¶
type LockAnchor struct {
// contains filtered or unexported fields
}
LockAnchor is the two directory descriptors every lock of one hold is addressed through.
Opened by a single no-follow component walk from the anchor the tree permits — the namespace root for the manager's own tree, the resolved live store's parent for the live tree — and kept for the life of the hold. Two consequences, and both are the point: a symbolic link anywhere below the anchor is refused before the first mkdir, so nothing is created in whatever it pointed at; and the tree stops being a claim and becomes a derived fact, which is what the containment rule needs it to be.
func OpenLockAnchor ¶
func OpenLockAnchor(subject LockSubject, paths *config.Paths, live *LiveStoreEnv) (*LockAnchor, error)
OpenLockAnchor opens the two descriptors a hold of subject needs, refusing a store that is not in the tree it claims and a way to it that cannot be walked without following a symbolic link.
For the live tree, two refusals come before the walk: an environment whose secure-storage variable holds a non-empty value names a namespace rather than the live store, and a live store that cannot be resolved at all is unreachable. Only the acceptance consults the caller's spelling; what is walked and locked is derived from the environment's own name for the store, so no swap between the two resolutions can point the hold at a directory the environment does not name.
func (*LockAnchor) Close ¶
func (a *LockAnchor) Close()
Close gives the two descriptors back. Safe to call twice.
func (*LockAnchor) InStore ¶
func (a *LockAnchor) InStore(name, shown string) LockSlot
InStore returns a slot for one name inside the store directory, for a caller that runs the break rule against a single artefact.
func (*LockAnchor) StoreDir ¶
func (a *LockAnchor) StoreDir() string
StoreDir returns the store directory this hold is about.
func (*LockAnchor) Tree ¶
func (a *LockAnchor) Tree() Tree
Tree returns which tree the walk proved the store directory is in.
type LockBody ¶
type LockBody struct {
// PID is the holder's process id.
PID uint32 `json:"pid"`
// PIDStartTime is when that process started, for telling a live
// holder from a recycled pid. It is written as null when the kernel
// would not answer: doctor then falls back to the process id alone,
// which is weaker but still useful, rather than the acquisition
// failing over a diagnostic field. The string is compared, never
// parsed, so its spelling is private to the proc package.
PIDStartTime *string `json:"pid_start_time"`
// AcquiredAt is when the lock was taken, RFC 3339.
AcquiredAt string `json:"acquired_at"`
}
LockBody is what an acquired lock records about its holder.
Written into the lock file's body so doctor can say who holds a lock, and whether that process is alive, stopped or gone.
func ReadBody ¶
ReadBody reads a lock file's holder record, if it has a readable one.
It reports false for every failure — a missing file, a body larger than a record can be, or bytes that do not parse into one. An unreadable body means the holder is older, or newer, or crashed mid-write, and none of those is worth failing a doctor run over.
type LockBreakRecord ¶
type LockBreakRecord struct {
// Path is the lock directory itself.
Path string `json:"path"`
// StoreDir is the credential store directory it guards.
StoreDir string `json:"store_dir"`
// Tree is which tree that store is in.
Tree Tree `json:"tree"`
// Service is the keychain service name of the item the store holds.
Service string `json:"service"`
// Target says which item the hold was for.
Target Target `json:"target"`
// SampleA is the first stat.
SampleA LockSample `json:"sample_a"`
// SampleB is the second stat, after the sampling interval. Nil when
// the rule abandoned before it.
SampleB *LockSample `json:"sample_b"`
// SampleC is the third stat, immediately before the removal, with no
// I/O between.
SampleC *LockSample `json:"sample_c"`
// IntervalWallMS is the wall-clock milliseconds between samples A
// and B.
IntervalWallMS uint64 `json:"interval_wall_ms"`
// IntervalMonotonicMS is the monotonic milliseconds between the same
// two samples. A divergence from IntervalWallMS over a second is a
// clock step in either direction.
IntervalMonotonicMS uint64 `json:"interval_monotonic_ms"`
// Evidence is what the holder check saw.
Evidence HolderEvidence `json:"holder_evidence"`
// Outcome says whether the artefact was removed.
Outcome BreakOutcome `json:"outcome"`
// Reason says why not, when it was not — and retaken when it was
// removed and a peer took it back first. Absent for a clean break:
// every reason is a reason not to have broken a lock, so a break
// with nothing to explain records no reason rather than inventing a
// word for success.
Reason BreakReason `json:"reason,omitzero"`
}
LockBreakRecord is the record of one break decision, as the audit log carries it.
Only the lock protocol builds one, and it cannot build one in halves: the rule fills everything it can observe and the caller supplies the service and target through BreakDraft.Complete, so no field arrives at the log empty because somebody forgot it.
type LockFS ¶
type LockFS interface {
// Mkdir makes one non-blocking mkdir at 0700. It returns
// [ErrLockExists] when somebody already holds the lock and
// [ErrLockGone] when the parent directory is missing.
Mkdir(at LockSlot) error
// Rmdir removes the directory, refusing anything that is not an
// empty directory, so a regular file or a symbolic link at the
// artefact's name is never removed. It returns [ErrLockGone] when
// the directory has already gone.
Rmdir(at LockSlot) error
// Mtime returns the artefact's modification time at full nanosecond
// resolution, or false when it is not there. The read never follows
// a symbolic link at the artefact's name: a link where a lock
// directory should be is somebody else's plant, and reading its
// target's time would hand the sampling to the planter.
Mtime(at LockSlot) (time.Time, bool)
}
LockFS is the three directory operations a peer lock is made of, all relative to an already-opened directory.
An interface rather than three functions so a spy can record the sequence of operations: "the storage-write mutex is attempted once" and "no wait happens while anything is held" are claims about the sequence, and no after-the-fact inspection of the filesystem can check them.
type LockGuard ¶
LockGuard is an acquired lock, released by [LockGuard.Release] and by the kernel if the process dies. The lock file is never unlinked.
func Acquire ¶
Acquire takes the exclusive lock on the file name inside locksDir, creating the directory at 0700 when it is missing.
The waiting, creation and symlink-refusal rules are the store's shared flock rules; a name that is not one plain path component is refused before anything is created. The acquired lock's body records this holder — pid, start-time identity, acquisition time — so doctor can tell a live holder from a recycled pid; a holder that cannot record itself releases the lock and reports the failure, because a silent lock is exactly what the body exists to prevent.
func LockFile ¶
LockFile takes an exclusive flock on path, creating the file if needed, with Acquire's waiting rules but no holder body.
Unlike Acquire, the parent directory is resolved normally: the one caller is the registry's configuration lock, whose parent is the configuration directory — a directory the user names, and one a dotfile manager may well have made a symbolic link. Everything else in the store follows that link, so refusing it only here would half-support a layout rather than support or reject it.
type LockIOError ¶
type LockIOError struct {
// Context says what was being attempted.
Context string
// Message is the underlying failure.
Message string
}
LockIOError reports that a filesystem operation on a lock failed for a reason that is not contention.
func (*LockIOError) Error ¶
func (e *LockIOError) Error() string
Error implements the error interface.
type LockProfile ¶
type LockProfile struct {
// Stale is how old the peer lets a lock get before treating it as
// abandoned.
Stale time.Duration
// Update is the peer's heartbeat period for this family.
Update time.Duration
// Retries is how many times a blocked mkdir is retried. Always zero:
// a retry is a wait, and a wait belongs outside the hold.
Retries int
// HoldBudget is the longest a hold of this family may last.
HoldBudget time.Duration
}
LockProfile is one lock family's timing options.
Two families exist and they do not share options: the credential-store locks, and the configuration lock. Keeping them in one type is what stops a caller borrowing the wrong staleness window for the wrong lock.
func ConfigProfile ¶
func ConfigProfile() LockProfile
ConfigProfile returns the configuration lock's profile.
func RefreshProfile ¶
func RefreshProfile() LockProfile
RefreshProfile returns the primary and legacy refresh locks' profile.
func StorageWriteProfile ¶
func StorageWriteProfile() LockProfile
StorageWriteProfile returns the storage-write mutex's profile, taken with a single non-blocking mkdir.
type LockSample ¶
type LockSample struct {
// At is when the sample was taken.
At time.Time `json:"at"`
// MtimeNS is the directory's modification time in nanoseconds since
// the epoch. Nanoseconds are load-bearing: the comparison between two
// samples is exact, and a resolution that rounded would silently
// grant a tolerance the rule forbids.
MtimeNS int64 `json:"mtime_ns"`
// AgeMS is how old the directory was at that moment.
AgeMS uint64 `json:"age_ms"`
}
LockSample is one stat of a lock directory.
type LockSlot ¶
type LockSlot struct {
// Dir is the descriptor of the directory the artefact lives in.
Dir int
// Name is the artefact's own name inside Dir: exactly one component.
Name string
// Shown is what the artefact is called in a record or a message.
// Carried rather than derived so a test's spy can record a timeline
// a reader recognises; no operation is ever performed on it.
Shown string
}
LockSlot is one lock artefact, addressed the way it is operated on: a directory descriptor and a name, never a path.
A path is re-resolved from the root on every system call, and a symbolic link planted anywhere along it redirects the operation into a directory of somebody else's choosing — the live store's lock being the obvious target. The descriptor is opened once per acquisition by a no-follow component walk and kept for the life of the hold, so the directory a removal lands in is the same inode the creation landed in.
type LockSubject ¶
type LockSubject struct {
// StoreDir is the credential store directory the locks guard.
StoreDir string
// Tree is which tree the caller believes it is in.
Tree Tree
}
LockSubject is what one hold is about: the store directory, and which tree it is in. The two travel together because the tree is checked against the store directory rather than believed: a tree a caller could simply assert would put the containment rule in the caller's hands.
type LockUnavailableError ¶
type LockUnavailableError = lockfile.LockUnavailableError
LockUnavailableError reports that locking failed for a reason that is not contention. The caller must not write.
type NotEmptyError ¶
type NotEmptyError struct {
// Path is the non-empty directory.
Path string
}
NotEmptyError reports a directory that had to be empty and was not. Reported rather than recursed: removing a tree is never this store's job.
type NotRegularError ¶
type NotRegularError struct {
// Path is the offending path.
Path string
}
NotRegularError reports a path that exists but is not what belongs there: a credential or metadata file must be a regular file, and a component of the namespace directory must be a directory.
func (*NotRegularError) Error ¶
func (e *NotRegularError) Error() string
Error names the offending path.
type Outcome ¶
type Outcome struct {
// Kind says what the attempt produced.
Kind OutcomeKind
// Reason carries the transient failure, phrased for a status row. It is
// set only for [OutcomeTransient].
Reason string
}
Outcome is one read attempt classified for the caller's data flow.
type OutcomeKind ¶
type OutcomeKind int
OutcomeKind is what resolving one credential location produced. The set is closed on purpose: there is no kind that says "try the other store".
const ( // OutcomeCredential means the read produced the credential. OutcomeCredential OutcomeKind = iota + 1 // OutcomeAbsent means the store answered "nothing here" — for an owned // account, "needs login". OutcomeAbsent // OutcomeLocked means the keychain is locked. Split out from transient // because the recovery is "unlock it", not "retry". OutcomeLocked // OutcomeTransient means the read failed in a way a later pass might // not. Never a reason to consult another store. OutcomeTransient )
type OutsideRootError ¶
type OutsideRootError struct {
// Path is the refused path.
Path string
}
OutsideRootError reports a path that is not inside the store root it had to stay under.
func (*OutsideRootError) Error ¶
func (e *OutsideRootError) Error() string
Error names the refused path.
type OwnedMeta ¶
type OwnedMeta struct {
// ExportSHA8 is the hash of the namespace directory as it was
// spelled at login.
ExportSHA8 string
// CanonicalSHA8 is the hash of the same directory after symbolic
// link resolution, or empty when it does not differ.
CanonicalSHA8 string
}
OwnedMeta is the two spellings a namespace can be hashed under.
A Claude Code session pointed at the namespace hashes the string it was given, so that is the primary spelling; but the user may have been given a path through a symbolic link, in which case the resolved spelling hashes differently and names a second possible item. Both are checked, because missing either one means writing into a migrated namespace.
type PendingCredential ¶
type PendingCredential interface {
// MetaRequiresExpiry says whether a pending meta must carry
// new_expires_at to be valid. A store whose metas have always required
// it keeps discarding a meta without one as invalid; a vendor whose
// credential has no expiry to record answers false.
MetaRequiresExpiry() bool
// UnusableIsAbsent says whether a file that is present but unusable
// counts as absent: a target that does not parse, or a target, pending
// file or meta that exists and cannot be opened. A vendor whose own
// writer rewrites the file in place can leave it torn for a moment,
// and whose pending file may be the only copy of a rotated grant,
// answers false: the resolver then reads strictly and returns an error
// for any such file, keeping everything for the next run. A pending
// file that is a link, not a regular file, or oversized is still
// invalid either way — no writer of ours leaves one.
UnusableIsAbsent() bool
// Validate says whether the bytes are a credential this vendor's
// writer would have written.
Validate(b []byte) bool
// Digests returns the digests of the credential in the bytes, or
// false when they do not parse.
Digests(b []byte) (Digests, bool)
}
PendingCredential is what the pending protocol needs to know about one vendor's credential file. The protocol only ever has bytes in hand, so every question is asked of bytes.
type PendingDecision ¶
type PendingDecision struct {
// Kind says which row of the table applied.
Kind PendingDecisionKind
// FirstWrite reports whether a replayed credential was parked before
// any file existed; meaningful only for [PendingReplayed].
FirstWrite bool
// Reason says why a credential was discarded; meaningful only for
// [PendingDiscarded].
Reason PendingDiscardReason
}
PendingDecision is the outcome of one resolution.
type PendingDecisionKind ¶
type PendingDecisionKind int
PendingDecisionKind is what resolving a pending file decided.
const ( // PendingNone means there was nothing pending. PendingNone PendingDecisionKind = iota + 1 // PendingReplayed means the pending credentials were moved into place. PendingReplayed // PendingDiscarded means the pending credentials were deleted unused. PendingDiscarded )
type PendingDiscardReason ¶
type PendingDiscardReason int
PendingDiscardReason is why a pending file was discarded.
const ( // PendingInvalid means the metadata was missing, unparseable, or one // of the files was not a plain file of a sane size. PendingInvalid PendingDiscardReason = iota + 1 // PendingNamespaceTakenOver means a session or a keychain migration // has taken the namespace over since the pending file was written. PendingNamespaceTakenOver // PendingFileChanged means the credential file changed since the // pending file was derived from it, so replaying would undo that // change. PendingFileChanged // PendingFileRemoved means the credential file was removed, and the // pending file was derived from one that existed. PendingFileRemoved )
func (PendingDiscardReason) Label ¶
func (r PendingDiscardReason) Label() string
Label is the reason as it appears in the state column.
type PendingSpec ¶
type PendingSpec struct {
// TargetName is the credential file a replay renames onto.
TargetName string
// PendingName is where a credential waits after a failed rename.
PendingName string
// MetaName is what the pending credential was derived from.
MetaName string
// Prior holds the digests of the file the credential was derived from;
// nil on a first write. Recorded in the meta when a write parks a
// credential.
Prior *Digests
// ExpiresAtMS is the new access token's expiry in milliseconds since
// the epoch, when the vendor has one to record; nil otherwise.
ExpiresAtMS *int64
}
PendingSpec names one pending credential and what it was derived from.
The three names are single path components inside the namespace directory the operation is handed as a descriptor.
type PendingWrite ¶
type PendingWrite struct {
// Before holds the digests of the target file before the resolution,
// when it parsed.
Before *Digests
// Pending holds the digests of the pending credential that was
// replayed or dropped, when it parsed.
Pending *Digests
}
PendingWrite is what a resolution changed on disk, for a caller that records every write. A nil result from ResolvePendingWith means no credential file was touched: nothing was pending, or only a lone meta was cleared.
type ProcHolders ¶
type ProcHolders struct{}
ProcHolders is the real holder check: the kernel's process table, in-process, no child process and no argument list.
func (ProcHolders) StoppedClaudePresent ¶
func (ProcHolders) StoppedClaudePresent(ctx context.Context) HolderEvidence
StoppedClaudePresent implements HolderSightings.
func (ProcHolders) StoppedPIDs ¶
func (ProcHolders) StoppedPIDs(ctx context.Context) []int32
StoppedPIDs implements HolderSightings.
type ReadOutcome ¶
type ReadOutcome struct {
// Present reports whether a file was read at all.
Present bool
// Bytes holds the contents when Present.
Bytes []byte
// Snap is the file's identity at the moment it was read, when Present.
Snap FileSnapshot
}
ReadOutcome is what one credential read found: nothing, or the bytes together with the identity of the file that held them.
func ReadAdopted ¶
func ReadAdopted(nsDir string) (ReadOutcome, error)
ReadAdopted reads `<nsDir>/.credentials.adopted.json` — the credential a swap displaced — under the same rules as ReadCredentials. Separate rather than a parameter, because the two answer different questions: that one asks what this namespace's store is, this one asks what a swap parked here.
func ReadCredentials ¶
func ReadCredentials(nsDir string) (ReadOutcome, error)
ReadCredentials reads `<nsDir>/.credentials.json`. An unreadable or missing path is absent; a symlink, a non-regular file or an oversized file is a failure, never an absence.
func ReadFile ¶
func ReadFile(path string, limit int64) (ReadOutcome, error)
ReadFile opens a file with O_NOFOLLOW and reads it, applying the size limit. The limit is the caller's, because the callers differ by orders of magnitude: a credential blob, its metadata, and a configuration file a session grows without bound.
func ReadFileFollowing ¶
func ReadFileFollowing(path string, limit int64) (ReadOutcome, error)
ReadFileFollowing is ReadFile for a file this store only ever reads: the same regular-file and size rules, but a symbolic link is followed rather than refused (see SnapshotFollowing).
func (ReadOutcome) Format ¶
func (ReadOutcome) Format(state fmt.State, _ rune)
Format redacts every formatting verb and flag.
func (ReadOutcome) GoString ¶
func (ReadOutcome) GoString() string
GoString returns a redacted Go-syntax representation.
func (ReadOutcome) LogValue ¶
func (ReadOutcome) LogValue() slog.Value
LogValue returns a redacted structured-log value.
func (ReadOutcome) MarshalJSON ¶
func (ReadOutcome) MarshalJSON() ([]byte, error)
MarshalJSON returns a redacted JSON string.
func (ReadOutcome) MarshalJSONTo ¶
func (ReadOutcome) MarshalJSONTo(encoder *jsontext.Encoder) error
MarshalJSONTo writes a redacted JSON string through the streaming hook.
func (ReadOutcome) String ¶
func (ReadOutcome) String() string
String returns a redacted representation of the read result.
type Reader ¶
type Reader interface {
// Preflight asks whether the keychain can be read at all, before any
// item is named, by running security(1) show-keychain-info under the
// read budget. The answer is not memoized: a process that runs for
// hours must notice a keychain that locks, or is unlocked, between
// passes.
Preflight(ctx context.Context) KeychainStatus
// ListServices lists the generic-password items whose service name
// starts with prefix, attributes only, by running security(1)
// dump-keychain under the dump budget. No password material is
// requested and none is returned.
ListServices(ctx context.Context, prefix string) ([]ServiceEntry, error)
// Read reads one item's password into a sealed [Secret] by running
// security(1) find-generic-password under the read budget. A keychain
// that is readable but holds no item under that service name returns an
// error matching [ErrItemNotFound]; callers treat that as the normal
// "needs login" answer, never as a reason to consult another store.
Read(ctx context.Context, service string) (*Secret, error)
}
Reader reads credential blobs out of the macOS keychain.
It is deliberately a reader: it declares no write and no delete, and it never grows one. Giving the rest of the program no vocabulary for a mutating keychain call is the cheapest way to keep "the read path never writes or deletes a keychain item" true. The write side, when it exists, is a separate transport that constructs its own targets.
func NewReader ¶
func NewReader() Reader
NewReader builds the keychain reader this build selects.
A release build always reads through /usr/bin/security — an absolute path, never resolved through PATH, because this process must not be talked into running some other program by an inherited environment — and refuses on a platform without the transport. A tagged build selects its backend through the testing seam, and with nothing wired it falls closed to DisabledReader: a test that has not decided to talk to a keychain must not be able to reach the developer's own by omission.
Constructing a reader runs nothing; the first call does.
type RefusedSymlinkError ¶
type RefusedSymlinkError = lockfile.RefusedSymlinkError
RefusedSymlinkError reports a symbolic link where a lock file or the locks directory should be.
type Seams ¶
type Seams struct {
// FS is the three directory operations.
FS LockFS
// Holders is the holder check, asked its question at the moment the
// rule asks it rather than in advance.
Holders HolderSightings
// Clock is both clocks, the sleeper and the jitter draw.
Clock Clock
// Cleanup is the emergency-release registry. RealSeams installs the
// process-wide registry; isolated callers may supply their own.
Cleanup CleanupRegistry
// Fault is the injected fault hook, or nil for none.
Fault func(name string) bool
// Pause is an optional named pause hook for the caller. Acquisition
// itself has no pause points.
Pause func(name string)
}
Seams is everything the lock protocol talks to that a test — or a later runtime — replaces.
type Secret ¶
type Secret struct {
// contains filtered or unexported fields
}
Secret keeps immutable token bytes encrypted outside plaintext operations. Copies share the encrypted backing. The zero value contains no secret. Formatting and ordinary serialization never expose its contents.
func NewSecret ¶
NewSecret seals b and wipes its contents. Empty input returns an error. The caller must own writable b and must not use it concurrently. Failure to allocate locked memory panics after memguard purges the session.
func (*Secret) AppendPlaintextTo ¶
AppendPlaintextTo is the only serialization path that copies plaintext out of the enclave. It is reserved for credential documents sent to their store. The caller must wipe the returned slice after that I/O; ordinary logging and serialization must never use it. An open failure leaves dst unchanged.
func (Secret) AppendText ¶
AppendText appends redacted text without changing the existing prefix.
func (*Secret) Clone ¶
Clone returns a new wrapper sharing immutable encrypted backing, without opening plaintext. A nil receiver stays nil; Purge invalidates all copies.
func (*Secret) Digest ¶
Digest returns the lowercase SHA-256 hex digest, computed while locked. It returns an error when the secret cannot be opened.
func (*Secret) Digest8 ¶
Digest8 returns the eight-hex-digit digest prefix used by audit records. It returns an error when the secret cannot be opened.
func (*Secret) Equal ¶
Equal compares equally sized secrets in constant time with both locked. Lengths are not secret. It returns an error if either secret cannot be opened.
func (Secret) MarshalJSON ¶
MarshalJSON returns a redacted JSON string.
func (Secret) MarshalJSONTo ¶
MarshalJSONTo writes a redacted JSON string through the streaming hook.
func (Secret) MarshalText ¶
MarshalText returns redacted text.
func (*Secret) WithPlaintext ¶
WithPlaintext opens a locked, read-only buffer for one synchronous operation. The callback must not modify, retain, or use the slice after it returns. The buffer is destroyed on return or panic. An invalid or purged secret returns an error without calling fn; callback errors are passed through. Allocation failures panic after memguard purges the session.
type SecretFile ¶
type SecretFile struct {
// contains filtered or unexported fields
}
SecretFile is one named file in an already-opened directory, bound to the root it must stay under.
NewSecretFile is the only constructor and every field is unexported, so a value cannot exist without naming its root. Every operation first checks that the file's path spells a location strictly below that root and that the name is one plain path component. The check is lexical: the directory descriptor comes from the caller's no-follow walk, and the leaf operation is relative to it, so no path is resolved again here.
func NewSecretFile ¶
func NewSecretFile(root string, dir int, name, shown string) *SecretFile
NewSecretFile binds a file name in the directory dir describes to the root it must stay under.
Infallible on purpose: the checks run at the start of every operation, so a value that fails them can be built but never used.
func (*SecretFile) Park ¶
func (f *SecretFile) Park(ctx context.Context, doc []byte, spec PendingSpec) (WriteOutcome, error)
Park parks doc as pending without trying the target at all.
For a writer that already knows the rename cannot happen: a rotated grant whose writes kept failing after the refresh was applied. The target is not examined — a target that is torn, a link, or unreadable is exactly why the caller is here — and the temporary is staged and parked under StopComplete's rules: a failure to park keeps it, naming it in the error, because it may be the only copy of the grant.
func (*SecretFile) Read ¶
func (f *SecretFile) Read(limit int64) (ReadOutcome, error)
Read reads the file under the regular-file and size rules of [readFileAt]: a file that exists and cannot be opened reads as absent.
func (*SecretFile) ReadSecret ¶
func (f *SecretFile) ReadSecret(limit int64) (*Secret, *FileSnapshot, error)
ReadSecret reads the file under SecretFile.Read's rules and seals its contents into a Secret, wiping the intermediate buffer. An absent file returns a nil secret and no error.
func (*SecretFile) ReadStrict ¶
func (f *SecretFile) ReadStrict(limit int64) (ReadOutcome, error)
ReadStrict is SecretFile.Read, but a file that exists and cannot be opened is an error rather than absent. For a caller whose "absent" decides whether a credential is discarded.
func (*SecretFile) Remove ¶
func (f *SecretFile) Remove() (bool, error)
Remove removes the file, returning whether one was there. An absent file is not an error: the same operation re-run, or a peer that removed it first, both land here.
func (*SecretFile) Write ¶
func (f *SecretFile) Write(ctx context.Context, doc []byte, pending *PendingSpec, stop StopPolicy) (WriteOutcome, error)
Write replaces the file atomically with doc.
The sequence is: an exclusive 0600 temporary `<name>.tmp.<8 hex>`, an fsync of the file, the rename, an fsync of the directory. There is no in-place fallback: a truncate-then-write of a credential file is a window in which a crash leaves no credentials at all, so the old file stays in place until the rename replaces it. A failed rename parks the bytes as pending when a spec is given; with nil it is an error and the temporary is removed under either policy because the caller still holds the bytes. StopComplete retains staged bytes only when a requested pending save fails.
ctx is consulted only under StopDiscardStaged, at the one point where abandoning leaves the file exactly as it was found.
func (*SecretFile) WriteWithFaults ¶
func (f *SecretFile) WriteWithFaults(ctx context.Context, doc []byte, pending *PendingSpec, stop StopPolicy, names WriteFaultNames) (WriteOutcome, error)
WriteWithFaults replaces the file with the same rules as Write, using caller-named fault points.
type ServiceEntry ¶
type ServiceEntry struct {
// Service is the svce attribute.
Service string
// Account is the acct attribute, or empty when the item has none.
Account string
// CreatedAt is the cdat attribute as printed, or empty.
CreatedAt string
// ModifiedAt is the mdat attribute as printed, or empty.
ModifiedAt string
}
ServiceEntry is one keychain item, attributes only. Every field but the service name may be empty, because security(1) prints <NULL> for anything unset; an item without a service name is dropped before it becomes an entry at all.
type StagedAdoption ¶
type StagedAdoption struct {
// contains filtered or unexported fields
}
StagedAdoption holds a flushed temporary beside an untouched adopted copy. Always defer Discard after staging: Go does not automatically clean up a value abandoned on an early return. CommitStaged also consumes the staging, on either success or failure. The value must not be copied.
func StageAdopted ¶
func StageAdopted(ctx context.Context, paths *config.Paths, nsDir string, blobJSON *Secret) (*StagedAdoption, error)
StageAdopted writes and flushes a private 0600 temporary without replacing AdoptedFile. It refuses paths outside the namespace, symbolic links and nonregular targets. Cancellation discards the staged file. Plaintext is opened only for the synchronous write. The caller must defer Discard.
func (*StagedAdoption) Discard ¶
func (s *StagedAdoption) Discard()
Discard removes an uncommitted temporary and withdraws emergency cleanup. It is idempotent and safe to race with CommitStaged or emergency cleanup.
type StderrClass ¶
type StderrClass int
StderrClass is one of the ten classes a security(1) stderr message falls into. The order of the constants is the order the classifier tries them in, and both match the vendor tooling this program coexists with; agreeing about the order is agreeing about whether a given failure is transient.
const ( // StderrEmpty means nothing was written to stderr. StderrEmpty StderrClass = iota // StderrDuplicateItem means an item with that service name already // exists. StderrDuplicateItem // opened. StderrKeychainUnavailable // StderrNoKeychain means there is no default keychain. StderrNoKeychain // StderrItemNotFound means no item matched. StderrItemNotFound // StderrInteractionNotAllowed means the item exists but this process // may not be shown it without a prompt. StderrInteractionNotAllowed // StderrUserCanceled means the user dismissed the prompt. StderrUserCanceled // StderrAuthFailed means authentication or authorization failed. StderrAuthFailed // StderrKeychainLocked means the keychain is locked. StderrKeychainLocked // StderrOther is anything else. StderrOther )
func ClassifyStderr ¶
func ClassifyStderr(stderr string) StderrClass
ClassifyStderr classifies a security(1) stderr message. First match wins, tried in the order the StderrClass constants are declared, matching case-insensitively on substrings. Several real messages contain more than one of the substrings below, so the order is load-bearing: it decides which class — and therefore which recovery advice — the user is shown.
func (StderrClass) KeychainClass ¶
func (c StderrClass) KeychainClass() errs.KeychainClass
KeychainClass maps the stderr class onto the user-facing failure class. The three classes the data flow branches on get their named errs value; everything else travels verbatim.
func (StderrClass) String ¶
func (c StderrClass) String() string
String names the class the way the error vocabulary carries it verbatim, so a class that reaches the user through an unnamed errs.KeychainClass reads the same here as in the vendor tooling's reports.
type StopPolicy ¶
type StopPolicy int
StopPolicy says what a write does when its context is cancelled while the replacement is staged but not yet renamed into place.
const ( // StopDiscardStaged abandons a staged replacement when the context was // cancelled: the temporary is unlinked, the old file stays in place, // and the write returns a [WriteCancelledError]. The window between // the fsync and the rename is the last one in which stopping costs // nothing, so an interrupt there must cost nothing. StopDiscardStaged StopPolicy = iota + 1 // StopComplete ignores cancellation once the file is staged, and a // failure afterwards keeps the staged bytes on disk: they may be the // only copy of a server-rotated grant, and deleting them would cost // the user a login. StopComplete )
type SymlinkRefusedError ¶
type SymlinkRefusedError struct {
// Path is where the link was found.
Path string
}
SymlinkRefusedError reports a symbolic link at a credential path, or at a directory on the way to one. Never followed, never overwritten: a link there is somebody else deciding where this store's files live.
func (*SymlinkRefusedError) Error ¶
func (e *SymlinkRefusedError) Error() string
Error names the refused path.
type Target ¶
type Target string
Target says which keychain item an event is about, rendered as one string so a report line, a filter and a human reading the file all see the same token: `live` for the unsuffixed item, `namespace:<sha8>` for a namespaced one.
const TargetLive Target = "live"
TargetLive is the unsuffixed live keychain item.
func NamespaceTarget ¶
NamespaceTarget returns the target token for a namespaced item, by the eight hex digits of its suffix.
type TooLargeError ¶
type TooLargeError struct {
// Path is the offending path.
Path string
// Size is its size in bytes.
Size int64
// Limit is the limit it broke.
Limit int64
}
TooLargeError reports a file larger than its format allows.
func (*TooLargeError) Error ¶
func (e *TooLargeError) Error() string
Error names the path, its size, and the limit.
type Tree ¶
type Tree string
Tree says which credential tree a lock artefact belongs to.
The one spelling of the distinction: the lock protocol records it, the held-lock records carry it, and doctor prints it. The on-disk words are fixed by the store format, so a reader and a writer cannot drift apart.
type Undoable ¶
type Undoable struct {
Kind UndoKind
Line int
SHA8 string
FromDigest8 *string
ToDigest8 string
Outcome KeychainWriteOutcome
Direction WriteDirection
IncomingIdentity *IncomingIdentity
}
Undoable names a reversible write, or why no write can be selected. It contains audit metadata only, never a credential.
func SelectUndo ¶
SelectUndo prefers an outstanding live forward swap over namespace refreshes. The caller must supply the whole log: any unreadable line refuses selection, because skipping a truncated write could reverse the wrong credential.
type UnquotableError ¶
type UnquotableError struct {
// Field is "account" or "service".
Field string
}
UnquotableError reports an account or service name that cannot be written into the quoted keychain line without changing what the line means.
func (*UnquotableError) Error ¶
func (e *UnquotableError) Error() string
Error names the unquotable field.
type UnreachableError ¶
type UnreachableError struct {
// Path is the directory the walk was heading for.
Path string
// Message says what the walk refused, in its own words.
Message string
}
UnreachableError reports that the way from the permitted anchor down to a directory could not be walked without following a symbolic link, or that a component of it is missing or is not a directory.
func (*UnreachableError) Error ¶
func (e *UnreachableError) Error() string
Error implements the error interface.
type UnreadableAuditLine ¶
type UnreadableAuditLine struct {
// Line is the 1-based line number.
Line int
// Reason says why it did not parse.
Reason string
}
UnreadableAuditLine names one line a tail could not read, so doctor can report it instead of hiding it.
type UnrecognizedEvent ¶
type UnrecognizedEvent struct{}
UnrecognizedEvent is an event kind this build does not know: a later build's additive kind, read rather than refused. Read-only: the append refuses to write it, so a line can only ever arrive here from a newer build.
type WriteCancelledError ¶
type WriteCancelledError struct {
// Path is the file that was not replaced.
Path string
}
WriteCancelledError reports a write abandoned while its replacement was staged but not yet renamed into place. The old file is untouched.
func (*WriteCancelledError) Error ¶
func (e *WriteCancelledError) Error() string
Error names the file that kept its old contents.
type WriteDirection ¶
type WriteDirection string
WriteDirection is which way round a swap ran.
const ( // DirectionForward is a forward swap — and any write that is not a // reversal, such as a refresh saved in place. An entry written before // the member existed reads as forward, the conservative reading for // the guard that consults it: it arms rather than disarms. DirectionForward WriteDirection = "forward" // DirectionUndo is a reversal. DirectionUndo WriteDirection = "undo" )
type WriteEvent ¶
type WriteEvent struct {
// Target says which item was written.
Target Target
// FromDigest8 is the first eight hex digits of the outgoing
// credential's access-token digest, or nil when the item did not exist
// — which is what records a first write as a first write.
FromDigest8 *string
// ToDigest8 is the first eight hex digits of the incoming credential's
// access-token digest.
ToDigest8 string
// Outcome says how the write ended.
Outcome KeychainWriteOutcome
// Direction says which way round the swap ran.
Direction WriteDirection
// IncomingIdentity names the account a live write installed, by id
// alone, forward and undo alike; nil on namespace entries and on live
// entries written before the member existed — which is why an entry
// without it refuses rather than being guessed at.
IncomingIdentity *IncomingIdentity
}
WriteEvent is one keychain write.
type WriteFaultNames ¶
WriteFaultNames lets a writer name the pause point and rename failure its tests drive.
type WriteOutcome ¶
type WriteOutcome struct {
// SavedToPending reports that the rename failed and the bytes wait
// under the pending name instead.
SavedToPending bool
// PendingError carries the rename failure for the user-facing row,
// only when SavedToPending.
PendingError string
// Snap identifies the file that now holds the bytes, when the write
// landed.
Snap FileSnapshot
}
WriteOutcome is what a write did: the file was replaced (Snap identifies it), or the rename failed and the bytes were parked as pending.
func WriteCredentials ¶
func WriteCredentials(ctx context.Context, req *WriteRequest) (WriteOutcome, error)
WriteCredentials replaces a namespace's credentials atomically.
The target is checked lexically against the namespace root before anything happens, the namespace directory is reached by the no-follow walk, and the replacement follows SecretFile.Write under StopDiscardStaged: a cancelled context abandons the staged file, and a failed rename parks the bytes as pending rather than failing.
type WriteRequest ¶
type WriteRequest struct {
// Paths is this store, for the namespace-root check.
Paths *config.Paths
// NSDir is the namespace directory to write into.
NSDir string
// BlobJSON is the serialized credential document. The caller builds it
// through the secret's explicit exposure path and wipes it afterwards.
BlobJSON []byte
// Prior holds the digests of the file these credentials were derived
// from, nil on a first write. Recorded in the pending metadata.
Prior *Digests
// NewExpiresAtMS is the new access-token expiry, recorded in the
// pending metadata.
NewExpiresAtMS int64
// contains filtered or unexported fields
}
WriteRequest is everything one credential write needs.
func (WriteRequest) Format ¶
func (WriteRequest) Format(state fmt.State, _ rune)
Format redacts every formatting verb and flag.
func (WriteRequest) GoString ¶
func (WriteRequest) GoString() string
GoString returns a redacted Go-syntax representation.
func (WriteRequest) LogValue ¶
func (WriteRequest) LogValue() slog.Value
LogValue returns a redacted structured-log value.
func (WriteRequest) MarshalJSON ¶
func (WriteRequest) MarshalJSON() ([]byte, error)
MarshalJSON returns a redacted JSON string.
func (WriteRequest) MarshalJSONTo ¶
func (WriteRequest) MarshalJSONTo(encoder *jsontext.Encoder) error
MarshalJSONTo writes a redacted JSON string through the streaming hook.
func (WriteRequest) String ¶
func (WriteRequest) String() string
String returns a redacted representation of the write request.
type WriteWindowClosedError ¶
type WriteWindowClosedError struct {
// Elapsed is how long the hold had lasted.
Elapsed time.Duration
// Budget is the hold budget the write had to fit inside.
Budget time.Duration
}
WriteWindowClosedError reports that too little of the hold budget remains for a write to start: a write takes up to the configuration hold budget, and starting one that cannot finish inside the hold budget would hand the peer a lock past its own give-up.
func (*WriteWindowClosedError) Error ¶
func (e *WriteWindowClosedError) Error() string
Error implements the error interface.
type WrongTreeError ¶
type WrongTreeError struct {
// StoreDir is the store directory as the caller spelled it.
StoreDir string
// Tree is the tree the caller claimed it was in.
Tree Tree
}
WrongTreeError reports that the store directory is not in the tree the caller named, so the hold — and any break inside it — would land somewhere the containment rule does not allow.
func (*WrongTreeError) Error ¶
func (e *WrongTreeError) Error() string
Error implements the error interface.
Source Files
¶
- adopted_store.go
- audit.go
- audit_swap.go
- claude_lock.go
- claude_lock_acquire.go
- claude_lock_anchor.go
- claude_lock_clock.go
- claude_lock_fs.go
- claude_lock_record.go
- claude_lock_stale.go
- config_lock.go
- doc.go
- file_store.go
- foreign_activity.go
- held_lock.go
- keychain_backend_release.go
- keychain_line.go
- keychain_write.go
- location.go
- namespace_lock.go
- namespace_lock_body.go
- namespace_lock_release.go
- payload_redaction.go
- pending.go
- purge.go
- reader.go
- secret.go
- secret_budget.go
- secret_file.go
- security_cli.go
- security_cli_dump.go