mesh-cli: give a terminal line the standard input mesh-cli carried, refuse it to any other, keep it nowhere
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check fail: its merge-check.sh failed: --- FAIL: TestTheInstallersFirstUserListIsWhatTheControllerWouldCompose (0.62s)
mesh/delivery-group group feat/a-terminal-line-takes-standard-input rejected: a member's own check failed
mesh/delivery superseded: a newer head of the same pull request

A secret given at the controller's terminal through mesh-cli (secret accept … --from -) never reached the
line: every line ran with no standard input. A line that runs as the terminal now reads what mesh-cli
carried (at most 64 KiB); an ordinary call carrying any is refused and nothing runs; the calls record keeps
only that some was given, the journal and the answer nothing of it.
This commit is contained in:
2026-10-09 17:01:36 +02:00
parent e6b00e2e51
commit ba97297f66
6 changed files with 172 additions and 3 deletions
+1 -1
View File
@@ -461,7 +461,7 @@ func kept(args json.RawMessage) json.RawMessage {
for k, v := range given {
s, isString := v.(string)
switch {
case k == "values" || k == "secret":
case k == "values" || k == "secret" || k == "stdin":
out[k] = "(given, not kept)"
case k == "line":
// A mesh-cli line (ADR 0272): its command word, never the rest, which may carry settings.
+29 -1
View File
@@ -41,6 +41,30 @@ type CLIAsked struct {
// (`session-<id>.scope` under the account's own slice), or empty: a service, a user unit. Only a login session is
// the terminal (novox/hq ADR 0272).
Session string `json:"session,omitempty"`
// Stdin is what mesh-cli's standard input held, at most CLIMaxStdin: given to a line that runs as the terminal,
// as its standard input, and refused with any other (novox/hq ADR 0259 §10, ADR 0272). It is kept nowhere: not in
// the calls record (cliRecorded), not in the journal, not in the answer.
Stdin []byte `json:"stdin,omitempty"`
}
// CLIMaxStdin bounds the standard input a mesh-cli line carries (mesh-sdk go/cli MaxStdin).
const CLIMaxStdin = 64 << 10
// cliRecorded is the request as the calls record keeps it: everything but the standard input, which the record
// never holds in any form. The line itself is cut to its command word by the record's own rule (kept).
func cliRecorded(raw json.RawMessage) json.RawMessage {
var m map[string]json.RawMessage
if json.Unmarshal(raw, &m) != nil {
return json.RawMessage(`{}`)
}
if _, given := m["stdin"]; given {
// Said as given, under a key of its own: the record still reads as the request it was (followCLI decodes
// it), and holds nothing of the input.
delete(m, "stdin")
m["stdin-given"] = json.RawMessage(`true`)
}
body, _ := json.Marshal(m)
return body
}
// CLIAnswer is what the controller answers, as the `result` of a call's answer: what the command printed, how it
@@ -135,6 +159,10 @@ func (b OverNATS) answerCLI(msg *nats.Msg, calls *CallLog, slots chan struct{},
case len(asked.Line) == 0:
respond(cliEnvelope(CLIRefusal("the request names no command, so nothing ran")))
return
case len(asked.Stdin) > CLIMaxStdin:
respond(cliEnvelope(CLIRefusal(fmt.Sprintf("standard input of more than %d bytes is not taken, so nothing ran",
CLIMaxStdin))))
return
}
select {
case slots <- struct{}{}:
@@ -145,7 +173,7 @@ func (b OverNATS) answerCLI(msg *nats.Msg, calls *CallLog, slots chan struct{},
return
}
limit := b.Conn.MaxPayload()
calls.serveCallWithin(CLISeat, node, msg.Data, msg.Reply, func(ctx context.Context, _ json.RawMessage) (any, error) {
calls.serveCallWithin(CLISeat, node, cliRecorded(msg.Data), msg.Reply, func(ctx context.Context, _ json.RawMessage) (any, error) {
return fitCLI(handle(ctx, node, asked), limit-4096), nil
}, msg.Respond, limit, logger)
}
+27
View File
@@ -30,6 +30,10 @@ func TestTheMeshCLIRequestAndAnswerKeepTheirFieldNames(t *testing.T) {
if got := keysIn(t, body); got != "cut exit refused stderr stdout terminal why" {
t.Fatalf("the answer's fields are %q", got)
}
body, _ = json.Marshal(CLIAsked{Line: []string{"secret"}, Account: "a", UID: 1, Stdin: []byte("x")})
if got := keysIn(t, body); got != "account line stdin uid" {
t.Fatalf("a request with standard input has the fields %q", got)
}
body, _ = json.Marshal(CLIAsked{Follow: "call-1", Account: "a", UID: 1})
if got := keysIn(t, body); got != "account follow uid" {
t.Fatalf("a follow's fields are %q", got)
@@ -267,3 +271,26 @@ func TestTheWithholdingTestsHoldSomething(t *testing.T) {
t.Fatalf("a record carrying the answer is not found carrying it: %s", body)
}
}
// novox/hq ADR 0259 §10: mesh-cli's standard input — a secret given at the terminal — is never in the calls record,
// in any form, whichever way the request reaches it.
func TestAMeshCLIRecordNeverKeepsItsStandardInput(t *testing.T) {
secret := "123456789:AAEhBOweik6ad9r_QxGivenAtTheTerminal"
raw, _ := json.Marshal(CLIAsked{Line: []string{"secret", "accept", "anchor", "telegram", "telegram-token", "--from", "-"},
Account: "operator", UID: 1000, Stdin: []byte(secret)})
for name, got := range map[string]json.RawMessage{"as answerCLI records it": kept(cliRecorded(raw)),
"as the record alone would keep it": kept(raw)} {
if carries(got, secret) || strings.Contains(string(got), base64.StdEncoding.EncodeToString([]byte(secret))[:20]) {
t.Fatalf("%s, the record keeps %s", name, got)
}
if !strings.Contains(string(got), "secret") || !strings.Contains(string(got), "given, not kept") &&
!strings.Contains(string(got), "stdin-given") {
t.Fatalf("%s, the record does not say a line was asked with standard input: %s", name, got)
}
}
// The record still reads as the request, so the asker can follow its call.
var asked CLIAsked
if err := json.Unmarshal(kept(cliRecorded(raw)), &asked); err != nil || asked.Account != "operator" || len(asked.Stdin) > 0 {
t.Fatalf("the recorded request reads as %+v (%v)", asked, err)
}
}