diff --git a/go/seats/nodeengine/contract.go b/go/seats/nodeengine/contract.go index 825e95e..44e19e5 100644 --- a/go/seats/nodeengine/contract.go +++ b/go/seats/nodeengine/contract.go @@ -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. diff --git a/go/seats/nodeengine/contract_test.go b/go/seats/nodeengine/contract_test.go index be4495f..441832c 100644 --- a/go/seats/nodeengine/contract_test.go +++ b/go/seats/nodeengine/contract_test.go @@ -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}, diff --git a/identity-and-access/go/seats/meshgrants/contract.go b/identity-and-access/go/seats/meshgrants/contract.go index a2c3e2c..6d40945 100644 --- a/identity-and-access/go/seats/meshgrants/contract.go +++ b/identity-and-access/go/seats/meshgrants/contract.go @@ -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 diff --git a/identity-and-access/go/seats/meshgrants/contract_test.go b/identity-and-access/go/seats/meshgrants/contract_test.go index ef84ac6..2c30e2e 100644 --- a/identity-and-access/go/seats/meshgrants/contract_test.go +++ b/identity-and-access/go/seats/meshgrants/contract_test.go @@ -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)) diff --git a/identity-and-access/seats/mesh-grants/fixtures/verbs.json b/identity-and-access/seats/mesh-grants/fixtures/verbs.json index e2bd9f1..622e0cb 100644 --- a/identity-and-access/seats/mesh-grants/fixtures/verbs.json +++ b/identity-and-access/seats/mesh-grants/fixtures/verbs.json @@ -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" + } } ] diff --git a/identity-and-access/seats/mesh-grants/seat.json b/identity-and-access/seats/mesh-grants/seat.json index 8fdb2ca..49e5cd8 100644 --- a/identity-and-access/seats/mesh-grants/seat.json +++ b/identity-and-access/seats/mesh-grants/seat.json @@ -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" + ] } } } diff --git a/identity-and-access/ts/seats/meshgrants/index.d.ts b/identity-and-access/ts/seats/meshgrants/index.d.ts index 167e2b8..899e889 100644 --- a/identity-and-access/ts/seats/meshgrants/index.d.ts +++ b/identity-and-access/ts/seats/meshgrants/index.d.ts @@ -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; diff --git a/identity-and-access/ts/seats/meshgrants/index.js b/identity-and-access/ts/seats/meshgrants/index.js index e19f333..2bd80bd 100644 --- a/identity-and-access/ts/seats/meshgrants/index.js +++ b/identity-and-access/ts/seats/meshgrants/index.js @@ -6,6 +6,7 @@ export const seat = "mesh-grants"; export const verbs = { grantsFor: "grants-for", + withheld: "withheld", }; export const events = { diff --git a/seats/node-engine/fixtures/states.json b/seats/node-engine/fixtures/states.json index db64100..f9b2f5b 100644 --- a/seats/node-engine/fixtures/states.json +++ b/seats/node-engine/fixtures/states.json @@ -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": { diff --git a/seats/node-engine/seat.json b/seats/node-engine/seat.json index 3225f06..6ff2c9e 100644 --- a/seats/node-engine/seat.json +++ b/seats/node-engine/seat.json @@ -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": [ diff --git a/ts/seats/nodeengine/index.d.ts b/ts/seats/nodeengine/index.d.ts index 8eeb44e..7051477 100644 --- a/ts/seats/nodeengine/index.d.ts +++ b/ts/seats/nodeengine/index.d.ts @@ -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[];