AM-D3/AM-H1 path: the node's record carries the runner's reading; mesh-grants gains withheld

The grants part must tell a reading of the running tool runner from an older
one, and the node-engine's composition reads the withheld subjects through a
verb of the seat, never the grants part's state.

Builds: design 46 7 §6 (the node's record takes the runner's capabilities and fingerprint; the seat mesh-grants' verb withheld, class read, callers the node-engine, persons and the operator, input node, with a fixture), lead answer A-203 (runner-capabilities, runner-fingerprint; withheld's input and answer)

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
This commit is contained in:
2026-11-12 06:04:56 +01:00
parent 49e26f5d27
commit ff63be3028
11 changed files with 201 additions and 8 deletions
+9 -5
View File
@@ -813,11 +813,15 @@ type NodeRecord struct {
NetworksSaid bool `json:"networks-said,omitempty"`
Node string `json:"node"`
OutwardLinks []string `json:"outward-links,omitempty"`
SealingKey string `json:"sealing-key,omitempty"`
ServingKey string `json:"serving-key,omitempty"`
Trusts []string `json:"trusts,omitempty"`
TunnelKey string `json:"tunnel-key,omitempty"`
V int64 `json:"v"`
// the capabilities this node's tool runner states of the calls it answers, as its announcement carries them (design 46 approval mark 6)
RunnerCapabilities []string `json:"runner-capabilities,omitempty"`
// the fingerprint of the tool runner that stated runner-capabilities, as its announcement carries it; empty when it stated none
RunnerFingerprint string `json:"runner-fingerprint,omitempty"`
SealingKey string `json:"sealing-key,omitempty"`
ServingKey string `json:"serving-key,omitempty"`
Trusts []string `json:"trusts,omitempty"`
TunnelKey string `json:"tunnel-key,omitempty"`
V int64 `json:"v"`
}
// PeerSeen is a tunnel peer and whether it is seen.
+1 -1
View File
@@ -85,7 +85,7 @@ func TestFixtures(t *testing.T) {
decode: func(b []byte) (any, error) { var v HaveByNodeModuleValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "the bus module's have carries the user list written (typed from the engine's values)", subject: "", given: "{\"health\":\"ok\",\"phase\":\"running\",\"user-list\":{\"at\":\"2026-11-05T12:00:00Z\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"follows\":\"grants\",\"handed\":{\"at\":\"2026-11-05T12:00:00Z\",\"box\":{\"ct\":\"c\",\"eph\":\"e\",\"iv\":\"i\",\"tag\":\"t\",\"v\":1},\"from\":\"ace\",\"to\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"users\":3},\"key\":\"-----BEGIN PUBLIC KEY-----\",\"revision\":7,\"source\":\"user-list\",\"users\":3,\"wanted\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"},\"v\":1}", wire: "{\"health\":\"ok\",\"phase\":\"running\",\"user-list\":{\"at\":\"2026-11-05T12:00:00Z\",\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"follows\":\"grants\",\"handed\":{\"at\":\"2026-11-05T12:00:00Z\",\"box\":{\"ct\":\"c\",\"eph\":\"e\",\"iv\":\"i\",\"tag\":\"t\",\"v\":1},\"from\":\"ace\",\"to\":\"sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\",\"users\":3},\"key\":\"-----BEGIN PUBLIC KEY-----\",\"revision\":7,\"source\":\"user-list\",\"users\":3,\"wanted\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"},\"v\":1}",
decode: func(b []byte) (any, error) { var v HaveByNodeModuleValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "a node's own facts with its record and an answer to a candidate (typed from the engine's values)", subject: "", given: "{\"candidates\":{\"p1\":{\"added\":[{\"module\":\"db\",\"unissued\":[\"db.secret\"]}],\"differs\":[{\"bodies\":[{\"id\":\"api.cfg\",\"paths\":[\"/etc/api.conf\"],\"why\":\"content\"}],\"composed\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"module\":\"api\",\"running\":\"sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"why\":\"a file differs\"}],\"proposal\":\"p1\",\"refusals\":[{\"module\":\"mail\",\"today\":\"runs\",\"why\":\"a value is missing\"}],\"removed\":[\"old\"],\"revision\":4,\"same\":[\"web\"],\"verdict\":\"differs\"}},\"composed\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"composes\":\"own\",\"record\":{\"capabilities\":[\"docker\"],\"contract\":3,\"networks\":[{\"link\":\"eth0\",\"network\":\"192.0.2.0/24\"}],\"networks-said\":true,\"node\":\"ace\",\"tunnel-key\":\"k\",\"v\":1},\"v\":1}", wire: "{\"candidates\":{\"p1\":{\"added\":[{\"module\":\"db\",\"unissued\":[\"db.secret\"]}],\"differs\":[{\"bodies\":[{\"id\":\"api.cfg\",\"paths\":[\"/etc/api.conf\"],\"why\":\"content\"}],\"composed\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"module\":\"api\",\"running\":\"sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"why\":\"a file differs\"}],\"proposal\":\"p1\",\"refusals\":[{\"module\":\"mail\",\"today\":\"runs\",\"why\":\"a value is missing\"}],\"removed\":[\"old\"],\"revision\":4,\"same\":[\"web\"],\"verdict\":\"differs\"}},\"composed\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"composes\":\"own\",\"record\":{\"capabilities\":[\"docker\"],\"contract\":3,\"networks\":[{\"link\":\"eth0\",\"network\":\"192.0.2.0/24\"}],\"networks-said\":true,\"node\":\"ace\",\"tunnel-key\":\"k\",\"v\":1},\"v\":1}",
{name: "a node's own facts with its record and an answer to a candidate (typed from the engine's values)", subject: "", given: "{\"candidates\":{\"p1\":{\"added\":[{\"module\":\"db\",\"unissued\":[\"db.secret\"]}],\"differs\":[{\"bodies\":[{\"id\":\"api.cfg\",\"paths\":[\"/etc/api.conf\"],\"why\":\"content\"}],\"composed\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"module\":\"api\",\"running\":\"sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"why\":\"a file differs\"}],\"proposal\":\"p1\",\"refusals\":[{\"module\":\"mail\",\"today\":\"runs\",\"why\":\"a value is missing\"}],\"removed\":[\"old\"],\"revision\":4,\"same\":[\"web\"],\"verdict\":\"differs\"}},\"composed\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"composes\":\"own\",\"record\":{\"capabilities\":[\"approval-mark/1\",\"docker\"],\"contract\":3,\"networks\":[{\"link\":\"eth0\",\"network\":\"192.0.2.0/24\"}],\"networks-said\":true,\"node\":\"ace\",\"runner-capabilities\":[\"approval-mark/1\"],\"runner-fingerprint\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"tunnel-key\":\"k\",\"v\":1},\"v\":1}", wire: "{\"candidates\":{\"p1\":{\"added\":[{\"module\":\"db\",\"unissued\":[\"db.secret\"]}],\"differs\":[{\"bodies\":[{\"id\":\"api.cfg\",\"paths\":[\"/etc/api.conf\"],\"why\":\"content\"}],\"composed\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"module\":\"api\",\"running\":\"sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd\",\"why\":\"a file differs\"}],\"proposal\":\"p1\",\"refusals\":[{\"module\":\"mail\",\"today\":\"runs\",\"why\":\"a value is missing\"}],\"removed\":[\"old\"],\"revision\":4,\"same\":[\"web\"],\"verdict\":\"differs\"}},\"composed\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"composes\":\"own\",\"record\":{\"capabilities\":[\"approval-mark/1\",\"docker\"],\"contract\":3,\"networks\":[{\"link\":\"eth0\",\"network\":\"192.0.2.0/24\"}],\"networks-said\":true,\"node\":\"ace\",\"runner-capabilities\":[\"approval-mark/1\"],\"runner-fingerprint\":\"sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"tunnel-key\":\"k\",\"v\":1},\"v\":1}",
decode: func(b []byte) (any, error) { var v HaveByNodeNodeValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "a module's data items on a node (typed from the engine's values)", subject: "", given: "{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"items\":[{\"class\":\"database\",\"covered_by\":\"/var/lib/postgres\",\"item\":\"cluster\",\"measure\":\"dataset\",\"module\":\"postgres\",\"path\":\"/var/lib/postgres\",\"protection\":\"backup\"}],\"module\":\"postgres\",\"node\":\"ace\",\"v\":1}", wire: "{\"digest\":\"sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"items\":[{\"class\":\"database\",\"covered_by\":\"/var/lib/postgres\",\"item\":\"cluster\",\"measure\":\"dataset\",\"module\":\"postgres\",\"path\":\"/var/lib/postgres\",\"protection\":\"backup\"}],\"module\":\"postgres\",\"node\":\"ace\",\"v\":1}",
decode: func(b []byte) (any, error) { var v DataByNodeModuleValue; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
@@ -10,6 +10,7 @@ const Seat = "mesh-grants"
const (
VerbGrantsFor = "grants-for"
VerbWithheld = "withheld"
)
const (
@@ -21,7 +22,7 @@ const (
// Verbs is every verb the seat serves, in its definition's order: a new copy each call.
func Verbs() []string {
return []string{VerbGrantsFor}
return []string{VerbGrantsFor, VerbWithheld}
}
// Events is every event published on the seat's subject: a new copy each call.
@@ -113,6 +114,24 @@ const (
WaitStepGrant = "grant"
)
// WithheldModulesItem is a type of the contract.
type WithheldModulesItem struct {
Module string `json:"module"`
Subjects []WithheldSubject `json:"subjects"`
}
// Withheld is a node's withheld subjects, by module.
type Withheld struct {
Modules []WithheldModulesItem `json:"modules"`
Node string `json:"node"`
}
// WithheldSubject is one subject withheld, and why.
type WithheldSubject struct {
Subject string `json:"subject"`
Why string `json:"why"`
}
// GrantsForInput is what grants-for takes.
type GrantsForInput struct {
// its watched branch
@@ -126,6 +145,15 @@ type GrantsForInput struct {
// GrantsForAnswer is what grants-for answers.
type GrantsForAnswer = GrantsFor
// WithheldInput is what withheld takes.
type WithheldInput struct {
// the node whose runtime's withheld subjects are asked
Node string `json:"node"`
}
// WithheldAnswer is what withheld answers.
type WithheldAnswer = Withheld
// WaitHeldEvent is the body of the event wait-held.
type WaitHeldEvent = Wait
@@ -29,6 +29,10 @@ func TestFixtures(t *testing.T) {
decode: func(b []byte) (any, error) { var v GrantsForAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "grants-for asks a source revision on a branch", subject: "", given: "{\"branch\":\"main\",\"source\":\"a1b2c3d4\"}", wire: "{\"branch\":\"main\",\"source\":\"a1b2c3d4\"}",
decode: func(b []byte) (any, error) { var v GrantsForInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "withheld answers a node's withheld subjects by module", subject: "", given: "{\"modules\":[{\"module\":\"zsh\",\"subjects\":[{\"subject\":\"mesh.mod.zsh.tool.execute\",\"why\":\"its tool runner does not state approval-mark/1\"},{\"subject\":\"mesh.mod.zsh.tool.execute.*\",\"why\":\"its tool runner does not state approval-mark/1\"},{\"subject\":\"mesh.mod.zsh.call.execute.\\u003e\",\"why\":\"its tool runner does not state approval-mark/1\"}]}],\"node\":\"workstation\"}", wire: "{\"modules\":[{\"module\":\"zsh\",\"subjects\":[{\"subject\":\"mesh.mod.zsh.tool.execute\",\"why\":\"its tool runner does not state approval-mark/1\"},{\"subject\":\"mesh.mod.zsh.tool.execute.*\",\"why\":\"its tool runner does not state approval-mark/1\"},{\"subject\":\"mesh.mod.zsh.call.execute.\\u003e\",\"why\":\"its tool runner does not state approval-mark/1\"}]}],\"node\":\"workstation\"}",
decode: func(b []byte) (any, error) { var v WithheldAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
{name: "withheld asks a node", subject: "", given: "{\"node\":\"workstation\"}", wire: "{\"node\":\"workstation\"}",
decode: func(b []byte) (any, error) { var v WithheldInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
}
for _, c := range cases {
v, err := c.decode([]byte(c.given))
@@ -24,5 +24,38 @@
"source": "a1b2c3d4",
"branch": "main"
}
},
{
"name": "withheld answers a node's withheld subjects by module",
"of": "verb:withheld.answer",
"given": {
"node": "workstation",
"modules": [
{
"module": "zsh",
"subjects": [
{
"subject": "mesh.mod.zsh.tool.execute",
"why": "its tool runner does not state approval-mark/1"
},
{
"subject": "mesh.mod.zsh.tool.execute.*",
"why": "its tool runner does not state approval-mark/1"
},
{
"subject": "mesh.mod.zsh.call.execute.>",
"why": "its tool runner does not state approval-mark/1"
}
]
}
]
}
},
{
"name": "withheld asks a node",
"of": "verb:withheld.input",
"given": {
"node": "workstation"
}
}
]
@@ -41,6 +41,32 @@
"answer": {
"$ref": "#/defs/GrantsFor"
}
},
{
"name": "withheld",
"description": "The marked subjects withheld from each module a node's runtime carries, and why: where its tool runner does not state approval-mark/1, or its reading is not the running runner's (design 46 approval mark 6).",
"class": "read",
"callers": [
"seat:node-engine",
"persons",
"operator"
],
"input": {
"type": "object",
"properties": {
"node": {
"type": "string",
"description": "the node whose runtime's withheld subjects are asked"
}
},
"required": [
"node"
],
"additionalProperties": false
},
"answer": {
"$ref": "#/defs/Withheld"
}
}
],
"events": [
@@ -217,6 +243,56 @@
"description": "why it could not be answered: the catalog half of the candidate is not held"
}
}
},
"Withheld": {
"type": "object",
"description": "a node's withheld subjects, by module",
"properties": {
"node": {
"type": "string"
},
"modules": {
"type": "array",
"items": {
"type": "object",
"properties": {
"module": {
"type": "string"
},
"subjects": {
"type": "array",
"items": {
"$ref": "#/defs/WithheldSubject"
}
}
},
"required": [
"module",
"subjects"
]
}
}
},
"required": [
"node",
"modules"
]
},
"WithheldSubject": {
"type": "object",
"description": "one subject withheld, and why",
"properties": {
"subject": {
"type": "string"
},
"why": {
"type": "string"
}
},
"required": [
"subject",
"why"
]
}
}
}
+27
View File
@@ -6,6 +6,7 @@ export declare const seat: "mesh-grants";
export declare const verbs: {
readonly grantsFor: "grants-for";
readonly withheld: "withheld";
};
export declare const events: {
@@ -78,6 +79,23 @@ export interface Wait {
"version": string;
}
export interface WithheldModulesItem {
"module": string;
"subjects": WithheldSubject[];
}
/** a node's withheld subjects, by module */
export interface Withheld {
"modules": WithheldModulesItem[];
"node": string;
}
/** one subject withheld, and why */
export interface WithheldSubject {
"subject": string;
"why": string;
}
/** is what grants-for takes. */
export interface GrantsForInput {
/** its watched branch */
@@ -91,6 +109,15 @@ export interface GrantsForInput {
/** is what grants-for answers. */
export type GrantsForAnswer = GrantsFor;
/** is what withheld takes. */
export interface WithheldInput {
/** the node whose runtime's withheld subjects are asked */
"node": string;
}
/** is what withheld answers. */
export type WithheldAnswer = Withheld;
/** is the body of the event wait-held. */
export type WaitHeldEvent = Wait;
@@ -6,6 +6,7 @@ export const seat = "mesh-grants";
export const verbs = {
grantsFor: "grants-for",
withheld: "withheld",
};
export const events = {
+6 -1
View File
@@ -388,6 +388,7 @@
"node": "ace",
"tunnel-key": "k",
"capabilities": [
"approval-mark/1",
"docker"
],
"networks": [
@@ -397,7 +398,11 @@
}
],
"networks-said": true,
"contract": 3
"contract": 3,
"runner-capabilities": [
"approval-mark/1"
],
"runner-fingerprint": "sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
},
"composes": "own",
"candidates": {
+11
View File
@@ -2342,6 +2342,17 @@
"items": {
"type": "string"
}
},
"runner-capabilities": {
"type": "array",
"items": {
"type": "string"
},
"description": "the capabilities this node's tool runner states of the calls it answers, as its announcement carries them (design 46 approval mark 6)"
},
"runner-fingerprint": {
"type": "string",
"description": "the fingerprint of the tool runner that stated runner-capabilities, as its announcement carries it; empty when it stated none"
}
},
"required": [
+4
View File
@@ -473,6 +473,10 @@ export interface NodeRecord {
"networks-said"?: boolean;
"node": string;
"outward-links"?: string[];
/** the capabilities this node's tool runner states of the calls it answers, as its announcement carries them (design 46 approval mark 6) */
"runner-capabilities"?: string[];
/** the fingerprint of the tool runner that stated runner-capabilities, as its announcement carries it; empty when it stated none */
"runner-fingerprint"?: string;
"sealing-key"?: string;
"serving-key"?: string;
"trusts"?: string[];