| name | naming |
| description | Naming rules for sagent (single-package Rust secret daemon). Module layout (src/auth.rs, sys/, source/, cache.rs, daemon/, proto.rs, log.rs, main.rs), snake_case files, canonical type names per module (SecretBytes, SecretSource, OpCli, KeychainCli, Lease, AuditToken, CdHash, VerifiedPeer, Cache, Daemon, Frame, Reference), trait suffix convention (no -Trait/-able), newtype and constant discipline in the wire and auth layer, blocked alternative names. Read before creating a new public type, renaming an existing one, or reviewing a PR for naming compliance. |
| disable-model-invocation | true |
Naming Rules (sagent)
Strict patterns. Reviews cite this file when a PR blocks on naming.
sagent is ONE package, no workspace. There are no crate naming rules. Naming covers modules, files, types, functions and constants inside src/.
File and module layout
snake_case everywhere: no kebab-case, no CamelCase files. A module starts as a single file; when it grows it becomes <module>.rs plus a <module>/ directory for submodules and a split-out tests.rs (the current shape of auth, daemon, source, op_item).
| Module | Responsibility |
|---|
src/main.rs | Entry point, arg dispatch, CLI path (get/get-many/status/flush/daemon) |
src/daemon.rs + src/daemon/ | Long-running custodian: socket bind (socket.rs), request dispatch (dispatch.rs), policy (policy.rs) |
src/auth.rs | Peer authentication (cdhash self-pin) at the kernel boundary, TOCTOU recheck |
src/sys/ | macOS syscall FFI and filesystem hygiene. ALL unsafe lives here; spawn.rs holds the Team-ID-pinned suspended spawn |
src/source.rs + src/source/ | Secret sources: SecretSource trait plus concrete backends |
src/cache.rs | In-memory TTL cache with two-tier expiry |
src/secret.rs | Zeroizing container for plaintext bytes |
src/proto.rs | Wire protocol: frames, opcodes, status codes |
src/reference.rs | Validated secret reference newtype |
src/op_item.rs + src/op_item/ | op item JSON handling for get-many |
src/log.rs | Minimal logging (never secret values) |
New file: snake_case, one primary public type per file, filename = snake_case(TypeName) when there is a clear owner type. Without a single owner (free functions, a constant group): domain name (auth.rs, proto.rs).
unsafe belongs in src/sys/. A new file introducing raw FFI or raw syscalls is a BLOCK: concentrate the unsafe in src/sys/ and let the rest of the code call the safe wrapper.
Pass: rg --files src/ shows only snake_case names without hyphens or capitals.
Block: a file in src/ with capitals or hyphens.
Canonical type names
A diverging spelling in new code is a BLOCK. The list follows the source files.
Secret container (src/secret.rs)
| Name | Kind |
|---|
SecretBytes | struct (newtype over Vec<u8>, wiped on drop) |
SecretBytes has exactly one read path (expose(&self) -> &[u8]) and exactly one construction path (from_vec(Vec<u8>) -> Self, no realloc). No Clone, no Debug/Display of the bytes, no serialization. Whoever adds a second read method justifies it in the PR body: every extra read path widens the audit surface.
Blocked: Secret, SecretBuf, SecretVec, Zeroizing, SensitiveBytes, SecureBytes, Plaintext.
Secret sources (src/source.rs, src/source/keychain.rs)
| Name | Kind |
|---|
SecretSource | trait (name, validate, fetch_capped) |
OpCli | struct (1Password via the op CLI, Team-ID-pinned) |
KeychainCli | struct (macOS Keychain via the security CLI) |
Lease | struct (cache policy returned per fetch) |
A new backend is a new type implementing SecretSource, registered in Daemon::new. Naming shape: the type names the mechanism, with a Cli suffix when it shells out to a CLI (OpCli, KeychainCli); no Source suffix, because SecretSource already carries the role. Constructors that probe for the tool are called discover().
Lease is the policy a source returns with a freshly fetched secret. Not CachePolicy, not a Ttl struct: a lease (validity bounded in time) is the established term, and the shipped sources return no-cache leases.
Blocked: Source (too generic without the Secret prefix), Provider, Backend as a public trait name, OpSource, OnePasswordSource, OnePassword, KeychainSource, CachePolicy/Ttl/Lifetime instead of Lease.
Auth (src/auth.rs)
| Name | Kind |
|---|
verify_fd | fn (verifies the peer at the kernel boundary) |
read_request | fn (reads the frame with the before/after principal recheck) |
same_principal | fn (compares two tokens on pid plus pidversion) |
VerifiedPeer | struct (proof of a passed check) |
AuthPhase | enum (which phase a rejection happened in) |
RejectReason | struct (human-readable, log-safe deny reason) |
RequestError | enum (auth or framing failure while reading) |
AuditBoundary | trait (seam over the libc calls; LibcBoundary is the production impl) |
Blocked: Authn, Verifier, a bare Token (collides with the audit token in meaning).
Syscall FFI (src/sys/)
| Name | Kind |
|---|
AuditToken | struct (#[repr(C)], kernel-attested peer identity) |
CdHash | type alias ([u8; 20], code directory hash) |
SpawnedChild | struct (spawn.rs, suspended child with kill-on-drop group) |
Code-signing flags are const u32 with the kernel names: CS_VALID, CS_RUNTIME, CS_GET_TASK_ALLOW, CS_OPS_*. No renamed aliases: the name must be findable in the <sys/codesign.h> header. extern "C" bindings carry the real libc or Mach symbol name (csops, csops_audittoken, getsockopt), no Rust-ified renaming.
Blocked: Token without the Audit prefix, CodeHash/Hash/CDHash (wrong casing) instead of CdHash, renamed CS flag constants.
Cache (src/cache.rs)
| Name | Kind |
|---|
Cache | struct (in-memory map, two-tier TTL) |
Entry | struct (private: secret plus timestamps plus TTLs) |
Entry stays private. If the cache ever becomes persistent or gains a second backend, a trait name arrives then, not speculatively now. Blocked: SecretCache (redundant, the package only handles secrets), Store (implies persistence), TtlMap, CacheEntry (prefix redundant to the module).
Daemon (src/daemon.rs, src/daemon/)
| Name | Kind |
|---|
Daemon | struct (cache plus sources plus own cdhash/status) |
run | fn (entry point: daemonize, lock, bind, accept loop) |
PolicyDecision | enum (policy.rs: what an insecure-dev daemon may serve) |
Request handlers are named handle_<verb> in dispatch.rs (handle_get, handle_get_many, handle_status, handle_flush). Connection limits and timeouts are named constants (MAX_CONNS, CONN_TIMEOUT), no magic numbers in the loop. Blocked: Server, Agent, Custodian as a type name (that is the role in the doc comment, not the type).
Wire protocol (src/proto.rs)
| Name | Kind |
|---|
Frame | struct (tag: u8, payload: Vec<u8>) |
SecretFrame | struct (response frame that owns secret bytes) |
ResponseBudget | struct (caps the response size) |
read_frame / write_frame | fn |
Opcodes are const u8 with the OP_ prefix (OP_GET, OP_GET_MANY, OP_STATUS, OP_FLUSH). Status codes are const u8 with the ST_ prefix (ST_OK, ST_ERR, ST_DENIED). The frame size cap is a named const (MAX_PAYLOAD), no inline number.
A new opcode: OP_<verb>, a matching handler handle_<verb> in daemon/dispatch.rs, a matching CLI verb in main.rs. Three places, the same verb. Blocked: opcode constants without the OP_ prefix, status without the ST_ prefix, a Frame that carries more than tag plus payload (the protocol is deliberately a one-shot round trip).
References and op items (src/reference.rs, src/op_item.rs)
| Name | Kind |
|---|
Reference | struct (validated secret reference, MAX_REFERENCE_LEN cap) |
OpRef | struct (op_item.rs: one resolvable op:// item field) |
OpField | struct (op_item/parser.rs: parsed field from op item JSON) |
Trait suffix convention
No -Trait or -able suffix. Canonical examples are SecretSource and AuditBoundary. A PR renaming SecretSource to SecretSourceTrait or Sourceable is a BLOCK. New traits: a noun that reads naturally at the impl site.
Pass: rg "trait \w+Trait\b" src/ shows 0 hits.
Block: a public trait name with a Trait/able suffix when the base is already a noun.
Newtype and constant discipline
- Plaintext bytes live ONLY in
SecretBytes, never as a bare Vec<u8> or String across module boundaries. A new field or return that carries resolved secret bytes as Vec<u8> is a BLOCK, unless it is the one construction path (SecretBytes::from_vec).
- Security-relevant limits are named constants with a speaking name, no magic literal:
MAX_PAYLOAD (proto.rs), MAX_SECRET_BYTES, MAX_STDERR_BYTES (source.rs), MAX_CONNS, CONN_TIMEOUT (daemon.rs), MAX_REFERENCE_LEN (reference.rs), MAX_OP_ITEMS, MAX_JSON_DEPTH (op_item). A new cap arrives as a const with a MAX_ prefix, not as a number in an expression.
- Env var keys follow
SAGENT_<UPPER_SNAKE> (SAGENT_INSECURE_DEV, SAGENT_TEST_RUNTIME_DIR). No prefix-less key, no other spelling.
Pass: rg ": Vec<u8>" src/ on secret-bearing fields shows only the from_vec path. rg '"SAGENT_' src/ shows only SAGENT_-prefixed keys.
Blocked alternative names (master list)
In new code: BLOCK. Falsifiable via one rg scan:
rg "\b(SecretBuf|SecretVec|Zeroizing|Plaintext|Provider|OpSource|OnePasswordSource|KeychainSource|CachePolicy|SecretCache|TtlMap|CacheEntry|Server|Agent|Custodian|Verifier|Authn|CodeHash|CDHash)\b" src/
shows 0 public type hits (doc comments and strings exempt).