Take accept, secret-ask, issued and rotate into the vault (issue 675)
ADR 0349 clauses 8 to 10: a value given at the controller's terminal, typed at a desk or issued by a provider's software enters the vault and opens there alone, and rotation is the vault's. Each comes in two steps from the controller, which reaches the desk and the provider: the vault makes a key for the one call and opens what was sealed to it. An ask is recorded in the vault's store before the prompt opens and bounded to one open per secret and three an hour. Also V1's review follow-ups: the tool layer passes an act through, a rolled record is said, the export's fingerprint and vault_status are tested, and an unproven by is kept beside the proven caller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,12 @@ type Record struct {
|
||||
MadeAt time.Time `json:"made-at"`
|
||||
ChangedAt time.Time `json:"changed-at"`
|
||||
ChangedBy string `json:"changed-by"`
|
||||
// ChangedVia is the caller the runtime proved, when it is not who ChangedBy names: the controller
|
||||
// carrying a person's act (design 57's V2). Empty when they are the same.
|
||||
ChangedVia string `json:"changed-via,omitempty"`
|
||||
// Origin is where the value came from: made by the vault, accepted at the controller's terminal, typed at
|
||||
// a desk, or issued by a provider's software (ADR 0349 clauses 8 to 10). Empty is made, as V1 wrote it.
|
||||
Origin string `json:"origin,omitempty"`
|
||||
}
|
||||
|
||||
// OperatorCopy is a value sealed to the operator key named in Key.
|
||||
@@ -58,6 +64,8 @@ type Store struct {
|
||||
|
||||
handedMu sync.Mutex
|
||||
rolled int
|
||||
// asksMu serialises the record of asks, so two asks for one secret at once do not both pass its bounds.
|
||||
asksMu sync.Mutex
|
||||
}
|
||||
|
||||
// The record of hand-outs is bounded: rolled at HandedMaxDefault into handed.jsonl.1 … .HandedKept, the
|
||||
@@ -160,6 +168,7 @@ func (s *Store) RecordHanding(h Handing) (rolled bool, err error) {
|
||||
if err := s.roll(path); err != nil {
|
||||
return false, err
|
||||
}
|
||||
s.rolled++
|
||||
rolled = true
|
||||
}
|
||||
return rolled, appendSynced(path, append(raw, '\n'))
|
||||
@@ -176,7 +185,6 @@ func (s *Store) roll(path string) error {
|
||||
if err := os.Rename(path, path+".1"); err != nil {
|
||||
return err
|
||||
}
|
||||
s.rolled++
|
||||
return syncDir(s.dir)
|
||||
}
|
||||
|
||||
@@ -334,3 +342,99 @@ func syncDir(dir string) error {
|
||||
defer d.Close()
|
||||
return d.Sync()
|
||||
}
|
||||
|
||||
// The two lines an ask leaves in the record: when it opened, and how it ended.
|
||||
const (
|
||||
AskOpened = "opened"
|
||||
AskEnded = "ended"
|
||||
)
|
||||
|
||||
// AskRecord is one line of the record of asks for a module's own secret at a desk (ADR 0277 §25, in the vault's
|
||||
// store since ADR 0349 clause 9): who asked, as the bus named them, for what, at which desk, and how it ended.
|
||||
// Never a value.
|
||||
type AskRecord struct {
|
||||
ID string `json:"id"`
|
||||
At time.Time `json:"at"`
|
||||
Event string `json:"event"`
|
||||
Secret string `json:"secret"`
|
||||
Node string `json:"node,omitempty"`
|
||||
Module string `json:"module,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Desk string `json:"desk,omitempty"`
|
||||
AskedBy string `json:"asked_by,omitempty"`
|
||||
Caller string `json:"caller,omitempty"`
|
||||
Outcome string `json:"outcome,omitempty"`
|
||||
}
|
||||
|
||||
// RecordAsk appends one line to the record of asks, synced before it returns; rolled as the record of
|
||||
// hand-outs is, and it says whether it was.
|
||||
func (s *Store) RecordAsk(r AskRecord) (rolled bool, err error) {
|
||||
raw, err := json.Marshal(r)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
path := filepath.Join(s.dir, "asks.jsonl")
|
||||
if info, err := os.Stat(path); err == nil && info.Mode().IsRegular() && info.Size()+int64(len(raw))+1 > s.HandedBound() {
|
||||
if err := s.roll(path); err != nil {
|
||||
return false, err
|
||||
}
|
||||
rolled = true
|
||||
}
|
||||
return rolled, appendSynced(path, append(raw, '\n'))
|
||||
}
|
||||
|
||||
// asks is every line of the record of asks, oldest first, the rolled files before the current one.
|
||||
func (s *Store) asks() ([]AskRecord, error) {
|
||||
path := filepath.Join(s.dir, "asks.jsonl")
|
||||
var out []AskRecord
|
||||
for i := HandedKept; i >= 0; i-- {
|
||||
file := path
|
||||
if i > 0 {
|
||||
file = fmt.Sprintf("%s.%d", path, i)
|
||||
}
|
||||
raw, err := os.ReadFile(file)
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(raw)), "\n") {
|
||||
var r AskRecord
|
||||
if line != "" && json.Unmarshal([]byte(line), &r) == nil {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// AsksSince is every line of the record of asks at or after since, oldest first.
|
||||
func (s *Store) AsksSince(since time.Time) ([]AskRecord, error) {
|
||||
all, err := s.asks()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []AskRecord
|
||||
for _, r := range all {
|
||||
if !r.At.Before(since) {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Asks is the latest lines of the record of asks, newest first, at most limit.
|
||||
func (s *Store) Asks(limit int) ([]AskRecord, error) {
|
||||
s.asksMu.Lock()
|
||||
defer s.asksMu.Unlock()
|
||||
all, err := s.asks()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := []AskRecord{}
|
||||
for i := len(all) - 1; i >= 0 && len(out) < limit; i-- {
|
||||
out = append(out, all[i])
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
@@ -35,6 +35,9 @@ type Vault struct {
|
||||
Log func(format string, args ...any)
|
||||
|
||||
mu sync.Mutex
|
||||
// callsMu guards pending: the keys of the calls open now (intake.go), never written anywhere.
|
||||
callsMu sync.Mutex
|
||||
pending map[string]*callKey
|
||||
}
|
||||
|
||||
// Changed is what one change to a secret was. Never its value.
|
||||
@@ -65,6 +68,12 @@ func (v *Vault) log(format string, args ...any) {
|
||||
// Refused: a private key (it stays where it was made), and any value while the vault has no operator key —
|
||||
// a secret only the vault could open is what ADR 0113 §22 rules out.
|
||||
func (v *Vault) Put(name, value, by string) (Changed, error) {
|
||||
return v.keep(name, value, by, "", OriginMade)
|
||||
}
|
||||
|
||||
// keep is Put with where the value came from (origin) and the caller the runtime proved when it is not who
|
||||
// by names (via).
|
||||
func (v *Vault) keep(name, value, by, via, origin string) (Changed, error) {
|
||||
if err := validName(name); err != nil {
|
||||
return Changed{}, err
|
||||
}
|
||||
@@ -104,9 +113,12 @@ func (v *Vault) Put(name, value, by string) (Changed, error) {
|
||||
return Changed{}, err
|
||||
}
|
||||
now := v.Now().UTC()
|
||||
if via == by {
|
||||
via = ""
|
||||
}
|
||||
rec := Record{Name: name, Generation: 1, AtRest: atRest,
|
||||
Operator: OperatorCopy{Box: OperatorBox, Key: operator, Sealed: forOperator},
|
||||
MadeAt: now, ChangedAt: now, ChangedBy: by}
|
||||
MadeAt: now, ChangedAt: now, ChangedBy: by, ChangedVia: via, Origin: origin}
|
||||
if held {
|
||||
rec.Generation, rec.MadeAt = before.Generation+1, before.MadeAt
|
||||
}
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
"secret_holders",
|
||||
"secret_verify",
|
||||
"secret_handed",
|
||||
"secret_asks",
|
||||
"secret_vault_export",
|
||||
"provisioner_retirement",
|
||||
"provisioner_retire_approve",
|
||||
@@ -124,7 +125,11 @@
|
||||
"name": "mesh-vault",
|
||||
"scope": "mesh",
|
||||
"serves": [
|
||||
"get"
|
||||
"get",
|
||||
"accept",
|
||||
"secret-ask",
|
||||
"issued",
|
||||
"rotate"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user