Merge pull request 'ADR 0268: the login shell's execute is withheld on the control-node until a call to it needs a person's approval' (#235) from decision/0268-the-login-shells-execute-is-withheld-on-the-control-node into main
This commit was merged in pull request #235.
This commit is contained in:
+139
@@ -0,0 +1,139 @@
|
||||
---
|
||||
topic: the mesh
|
||||
status: accepted
|
||||
date: 2026-10-09
|
||||
deciders: jochen
|
||||
reconstructed: false
|
||||
extends: 02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md
|
||||
---
|
||||
|
||||
# 268. The login shell's `execute` is withheld on the control-node until a call to it needs a person's approval
|
||||
|
||||
## Context
|
||||
|
||||
The mesh's `node-login-shell` seat has one verb, `execute`
|
||||
([ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md),
|
||||
[ADR 0204](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md)). It runs any
|
||||
command it is given as the node's operator account, in a login shell. That account has passwordless
|
||||
sudo, so one call is root on that node, and any caller of the mesh's tools can make it: an agent
|
||||
session on any node, through the mesh MCP server, on any other node.
|
||||
|
||||
ADR 0266 (hq pull request 225, not yet merged) moves agents on the control-node to an account of their
|
||||
own that cannot become root without a person. From then on **the tools are the boundary** on that node,
|
||||
and `execute` is a way round it. ADR 0259 §8 (hq pull request 211, not yet merged) counts the same
|
||||
path in its self-check of who can become root where the trusted parties run.
|
||||
|
||||
The operator first decided, on 2026-10-09, to drop the verb on every node. Agents use it a lot, though:
|
||||
research 039 (hq pull request 233, not yet merged) counted about 300 calls across the four nodes on
|
||||
2026-10-08, about a third of them running something with sudo. Most of the rest were reads: reading or
|
||||
searching a file, a container's details or logs, a unit's state, the listening sockets, a user or a
|
||||
name. Dropping it everywhere at once leaves those reads with no tool. So the operator narrowed the
|
||||
decision the same day: withhold it on the control-node now, and keep it on the other nodes until a
|
||||
call to it needs a person's approval.
|
||||
|
||||
Two things in the mesh stood in the way of withholding it on one node only:
|
||||
|
||||
- `execute` was a **required** verb of the seat. A holder that does not serve a required verb is judged
|
||||
silent by the self-check, and holding the seat is conditional on serving it
|
||||
([ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md)).
|
||||
- A seat's verbs are the same on every node. Nothing let one node's holder serve fewer than another's.
|
||||
|
||||
## Considered Options
|
||||
|
||||
1. **Remove `execute` from the seat on every node.** This was the first decision. The verb would be
|
||||
gone from every machine at once, and so would every read agents use it for. Removing a verb is
|
||||
also a new seat version under
|
||||
[ADR 0246](0246-a-seats-new-verb-is-promised-before-it-is-required.md): three steps across two
|
||||
repositories.
|
||||
2. **Refuse the address in the mesh MCP server, per node.** That refuses one client's calls, not the
|
||||
verb. Any tool runner may call any tool on any node
|
||||
([ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md)), so the
|
||||
verb would still answer on the bus.
|
||||
3. **Wait for the approval mark.** The mark is not designed yet (research 039), and the control-node
|
||||
is the node whose root undoes ADR 0266 and ADR 0259 §8.
|
||||
4. **Withhold it per node, through the holder's own setting.** The seat marks `execute` optional, and
|
||||
the module holding the seat reads a per-node setting and does not register the verb where the
|
||||
setting says so. The tool runner on that node then offers no `execute`, so nothing on the bus
|
||||
answers it there.
|
||||
|
||||
## Decision
|
||||
|
||||
Option 4.
|
||||
|
||||
1. **`execute` is an optional verb of `node-login-shell`.** This is
|
||||
[ADR 0246](0246-a-seats-new-verb-is-promised-before-it-is-required.md)'s mark, used here for a verb
|
||||
a node may withhold rather than one its holders are catching up to. A holder that does not serve it
|
||||
on a node still holds the seat there, and is not judged silent.
|
||||
2. **The holder has a setting, `execute`,** a preference whose default is `serve`
|
||||
([ADR 0262](0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md)).
|
||||
A node's own value `withhold` withholds the verb there. The mesh writes the value into a file that
|
||||
root owns and the tool runner's account can only read. The holder's tools bundle reads it when the
|
||||
tool runner starts it, and a change to the file restarts the tool runner. **Anything but `serve`
|
||||
withholds the verb**: a wrong value, a missing line, a missing file. A verb that roots a node is not
|
||||
served by accident. The holder says why, on stderr and in its own read tool.
|
||||
3. **The control-node's value is `withhold`, from now on.** Its tool runner offers no `execute`, so a
|
||||
search finds none there and a call to it is refused as an address nobody serves. The other nodes keep
|
||||
the default for now.
|
||||
4. **Once the approval mark exists, `execute` needs a person's approval on every node, the
|
||||
control-node included.** The mark is being designed in research 039 (hq pull request 233): a tool
|
||||
or verb declares that a call needs a person's approval, and the mesh refuses the call without a
|
||||
warrant from ADR 0259's flow. The control-node may then serve `execute` again, under the mark. The
|
||||
other nodes keep it unmarked until then.
|
||||
5. **Narrow read tools replace the common reads first,** each a tool of the module that owns the area,
|
||||
so that an approval is asked for an act and not for a read. Otherwise nearly every approval is for a
|
||||
read, the operator learns to approve without reading, and approval is worth nothing. The reads
|
||||
measured on 2026-10-08, as follow-ups:
|
||||
- reading or searching a file on a node (`cat`, `grep`), bounded to what the account may read;
|
||||
- a container's details and logs (`docker inspect`, `docker logs`), in the module that holds the
|
||||
container runtime;
|
||||
- a unit's state and journal (`systemctl`, `journalctl`), checking first whether
|
||||
`node-service-manager`'s `status`, `journal` and `failed` cover the calls, and widening them where
|
||||
they do not;
|
||||
- the listening sockets (`ss`);
|
||||
- an account or a name (`getent`, `id`); `node-hostname`'s `entries` already covers `getent hosts`.
|
||||
|
||||
## Consequences
|
||||
|
||||
- On the control-node, no mesh tool runs an arbitrary command any more. Agents there use the narrow
|
||||
tools that exist, or ask the operator.
|
||||
- On the other nodes, any caller of the mesh's tools can still become root through `execute` until the
|
||||
approval mark exists. This is the operator's choice, made with the count above.
|
||||
- The seat now has no required verb, so a holder's silence on the bus is not judged on any node, the
|
||||
nodes that still serve `execute` included. A node whose holder stops answering is noticed when a call
|
||||
fails, not by the self-check.
|
||||
- ADR 0259 §8's self-check asks whether the login shell runs commands as an account that can become
|
||||
root on a node. It must read whether `execute` is served there, not whether the seat is held. Its code
|
||||
is on an open branch (mesh-controller pull request 154) and is changed there.
|
||||
- Someone with root on the control-node can still change the file and restart the tool runner. That is
|
||||
the root ADR 0266 takes away from agents. This decision closes the remote path, not that one.
|
||||
|
||||
## How it is checked
|
||||
|
||||
- **The seat:** `TestTheLoginShellsExecuteIsOptionalSoAMachineMayWithholdIt` (mesh-controller) holds
|
||||
`execute` optional, in the compiled seat and in a seat row read back from the store, and a holder
|
||||
that serves none still holds the seat. `TestALoginShellWithholdingExecuteIsNotSilent` holds that the
|
||||
self-check does not expect it to answer. Both are in mesh-controller pull request 169.
|
||||
- **The holder:** the zsh module's tests (mesh-catalog pull request 145) hold that only `serve` serves,
|
||||
that every other case withholds and says why, that the file is root's and named exactly by the
|
||||
bundle's word, and that the bundle registers no seat verb when withheld.
|
||||
- **Live, on the control-node:** `mesh-controller.settings` for `zsh` lists the control-node's value
|
||||
as `withhold`. The holder's `zsh_config` there answers `execute` as not served, with the reason.
|
||||
`mesh_search` for `node-login-shell execute` names the other three nodes and not the control-node,
|
||||
and a call to the control-node's address is refused.
|
||||
- **Not yet checked:** that the follow-up read tools exist, and that `execute` is marked once the mark
|
||||
exists. Research 039 carries both until a decision does.
|
||||
|
||||
## References
|
||||
|
||||
- [ADR 0176](0176-the-login-shell-is-a-node-seat-and-execute-is-its-contract.md),
|
||||
[ADR 0204](0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md): the seat and its
|
||||
verb.
|
||||
- [ADR 0132](0132-a-seat-carries-the-tools-its-holder-must-serve.md),
|
||||
[ADR 0246](0246-a-seats-new-verb-is-promised-before-it-is-required.md): required and optional verbs.
|
||||
- [ADR 0262](0262-a-preference-has-a-default-in-its-manifest-and-the-operators-own-value-has-none.md):
|
||||
a preference and a node's own value.
|
||||
- [ADR 0175](0175-one-tool-runtime-per-node-serves-every-modules-tools-on-the-host-side.md): any tool
|
||||
runner may call any tool on any node.
|
||||
- ADR 0266 (hq pull request 225), ADR 0259 (hq pull request 211), research 039 (hq pull request 233):
|
||||
not yet merged, so named without links.
|
||||
- mesh-controller pull request 169, mesh-catalog pull request 145.
|
||||
@@ -2,8 +2,9 @@
|
||||
layer: to-be
|
||||
status: in-progress
|
||||
code: [mesh-host, mesh-controller, mesh-catalog]
|
||||
updated: 2026-10-08
|
||||
updated: 2026-10-09
|
||||
decisions:
|
||||
- 02-DECISIONS/0268-the-login-shells-execute-is-withheld-on-the-control-node-until-a-call-to-it-needs-a-persons-approval.md
|
||||
- 02-DECISIONS/0252-a-module-puts-an-account-in-a-group-and-the-mesh-says-when-a-new-login-is-needed.md
|
||||
- 02-DECISIONS/0203-the-accounts-environment-is-one-modules-and-every-module-contributes-to-it.md
|
||||
- 02-DECISIONS/0204-a-module-contributes-shell-code-to-the-login-shell-in-named-slots.md
|
||||
@@ -51,7 +52,8 @@ Its two files are written by the node-engine from placeholders the controller fi
|
||||
keybindings, the aliases and the two small functions, with the three slots in place;
|
||||
- contributes its own environment: the editor, the configuration home, and `~/.local/bin` plus the
|
||||
two script directories on `PATH`;
|
||||
- serves `execute` and its own `zsh_config`.
|
||||
- serves `execute` where the node's setting serves it, and its own `zsh_config`. The control-node
|
||||
withholds `execute` (WP7).
|
||||
|
||||
**The prompt module** (`powerlevel10k`) ships the theme as a pinned vendored archive, and the prompt's
|
||||
configuration as its own file in a directory it owns. It contributes the zsh code that loads both.
|
||||
@@ -85,6 +87,7 @@ WP3 the modules (mesh-catalog) needs WP
|
||||
WP4 the service manager's module, finished (mesh-catalog) independent
|
||||
WP5 assign and prove (operator-gated) needs WP1–WP3 merged and rolled
|
||||
WP6 a module puts the account in a group (mesh-host, mesh-controller, mesh-catalog) issue 247
|
||||
WP7 the control-node withholds execute (mesh-controller, mesh-catalog) ADR 0268
|
||||
```
|
||||
|
||||
WP1, WP2 and WP4 are independent, and are built in parallel on one feature branch per repository
|
||||
@@ -292,3 +295,42 @@ on the first workstation the operator sends the lighting module to:
|
||||
**Order.** The three repositories merge in any order. A node-engine older than WP6 already applies the
|
||||
lighting module's resource: it puts the account in the group. It does not say the login, and does not
|
||||
take the group back.
|
||||
|
||||
## WP7 — The control-node withholds `execute`
|
||||
|
||||
*`mesh-controller`, `mesh-catalog`. Half a day.
|
||||
[ADR 0268](../../02-DECISIONS/0268-the-login-shells-execute-is-withheld-on-the-control-node-until-a-call-to-it-needs-a-persons-approval.md).*
|
||||
|
||||
`execute` runs any command as the operator account, which can become root without a person. The
|
||||
control-node withholds it until a call to it needs a person's approval. The other nodes keep it for now.
|
||||
|
||||
```
|
||||
settings zsh on the control-node: execute = withhold (the default is serve)
|
||||
│
|
||||
node-engine writes <zsh's directory>/tools.conf, root's, 0644; its change restarts the tool runner
|
||||
│
|
||||
zsh bundle reads it at start: anything but serve ── registers no execute
|
||||
│
|
||||
tool runner offers no node-login-shell.execute on the control-node; a call to it is refused
|
||||
```
|
||||
|
||||
**What changes.**
|
||||
|
||||
- **The seat table.** `execute` becomes an optional verb of `node-login-shell`
|
||||
([ADR 0246](../../02-DECISIONS/0246-a-seats-new-verb-is-promised-before-it-is-required.md)'s mark).
|
||||
A holder that withholds it still holds the seat and is not judged silent. The seat now has no
|
||||
required verb, so no node's holder is judged silent for it.
|
||||
- **The shell module.** It gains the setting `execute` (default `serve`), a root-owned file the mesh
|
||||
writes it into, and a word naming that file for its tools bundle. The bundle registers `execute` only
|
||||
when the file says `serve`. `zsh_config` also answers whether `execute` is served on the node, and why.
|
||||
|
||||
**Merge order.** The controller first, then the catalogue. Either order composes. With the catalogue
|
||||
alone and the control-node's value set, the self-check would say the holder there is silent.
|
||||
|
||||
**Proof.** The tests ADR 0268 lists under *how it is checked*. Live, after both are merged and rolled
|
||||
out, the control-node's value is set to `withhold` and zsh is pushed to it. Then:
|
||||
|
||||
1. `mesh-controller.settings` for `zsh` lists the control-node's value as `withhold`;
|
||||
2. the control-node's `zsh_config` answers `execute` as not served, and says why;
|
||||
3. `mesh_search` for `node-login-shell execute` names the other three nodes and not the control-node;
|
||||
4. a call to the control-node's `node-login-shell.execute` is refused, and the other nodes still answer.
|
||||
|
||||
Reference in New Issue
Block a user