545 lines
22 KiB
Go
545 lines
22 KiB
Go
package main
|
|
|
|
// What the vault serves: the seat's verb `get`, and the three tools the TypeScript vault offered —
|
|
// secret_export, secret_holders and secret_verify — answering as they did; beside them secret_handed and
|
|
// secret_vault_export over the vault's own store, and ADR 0230's four retirement tools over the custody
|
|
// ledger, and vault_status. None returns a value: `get` returns one sealed to the asker's key, the others
|
|
// fingerprints and ciphertext only.
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
stdio "git.novox.be/novox/mesh-sdk/go"
|
|
)
|
|
|
|
// Env is what the tools work on. A part the module's words did not give is nil, and a tool that needs it
|
|
// says so.
|
|
type Env struct {
|
|
Vault *Vault
|
|
Ledger *Ledger
|
|
Receives string
|
|
Root string
|
|
Custody *Custody
|
|
// refused is why the operator key in the mesh's export was not taken, or empty.
|
|
mu sync.Mutex
|
|
refused string
|
|
// Broken is why the vault could not be built, said by `get`.
|
|
Broken error
|
|
}
|
|
|
|
func (e *Env) keyRefused() string {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
return e.refused
|
|
}
|
|
|
|
func (e *Env) refuseKey(why string) {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
e.refused = why
|
|
}
|
|
|
|
// toolDef is a tool with its caller given apart from its arguments, so a test can be the runtime.
|
|
type toolDef struct {
|
|
Name string
|
|
Description string
|
|
Input map[string]any
|
|
run func(caller string, a map[string]any) (any, error)
|
|
}
|
|
|
|
// Run answers a call the runtime made: the caller is the one it proved.
|
|
func (t toolDef) Run(a map[string]any) (any, error) { return t.run(stdio.CallerOf(a), a) }
|
|
|
|
func (t toolDef) runAs(caller string, a map[string]any) (any, error) { return t.run(caller, a) }
|
|
|
|
func str(description string) map[string]any {
|
|
return map[string]any{"type": "string", "description": description}
|
|
}
|
|
|
|
func definitions(env *Env) []toolDef {
|
|
return []toolDef{
|
|
{Name: Seat + ".get",
|
|
Description: "One secret, sealed to the public key the asker sends (the sealed box of ADR 0206), with its " +
|
|
"generation; answered only to an asker the vault allows, as the runtime proved it (ADR 0349 §11). " +
|
|
"Never the value in the clear, and never to a private key.",
|
|
Input: map[string]any{"name": str("the secret's name"),
|
|
"public_key": str("the asker's X25519 public key as SPKI PEM — never a private key")},
|
|
run: func(caller string, a map[string]any) (any, error) {
|
|
if env.Vault == nil {
|
|
if env.Broken != nil {
|
|
return nil, env.Broken
|
|
}
|
|
return nil, errors.New("the vault is not ready")
|
|
}
|
|
name, _ := a["name"].(string)
|
|
key, _ := a["public_key"].(string)
|
|
return env.Vault.Get(caller, strings.TrimSpace(name), key)
|
|
}},
|
|
{Name: Seat + ".accept",
|
|
Description: "Take a value given at the controller's terminal (ADR 0349 clause 8), in two steps from the " +
|
|
"controller alone: name the secret (node, module and name for a module's own; provider and provision " +
|
|
"for a provision's one credential) and get a call and a key made for it; then send the call and the " +
|
|
"value sealed to that key. The vault alone opens it, and never remakes an accepted credential.",
|
|
Input: intakeInput(map[string]string{"node": "the machine, for a module's own secret",
|
|
"module": "the module, for a module's own secret", "name": "the secret's name in the module",
|
|
"provider": "the provider's machine, for a provision's credential", "provision": "the provision",
|
|
"by": "who gave it at the terminal, as the controller names them"}),
|
|
run: func(caller string, a map[string]any) (any, error) {
|
|
v, err := env.ready()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if call := arg(a, "call"); call != "" {
|
|
return v.AcceptTake(caller, call, arg(a, "sealed"))
|
|
}
|
|
secret, err := secretNamed(a)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return v.AcceptOpen(caller, secret, arg(a, "by"))
|
|
}},
|
|
{Name: Seat + ".secret-ask",
|
|
Description: "Ask the operator for a module's own secret at a desk (ADR 0349 clause 9, ADR 0277 §22–§27), in " +
|
|
"two steps from the controller alone, which reaches the desk's prompt: node, module, name, desk and " +
|
|
"asked_by open the ask — recorded before the prompt opens, one open ask per secret and three an " +
|
|
"hour — and answer the key the prompt seals to and who asked as the record names them; then the call " +
|
|
"with what the prompt sealed, or with how it ended (dismissed, timed-out, empty, refused, failed). " +
|
|
"What is typed opens in the vault alone.",
|
|
Input: intakeInput(map[string]string{"node": "the machine the secret is for", "module": "the module",
|
|
"name": "the secret's name in the module", "desk": "the machine whose prompt asks",
|
|
"asked_by": "who asked the controller, as the bus named its caller",
|
|
"outcome": "instead of sealed: how the prompt ended — dismissed, timed-out, empty, refused or failed"}),
|
|
run: func(caller string, a map[string]any) (any, error) {
|
|
v, err := env.ready()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if call := arg(a, "call"); call != "" {
|
|
return v.AskEnd(caller, call, arg(a, "sealed"), arg(a, "outcome"))
|
|
}
|
|
return v.AskOpen(caller, AskOf{Node: arg(a, "node"), Module: arg(a, "module"), Name: arg(a, "name"),
|
|
Desk: arg(a, "desk"), AskedBy: arg(a, "asked_by")})
|
|
}},
|
|
{Name: Seat + ".issued",
|
|
Description: "Take the credential a provider's software issued for a provision (ADR 0349 clause 10, ADR 0305), " +
|
|
"in two steps from the controller alone, which asks the provider's tool: provision, provider and module " +
|
|
"open a call and answer the key the tool seals to; then the call with what the tool sealed. Kept as the " +
|
|
"provision's one credential; the same value again writes nothing.",
|
|
Input: intakeInput(map[string]string{"provision": "the provision", "provider": "the provider's machine",
|
|
"module": "the provider's module, whose software issued it"}),
|
|
run: func(caller string, a map[string]any) (any, error) {
|
|
v, err := env.ready()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if call := arg(a, "call"); call != "" {
|
|
return v.IssuedTake(caller, call, arg(a, "sealed"))
|
|
}
|
|
return v.IssuedOpen(caller, arg(a, "provision"), arg(a, "provider"), arg(a, "module"))
|
|
}},
|
|
{Name: Seat + ".rotate",
|
|
Description: "Make a secret anew (ADR 0114, ADR 0228): node, module and name for a module's own, or provider " +
|
|
"and provision for a provision's credential. Refused for a value a provider's software issued, which " +
|
|
"the software rotates, and for an accepted credential, which the vault does not remake. Never answers " +
|
|
"the value.",
|
|
Input: map[string]any{"type": "object", "properties": map[string]any{
|
|
"node": str("the machine, for a module's own secret"), "module": str("the module"),
|
|
"name": str("the secret's name in the module"), "provider": str("the provider's machine"),
|
|
"provision": str("the provision"),
|
|
"by": str("who asked, as the controller names them when it carries the act")}},
|
|
run: func(caller string, a map[string]any) (any, error) {
|
|
v, err := env.ready()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
secret, err := secretNamed(a)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return v.Rotate(caller, secret, arg(a, "by"))
|
|
}},
|
|
{Name: "secret_asks",
|
|
Description: "The record of asks for a module's own secret at a desk, newest first: who asked as the bus named " +
|
|
"them, for what, at which desk, when, and how each ended. Never a value.",
|
|
Input: map[string]any{"limit": map[string]any{"type": "number", "description": "how many lines (default 50)"}},
|
|
run: func(_ string, a map[string]any) (any, error) {
|
|
v, err := env.ready()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return v.Store.Asks(limitOf(a))
|
|
}},
|
|
{Name: "secret_export",
|
|
Description: "The mesh's root secrets as this vault keeps them: every secret a module holds for itself, " +
|
|
"sealed to the operator's key (novox/hq ADR 0085, amended). Ciphertext — nothing here can open a line " +
|
|
"of it; the operator, holding the private key off the mesh, recovers one with " +
|
|
"`nox secret recover --from-export` on the control-node. Also lists what is NOT recoverable: secrets " +
|
|
"made before the mesh had an operator key.",
|
|
Input: map[string]any{"sealed": map[string]any{"type": "boolean",
|
|
"description": "include the sealed blobs (default true); false lists holders and the key only"}},
|
|
run: func(_ string, a map[string]any) (any, error) {
|
|
return exportAnswer(env.Root, a["sealed"] != false), nil
|
|
}},
|
|
{Name: "secret_holders",
|
|
Description: "Who holds a secret from this vault: each consumer's login, node and module, when it was " +
|
|
"granted, how many times it has been rotated and when, and the fingerprint of the current value. " +
|
|
"Fingerprints only — the value is never returned.",
|
|
run: func(string, map[string]any) (any, error) {
|
|
if env.Ledger == nil {
|
|
return map[string]any{"holders": []any{}, "count": 0, "error": "the vault has no ledger (MESH_VAULT_LEDGER is not set)"}, nil
|
|
}
|
|
var asked []Contribution
|
|
if env.Receives != "" {
|
|
asked, _ = Contributions(env.Receives)
|
|
}
|
|
holders := []map[string]any{}
|
|
for _, h := range env.Ledger.List() {
|
|
row := map[string]any{}
|
|
raw, _ := json.Marshal(h)
|
|
_ = json.Unmarshal(raw, &row)
|
|
row["module"], row["asked"] = nil, false
|
|
for _, c := range asked {
|
|
if c.As == h.As {
|
|
row["module"], row["asked"] = c.From, true
|
|
}
|
|
}
|
|
holders = append(holders, row)
|
|
}
|
|
return map[string]any{"holders": holders, "count": len(holders)}, nil
|
|
}},
|
|
{Name: "secret_verify",
|
|
Description: "Check one holder's secret without seeing it: the fingerprint the vault recorded against the " +
|
|
"fingerprint of the value the mesh currently delivers here, and optionally against a fingerprint " +
|
|
"computed on the holder's own machine (sha256 of the file, as `sha256:<hex>`). Two ends agreeing " +
|
|
"proves they agree, not that either works — the login itself is the test.",
|
|
Input: map[string]any{"as": str("the holder's login, e.g. anchor-redis"),
|
|
"fingerprint": str("optional: sha256:<hex> of the value as the holder reads it, computed there — never the value")},
|
|
run: func(_ string, a map[string]any) (any, error) {
|
|
as, _ := a["as"].(string)
|
|
given, _ := a["fingerprint"].(string)
|
|
return verifyAnswer(env, as, given), nil
|
|
}},
|
|
{Name: "secret_handed",
|
|
Description: "Who was handed which secret by `get`, and when, and who was refused, newest first, as the " +
|
|
"vault's own store records it. Never a value.",
|
|
Input: map[string]any{"limit": map[string]any{"type": "number", "description": "how many (default 50)"}},
|
|
run: func(_ string, a map[string]any) (any, error) {
|
|
if env.Vault == nil {
|
|
return nil, errors.New("the vault is not ready")
|
|
}
|
|
return env.Vault.Store.Handed(limitOf(a))
|
|
}},
|
|
{Name: "secret_vault_export",
|
|
Description: "Every secret the vault itself holds, as its copy sealed to the operator key, in the shape of " +
|
|
"`secret export`: saved to a file, `nox secret recover vault mesh-vault <name> --key <operator key file> " +
|
|
"--from-export <file> --out <file>` opens one with the operator's private key. Ciphertext only.",
|
|
run: func(string, map[string]any) (any, error) {
|
|
if env.Vault == nil {
|
|
return nil, errors.New("the vault is not ready")
|
|
}
|
|
return vaultExport(env.Vault)
|
|
}},
|
|
{Name: "provisioner_retirement",
|
|
Description: "This provider's retirement (novox/hq ADR 0230): the holders it keeps active, a retirement waiting " +
|
|
"for a person, one a person rejected, and every holder retired with when and why. Mark-only: a retired " +
|
|
"holder keeps what it has until a person deletes it.",
|
|
run: func(string, map[string]any) (any, error) {
|
|
if env.Custody == nil {
|
|
return nil, errors.New("the vault has no custody ledger")
|
|
}
|
|
return env.Custody.Retirement(), nil
|
|
}},
|
|
{Name: "provisioner_retire_approve",
|
|
Description: "Retire exactly the set waiting for a person (or rejected earlier). Repeat the set in consumers; why is required.",
|
|
Input: actInput(true, false),
|
|
run: func(caller string, a map[string]any) (any, error) {
|
|
if env.Custody == nil {
|
|
return nil, errors.New("the vault has no custody ledger")
|
|
}
|
|
done, err := env.Custody.Approve(stringsOf(a["consumers"]), actOf(caller, a))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"retired": done}, nil
|
|
}},
|
|
{Name: "provisioner_retire_reject",
|
|
Description: "Keep the set waiting for a person active, though the mesh no longer asks for it. Repeat the set in consumers; why is required.",
|
|
Input: actInput(true, false),
|
|
run: func(caller string, a map[string]any) (any, error) {
|
|
if env.Custody == nil {
|
|
return nil, errors.New("the vault has no custody ledger")
|
|
}
|
|
kept, err := env.Custody.Reject(stringsOf(a["consumers"]), actOf(caller, a))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"kept": kept}, nil
|
|
}},
|
|
{Name: "provisioner_delete",
|
|
Description: "Delete one RETIRED holder from the custody ledger, its rotation history with it — never an active one. " +
|
|
"Repeat its name in confirm; why is required.",
|
|
Input: actInput(false, true),
|
|
run: func(caller string, a map[string]any) (any, error) {
|
|
if env.Custody == nil {
|
|
return nil, errors.New("the vault has no custody ledger")
|
|
}
|
|
consumer, _ := a["consumer"].(string)
|
|
confirm, _ := a["confirm"].(string)
|
|
if err := env.Custody.Delete(consumer, confirm, actOf(caller, a)); err != nil {
|
|
return nil, err
|
|
}
|
|
return map[string]any{"deleted": consumer, "freed_bytes": -1}, nil
|
|
}},
|
|
{Name: "vault_status",
|
|
Description: "What needs a person's eye in the vault: operator copies still sealed to an earlier key, the operator " +
|
|
"key the mesh's export holds if the vault refused it, and how often the record of hand-outs was rolled.",
|
|
run: func(string, map[string]any) (any, error) {
|
|
if env.Vault == nil {
|
|
return map[string]any{"ready": false, "error": fmt.Sprint(env.Broken)}, nil
|
|
}
|
|
left, err := env.Vault.StillSealedToAnother()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
key, _ := env.Vault.Store.OperatorKey()
|
|
return map[string]any{"ready": true, "operator_key": key, "copies_on_an_earlier_key": left,
|
|
"operator_key_refused": env.keyRefused(), "handed_rolled": env.Vault.Store.Rolled(),
|
|
"ok": left == 0 && env.keyRefused() == "" && key != ""}, nil
|
|
}},
|
|
}
|
|
}
|
|
|
|
// actInput is the arguments of an act on retirement, as the SDK's harness takes them.
|
|
func actInput(set, one bool) map[string]any {
|
|
text := map[string]any{"type": "string"}
|
|
in := map[string]any{"why": text, "by": text, "via": text}
|
|
if set {
|
|
in["consumers"] = map[string]any{"type": "array", "items": text}
|
|
}
|
|
if one {
|
|
in["consumer"], in["confirm"] = text, text
|
|
}
|
|
return in
|
|
}
|
|
|
|
// actOf is an act's why, by and via. `by` is the person the controller names; with none, the caller the
|
|
// runtime proved. `by` is an argument, so it is not proven: when it names somebody other than the caller the
|
|
// runtime proved, the proven caller is kept beside it in via, so the record says both.
|
|
func actOf(caller string, a map[string]any) Act {
|
|
act := Act{}
|
|
act.Why, _ = a["why"].(string)
|
|
act.By, _ = a["by"].(string)
|
|
act.Via, _ = a["via"].(string)
|
|
if act.By == "" {
|
|
act.By = caller
|
|
}
|
|
if act.By != caller {
|
|
proven := "called by " + caller
|
|
if act.Via == "" {
|
|
act.Via = proven
|
|
} else {
|
|
act.Via += ", " + proven
|
|
}
|
|
}
|
|
return act
|
|
}
|
|
|
|
// ready is the vault, or why there is none.
|
|
func (e *Env) ready() (*Vault, error) {
|
|
if e.Vault != nil {
|
|
return e.Vault, nil
|
|
}
|
|
if e.Broken != nil {
|
|
return nil, e.Broken
|
|
}
|
|
return nil, errors.New("the vault is not ready")
|
|
}
|
|
|
|
func arg(a map[string]any, key string) string {
|
|
s, _ := a[key].(string)
|
|
return strings.TrimSpace(s)
|
|
}
|
|
|
|
// secretNamed is the secret a call names: a module's own (node, module, name), or a provision's one credential
|
|
// (provider, provision) — one or the other.
|
|
func secretNamed(a map[string]any) (string, error) {
|
|
own := arg(a, "node") != "" || arg(a, "module") != "" || arg(a, "name") != ""
|
|
pair := arg(a, "provider") != "" || arg(a, "provision") != ""
|
|
switch {
|
|
case own && !pair:
|
|
return OwnSecret(arg(a, "node"), arg(a, "module"), arg(a, "name"))
|
|
case pair && !own:
|
|
return ProvisionSecret(arg(a, "provider"), arg(a, "provision"))
|
|
}
|
|
return "", errors.New("name a module's own secret (node, module, name) or a provision's credential " +
|
|
"(provider, provision), one of the two")
|
|
}
|
|
|
|
// intakeInput is a two-step verb's arguments: what its first step names, then the call and what was sealed.
|
|
func intakeInput(first map[string]string) map[string]any {
|
|
props := map[string]any{}
|
|
for k, d := range first {
|
|
props[k] = str(d)
|
|
}
|
|
props["call"] = str("the second step: the call the first step answered")
|
|
props["sealed"] = str("the second step: the value sealed to the call's key (NaCl anonymous box, base64) — never the value")
|
|
return map[string]any{"type": "object", "properties": props}
|
|
}
|
|
|
|
func stringsOf(v any) []string {
|
|
list, _ := v.([]any)
|
|
out := []string{}
|
|
for _, x := range list {
|
|
if s, ok := x.(string); ok {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func limitOf(a map[string]any) int {
|
|
if v, ok := a["limit"].(float64); ok && v > 0 {
|
|
return int(v)
|
|
}
|
|
return 50
|
|
}
|
|
|
|
// keyFingerprint is a key's fingerprint as the controller's secrets.Fingerprint says it: sha256 of the key,
|
|
// its first eight bytes in hex.
|
|
func keyFingerprint(key string) string {
|
|
if key == "" {
|
|
return ""
|
|
}
|
|
sum := sha256.Sum256([]byte(strings.TrimSpace(key)))
|
|
return "sha256:" + hex.EncodeToString(sum[:8])
|
|
}
|
|
|
|
// vaultExport is the vault's operator copies in the shape `secret export` writes and `secret recover
|
|
// --from-export` reads: each as node "vault", module "mesh-vault" and the secret's name.
|
|
func vaultExport(v *Vault) (map[string]any, error) {
|
|
key, err := v.Store.OperatorKey()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
all, err := v.Store.List()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
kept, earlier := []map[string]any{}, []map[string]any{}
|
|
for _, rec := range all {
|
|
k := map[string]any{"node": "vault", "module": Seat, "name": rec.Name, "kind": "own", "origin": "made",
|
|
"sealed": rec.Operator.Sealed, "key": rec.Operator.Key, "made-at": rec.MadeAt}
|
|
if rec.Operator.Key == key {
|
|
kept = append(kept, k)
|
|
} else {
|
|
earlier = append(earlier, k)
|
|
}
|
|
}
|
|
return map[string]any{"export": 1, "operator-key": key, "fingerprint": keyFingerprint(key), "kept": kept,
|
|
"sealed-to-earlier-key": earlier}, nil
|
|
}
|
|
|
|
func exportAnswer(root string, withBlobs bool) map[string]any {
|
|
if root == "" {
|
|
return map[string]any{"available": false, "error": "this vault keeps no root secrets (MESH_VAULT_ROOT is not set)"}
|
|
}
|
|
raw, err := os.ReadFile(filepath.Join(root, "export.json"))
|
|
if err != nil {
|
|
return map[string]any{"available": false, "error": "the mesh has not written an export here yet: " + err.Error()}
|
|
}
|
|
var doc struct {
|
|
Export int `json:"export"`
|
|
OperatorKey string `json:"operator-key"`
|
|
Fingerprint string `json:"fingerprint"`
|
|
Kept []map[string]any `json:"kept"`
|
|
Unrecoverable []map[string]any `json:"unrecoverable"`
|
|
}
|
|
if err := json.Unmarshal(raw, &doc); err != nil {
|
|
return map[string]any{"available": false, "error": "the export is not readable: " + err.Error()}
|
|
}
|
|
kept := []map[string]any{}
|
|
for _, k := range doc.Kept {
|
|
if !withBlobs {
|
|
k = map[string]any{"node": k["node"], "module": k["module"], "name": k["name"], "origin": k["origin"], "made-at": k["made-at"]}
|
|
}
|
|
kept = append(kept, k)
|
|
}
|
|
if doc.Unrecoverable == nil {
|
|
doc.Unrecoverable = []map[string]any{}
|
|
}
|
|
return map[string]any{"available": true, "export": doc.Export, "operator-key": doc.OperatorKey,
|
|
"fingerprint": doc.Fingerprint, "count": len(kept), "kept": kept, "unrecoverable": doc.Unrecoverable}
|
|
}
|
|
|
|
// exportedOperatorKey is the operator key in the export the mesh writes to the vault's root, or "".
|
|
func exportedOperatorKey(root string) string {
|
|
raw, err := os.ReadFile(filepath.Join(root, "export.json"))
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
var doc struct {
|
|
OperatorKey string `json:"operator-key"`
|
|
}
|
|
_ = json.Unmarshal(raw, &doc)
|
|
return doc.OperatorKey
|
|
}
|
|
|
|
func verifyAnswer(env *Env, as, given string) map[string]any {
|
|
if env.Ledger == nil {
|
|
return map[string]any{"as": as, "known": false, "error": "the vault has no ledger (MESH_VAULT_LEDGER is not set)"}
|
|
}
|
|
recorded, ok := env.Ledger.Get(as)
|
|
if !ok {
|
|
return map[string]any{"as": as, "known": false, "error": "this vault holds nothing for " + as}
|
|
}
|
|
var delivered, deliveredErr any
|
|
var deliveredMatches any
|
|
var asked *Contribution
|
|
if env.Receives != "" {
|
|
all, _ := Contributions(env.Receives)
|
|
for i := range all {
|
|
if all[i].As == as {
|
|
asked = &all[i]
|
|
}
|
|
}
|
|
}
|
|
if asked == nil {
|
|
deliveredErr = "the mesh does not currently ask this vault to serve that login"
|
|
} else if fp, _, err := DeliveredFingerprint(*asked); err != nil {
|
|
deliveredErr = err.Error()
|
|
} else {
|
|
delivered, deliveredMatches = fp, fp == recorded.Fingerprint
|
|
}
|
|
var givenAny, givenMatches, earlier any
|
|
if given != "" {
|
|
givenAny, givenMatches = given, given == recorded.Fingerprint
|
|
was := false
|
|
for _, h := range recorded.History {
|
|
was = was || h.Fingerprint == given
|
|
}
|
|
earlier = was
|
|
}
|
|
return map[string]any{"as": recorded.As, "known": true, "recorded": recorded.Fingerprint,
|
|
"rotations": recorded.Rotations, "changed": recorded.Changed,
|
|
"delivered": delivered, "deliveredError": deliveredErr, "deliveredMatchesRecorded": deliveredMatches,
|
|
"given": givenAny, "givenMatchesRecorded": givenMatches, "givenIsAnEarlierValue": earlier,
|
|
"ok": deliveredMatches != false && givenMatches != false}
|
|
}
|
|
|
|
// tools are the definitions as the SDK serves them.
|
|
func tools(env *Env) []stdio.Tool {
|
|
var out []stdio.Tool
|
|
for _, d := range definitions(env) {
|
|
out = append(out, stdio.Tool{Name: d.Name, Description: d.Description, Input: d.Input, Run: d.Run})
|
|
}
|
|
return out
|
|
}
|