feat(credentials): move the store to .credentials.yaml and layer $DSH_HOME/.env
$DSH_HOME/.env carried two incompatible jobs. As credentials-local's writable secret store it could not be hoisted into process.env — hoisting makes every stored key read as a read-only launch override and blocks rotation from the TUI and the web page. But its name and dotenv format promise an environment file, so a DEEPSEEK_BASE_URL sitting beside a working DEEPSEEK_API_KEY in the same file was silently ignored: only the credential provider read the document, and it addresses credential references alone. Split the two jobs into two files. .credentials.yaml is the provider-managed store: a strict YAML mapping of CredentialRef to non-empty string, no version field, no wrapper level. Because it holds credentials and nothing else, a non-mapping root, a non-identifier key, a non-string value, an empty string, a duplicate key, and malformed YAML are all rejections rather than skipped entries — loud at boot and at a write, warn-and-keep-last-good on a live reload. The dotenv physical-line editor gives way to a patch of the parsed document, so comments and untouched entries keep their formatting and any string value round-trips, multi-line included. Writer lock, read-modify-write, atomic 0600 write under a 0700 directory, watcher, self-write suppression, and quiescent disposal are unchanged. $DSH_HOME/.env becomes the user's ordinary environment layer. app-boot's new loadLayeredEnv loads the invoking directory's .env then the Harness home's, giving user < project < inherited; the home resolves from the inherited environment first, so a project .env cannot redirect it. Credential precedence is unchanged: the live environment still wins read-only over the file, and shadowed writes still reject. Whether a provider-managed store should instead win over the environment is a separate decision. No migration: a key already in $DSH_HOME/.env keeps resolving through the new environment layer, as a read-only env source that shadows the stored one.
This commit is contained in:
+1
-1
@@ -14,7 +14,7 @@ Two request-path defects sat beside them. DeepSeek's per-request resolution kept
|
||||
|
||||
## Decision
|
||||
|
||||
**`$DSH_HOME/.env` belongs to the credential provider alone.** No surface loads it into `process.env`. The genuine launch environment and the invoking directory's `.env` (loaded by the bin) stay the read-only ambient layer, so a composition without the provider resolves keys exactly as before, while a stored key stays file-sourced and writable across restarts — proven by a real restart in the loader composition rather than by a unit assertion about `describe()`.
|
||||
**The credential document belongs to the credential provider alone.** No surface loads it into `process.env`. It was `$DSH_HOME/.env` here; the [credentials document split](2026-08-04-credentials-yaml-and-user-environment-layer.md) later moved it to `$DSH_HOME/.credentials.yaml`, so today it is the old path that is loaded — as the user's ordinary environment layer, holding no provider-managed secret. The genuine launch environment and the invoking directory's `.env` (loaded by the bin) stay the read-only ambient layer, so a composition without the provider resolves keys exactly as before, while a stored key stays file-sourced and writable across restarts — proven by a real restart in the loader composition rather than by a unit assertion about `describe()`.
|
||||
|
||||
**The stored credential has no boundary against the model, and the READMEs say so.** `0600` under a `0700` directory stops other OS users; the model's bash and filesystem tools run as that same user, and the shipped default confines nothing. What the harness does hold to is narrower and stated as exactly that: no surface hoists the document into `process.env`, and the model is never handed a resolved path to it, so reaching the value takes a deliberate read of a path it was not given. An OS-keychain provider — a store the model's processes cannot read at all — is recorded as the real answer rather than implied by a partial one.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user