A settings proposal carries every key with its whole value in its Details (issue 383)
mesh/delivery-group group fix/383-an-ask-reads-at-a-glance checking: composed check out of date: novox/mesh-controller@52bcef159d95 was judged against trunk 6f…
mesh/merge-gate out of date: judged against trunk 6f1a05366c1d, the trunk is now 8ddf8b700558; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 6f1a05366c1d, the trunk is now 8ddf8b700558; judged again on a recheck or when it is merged
mesh/delivery superseded: a newer head of the same pull request
mesh/delivery-group group fix/383-an-ask-reads-at-a-glance checking: composed check out of date: novox/mesh-controller@52bcef159d95 was judged against trunk 6f…
mesh/merge-gate out of date: judged against trunk 6f1a05366c1d, the trunk is now 8ddf8b700558; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 6f1a05366c1d, the trunk is now 8ddf8b700558; judged again on a recheck or when it is merged
mesh/delivery superseded: a newer head of the same pull request
The review of #304: with more than three keys the ask showed only the names of the rest, a long value was cut without a word, and the Details held no key, so Approve bound values the operator never saw. The Details now list every key uncut — masked, and whole in WholeDetails for a channel that proves who answers — before who proposed it and the fingerprint; the glance says what it cut. Vendors mesh-sdk at f474544 and captures its conformance events there.
This commit is contained in:
@@ -162,29 +162,38 @@ func (p settingsProposal) ask(id string, machines []string) (asks.Ask, map[strin
|
|||||||
headline = verb + " settings?"
|
headline = verb + " settings?"
|
||||||
}
|
}
|
||||||
// The message reads at a glance (issues 383 and 639): a summary of what changes where, then at most three
|
// The message reads at a glance (issues 383 and 639): a summary of what changes where, then at most three
|
||||||
// points, a key each; who proposed it, the fingerprint and how to read it whole are its Details, which the
|
// points, a key each; every key with its whole value, who proposed it, the fingerprint and how to read it at the
|
||||||
// router notes after the answers.
|
// terminal are its Details, which the router notes after the answers.
|
||||||
shown, shownWhole := p.change(machines, false)
|
shown, shownAll, shownWhole := p.change(machines, false)
|
||||||
whole, _ := p.change(machines, true)
|
whole, wholeAll, _ := p.change(machines, true)
|
||||||
summary := p.summary()
|
summary := p.summary()
|
||||||
compose := func(points []string) string {
|
compose := func(points []string) string {
|
||||||
return strings.Join(append([]string{summary}, points...), "\n")
|
return strings.Join(append([]string{summary}, points...), "\n")
|
||||||
}
|
}
|
||||||
var d strings.Builder
|
// The Details: every setting it sets, one line each, uncut — whole where the sender is proven (WholeDetails),
|
||||||
fmt.Fprintf(&d, "Proposed by %s at %s. Fingerprint %s.\n", sayable(p.From, machines), p.At.Local().Format("15:04 on 2 Jan"),
|
// masked elsewhere — then who proposed it, the fingerprint and how to read it at the terminal.
|
||||||
fingerprint(p.Digest))
|
details := func(lines []string) string {
|
||||||
if !shownWhole {
|
var d strings.Builder
|
||||||
d.WriteString("Where who answers is not proven, its values are shown as ‹address›, ‹path› or ‹withheld›.\n")
|
d.WriteString(strings.Join(lines, "\n") + "\n")
|
||||||
|
fmt.Fprintf(&d, "Proposed by %s at %s. Fingerprint %s.\n", sayable(p.From, machines), p.At.Local().Format("15:04 on 2 Jan"),
|
||||||
|
fingerprint(p.Digest))
|
||||||
|
if !shownWhole {
|
||||||
|
d.WriteString("Where who answers is not proven, its values are shown as ‹address›, ‹path› or ‹withheld›.\n")
|
||||||
|
}
|
||||||
|
fmt.Fprintf(&d, "Read it whole, with this fingerprint: settings proposals %s at the controller's terminal, or the "+
|
||||||
|
"controller's settings verb with proposal %s through the mesh MCP server.", id, id)
|
||||||
|
return d.String()
|
||||||
}
|
}
|
||||||
fmt.Fprintf(&d, "Read it whole, with this fingerprint: settings proposals %s at the controller's terminal, or the "+
|
|
||||||
"controller's settings verb with proposal %s through the mesh MCP server.", id, id)
|
|
||||||
q := asks.Ask{ID: id, Headline: headline, Explanation: compose(shown), Who: asks.Operator,
|
q := asks.Ask{ID: id, Headline: headline, Explanation: compose(shown), Who: asks.Operator,
|
||||||
Expires: p.At.Add(askApproveFor), OnExpiry: "the proposal is discarded; nothing changes",
|
Expires: p.At.Add(askApproveFor), OnExpiry: "the proposal is discarded; nothing changes",
|
||||||
About: p.about(), Details: d.String()}
|
About: p.about(), Details: details(shownAll)}
|
||||||
|
// Only where a value was masked: an ask whose values all pass the content rule shows the same everywhere.
|
||||||
if w := compose(whole); w != q.Explanation {
|
if w := compose(whole); w != q.Explanation {
|
||||||
// Only where a value was masked: an ask whose values all pass the content rule shows the same everywhere.
|
|
||||||
q.Whole = w
|
q.Whole = w
|
||||||
}
|
}
|
||||||
|
if w := details(wholeAll); w != q.Details {
|
||||||
|
q.WholeDetails = w
|
||||||
|
}
|
||||||
options := map[string]int{}
|
options := map[string]int{}
|
||||||
for i, act := range p.actions(id) {
|
for i, act := range p.actions(id) {
|
||||||
binds, _ := asks.ActDigest(boundAct(act))
|
binds, _ := asks.ActDigest(boundAct(act))
|
||||||
@@ -231,12 +240,14 @@ func (p settingsProposal) summary() string {
|
|||||||
n, p.Module, p.where(), stay, takes)
|
n, p.Module, p.where(), stay, takes)
|
||||||
}
|
}
|
||||||
|
|
||||||
// change is what changes, as at most asks.MostPoints points, and whether every value was shown whole: "+ key: value"
|
// change is what changes: as at most asks.MostPoints points for the glance, as every line whole for the note, and
|
||||||
|
// whether every value was shown exactly: "+ key: value"
|
||||||
// added, "~ key: value (was: old)" changed, "- key (was: old)" removed. Each value is shown under the content rule
|
// added, "~ key: value (was: old)" changed, "- key (was: old)" removed. Each value is shown under the content rule
|
||||||
// (sayableValue), or — exact, for a channel that proves who answers — as it is, withheld only when shaped like a
|
// (sayableValue), or — exact, for a channel that proves who answers — as it is, withheld only when shaped like a
|
||||||
// secret (wholeValue). More changes than points: the first two, and the keys of the rest; a point longer than a
|
// secret (wholeValue). More changes than points: the first two, and the keys of the rest; a point longer than a
|
||||||
// phone shows at a glance is cut. Either is read whole with `settings proposals`.
|
// phone shows at a glance is cut and says so. The note carries every line uncut (the review of #304: Approve sets
|
||||||
func (p settingsProposal) change(machines []string, exact bool) ([]string, bool) {
|
// exactly what the operator can read).
|
||||||
|
func (p settingsProposal) change(machines []string, exact bool) ([]string, []string, bool) {
|
||||||
whole := true
|
whole := true
|
||||||
say := func(v any) string {
|
say := func(v any) string {
|
||||||
s, w := sayableValue(v, machines)
|
s, w := sayableValue(v, machines)
|
||||||
@@ -266,6 +277,10 @@ func (p settingsProposal) change(machines []string, exact bool) ([]string, bool)
|
|||||||
lines = append(lines, line{k, fmt.Sprintf("- %s (was: %s)", k, say(was[k]))})
|
lines = append(lines, line{k, fmt.Sprintf("- %s (was: %s)", k, say(was[k]))})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
all := make([]string, len(lines))
|
||||||
|
for i, l := range lines {
|
||||||
|
all[i] = l.text
|
||||||
|
}
|
||||||
var out []string
|
var out []string
|
||||||
for i, l := range lines {
|
for i, l := range lines {
|
||||||
if len(lines) > asks.MostPoints && i == asks.MostPoints-1 {
|
if len(lines) > asks.MostPoints && i == asks.MostPoints-1 {
|
||||||
@@ -273,19 +288,19 @@ func (p settingsProposal) change(machines []string, exact bool) ([]string, bool)
|
|||||||
for _, r := range lines[i:] {
|
for _, r := range lines[i:] {
|
||||||
rest = append(rest, r.key)
|
rest = append(rest, r.key)
|
||||||
}
|
}
|
||||||
out = append(out, fmt.Sprintf("and %d more: %s", len(rest), strings.Join(rest, ", ")))
|
out = append(out, fmt.Sprintf("and %d more, each whole in the note: %s", len(rest), strings.Join(rest, ", ")))
|
||||||
whole = false
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
out = append(out, l.text)
|
out = append(out, l.text)
|
||||||
}
|
}
|
||||||
|
// A point too long for a glance is cut, and says so: its whole value is the note's.
|
||||||
|
const cut = "… (cut: whole in the note)"
|
||||||
for i, l := range out {
|
for i, l := range out {
|
||||||
if r := []rune(l); len(r) > asks.PointLength {
|
if r := []rune(l); len(r) > asks.PointLength {
|
||||||
out[i] = string(r[:asks.PointLength-1]) + "…"
|
out[i] = string(r[:asks.PointLength-len([]rune(cut))]) + cut
|
||||||
whole = false
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out, whole
|
return out, all, whole
|
||||||
}
|
}
|
||||||
|
|
||||||
func layerKeys(m map[string]any) []string {
|
func layerKeys(m map[string]any) []string {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
@@ -222,8 +223,8 @@ func TestAProposalShowsItsValuesWholeOnlyWhereTheSenderIsProvenAndNeverASecret(t
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
q := r.askSent(t)
|
q := r.askSent(t)
|
||||||
masked, _ := kept(r).Proposal.change(machines, false)
|
masked, _, _ := kept(r).Proposal.change(machines, false)
|
||||||
whole, _ := kept(r).Proposal.change(machines, true)
|
whole, _, _ := kept(r).Proposal.change(machines, true)
|
||||||
if !strings.HasSuffix(q.Explanation, "\n"+strings.Join(masked, "\n")) || !strings.HasSuffix(q.Whole, "\n"+strings.Join(whole, "\n")) {
|
if !strings.HasSuffix(q.Explanation, "\n"+strings.Join(masked, "\n")) || !strings.HasSuffix(q.Whole, "\n"+strings.Join(whole, "\n")) {
|
||||||
t.Fatalf("the ask does not carry the change masked and whole:\n%s\n--\n%s", q.Explanation, q.Whole)
|
t.Fatalf("the ask does not carry the change masked and whole:\n%s\n--\n%s", q.Explanation, q.Whole)
|
||||||
}
|
}
|
||||||
@@ -272,7 +273,7 @@ func TestAProposalShowsItsValuesWholeOnlyWhereTheSenderIsProvenAndNeverASecret(t
|
|||||||
if _, err := r3.pr.propose(context.Background(), proposeInput{module: "notes", node: "laptop", values: five}); err != nil {
|
if _, err := r3.pr.propose(context.Background(), proposeInput{module: "notes", node: "laptop", values: five}); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if q3 := r3.askSent(t); !strings.HasSuffix(q3.Explanation, "\n+ a: 1\n+ b: 2\nand 3 more: c, d, e") || asks.Glance(q3.Explanation) != nil ||
|
if q3 := r3.askSent(t); !strings.HasSuffix(q3.Explanation, "\n+ a: 1\n+ b: 2\nand 3 more, each whole in the note: c, d, e") || asks.Glance(q3.Explanation) != nil ||
|
||||||
!strings.Contains(q3.Details, "settings proposals") {
|
!strings.Contains(q3.Details, "settings proposals") {
|
||||||
t.Errorf("five keys:\n%s", q3.Explanation)
|
t.Errorf("five keys:\n%s", q3.Explanation)
|
||||||
}
|
}
|
||||||
@@ -854,3 +855,60 @@ func captureStdout(t *testing.T, f func()) string {
|
|||||||
_ = w.Close()
|
_ = w.Close()
|
||||||
return <-done
|
return <-done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The review of #304: Approve sets exactly what the operator can read. A proposal of five keys, one value 300
|
||||||
|
// characters long, shows two keys at a glance, says what it cut, and carries every key with its whole value in its
|
||||||
|
// Details for a channel that proves who answers (WholeDetails), and every key masked for one that does not.
|
||||||
|
func TestEveryKeyAndItsWholeValueReachTheOperator(t *testing.T) {
|
||||||
|
var long strings.Builder
|
||||||
|
long.WriteString("library=/storage")
|
||||||
|
for i := 0; long.Len() < 300; i++ {
|
||||||
|
long.WriteString(fmt.Sprintf("/album-%02d", i))
|
||||||
|
}
|
||||||
|
value := long.String()[:300]
|
||||||
|
values := map[string]any{"a": "1", "b": "2", "c": "3", "d": "4", "shares": value}
|
||||||
|
r := newProposerRig(t)
|
||||||
|
if _, err := r.pr.propose(context.Background(), proposeInput{module: "mounts", node: "shanks", values: values}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
q := r.askSent(t)
|
||||||
|
if err := q.Check(r.now); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// What the operator reads on a proving channel: the whole words, then the note's whole details.
|
||||||
|
visible := q.Whole + "\n" + q.WholeDetails
|
||||||
|
if q.Whole == "" {
|
||||||
|
visible = q.Explanation + "\n" + q.WholeDetails
|
||||||
|
}
|
||||||
|
for k, v := range values {
|
||||||
|
if !strings.Contains(visible, "+ "+k+": "+v.(string)) {
|
||||||
|
t.Errorf("the operator never reads %s whole:\n%s", k, visible)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if asks.Glance(q.Explanation) != nil || !strings.HasSuffix(q.Explanation, "\nand 3 more, each whole in the note: c, d, shares") {
|
||||||
|
t.Errorf("the glance:\n%s", q.Explanation)
|
||||||
|
}
|
||||||
|
for k := range values {
|
||||||
|
if !strings.Contains(q.Details, "+ "+k+": ") {
|
||||||
|
t.Errorf("the masked details lack %s:\n%s", k, q.Details)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.Contains(q.Details, "/storage") {
|
||||||
|
t.Errorf("the masked details show a path:\n%s", q.Details)
|
||||||
|
}
|
||||||
|
if refusal, ok := outward.Check(q.Details, "shanks"); !ok {
|
||||||
|
t.Errorf("the router would withhold the masked details: %s", refusal)
|
||||||
|
}
|
||||||
|
if _, ok := outward.Secret(q.WholeDetails, "shanks"); !ok {
|
||||||
|
t.Error("the router would refuse the whole details")
|
||||||
|
}
|
||||||
|
// A value too long for a glance is cut there, and says so.
|
||||||
|
r2 := newProposerRig(t)
|
||||||
|
if _, err := r2.pr.propose(context.Background(), proposeInput{module: "mounts", node: "shanks", values: map[string]any{"shares": value}}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
q2 := r2.askSent(t)
|
||||||
|
if !strings.HasSuffix(q2.Whole, "… (cut: whole in the note)") || !strings.Contains(q2.WholeDetails, "+ shares: "+value) || asks.Glance(q2.Whole) != nil {
|
||||||
|
t.Errorf("the cut is not said, or the note lacks the whole:\n%s\n--\n%s", q2.Whole, q2.WholeDetails)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ module github.com/novox/mesh-controller
|
|||||||
go 1.26.0
|
go 1.26.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013141520-9588871d6629
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013145622-f4745447ba6d
|
||||||
github.com/jackc/pgx/v5 v5.10.0
|
github.com/jackc/pgx/v5 v5.10.0
|
||||||
github.com/nats-io/nats-server/v2 v2.11.17
|
github.com/nats-io/nats-server/v2 v2.11.17
|
||||||
github.com/nats-io/nats.go v1.54.0
|
github.com/nats-io/nats.go v1.54.0
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261010191001-33917f91ac3c h1:l5onJwoI
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261010191001-33917f91ac3c/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261010191001-33917f91ac3c/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013141520-9588871d6629 h1:7TYFV1Q6Mcz+lsLP/sds87tysET4rKCn3rRIQV8gNmA=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013141520-9588871d6629 h1:7TYFV1Q6Mcz+lsLP/sds87tysET4rKCn3rRIQV8gNmA=
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013141520-9588871d6629/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013141520-9588871d6629/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013145622-f4745447ba6d h1:HEJyqorKktrkJPeeJdwEo0kdrJjH3fm/YVS9MGftCqY=
|
||||||
|
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013145622-f4745447ba6d/go.mod h1:GFuZUElBZ9A++mxgIKo97aXXo+kV0uJ/UkbhQPPIbrY=
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op h1:Z/MZK75wC/NSrkgqeNIa7jexam9uWzhLmFTSCPI/kn0=
|
||||||
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op/go.mod h1:FQyySiasQQM8735Ddel3MRojmy4dA1IqCeyJ5jmPMbI=
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
|||||||
Vendored
+1
-1
@@ -9,7 +9,7 @@ a second place to clean besides the catalogue itself.
|
|||||||
|
|
||||||
mesh-catalog e0bc919d4945bb5f13f971f74643a0b714cf45da modules/*/module.json, modules/nats/Dockerfile
|
mesh-catalog e0bc919d4945bb5f13f971f74643a0b714cf45da modules/*/module.json, modules/nats/Dockerfile
|
||||||
mesh-host 7a41e120635b3f7b6cb94cca8b3fc4093e4e6110 examples/foundation-first-node.lock
|
mesh-host 7a41e120635b3f7b6cb94cca8b3fc4093e4e6110 examples/foundation-first-node.lock
|
||||||
mesh-sdk 9588871d6629e160bb0667f3d97566a0f8f7a4a6 conformance/events/module-event.json
|
mesh-sdk f4745447ba6d0c4fd7dc6b1071e26ce012806233 conformance/events/module-event.json
|
||||||
|
|
||||||
To move them, from this repository's root, with the two repositories checked out beside it:
|
To move them, from this repository's root, with the two repositories checked out beside it:
|
||||||
|
|
||||||
|
|||||||
@@ -174,6 +174,12 @@ func (a Ask) Digest() string {
|
|||||||
canonical(&b, a.Whole)
|
canonical(&b, a.Whole)
|
||||||
canonical(&b, a.Details)
|
canonical(&b, a.Details)
|
||||||
}
|
}
|
||||||
|
// WholeDetails (novox/hq issue 383, the review of mesh-controller #304) follows only when given, so every
|
||||||
|
// ask without it digests as it did before it existed.
|
||||||
|
if a.WholeDetails != "" {
|
||||||
|
canonical(&b, "whole-details")
|
||||||
|
canonical(&b, a.WholeDetails)
|
||||||
|
}
|
||||||
sum := sha256.Sum256([]byte(b.String()))
|
sum := sha256.Sum256([]byte(b.String()))
|
||||||
return "sha256:" + hex.EncodeToString(sum[:])
|
return "sha256:" + hex.EncodeToString(sum[:])
|
||||||
}
|
}
|
||||||
@@ -207,6 +213,12 @@ type Ask struct {
|
|||||||
// (Message.Note), after what the ask's options do and when it expires — never in its body (novox/hq
|
// (Message.Note), after what the ask's options do and when it expires — never in its body (novox/hq
|
||||||
// issue 639).
|
// issue 639).
|
||||||
Details string `json:"details,omitempty"`
|
Details string `json:"details,omitempty"`
|
||||||
|
// WholeDetails are the Details with their exact values whole, shown in place of Details where Whole is shown
|
||||||
|
// in place of the explanation (a kind that proves who answers, given the ask's answers): every value an
|
||||||
|
// approval sets, whole, so the person reads exactly what they approve (novox/hq issue 383). Held, like
|
||||||
|
// Whole, to the secret rule alone; the router refuses the ask when a secret's shape is left in it. Empty,
|
||||||
|
// Details are shown everywhere.
|
||||||
|
WholeDetails string `json:"whole-details,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// What an ask's explanation is held to, so it reads at a glance on a phone (novox/hq issue 639): a summary
|
// What an ask's explanation is held to, so it reads at a glance on a phone (novox/hq issue 639): a summary
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
# git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013141520-9588871d6629
|
# git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261013145622-f4745447ba6d
|
||||||
## explicit; go 1.22
|
## explicit; go 1.22
|
||||||
git.novox.be/novox/mesh-sdk/go/asks
|
git.novox.be/novox/mesh-sdk/go/asks
|
||||||
# github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op
|
# github.com/antithesishq/antithesis-sdk-go v0.7.0-default-no-op
|
||||||
|
|||||||
Reference in New Issue
Block a user