Files
mesh-controller/internal/inventory/secret_ask.go
T
jochen c97942d591
mesh/merge-gate pass: builds build-agent, mesh-controller → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without …
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
A module's own secret is asked for by a verb and typed at the desk, bounded, the prompt naming who asked (hq ADR 0277)
The give verb opened the desk's hidden prompt for a module's own secret, with the desk named. Now the
secret-ask verb (secret ask <node> <module> <name> [--at <desk>]) opens the same prompt from anywhere on
the mesh, with the desk the module's machine unless named, and give composes the same line. Every ask
is recorded in the store before the prompt opens (migration 0091): one open ask per secret, three an
hour, so an agent cannot keep a prompt in front of the operator. The prompt names who asked, from the
bus's word on the caller cut to a name's characters, never an argument of the call. The value stays
typed at the desk, sealed to the one call and then to the module's machine, never in an argument, a
log or an event; a secret the mesh makes itself and a trusted party's secret are refused as before.
2026-10-10 15:24:08 +02:00

111 lines
4.1 KiB
Go

package inventory
import (
"context"
"fmt"
"time"
)
// An ask for a module's own secret at a desk (novox/hq ADR 0277, migration 0091): every one is recorded
// before the prompt opens, and the bounds are read from the record. A verb may ask the operator to type a
// secret; it may not keep a prompt in front of them. **One open ask per secret, and few per hour.**
// The bounds of asking for one secret.
const (
// SecretAskOpenFor is how long an ask that has not ended counts as open: longer than any prompt waits,
// so a process that died with its prompt does not hold the secret for ever.
SecretAskOpenFor = 2 * time.Minute
// SecretAsksPerHour is how many asks for one secret an hour takes.
SecretAsksPerHour = 3
)
// OpenSecretAsk records that an ask for a module's own secret on a machine opens at a desk, or refuses it in
// words when one is still open for that secret or the hour's asks are spent. It answers the record's id, which
// EndSecretAsk closes.
func (i *Inventory) OpenSecretAsk(ctx context.Context, node, module, name, askedBy, desk string) (int64, error) {
record, err := i.NodeByName(ctx, node)
if err != nil {
return 0, err
}
tx, err := i.store.Pool().Begin(ctx)
if err != nil {
return 0, err
}
defer func() { _ = tx.Rollback(context.WithoutCancel(ctx)) }()
// Serialised per secret, so two asks at once do not both pass the count.
if _, err := tx.Exec(ctx, `select pg_advisory_xact_lock(hashtext($1))`, node+"/"+module+"/"+name); err != nil {
return 0, err
}
var open int
var openBy string
if err := tx.QueryRow(ctx,
`select count(*), coalesce(min(asked_by), '') from secret_ask
where node = $1 and module = $2 and name = $3 and ended_at is null and opened_at > now() - $4::interval`,
record.ID, module, name, SecretAskOpenFor.String()).Scan(&open, &openBy); err != nil {
return 0, err
}
if open > 0 {
return 0, fmt.Errorf("an ask for %s of %s on %s is still open (asked by %s): one prompt at a time for a secret, "+
"and this one ends within %s", name, module, node, openBy, SecretAskOpenFor)
}
var lastHour int
if err := tx.QueryRow(ctx,
`select count(*) from secret_ask
where node = $1 and module = $2 and name = $3 and opened_at > now() - interval '1 hour'`,
record.ID, module, name).Scan(&lastHour); err != nil {
return 0, err
}
if lastHour >= SecretAsksPerHour {
return 0, fmt.Errorf("%s of %s on %s was asked for %d times in the last hour, and an hour takes %d asks for one "+
"secret: the operator is not kept at a prompt", name, module, node, lastHour, SecretAsksPerHour)
}
var id int64
if err := tx.QueryRow(ctx,
`insert into secret_ask (node, module, name, asked_by, desk) values ($1, $2, $3, $4, $5) returning id`,
record.ID, module, name, askedBy, desk).Scan(&id); err != nil {
return 0, err
}
return id, tx.Commit(ctx)
}
// EndSecretAsk closes an ask with how it ended: given, dismissed, timed-out, empty, refused or failed.
func (i *Inventory) EndSecretAsk(ctx context.Context, id int64, outcome string) error {
_, err := i.store.Pool().Exec(ctx,
`update secret_ask set ended_at = now(), outcome = $2 where id = $1 and ended_at is null`, id, outcome)
return err
}
// SecretAsk is one recorded ask for a module's own secret.
type SecretAsk struct {
ID int64
Node string
Module string
Name string
AskedBy string
Desk string
OpenedAt time.Time
EndedAt *time.Time
Outcome string
}
// SecretAsks is every ask for secrets since a moment, newest first.
func (i *Inventory) SecretAsks(ctx context.Context, since time.Time) ([]SecretAsk, error) {
rows, err := i.store.Pool().Query(ctx,
`select a.id, n.name, a.module, a.name, a.asked_by, a.desk, a.opened_at, a.ended_at, coalesce(a.outcome, '')
from secret_ask a join node n on n.id = a.node
where a.opened_at >= $1 order by a.opened_at desc`, since)
if err != nil {
return nil, err
}
defer rows.Close()
var out []SecretAsk
for rows.Next() {
var a SecretAsk
if err := rows.Scan(&a.ID, &a.Node, &a.Module, &a.Name, &a.AskedBy, &a.Desk, &a.OpenedAt, &a.EndedAt, &a.Outcome); err != nil {
return nil, err
}
out = append(out, a)
}
return out, rows.Err()
}