D261 — service_get redacts resolved secrets unless reveal, and the audit records reveal¶
Decision. service_get(resolve_secrets: true) has the same contract as
secret_resolve (D158): it lists the key names with <redacted> values, and a
new boolean reveal: true returns the values. reveal without
resolve_secrets is a no-op, not an error, and adds no authorization check —
resolve_secrets still requires rw. The audit trail records allow-listed
boolean arguments as "true"/"false", so secret_resolve's reveal and
service_get's resolve_secrets and reveal now reach it. The dead
sops.EnvForSkill is deleted. rw scope on a KB is documented as authority
over every secret that KB can decrypt. Amends D158.
Why. D158 left service_get printing values because "its callers are the
skill-execution path, not an agent transcript". That path did not exist:
EnvForSkill had no caller outside its own test, and the only caller of
service_get is an MCP client — the exact leak D158 closed on the sibling tool.
D158 also said reveal "is recorded in the audit trail", but extractResources
kept only string values, so the boolean was dropped silently: the one event the
decision wanted audited left no trace. The cost: a caller that parsed values out
of service_get(resolve_secrets: true) breaks until it passes reveal: true.
Alternatives rejected.
- Keep service_get in clear, fix only the audit — leaves the safe default as
an opt-in on one of the two tools that print credentials.
- Error on reveal without resolve_secrets — nothing is revealed either way;
a refusal would only make a harmless flag combination fail.
- Record every boolean argument in the audit — the allow-list is the only gate
on what reaches the trail; a type-based rule would admit fields nobody chose.
- Wire EnvForSkill into a skill-execution path — it would build a feature to
justify the exemption instead of removing the exemption.
- A dedicated secrets scope (kb:<name>:secrets) — rw can already
concept_write a secret_refs entry pointing at any
secrets/*.sops.yaml#/pointer, so secret_refs bound what is returned, not
who may read. A separate scope is real access-control work (it would have to
cover concept_write of secret_refs too) that today's single-operator
deployments do not need; the boundary is stated instead.
Consequences. Every tool that decrypts a secret redacts unless reveal: true;
a new one must follow the same contract and list its revealing flag in
auditResourceFields. The audit allow-list may now name boolean fields; secret
key names (names, secret_set's key) and values still never enter it. A KB
whose secrets must be readable by fewer principals than may edit it needs its own
KB, key and token. sops.validatePath checks a path before sops opens it; the
window lets a writer on the KB filesystem swap a component, which is accepted
because that writer can already rewrite the KB.