Answer mesh-cli: the control-node's operator is the terminal, everyone else is not (hq ADR 0272)
The records send the operator to "the controller's terminal", and nothing reached it (hq issue 343). The serving controller now answers each node's mesh.control.<node>.cli, where only that node's engine may publish, with the account the engine read from the kernel. A line from the control-node's operator account runs as the terminal: a fresh process of this binary with no MESH_VERB, whatever the serving process carries. The same account on any other node, where agents may run as it, is an ordinary call: the generic command verb's refusals (one function now, so the two routes cannot drift) and MESH_VERB=mesh-cli, so a terminal-only change is refused with its reason. Any other account, root included, runs nothing. Servers are never run, and an answer over one bus message is cut and says so. The terminal-only refusals and the usage now name mesh-cli on the control-node as the way to the terminal.
This commit is contained in:
@@ -216,6 +216,10 @@ func run() error {
|
||||
func usage() {
|
||||
fmt.Fprint(os.Stderr, `mesh-controller — the control plane
|
||||
|
||||
On a node the operator types these as 'mesh-cli <command>' (in zsh, 'nox <command>'): asked
|
||||
through the node's engine, and run as the controller's terminal only on the control-node
|
||||
(novox/hq ADR 0272).
|
||||
|
||||
migrate bring each context's schema up to date
|
||||
prepare the same, asked the way the mesh asks any module (ADR 0135)
|
||||
node add <name> [--adopted] create a node record; --adopted: the machine is in use
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
)
|
||||
|
||||
// The operator's command, `mesh-cli` (alias `nox`), answered here (novox/hq ADR 0272).
|
||||
//
|
||||
// mesh-cli asks the node-engine on its own machine; the engine reads the asking account from the kernel and asks
|
||||
// this controller on its own node's subject. Here it is judged — the controller's terminal, an ordinary call, or
|
||||
// nothing — and run as every verb's line is: a fresh process of this binary, with this process's environment.
|
||||
//
|
||||
// **The terminal** is a line from a node's operator account, on the control-node (§4). Phase 2 adds a node whose
|
||||
// agents run under an account of their own, judged unable to become root (ADR 0266, not built yet); until then no
|
||||
// other node is the terminal, because agents there run as its operator. **An ordinary call** is a line from that
|
||||
// account elsewhere: it meets every refusal of the generic `command` verb and runs with `MESH_VERB=mesh-cli`, so a
|
||||
// terminal-only change is refused with its reason. **Any other account is refused**, root included: those two
|
||||
// accounts already reach the mesh's verbs through the mesh MCP server, and no other account gains anything here.
|
||||
|
||||
// cliVerb is what a line from mesh-cli that is not the terminal runs as: the verb its process names, so every
|
||||
// terminal-only refusal applies and says it came through mesh-cli.
|
||||
const cliVerb = "mesh-cli"
|
||||
|
||||
// cliServers are commands that serve until stopped. Run for mesh-cli they would hold a second server under this
|
||||
// one until the call's bound killed it.
|
||||
var cliServers = map[string]bool{"serve": true, "api": true, "board": true}
|
||||
|
||||
// cliVerdict is how a line is run, or why it is not.
|
||||
type cliVerdict struct {
|
||||
terminal bool
|
||||
// why says why the line is or is not the terminal, in words the operator reads under the answer.
|
||||
why string
|
||||
// refused says why nothing runs.
|
||||
refused string
|
||||
}
|
||||
|
||||
// judgeCLI decides how a line from mesh-cli runs (ADR 0272 §4). nodes is every node the mesh knows; control is
|
||||
// every node the controller's module is assigned to, which is exactly one in a mesh that is well.
|
||||
func judgeCLI(node string, asked link.CLIAsked, nodes []inventory.Node, control []string) cliVerdict {
|
||||
var record *inventory.Node
|
||||
for i := range nodes {
|
||||
if nodes[i].Name == node {
|
||||
record = &nodes[i]
|
||||
break
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case record == nil:
|
||||
return cliVerdict{refused: fmt.Sprintf("%s is not a node this mesh knows, so nothing ran", node)}
|
||||
case asked.UID == 0 || asked.Account == "root":
|
||||
return cliVerdict{refused: "mesh-cli answers the operator's own account, never root: run it as yourself, " +
|
||||
"not under sudo. Nothing ran"}
|
||||
case record.Account == "":
|
||||
return cliVerdict{refused: fmt.Sprintf("the mesh does not know %s's operator account (`node account <node> <login>`), "+
|
||||
"so no account there is answered. Nothing ran", node)}
|
||||
case asked.Account != record.Account:
|
||||
return cliVerdict{refused: fmt.Sprintf("mesh-cli answers %s's operator account (%s) only, and this line came "+
|
||||
"from %s. Nothing ran", node, record.Account, asked.Account)}
|
||||
}
|
||||
if len(control) != 1 {
|
||||
return cliVerdict{why: fmt.Sprintf("not the controller's terminal: the mesh names %d control-nodes, and the "+
|
||||
"terminal is the one control-node's operator account", len(control))}
|
||||
}
|
||||
if control[0] == node {
|
||||
return cliVerdict{terminal: true, why: fmt.Sprintf("the controller's terminal: %s on the control-node %s",
|
||||
asked.Account, node)}
|
||||
}
|
||||
return cliVerdict{why: fmt.Sprintf("not the controller's terminal: agents on %s may run as %s, so a "+
|
||||
"terminal-only change is made from the control-node %s (novox/hq ADR 0272)", node, asked.Account, control[0])}
|
||||
}
|
||||
|
||||
// controlNodes is every node the controller's module is assigned to.
|
||||
func controlNodes(ctx context.Context, inv *inventory.Inventory, nodes []inventory.Node) ([]string, error) {
|
||||
var out []string
|
||||
for _, n := range nodes {
|
||||
modules, err := inv.Assigned(ctx, n.Name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if slices.Contains(modules, controllerModule) {
|
||||
out = append(out, n.Name)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// controllerModule is the module that runs the controller, and so marks the control-node.
|
||||
const controllerModule = "mesh-controller"
|
||||
|
||||
// answerMeshCLI is the serving controller's answer to mesh-cli.
|
||||
func answerMeshCLI(inv *inventory.Inventory) link.CLIHandler {
|
||||
return func(ctx context.Context, node string, asked link.CLIAsked) link.CLIAnswer {
|
||||
if handingOver.Load() {
|
||||
return link.CLIRefusal("this controller is stopping and runs no new command; ask again in a moment. Nothing ran")
|
||||
}
|
||||
nodes, err := inv.Nodes(ctx)
|
||||
if err != nil {
|
||||
return link.CLIRefusal("the mesh's nodes cannot be read, so nothing ran: " + err.Error())
|
||||
}
|
||||
control, err := controlNodes(ctx, inv, nodes)
|
||||
if err != nil {
|
||||
return link.CLIRefusal("which node is the control-node cannot be read, so nothing ran: " + err.Error())
|
||||
}
|
||||
return runForMeshCLI(ctx, node, asked, judgeCLI(node, asked, nodes, control))
|
||||
}
|
||||
}
|
||||
|
||||
// runForMeshCLI runs a judged line and answers what it said.
|
||||
func runForMeshCLI(ctx context.Context, node string, asked link.CLIAsked, v cliVerdict) link.CLIAnswer {
|
||||
if v.refused != "" {
|
||||
return link.CLIRefusal(v.refused)
|
||||
}
|
||||
if cliServers[asked.Line[0]] {
|
||||
return link.CLIAnswer{Exit: 1, Why: v.why, Refused: fmt.Sprintf("%s serves until stopped, and is not a "+
|
||||
"command line mesh-cli runs. Nothing ran", asked.Line[0])}
|
||||
}
|
||||
verb := ""
|
||||
if !v.terminal {
|
||||
if err := refusedAsTheGenericCommand(asked.Line); err != nil {
|
||||
return link.CLIAnswer{Exit: 1, Why: v.why, Refused: err.Error()}
|
||||
}
|
||||
verb = cliVerb
|
||||
}
|
||||
cmd := selfCommand(ctx, asked.Line)
|
||||
cmd.Env = commandEnvironment(fmt.Sprintf("%s through mesh-cli on %s", asked.Account, node), verb)
|
||||
// No standard input: a command that reads one gets nothing, and fails saying so (ADR 0272 §5).
|
||||
cmd.Stdin = nil
|
||||
var stdout, stderr bytes.Buffer
|
||||
cmd.Stdout, cmd.Stderr = &stdout, &stderr
|
||||
err := cmd.Run()
|
||||
answer := link.CLIAnswer{Stdout: stdout.Bytes(), Stderr: stderr.Bytes(), Terminal: v.terminal, Why: v.why}
|
||||
var exit *exec.ExitError
|
||||
switch {
|
||||
case err == nil:
|
||||
case errors.As(err, &exit):
|
||||
answer.Exit = exit.ExitCode()
|
||||
if answer.Exit < 0 {
|
||||
// Killed: by the call's bound, or by this controller stopping.
|
||||
answer.Exit = 1
|
||||
answer.Stderr = append(answer.Stderr, []byte(fmt.Sprintf("\nmesh-cli: the command was stopped (%v)\n",
|
||||
exit))...)
|
||||
}
|
||||
default:
|
||||
answer.Exit = 1
|
||||
answer.Refused = fmt.Sprintf("could not run %s from this controller's own build: %v", strings.Join(asked.Line, " "), err)
|
||||
}
|
||||
return answer
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/inventory"
|
||||
"github.com/novox/mesh-controller/internal/link"
|
||||
)
|
||||
|
||||
// echoEnvironment makes the test binary, run as a command line, say the verb and caller it was given (TestMain).
|
||||
const echoEnvironment = "MESH_TEST_ECHO_ENVIRONMENT"
|
||||
|
||||
var cliNodes = []inventory.Node{
|
||||
{Name: "control", Account: "operator"},
|
||||
{Name: "laptop", Account: "operator"},
|
||||
{Name: "unnamed"},
|
||||
}
|
||||
|
||||
func asked(account string, uid uint32, line ...string) link.CLIAsked {
|
||||
return link.CLIAsked{Line: line, Account: account, UID: uid}
|
||||
}
|
||||
|
||||
// Who is the controller's terminal, and who is an ordinary call or refused (novox/hq ADR 0272 §4).
|
||||
func TestMeshCLIIsTheTerminalOnlyForTheControlNodesOperator(t *testing.T) {
|
||||
control := []string{"control"}
|
||||
cases := []struct {
|
||||
name, node string
|
||||
asked link.CLIAsked
|
||||
control []string
|
||||
terminal bool
|
||||
refused string
|
||||
why string
|
||||
}{
|
||||
{"the control-node's operator", "control", asked("operator", 1000, "status"), control, true, "", "the controller's terminal"},
|
||||
{"another node's operator", "laptop", asked("operator", 1000, "status"), control, false, "", "agents on laptop may run as operator"},
|
||||
{"another account", "control", asked("agent", 1001, "status"), control, false, "operator account (operator) only", ""},
|
||||
{"root", "control", asked("root", 0, "status"), control, false, "never root", ""},
|
||||
{"a node with no operator account", "unnamed", asked("operator", 1000, "status"), control, false, "does not know unnamed's operator account", ""},
|
||||
{"a node the mesh does not know", "elsewhere", asked("operator", 1000, "status"), control, false, "not a node this mesh knows", ""},
|
||||
{"two control-nodes", "control", asked("operator", 1000, "status"), []string{"control", "laptop"}, false, "", "2 control-nodes"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
v := judgeCLI(c.node, c.asked, cliNodes, c.control)
|
||||
if v.terminal != c.terminal {
|
||||
t.Errorf("%s: terminal %v, want %v (%+v)", c.name, v.terminal, c.terminal, v)
|
||||
}
|
||||
if c.refused == "" && v.refused != "" || c.refused != "" && !strings.Contains(v.refused, c.refused) {
|
||||
t.Errorf("%s: refused %q, want %q", c.name, v.refused, c.refused)
|
||||
}
|
||||
if c.why != "" && !strings.Contains(v.why, c.why) {
|
||||
t.Errorf("%s: why %q, want %q", c.name, v.why, c.why)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The terminal runs its line without MESH_VERB, even where this process carries one; an ordinary call names
|
||||
// mesh-cli; both record who asked through mesh-cli.
|
||||
func TestTheTerminalRunsWithoutAVerbAndAnOrdinaryCallNamesMeshCLI(t *testing.T) {
|
||||
t.Setenv(echoEnvironment, "1")
|
||||
t.Setenv("MESH_VERB", "leaked-from-the-serving-process")
|
||||
ctx := context.Background()
|
||||
|
||||
a := runForMeshCLI(ctx, "control", asked("operator", 1000, "status"), cliVerdict{terminal: true, why: "the terminal"})
|
||||
if a.Exit != 0 || a.Refused != "" || !a.Terminal {
|
||||
t.Fatalf("the terminal's line did not run: %+v", a)
|
||||
}
|
||||
if got := string(a.Stdout); !strings.Contains(got, `verb=""`) || !strings.Contains(got, "operator through mesh-cli on control") {
|
||||
t.Fatalf("the terminal's line ran with %s", got)
|
||||
}
|
||||
|
||||
a = runForMeshCLI(ctx, "laptop", asked("operator", 1000, "status"), cliVerdict{why: "not the terminal"})
|
||||
if a.Exit != 0 || a.Terminal || a.Why != "not the terminal" {
|
||||
t.Fatalf("an ordinary line did not run as one: %+v", a)
|
||||
}
|
||||
if got := string(a.Stdout); !strings.Contains(got, `verb="mesh-cli"`) {
|
||||
t.Fatalf("an ordinary line ran with %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// An ordinary call meets every refusal of the generic command verb, and nothing runs; a server is never run.
|
||||
func TestAnOrdinaryCallMeetsTheCommandVerbsRefusals(t *testing.T) {
|
||||
t.Setenv(echoEnvironment, "1")
|
||||
ctx := context.Background()
|
||||
ordinary := cliVerdict{why: "not the terminal"}
|
||||
a := runForMeshCLI(ctx, "laptop", asked("operator", 1000, "settings", "set", "claude-code", "{}"), ordinary)
|
||||
if a.Refused == "" || len(a.Stdout) != 0 || a.Exit != 1 {
|
||||
t.Fatalf("settings set ran as an ordinary call: %+v", a)
|
||||
}
|
||||
if !strings.Contains(a.Refused, "controller's terminal") || a.Why != "not the terminal" {
|
||||
t.Fatalf("the refusal does not say why: %+v", a)
|
||||
}
|
||||
if err := refusedAsTheGenericCommand([]string{"settings", "set", "x", "{}"}); err == nil {
|
||||
t.Fatal("the command verb no longer refuses settings set, and mesh-cli's ordinary call relies on it")
|
||||
}
|
||||
for _, server := range []string{"serve", "api", "board"} {
|
||||
a := runForMeshCLI(ctx, "control", asked("operator", 1000, server), cliVerdict{terminal: true})
|
||||
if a.Refused == "" || len(a.Stdout) != 0 {
|
||||
t.Fatalf("%s was run for mesh-cli: %+v", server, a)
|
||||
}
|
||||
}
|
||||
a = runForMeshCLI(ctx, "control", asked("agent", 1001, "status"), cliVerdict{refused: "agent is not answered"})
|
||||
if a.Refused != "agent is not answered" || len(a.Stdout) != 0 {
|
||||
t.Fatalf("a refused line ran: %+v", a)
|
||||
}
|
||||
}
|
||||
@@ -1107,7 +1107,7 @@ func refuseTerminalSettingsThroughAVerb(ctx context.Context, inv *inventory.Inve
|
||||
}
|
||||
// A trusted mergeable file takes any key, so its module's whole layer is the terminal's (novox/hq issue 340).
|
||||
if files := catalogue.TrustedMergeable(shelf[module]); len(files) > 0 && !sameLayer(before, after) {
|
||||
return fmt.Errorf("the settings of %s on %s are set at the controller's terminal only, never through a verb (this "+
|
||||
return fmt.Errorf("the settings of %s on %s are set at the controller's terminal only (`mesh-cli` on the control-node), never through a verb (this "+
|
||||
"line came through %q): %s merges whatever key a layer sets into a file root or a consumer trusts, so "+
|
||||
"any key could point the module at a listener of the caller's, and whoever may call a verb includes "+
|
||||
"agents (novox/hq issue 340; a file nothing trusts says \"trusted\": false). Nothing was changed",
|
||||
@@ -1119,7 +1119,7 @@ func refuseTerminalSettingsThroughAVerb(ctx context.Context, inv *inventory.Inve
|
||||
if string(was) == string(now) {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("%s of %s on %s is set at the controller's terminal only, never through a verb (this "+
|
||||
return fmt.Errorf("%s of %s on %s is set at the controller's terminal only (`mesh-cli` on the control-node), never through a verb (this "+
|
||||
"line came through %q): it says where root creates and owns a module's directories, which of "+
|
||||
"the machine's paths are mounted into its container, what the mesh's consumers trust, or what a file "+
|
||||
"root or a person's session obeys takes, and whoever may call a verb includes agents (novox/hq issue 339; "+
|
||||
|
||||
@@ -264,6 +264,12 @@ func serve(ctx context.Context) (err error) {
|
||||
return err
|
||||
}
|
||||
defer stopServing()
|
||||
// And the operator's command on every node, asked through each node's engine (novox/hq ADR 0272).
|
||||
stopCLI, err := bus.ServeCLI(answerMeshCLI(open.inventory), log.New(os.Stdout, "", log.LstdFlags))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stopCLI()
|
||||
// And says so on the bus (novox/hq ADR 0197): what it serves, as the NATS services protocol asks.
|
||||
stopAnnouncing, err := bus.Announce(seatAnnouncement(handlers), log.New(os.Stdout, "", log.LstdFlags))
|
||||
if err != nil {
|
||||
|
||||
@@ -26,6 +26,12 @@ import (
|
||||
// shape fails the suite, naming its source. The keeper would say such a summary in words at run time;
|
||||
// this is where the producer is made to say it rightly in the first place.
|
||||
func TestMain(m *testing.M) {
|
||||
// The process a mesh-cli test runs as a command line: it says the verb and the caller it was given, and
|
||||
// ends (meshcli_test.go).
|
||||
if os.Getenv(echoEnvironment) != "" {
|
||||
fmt.Printf("verb=%q caller=%q\n", os.Getenv("MESH_VERB"), os.Getenv("MESH_CALLER"))
|
||||
os.Exit(0)
|
||||
}
|
||||
conditions.Unsayable = func(o conditions.Observation, field string, r outward.Refusal) {
|
||||
unsaid.note(o, field, r)
|
||||
}
|
||||
|
||||
@@ -226,6 +226,30 @@ func quoteAll(xs []string) string {
|
||||
return strings.Join(q, ", ")
|
||||
}
|
||||
|
||||
// refusedAsTheGenericCommand is what the generic `command` verb refuses of a command line, and so what every
|
||||
// line asked through a verb's route refuses: the `command` verb's, and an ordinary line from mesh-cli (novox/hq ADR
|
||||
// 0272 §4). One function, so the two routes cannot drift apart.
|
||||
func refusedAsTheGenericCommand(argv []string) error {
|
||||
if len(argv) == 0 {
|
||||
return errors.New("command names no command")
|
||||
}
|
||||
// A layer is written through the settings verb, never the generic one (novox/hq issue 339): the
|
||||
// settings verb is where what a verb may not set is refused, and one route is one set of words.
|
||||
// The command refuses places and accesses through any verb as well; this says so before it runs.
|
||||
if argv[0] == "settings" && slices.ContainsFunc(argv[1:], func(w string) bool { return w == "set" || w == "clear" }) {
|
||||
return errors.New("settings are set and cleared through the settings verb, not the generic " +
|
||||
"command; and places and accesses only at the controller's terminal (novox/hq issue 339). " +
|
||||
"Nothing was done")
|
||||
}
|
||||
// The generic verb is no way round the hand-act log (novox/hq to-be 45 §7): a repair through
|
||||
// it says why, as it would through its own verb.
|
||||
if repair := repairingCommand(argv); repair != "" && !slices.ContainsFunc(argv, isWhyFlag) {
|
||||
return fmt.Errorf("%s is a repair done by hand, and says why: add --why <text> to the command "+
|
||||
"line (recorded in the hand-act log). Nothing was done", repair)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// commandLine composes the command. Every argument it reads is one it uses: a branch that reads an
|
||||
// argument and then drops it would pass it over, which is what the check after it exists to refuse.
|
||||
func (a *verbArguments) commandLine() ([]string, error) {
|
||||
@@ -242,22 +266,8 @@ func (a *verbArguments) commandLine() ([]string, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(argv) == 0 {
|
||||
return nil, errors.New("command names no command")
|
||||
}
|
||||
// A layer is written through the settings verb, never the generic one (novox/hq issue 339): the
|
||||
// settings verb is where what a verb may not set is refused, and one route is one set of words.
|
||||
// The command refuses places and accesses through any verb as well; this says so before it runs.
|
||||
if argv[0] == "settings" && slices.ContainsFunc(argv[1:], func(w string) bool { return w == "set" || w == "clear" }) {
|
||||
return nil, errors.New("settings are set and cleared through the settings verb, not the generic " +
|
||||
"command; and places and accesses only at the controller's terminal (novox/hq issue 339). " +
|
||||
"Nothing was done")
|
||||
}
|
||||
// The generic verb is no way round the hand-act log (novox/hq to-be 45 §7): a repair through
|
||||
// it says why, as it would through its own verb.
|
||||
if repair := repairingCommand(argv); repair != "" && !slices.ContainsFunc(argv, isWhyFlag) {
|
||||
return nil, fmt.Errorf("%s is a repair done by hand, and says why: add --why <text> to the command "+
|
||||
"line (recorded in the hand-act log). Nothing was done", repair)
|
||||
if err := refusedAsTheGenericCommand(argv); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return argv, nil
|
||||
case "tools":
|
||||
@@ -911,6 +921,25 @@ func isWhyFlag(word string) bool {
|
||||
return word == "--why" || word == "-why" || strings.HasPrefix(word, "--why=") || strings.HasPrefix(word, "-why=")
|
||||
}
|
||||
|
||||
// commandEnvironment is the environment of a command line this controller runs for someone: its own — the
|
||||
// stores' credentials, the bus, the broker, everything a command run from a shell beside it would have, because it
|
||||
// is that — with who asked, and the verb it came through. An empty verb is the controller's terminal (novox/hq ADR
|
||||
// 0272 §4): no `MESH_VERB` at all, whatever this process was started with.
|
||||
func commandEnvironment(caller, verb string) []string {
|
||||
env := make([]string, 0, len(os.Environ())+2)
|
||||
for _, kv := range os.Environ() {
|
||||
if strings.HasPrefix(kv, verbVar+"=") || strings.HasPrefix(kv, link.CallerVar+"=") {
|
||||
continue
|
||||
}
|
||||
env = append(env, kv)
|
||||
}
|
||||
env = append(env, link.CallerVar+"="+caller)
|
||||
if verb != "" {
|
||||
env = append(env, verbVar+"="+verb)
|
||||
}
|
||||
return env
|
||||
}
|
||||
|
||||
// runVerb runs this binary with the given command line and gathers what it said.
|
||||
//
|
||||
// **This binary is the image this process runs, never the file at the path it started from**
|
||||
@@ -924,21 +953,17 @@ func runVerb(ctx context.Context, argv []string) (verbAnswer, error) {
|
||||
return verbAnswer{}, fmt.Errorf("%w: this controller is stopping and runs no new command", link.ErrHandingOver)
|
||||
}
|
||||
cmd := selfCommand(ctx, argv)
|
||||
// The same environment: the stores' credentials, the bus, the broker — everything a command run
|
||||
// from a shell in this container would have, because it is that.
|
||||
cmd.Env = os.Environ()
|
||||
// And who asked, so an act it does by hand is recorded as theirs (novox/hq to-be 45 §7).
|
||||
caller := link.CallerIn(ctx)
|
||||
if caller == "" {
|
||||
caller = "a seat call whose caller the bus did not name"
|
||||
}
|
||||
cmd.Env = append(cmd.Env, link.CallerVar+"="+caller+", through the "+catalogue.ControllerSeatName+" seat")
|
||||
// And which verb, so the connection it dials says so in the bus's list (novox/hq issue 327).
|
||||
verb, _ := ctx.Value(verbKey{}).(string)
|
||||
if verb == "" {
|
||||
verb = argv[0]
|
||||
}
|
||||
cmd.Env = append(cmd.Env, verbVar+"="+verb)
|
||||
cmd.Env = commandEnvironment(caller+", through the "+catalogue.ControllerSeatName+" seat", verb)
|
||||
// Two buffers, one answer. What the command *says* is both streams, in the order a person at
|
||||
// a shell would read them; what it *answers as data* is standard output alone — `status --json`
|
||||
// prints its warnings beside the document, and a JSON parsed from the two together parsed
|
||||
|
||||
Reference in New Issue
Block a user