Seat list reader: calls the seat mesh-catalog's seat-definitions and hears its changed and removed events, acts on nothing new until read, two runtime tools; bootstrap gains mesh-conditions, operator-channel and the contracts' current files (ADR 0636 §1-§4, §7; issue 1966)
Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
This commit is contained in:
@@ -3,8 +3,22 @@
|
||||
"seat": "mesh-catalog",
|
||||
"scope": "mesh",
|
||||
"grain": "seat-list",
|
||||
"decision": "novox/hq ADR 0604",
|
||||
"decision": "novox/hq ADR 0636",
|
||||
"verbs": [
|
||||
{
|
||||
"name": "seat-definitions",
|
||||
"description": "The seat list (ADR 0636 §4): with no seat, every trunk seat with its digest and the revision of the list; with a seat, that trunk definition resolved with the parts it includes. Reads only.",
|
||||
"optional": true,
|
||||
"input": {
|
||||
"properties": {
|
||||
"seat": {
|
||||
"description": "the seat's name; none lists every trunk seat",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "modules",
|
||||
"description": "Every module the trunk holds, each with its repository, its directory there, the merge commit it was last changed at and its fingerprint (or why it has none): the catalog of the mesh, read from its holder's state. Reads only.",
|
||||
|
||||
@@ -0,0 +1,646 @@
|
||||
{
|
||||
"format": 1,
|
||||
"seat": "mesh-conditions",
|
||||
"scope": "mesh",
|
||||
"grain": "whole",
|
||||
"decision": "novox/hq ADR 0564, ADR 0634, ADR 0635",
|
||||
"description": "What is wrong with the mesh now (ADR 0564): the open conditions raised from the observers' findings and cleared when a finding goes.",
|
||||
"verbs": [
|
||||
{
|
||||
"name": "conditions",
|
||||
"description": "What is wrong with the mesh now: every open condition, urgent first, then oldest, raised from the observers' findings (the watchman, the self-check, the healers, the assigner) and cleared when the finding goes, never by hand. Given a key, that one whole with its evidence; with history, every transition lately.",
|
||||
"input": {
|
||||
"properties": {
|
||||
"days": {
|
||||
"description": "with history: how many days back (default 7, at most 90)",
|
||||
"type": "string"
|
||||
},
|
||||
"history": {
|
||||
"description": "\"true\": every raising, change, silence and clearing lately, oldest first",
|
||||
"enum": [
|
||||
"true",
|
||||
"false"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"key": {
|
||||
"description": "a condition's key: that one whole; with history, only its transitions",
|
||||
"type": "string"
|
||||
},
|
||||
"machine": {
|
||||
"description": "only those about this machine",
|
||||
"type": "string"
|
||||
},
|
||||
"scope": {
|
||||
"description": "only this scope: machine, plan, call, build, merge, provider, seat, bus, core, probe, mesh, delivery or module",
|
||||
"type": "string"
|
||||
},
|
||||
"severity": {
|
||||
"description": "only urgent, or only warning",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"answer": {
|
||||
"type": "object",
|
||||
"description": "the open conditions, or one whole, or the history"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "silence",
|
||||
"description": "Stop one condition's messages for a while, a hand act with why: it stays open and in status, and the silence is kept as its transition in history.",
|
||||
"input": {
|
||||
"properties": {
|
||||
"cause": {
|
||||
"description": "the cause in a word (the condition's kind when absent)",
|
||||
"type": "string"
|
||||
},
|
||||
"for": {
|
||||
"description": "how long: 30m, 4h, 2d; at most 7d",
|
||||
"type": "string"
|
||||
},
|
||||
"key": {
|
||||
"description": "the condition's key",
|
||||
"type": "string"
|
||||
},
|
||||
"why": {
|
||||
"description": "why, required and recorded",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key",
|
||||
"for",
|
||||
"why"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"answer": {
|
||||
"type": "object",
|
||||
"description": "the condition now silenced"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "resync",
|
||||
"description": "Read every observer's findings again and raise and clear what differs from the open conditions: the recovery of a keeper that missed a change.",
|
||||
"input": {
|
||||
"properties": {},
|
||||
"type": "object"
|
||||
},
|
||||
"answer": {
|
||||
"type": "object",
|
||||
"description": "what was raised and cleared again"
|
||||
}
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"name": "condition-raised",
|
||||
"body": {
|
||||
"$ref": "#/defs/ConditionEvent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "condition-changed",
|
||||
"body": {
|
||||
"$ref": "#/defs/ConditionEvent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "condition-cleared",
|
||||
"body": {
|
||||
"$ref": "#/defs/ConditionEvent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"family": {
|
||||
"name": "mesh-conditions.events",
|
||||
"max-age-days": 7
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "conditions",
|
||||
"template": "state",
|
||||
"key-forms": [
|
||||
{
|
||||
"key": "<key>",
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "one open condition",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "scope.id.token"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"subject": {
|
||||
"$ref": "#/defs/Subject"
|
||||
},
|
||||
"severity": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"urgent",
|
||||
"warning"
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"description": "one line"
|
||||
},
|
||||
"headline": {
|
||||
"type": "string"
|
||||
},
|
||||
"explanation": {
|
||||
"type": "string"
|
||||
},
|
||||
"resolved": {
|
||||
"type": "string"
|
||||
},
|
||||
"needs": {
|
||||
"type": "string"
|
||||
},
|
||||
"actions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/defs/Action"
|
||||
}
|
||||
},
|
||||
"evidence": {
|
||||
"type": "array",
|
||||
"description": "newest first, at most 10",
|
||||
"items": {
|
||||
"$ref": "#/defs/Evidence"
|
||||
}
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"description": "the row, probe or event that saw it"
|
||||
},
|
||||
"resolver": {
|
||||
"type": "string",
|
||||
"description": "self, operator, agent or healer:<name>"
|
||||
},
|
||||
"raised": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"last-observed": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"first": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"gaps": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cleared": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"reopened": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"observations": {
|
||||
"type": "integer"
|
||||
},
|
||||
"count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"tried": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"what": {
|
||||
"type": "string"
|
||||
},
|
||||
"outcome": {
|
||||
"type": "string"
|
||||
},
|
||||
"by": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"silenced": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"until": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"by": {
|
||||
"type": "string"
|
||||
},
|
||||
"why": {
|
||||
"type": "string"
|
||||
},
|
||||
"since": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"epoch": {
|
||||
"type": "integer"
|
||||
},
|
||||
"looks-unread": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key",
|
||||
"kind",
|
||||
"subject",
|
||||
"severity",
|
||||
"raised",
|
||||
"last-observed"
|
||||
]
|
||||
},
|
||||
"description": "one open condition, without its looks it keeps from its finding"
|
||||
}
|
||||
],
|
||||
"description": "the open conditions the keeper holds, one key each"
|
||||
},
|
||||
{
|
||||
"name": "findings",
|
||||
"template": "state",
|
||||
"writer": "caller",
|
||||
"key-forms": [
|
||||
{
|
||||
"key": "<key>",
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "one thing an observer sees wrong now",
|
||||
"properties": {
|
||||
"v": {
|
||||
"type": "integer",
|
||||
"description": "layout version, 1"
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "scope.id.token"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"subject": {
|
||||
"$ref": "#/defs/Subject"
|
||||
},
|
||||
"severity": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"urgent",
|
||||
"warning"
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"description": "one line"
|
||||
},
|
||||
"headline": {
|
||||
"type": "string"
|
||||
},
|
||||
"explanation": {
|
||||
"type": "string"
|
||||
},
|
||||
"resolved": {
|
||||
"type": "string"
|
||||
},
|
||||
"needs": {
|
||||
"type": "string"
|
||||
},
|
||||
"actions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/defs/Action"
|
||||
}
|
||||
},
|
||||
"evidence": {
|
||||
"type": "array",
|
||||
"description": "newest first, at most 10",
|
||||
"items": {
|
||||
"$ref": "#/defs/Evidence"
|
||||
}
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"description": "the row, probe or event that saw it"
|
||||
},
|
||||
"resolver": {
|
||||
"type": "string",
|
||||
"description": "self, operator, agent or healer:<name>"
|
||||
},
|
||||
"ref": {
|
||||
"type": "string",
|
||||
"description": "the key's last part when it is not the kind"
|
||||
},
|
||||
"observer": {
|
||||
"type": "string",
|
||||
"description": "the module that wrote it"
|
||||
},
|
||||
"run": {
|
||||
"type": "string",
|
||||
"description": "what of its own saw it last"
|
||||
},
|
||||
"at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"v",
|
||||
"key",
|
||||
"kind",
|
||||
"subject",
|
||||
"severity",
|
||||
"summary",
|
||||
"source",
|
||||
"observer",
|
||||
"at"
|
||||
]
|
||||
},
|
||||
"description": "one finding of the observer that wrote it"
|
||||
}
|
||||
],
|
||||
"description": "every observer's findings, one key per finding; a key written raises or changes the condition of the same key, a key deleted clears it"
|
||||
}
|
||||
],
|
||||
"defs": {
|
||||
"Subject": {
|
||||
"type": "object",
|
||||
"description": "what the condition is about",
|
||||
"properties": {
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"description": "machine, plan, call, build, merge, provider, seat, bus, core, probe, mesh, delivery or module"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "its id within the scope"
|
||||
},
|
||||
"machine": {
|
||||
"type": "string",
|
||||
"description": "the machine it concerns, when there is one"
|
||||
},
|
||||
"also": {
|
||||
"type": "array",
|
||||
"description": "the other machines it concerns",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"scope",
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"Evidence": {
|
||||
"type": "object",
|
||||
"description": "one observation, as it was said",
|
||||
"properties": {
|
||||
"at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"said": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"at",
|
||||
"said"
|
||||
]
|
||||
},
|
||||
"Action": {
|
||||
"type": "object",
|
||||
"description": "one answer the operator may give from a notification",
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"verb": {
|
||||
"type": "string",
|
||||
"description": "the seat verb the operator channel calls"
|
||||
},
|
||||
"machine": {
|
||||
"type": "string"
|
||||
},
|
||||
"arguments": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"level": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"acknowledge",
|
||||
"approve"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"label",
|
||||
"verb"
|
||||
]
|
||||
},
|
||||
"ConditionEvent": {
|
||||
"type": "object",
|
||||
"description": "a condition and what changed",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "scope.id.token"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"subject": {
|
||||
"$ref": "#/defs/Subject"
|
||||
},
|
||||
"severity": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"urgent",
|
||||
"warning"
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"description": "one line"
|
||||
},
|
||||
"headline": {
|
||||
"type": "string"
|
||||
},
|
||||
"explanation": {
|
||||
"type": "string"
|
||||
},
|
||||
"resolved": {
|
||||
"type": "string"
|
||||
},
|
||||
"needs": {
|
||||
"type": "string"
|
||||
},
|
||||
"actions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/defs/Action"
|
||||
}
|
||||
},
|
||||
"evidence": {
|
||||
"type": "array",
|
||||
"description": "newest first, at most 10",
|
||||
"items": {
|
||||
"$ref": "#/defs/Evidence"
|
||||
}
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"description": "the row, probe or event that saw it"
|
||||
},
|
||||
"resolver": {
|
||||
"type": "string",
|
||||
"description": "self, operator, agent or healer:<name>"
|
||||
},
|
||||
"raised": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"last-observed": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"first": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"gaps": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"cleared": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"reopened": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"observations": {
|
||||
"type": "integer"
|
||||
},
|
||||
"count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"tried": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"what": {
|
||||
"type": "string"
|
||||
},
|
||||
"outcome": {
|
||||
"type": "string"
|
||||
},
|
||||
"by": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"silenced": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"until": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"by": {
|
||||
"type": "string"
|
||||
},
|
||||
"why": {
|
||||
"type": "string"
|
||||
},
|
||||
"since": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
}
|
||||
},
|
||||
"epoch": {
|
||||
"type": "integer"
|
||||
},
|
||||
"looks-unread": {
|
||||
"type": "string"
|
||||
},
|
||||
"event": {
|
||||
"type": "string"
|
||||
},
|
||||
"at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"change": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"raised",
|
||||
"reopened",
|
||||
"severity",
|
||||
"resolver",
|
||||
"silenced",
|
||||
"silence-ended",
|
||||
"cleared",
|
||||
"words"
|
||||
]
|
||||
},
|
||||
"was": {
|
||||
"type": "string"
|
||||
},
|
||||
"why": {
|
||||
"type": "string"
|
||||
},
|
||||
"cleared": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"show": {
|
||||
"type": "string",
|
||||
"description": "the tool call that shows it"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"key",
|
||||
"event",
|
||||
"at",
|
||||
"change"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -218,5 +218,54 @@
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
{
|
||||
"name": "resolved",
|
||||
"template": "state",
|
||||
"key-forms": [
|
||||
{
|
||||
"key": "<node>.<module>.<key>",
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "the value a module on a node takes for a config key, and where it came from",
|
||||
"properties": {
|
||||
"node": {
|
||||
"type": "string"
|
||||
},
|
||||
"module": {
|
||||
"type": "string"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {},
|
||||
"source": {
|
||||
"type": "string"
|
||||
},
|
||||
"predefined": {
|
||||
"type": "string",
|
||||
"description": "the predefined key the value falls back to"
|
||||
},
|
||||
"required": {
|
||||
"type": "boolean",
|
||||
"description": "an unset value is the operator's own to set"
|
||||
},
|
||||
"trusted": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"node",
|
||||
"module",
|
||||
"key",
|
||||
"source"
|
||||
]
|
||||
},
|
||||
"description": "one resolved value"
|
||||
}
|
||||
],
|
||||
"description": "the value each module on each node takes for each config key, built from the sources alone"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
"format": 1,
|
||||
"seat": "mesh-hand-acts",
|
||||
"scope": "mesh",
|
||||
"grain": "seat-list",
|
||||
"decision": "novox/hq ADR 0565",
|
||||
"grain": "whole",
|
||||
"decision": "novox/hq ADR 0565, ADR 0634, ADR 0635",
|
||||
"description": "Acts done by hand, recorded with who, why and cause (ADR 0565): the log every module records through.",
|
||||
"verbs": [
|
||||
{
|
||||
"name": "hand-act",
|
||||
@@ -36,6 +37,10 @@
|
||||
"why"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"answer": {
|
||||
"type": "object",
|
||||
"description": "the act recorded"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -65,6 +70,10 @@
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"answer": {
|
||||
"type": "object",
|
||||
"description": "the acts lately"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -86,12 +95,226 @@
|
||||
"ask"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"answer": {
|
||||
"type": "object",
|
||||
"description": "the warrant recorded"
|
||||
}
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"name": "act-recorded"
|
||||
"name": "act-recorded",
|
||||
"body": {
|
||||
"$ref": "#/defs/Act"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"family": {
|
||||
"name": "mesh-hand-acts.events",
|
||||
"max-age-days": 7
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "acts",
|
||||
"template": "log",
|
||||
"key-forms": [
|
||||
{
|
||||
"key": "<id>",
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "one act done by hand",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"by": {
|
||||
"type": "string",
|
||||
"description": "the caller the runtime proved"
|
||||
},
|
||||
"verb": {
|
||||
"type": "string"
|
||||
},
|
||||
"arguments": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"why": {
|
||||
"type": "string"
|
||||
},
|
||||
"cause": {
|
||||
"type": "string"
|
||||
},
|
||||
"condition": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"description": "what the act was read to be from what it did"
|
||||
},
|
||||
"carried": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"via": {
|
||||
"type": "string"
|
||||
},
|
||||
"ask": {
|
||||
"type": "string"
|
||||
},
|
||||
"proofs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"requested-by": {
|
||||
"type": "string"
|
||||
},
|
||||
"outcome": {
|
||||
"type": "string"
|
||||
},
|
||||
"log": {
|
||||
"type": "string",
|
||||
"description": "which log holds it, set when it is read"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"at",
|
||||
"by",
|
||||
"verb",
|
||||
"why",
|
||||
"cause"
|
||||
]
|
||||
},
|
||||
"description": "one act"
|
||||
}
|
||||
],
|
||||
"max-age-days": 400,
|
||||
"description": "every act, kept 400 days"
|
||||
},
|
||||
{
|
||||
"name": "warrants",
|
||||
"template": "state",
|
||||
"key-forms": [
|
||||
{
|
||||
"key": "<asker>.<ask>",
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "what a module did on the operator's warrant, made once per ask",
|
||||
"properties": {
|
||||
"ask": {
|
||||
"type": "string"
|
||||
},
|
||||
"asker": {
|
||||
"type": "string"
|
||||
},
|
||||
"option": {
|
||||
"type": "string"
|
||||
},
|
||||
"by": {
|
||||
"type": "string"
|
||||
},
|
||||
"at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"ask",
|
||||
"asker"
|
||||
]
|
||||
},
|
||||
"description": "one warrant"
|
||||
}
|
||||
],
|
||||
"description": "warrants recorded, one key per asker and ask"
|
||||
}
|
||||
],
|
||||
"defs": {
|
||||
"Act": {
|
||||
"type": "object",
|
||||
"description": "one act done by hand",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"by": {
|
||||
"type": "string",
|
||||
"description": "the caller the runtime proved"
|
||||
},
|
||||
"verb": {
|
||||
"type": "string"
|
||||
},
|
||||
"arguments": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"why": {
|
||||
"type": "string"
|
||||
},
|
||||
"cause": {
|
||||
"type": "string"
|
||||
},
|
||||
"condition": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"description": "what the act was read to be from what it did"
|
||||
},
|
||||
"carried": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"via": {
|
||||
"type": "string"
|
||||
},
|
||||
"ask": {
|
||||
"type": "string"
|
||||
},
|
||||
"proofs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"requested-by": {
|
||||
"type": "string"
|
||||
},
|
||||
"outcome": {
|
||||
"type": "string"
|
||||
},
|
||||
"log": {
|
||||
"type": "string",
|
||||
"description": "which log holds it, set when it is read"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"at",
|
||||
"by",
|
||||
"verb",
|
||||
"why",
|
||||
"cause"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
"format": 1,
|
||||
"seat": "mesh-watchdogs",
|
||||
"scope": "mesh",
|
||||
"grain": "seat-list",
|
||||
"decision": "novox/hq ADR 0564",
|
||||
"grain": "whole",
|
||||
"decision": "novox/hq ADR 0564, ADR 0634, ADR 0635",
|
||||
"description": "The watchman's seat (ADR 0564, ADR 0433): signals that expire unless renewed, what stopped moving, who is present, the dead letters.",
|
||||
"verbs": [
|
||||
{
|
||||
"name": "signals",
|
||||
@@ -16,6 +17,10 @@
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"answer": {
|
||||
"type": "object",
|
||||
"description": "the watchdog table"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -29,6 +34,10 @@
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"answer": {
|
||||
"type": "object",
|
||||
"description": "what stopped moving"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -42,6 +51,10 @@
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"answer": {
|
||||
"type": "object",
|
||||
"description": "who is connected and who is silent"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -54,7 +67,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"consumer": {
|
||||
"description": "a consumer's name, or \u003cstream\u003e.\u003cconsumer\u003e: only what that one gave up on",
|
||||
"description": "a consumer's name, or <stream>.<consumer>: only what that one gave up on",
|
||||
"type": "string"
|
||||
},
|
||||
"deliver": {
|
||||
@@ -79,6 +92,10 @@
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"answer": {
|
||||
"type": "object",
|
||||
"description": "the dead letters, one whole, or the act done"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -87,7 +104,139 @@
|
||||
"input": {
|
||||
"properties": {},
|
||||
"type": "object"
|
||||
},
|
||||
"answer": {
|
||||
"type": "object",
|
||||
"description": "what was rebuilt"
|
||||
}
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
{
|
||||
"name": "presence",
|
||||
"template": "state",
|
||||
"key-forms": [
|
||||
{
|
||||
"key": "<node>",
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "whether a node is online",
|
||||
"properties": {
|
||||
"v": {
|
||||
"type": "integer"
|
||||
},
|
||||
"online": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"online"
|
||||
]
|
||||
},
|
||||
"description": "a node"
|
||||
},
|
||||
{
|
||||
"key": "part.<part>.<instance>",
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "whether a part's instance is online",
|
||||
"properties": {
|
||||
"v": {
|
||||
"type": "integer"
|
||||
},
|
||||
"online": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"online"
|
||||
]
|
||||
},
|
||||
"description": "a part's instance"
|
||||
}
|
||||
],
|
||||
"history": 20,
|
||||
"description": "who is connected; the entry's own time says since when"
|
||||
},
|
||||
{
|
||||
"name": "pulse",
|
||||
"template": "state",
|
||||
"key-forms": [
|
||||
{
|
||||
"key": "<bucket>.<key>",
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "a watched key mirrored, its revision, and who owes the next move",
|
||||
"properties": {
|
||||
"v": {
|
||||
"type": "integer"
|
||||
},
|
||||
"bucket": {
|
||||
"type": "string"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"revision": {
|
||||
"type": "integer"
|
||||
},
|
||||
"owes": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"bucket",
|
||||
"key",
|
||||
"revision"
|
||||
]
|
||||
},
|
||||
"description": "the move watched at a key of a state"
|
||||
}
|
||||
],
|
||||
"description": "one key per watched move, living for the move's bound minus its age"
|
||||
},
|
||||
{
|
||||
"name": "stuck",
|
||||
"template": "state",
|
||||
"key-forms": [
|
||||
{
|
||||
"key": "<bucket>.<key>",
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "what stopped moving",
|
||||
"properties": {
|
||||
"v": {
|
||||
"type": "integer"
|
||||
},
|
||||
"bucket": {
|
||||
"type": "string"
|
||||
},
|
||||
"key": {
|
||||
"type": "string"
|
||||
},
|
||||
"waited": {
|
||||
"type": "string",
|
||||
"description": "what it waited for, in words"
|
||||
},
|
||||
"since": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
},
|
||||
"moved": {
|
||||
"type": "boolean",
|
||||
"description": "it has moved again"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"bucket",
|
||||
"key",
|
||||
"since"
|
||||
]
|
||||
},
|
||||
"description": "a key whose pulse expired"
|
||||
}
|
||||
],
|
||||
"description": "what stopped moving; no key is removed, one that moves again is written as moved"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -709,6 +709,367 @@
|
||||
"name": "node-engine.events",
|
||||
"max-age-days": 7
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "have",
|
||||
"template": "state",
|
||||
"key-forms": [
|
||||
{
|
||||
"key": "<node>.<module>",
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "one module's state on a node: what the engine has built, composed and run",
|
||||
"properties": {
|
||||
"v": {
|
||||
"type": "integer"
|
||||
},
|
||||
"fp": {
|
||||
"type": "string",
|
||||
"description": "the fingerprint held"
|
||||
},
|
||||
"composed": {
|
||||
"type": "string",
|
||||
"description": "the composition's digest"
|
||||
},
|
||||
"build": {
|
||||
"type": "string"
|
||||
},
|
||||
"phase": {
|
||||
"type": "string",
|
||||
"description": "how far it has come"
|
||||
},
|
||||
"health": {
|
||||
"type": "string",
|
||||
"description": "how it is"
|
||||
},
|
||||
"detail": {
|
||||
"type": "string"
|
||||
},
|
||||
"user-list": {
|
||||
"type": "object",
|
||||
"description": "the user list written"
|
||||
},
|
||||
"judgement": {
|
||||
"type": "string"
|
||||
},
|
||||
"waits": {
|
||||
"type": "string",
|
||||
"description": "what it waits for"
|
||||
},
|
||||
"build-alone": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"kept": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"sealed": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"sealed-at": {
|
||||
"type": "object"
|
||||
},
|
||||
"pull-wait": {
|
||||
"type": "object",
|
||||
"description": "a pull the engine waits for"
|
||||
},
|
||||
"handed": {
|
||||
"type": "object",
|
||||
"description": "the accounts handed"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"v",
|
||||
"phase",
|
||||
"health"
|
||||
]
|
||||
},
|
||||
"description": "one module on a node"
|
||||
},
|
||||
{
|
||||
"key": "<node>.node",
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "a node's own record",
|
||||
"properties": {
|
||||
"v": {
|
||||
"type": "integer"
|
||||
},
|
||||
"composed": {
|
||||
"type": "string"
|
||||
},
|
||||
"windows": {
|
||||
"type": "object"
|
||||
},
|
||||
"busy": {
|
||||
"type": "object"
|
||||
},
|
||||
"strays": {
|
||||
"type": "object"
|
||||
},
|
||||
"filters": {
|
||||
"type": "object"
|
||||
},
|
||||
"profile": {
|
||||
"type": "object"
|
||||
},
|
||||
"record": {
|
||||
"type": "object",
|
||||
"description": "the node's record: its tunnel and certificate"
|
||||
},
|
||||
"sealed": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"sealed-at": {
|
||||
"type": "object"
|
||||
},
|
||||
"candidates": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"composes": {
|
||||
"type": "string",
|
||||
"description": "own, or the controller's"
|
||||
},
|
||||
"composes-why": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"v"
|
||||
]
|
||||
},
|
||||
"description": "the node's own record"
|
||||
}
|
||||
],
|
||||
"history": 20,
|
||||
"description": "what each node has of each module, written by its engine"
|
||||
},
|
||||
{
|
||||
"name": "composed",
|
||||
"template": "state",
|
||||
"key-forms": [
|
||||
{
|
||||
"key": "<node>.<module>.<fingerprint>",
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "one composition of a module on a node under a fingerprint",
|
||||
"properties": {
|
||||
"V": {
|
||||
"type": "integer"
|
||||
},
|
||||
"Node": {
|
||||
"type": "string"
|
||||
},
|
||||
"Module": {
|
||||
"type": "string"
|
||||
},
|
||||
"FP": {
|
||||
"type": "string"
|
||||
},
|
||||
"Data": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"Resources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"Filled": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"Own": {
|
||||
"type": "object",
|
||||
"description": "the node's own adoption, identities, groups, libraries and provisioner account"
|
||||
},
|
||||
"Plan": {
|
||||
"type": "object",
|
||||
"description": "the module's plan"
|
||||
},
|
||||
"Refused": {
|
||||
"type": "string",
|
||||
"description": "why the composition was refused"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"Node",
|
||||
"Module",
|
||||
"FP"
|
||||
]
|
||||
},
|
||||
"description": "one composition"
|
||||
}
|
||||
],
|
||||
"description": "the compositions of one node, kept in a bucket per node named composed-<node>"
|
||||
},
|
||||
{
|
||||
"name": "data",
|
||||
"template": "state",
|
||||
"key-forms": [
|
||||
{
|
||||
"key": "<node>.<module>",
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "the data items a module has on a node",
|
||||
"properties": {
|
||||
"v": {
|
||||
"type": "integer"
|
||||
},
|
||||
"node": {
|
||||
"type": "string"
|
||||
},
|
||||
"module": {
|
||||
"type": "string"
|
||||
},
|
||||
"digest": {
|
||||
"type": "string"
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"node",
|
||||
"module",
|
||||
"digest",
|
||||
"items"
|
||||
]
|
||||
},
|
||||
"description": "a module's data on a node"
|
||||
}
|
||||
],
|
||||
"description": "the data items of each module on each node"
|
||||
},
|
||||
{
|
||||
"name": "packages",
|
||||
"template": "state",
|
||||
"key-forms": [
|
||||
{
|
||||
"key": "<node>",
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "the packages a node declares",
|
||||
"properties": {
|
||||
"node": {
|
||||
"type": "string"
|
||||
},
|
||||
"packages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"package": {
|
||||
"type": "string"
|
||||
},
|
||||
"modules": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"absent": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"node",
|
||||
"packages"
|
||||
]
|
||||
},
|
||||
"description": "a node's declared packages"
|
||||
}
|
||||
],
|
||||
"description": "the packages each node declares"
|
||||
},
|
||||
{
|
||||
"name": "health",
|
||||
"template": "state",
|
||||
"key-forms": [
|
||||
{
|
||||
"key": "<node>",
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "a node's health as its engine reports it",
|
||||
"properties": {
|
||||
"v": {
|
||||
"type": "integer"
|
||||
},
|
||||
"node": {
|
||||
"type": "string"
|
||||
},
|
||||
"network": {
|
||||
"type": "object"
|
||||
},
|
||||
"peers": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"units": {
|
||||
"type": "object"
|
||||
},
|
||||
"relogin": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"found": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"fallback": {
|
||||
"type": "object"
|
||||
},
|
||||
"root": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"contract": {
|
||||
"type": "integer"
|
||||
},
|
||||
"at": {
|
||||
"type": "string",
|
||||
"format": "date-time"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"v",
|
||||
"node",
|
||||
"at"
|
||||
]
|
||||
},
|
||||
"description": "a node's health"
|
||||
}
|
||||
],
|
||||
"description": "each node's health as its engine reports it"
|
||||
}
|
||||
],
|
||||
"defs": {
|
||||
"Act": {
|
||||
"type": "object",
|
||||
|
||||
@@ -0,0 +1,567 @@
|
||||
{
|
||||
"format": 1,
|
||||
"seat": "operator-channel",
|
||||
"scope": "mesh",
|
||||
"grain": "whole",
|
||||
"decision": "novox/hq ADR 0259",
|
||||
"holder-declares": true,
|
||||
"description": "Asking a person and answering on a channel (ADR 0259): held by the router, the messenger module, whose manifest declares the seat; written from that seat entry (design 26).",
|
||||
"verbs": [
|
||||
{
|
||||
"name": "open",
|
||||
"description": "What is open now: every message the operator was sent about something still wrong, urgent first, oldest first.",
|
||||
"input": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"description": "no arguments"
|
||||
},
|
||||
"answer": {
|
||||
"type": "object",
|
||||
"description": "the open messages"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "history",
|
||||
"description": "What was said to the operator lately, newest first, the refusals, and what waits to be sent on each channel.",
|
||||
"input": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"limit": {
|
||||
"type": "integer",
|
||||
"description": "at most this many of each (default 50)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"answer": {
|
||||
"type": "object",
|
||||
"description": "the sends, refusals and what waits"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "notify",
|
||||
"description": "Tell the operator something, as a module that uses the seat: a key, urgent or warning, one line, and what it is about; clear says it is over.",
|
||||
"input": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "the message's key"
|
||||
},
|
||||
"severity": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"urgent",
|
||||
"warning"
|
||||
]
|
||||
},
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"description": "one line"
|
||||
},
|
||||
"subject": {
|
||||
"type": "string",
|
||||
"description": "what it is about"
|
||||
},
|
||||
"headline": {
|
||||
"type": "string",
|
||||
"description": "plain words"
|
||||
},
|
||||
"explanation": {
|
||||
"type": "string",
|
||||
"description": "one or two plain sentences"
|
||||
},
|
||||
"resolved": {
|
||||
"type": "string",
|
||||
"description": "the line said when it is over"
|
||||
},
|
||||
"clear": {
|
||||
"type": "boolean",
|
||||
"description": "it is over"
|
||||
}
|
||||
}
|
||||
},
|
||||
"answer": {
|
||||
"type": "object",
|
||||
"description": "what was sent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "tell",
|
||||
"description": "Tell the operator a plain notice that needs no answer and opens no condition (ADR 0380).",
|
||||
"input": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"headline": {
|
||||
"type": "string",
|
||||
"description": "plain words"
|
||||
},
|
||||
"lines": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "one or two plain lines"
|
||||
},
|
||||
"subject": {
|
||||
"type": "string",
|
||||
"description": "what it is about"
|
||||
}
|
||||
}
|
||||
},
|
||||
"answer": {
|
||||
"type": "object",
|
||||
"description": "what was sent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "asks",
|
||||
"description": "The asks the router holds: open first, then the last month's, each with how it ended.",
|
||||
"input": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
"description": "no arguments"
|
||||
},
|
||||
"answer": {
|
||||
"type": "object",
|
||||
"description": "the asks"
|
||||
}
|
||||
}
|
||||
],
|
||||
"accepts": [
|
||||
{
|
||||
"name": "ask",
|
||||
"by-caller": true,
|
||||
"body": {
|
||||
"$ref": "#/defs/Ask"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "cancel",
|
||||
"by-caller": true,
|
||||
"body": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "the ask's id"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "condition",
|
||||
"by-caller": true,
|
||||
"body": {
|
||||
"$ref": "#/defs/Condition"
|
||||
}
|
||||
}
|
||||
],
|
||||
"events": [
|
||||
{
|
||||
"name": "decided",
|
||||
"by-caller": true,
|
||||
"body": {
|
||||
"$ref": "#/defs/Warrant"
|
||||
}
|
||||
}
|
||||
],
|
||||
"family": {
|
||||
"name": "operator-channel.decided",
|
||||
"max-age-days": 7
|
||||
},
|
||||
"states": [
|
||||
{
|
||||
"name": "asks",
|
||||
"template": "state",
|
||||
"key-forms": [
|
||||
{
|
||||
"key": "<asker>.<id>",
|
||||
"value": {
|
||||
"type": "object",
|
||||
"description": "one ask the router holds and how it stands"
|
||||
},
|
||||
"description": "one ask the router holds and how it stands"
|
||||
}
|
||||
],
|
||||
"description": "the asks a module raised and the router holds, one key per asker and ask"
|
||||
}
|
||||
],
|
||||
"holder-events": [
|
||||
{
|
||||
"name": "refused",
|
||||
"subject": "mesh.mod.<holder>.event.refused"
|
||||
},
|
||||
{
|
||||
"name": "offered",
|
||||
"subject": "mesh.stream.<holder>.event.offered",
|
||||
"family": "router.asks",
|
||||
"body": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "the ask's id"
|
||||
},
|
||||
"asker": {
|
||||
"type": "string",
|
||||
"description": "its asker"
|
||||
},
|
||||
"headline": {
|
||||
"type": "string",
|
||||
"description": "its headline"
|
||||
},
|
||||
"channels": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "the channels it was offered on"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"asker"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "closed",
|
||||
"subject": "mesh.stream.<holder>.event.closed",
|
||||
"family": "router.asks",
|
||||
"body": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "the ask's id"
|
||||
},
|
||||
"asker": {
|
||||
"type": "string",
|
||||
"description": "its asker"
|
||||
},
|
||||
"how": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"answered",
|
||||
"expired",
|
||||
"cancelled",
|
||||
"replaced",
|
||||
"unlinked"
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"asker",
|
||||
"how"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"defs": {
|
||||
"Ask": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "the asker's own id"
|
||||
},
|
||||
"headline": {
|
||||
"type": "string",
|
||||
"description": "a few plain words"
|
||||
},
|
||||
"explanation": {
|
||||
"type": "string",
|
||||
"description": "what happened and what it means"
|
||||
},
|
||||
"options": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/defs/Option"
|
||||
},
|
||||
"description": "the answers it offers"
|
||||
},
|
||||
"who": {
|
||||
"type": "string",
|
||||
"description": "who may answer: operator"
|
||||
},
|
||||
"expires": {
|
||||
"type": "string",
|
||||
"description": "when it ends unanswered, RFC 3339"
|
||||
},
|
||||
"on-expiry": {
|
||||
"type": "string",
|
||||
"description": "what the asker then does"
|
||||
},
|
||||
"about": {
|
||||
"type": "string",
|
||||
"description": "what it is about: a newer ask about it replaces the older"
|
||||
},
|
||||
"urgent": {
|
||||
"type": "boolean",
|
||||
"description": "urgent"
|
||||
},
|
||||
"whole": {
|
||||
"type": "string",
|
||||
"description": "the explanation with its values whole"
|
||||
},
|
||||
"details": {
|
||||
"type": "string",
|
||||
"description": "its secondary words"
|
||||
},
|
||||
"whole-details": {
|
||||
"type": "string",
|
||||
"description": "the details whole"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"headline",
|
||||
"explanation",
|
||||
"options",
|
||||
"who",
|
||||
"expires",
|
||||
"on-expiry"
|
||||
]
|
||||
},
|
||||
"Option": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "the option's id"
|
||||
},
|
||||
"label": {
|
||||
"type": "string",
|
||||
"description": "the button's label"
|
||||
},
|
||||
"does": {
|
||||
"type": "string",
|
||||
"description": "what it does, in plain words"
|
||||
},
|
||||
"level": {
|
||||
"type": "string",
|
||||
"description": "the proof its answer needs"
|
||||
},
|
||||
"binds": {
|
||||
"type": "string",
|
||||
"description": "the digest of exactly what the asker performs when it is chosen"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"label",
|
||||
"does",
|
||||
"level"
|
||||
]
|
||||
},
|
||||
"Warrant": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ask": {
|
||||
"type": "string",
|
||||
"description": "the ask"
|
||||
},
|
||||
"asker": {
|
||||
"type": "string",
|
||||
"description": "its asker"
|
||||
},
|
||||
"about": {
|
||||
"type": "string",
|
||||
"description": "what it was about"
|
||||
},
|
||||
"outcome": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"chosen",
|
||||
"expired",
|
||||
"cancelled",
|
||||
"replaced",
|
||||
"refused"
|
||||
]
|
||||
},
|
||||
"option": {
|
||||
"type": "string",
|
||||
"description": "the option chosen"
|
||||
},
|
||||
"label": {
|
||||
"type": "string",
|
||||
"description": "its label"
|
||||
},
|
||||
"level": {
|
||||
"type": "string",
|
||||
"description": "its level"
|
||||
},
|
||||
"by": {
|
||||
"$ref": "#/defs/Person"
|
||||
},
|
||||
"channel": {
|
||||
"type": "string",
|
||||
"description": "the module it came through"
|
||||
},
|
||||
"proofs": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "the proofs present"
|
||||
},
|
||||
"at": {
|
||||
"type": "string",
|
||||
"description": "when, RFC 3339"
|
||||
},
|
||||
"ask-digest": {
|
||||
"type": "string",
|
||||
"description": "the digest of the ask as the router took it"
|
||||
},
|
||||
"words": {
|
||||
"type": "string",
|
||||
"description": "why it ended without a choice, or what refused it"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"ask",
|
||||
"asker",
|
||||
"outcome",
|
||||
"at"
|
||||
]
|
||||
},
|
||||
"Person": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"who": {
|
||||
"type": "string",
|
||||
"description": "the role"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"description": "the channel kind"
|
||||
},
|
||||
"identity": {
|
||||
"type": "string",
|
||||
"description": "the account"
|
||||
},
|
||||
"display": {
|
||||
"type": "string",
|
||||
"description": "the name shown"
|
||||
},
|
||||
"verified": {
|
||||
"type": "string",
|
||||
"description": "how the router knew"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"who",
|
||||
"kind",
|
||||
"identity",
|
||||
"verified"
|
||||
]
|
||||
},
|
||||
"Condition": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"v": {
|
||||
"type": "integer",
|
||||
"description": "the form's version, 1"
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "the condition's key"
|
||||
},
|
||||
"severity": {
|
||||
"type": "string",
|
||||
"description": "urgent or warning"
|
||||
},
|
||||
"subject": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"scope": {
|
||||
"type": "string",
|
||||
"description": "scope"
|
||||
},
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "id"
|
||||
},
|
||||
"machine": {
|
||||
"type": "string",
|
||||
"description": "machine"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"scope",
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"state": {
|
||||
"type": "string",
|
||||
"description": "raised or cleared"
|
||||
},
|
||||
"summary": {
|
||||
"type": "string",
|
||||
"description": "one line"
|
||||
},
|
||||
"headline": {
|
||||
"type": "string",
|
||||
"description": "plain words"
|
||||
},
|
||||
"needs": {
|
||||
"type": "string",
|
||||
"description": "what it needs"
|
||||
},
|
||||
"explanation": {
|
||||
"type": "string",
|
||||
"description": "plain words"
|
||||
},
|
||||
"resolved": {
|
||||
"type": "string",
|
||||
"description": "the line said when it is over"
|
||||
},
|
||||
"actions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "string",
|
||||
"description": "label"
|
||||
},
|
||||
"verb": {
|
||||
"type": "string",
|
||||
"description": "verb"
|
||||
},
|
||||
"machine": {
|
||||
"type": "string",
|
||||
"description": "machine"
|
||||
},
|
||||
"arguments": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"level": {
|
||||
"type": "string",
|
||||
"description": "level"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"label",
|
||||
"verb"
|
||||
]
|
||||
},
|
||||
"description": "the answers it offers"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"v",
|
||||
"key",
|
||||
"subject",
|
||||
"state"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
+248
-108
@@ -1,17 +1,21 @@
|
||||
// Package seatlist is the runtime's reader of the seat list (novox/hq ADR 0635 §1, §3): the seat definitions the
|
||||
// indexer keeps on the bus, asked once through the owner's tool and followed on its event `seat-definition-changed`.
|
||||
// The manifest validator, the node-engine, the tool runner, claude-code, the grants part and every other caller of
|
||||
// `catalogue` get their seats here and give them to `catalogue` as an input; mesh-sdk compiles no seat list and imports
|
||||
// no contracts module.
|
||||
// Package seatlist is the runtime's reader of the seat list (novox/hq ADR 0635 §1, ADR 0636 §1, §3, §4): the seat
|
||||
// definitions the seat mesh-catalog keeps, read through its verb `seat-definitions` and followed on its events
|
||||
// `seat-definition-changed` and `seat-definition-removed`. The manifest validator, the node-engine, the tool runner,
|
||||
// claude-code, the grants part and every other caller of `catalogue` get their seats here and give them to `catalogue`
|
||||
// as an input; mesh-sdk compiles no seat list and imports no contracts module.
|
||||
//
|
||||
// **What it asks and hears.** The owner's tool Tool, with no seat for every trunk definition (the answer's shape is
|
||||
// Listing) and with one seat for that seat's (the answer is the owner's own, read as Answer); the owner's event
|
||||
// ChangedEvent, whose body names the seat and its source (Changed). Only trunk definitions are held: a pull request's are
|
||||
// the indexer's to judge. Nothing polls and nothing waits on a timer: a reader that could not ask says so in its
|
||||
// Status and reads again on the next event or on Reload, which the host module offers as a tool (steering it).
|
||||
// **What it asks and hears.** The seat's verb with no seat (every trunk definition's name and digest, Listing) and with
|
||||
// one seat (that definition, Answer); the seat's two events, whose bodies name the seat (Changed). It calls the seat,
|
||||
// never the module that holds it, and only trunk definitions are held: a pull request's are the indexer's to judge.
|
||||
// Nothing polls and nothing waits on a timer: a reader that could not ask says so in its Status and reads again on the
|
||||
// next event or on Reload, which the host offers as a tool (Tools).
|
||||
//
|
||||
// **What it names.** The tool's and the event's names, which ADR 0635 §1 gives the owner. It names no module and no
|
||||
// seat: the owner's module is the caller's (Config.Owner), the one its manifest's `invokes` lists.
|
||||
// **A reader that has not heard acts on nothing new** (ADR 0636 §1): MayAct says what a host may start, compose, grant
|
||||
// or judge meanwhile, which is only the modules whose seats the bootstrapped definitions name (ADR 0636 §2). It keeps
|
||||
// running what it runs; a missing or removed definition only refuses a new claim, invoke or grant of that seat (Has).
|
||||
//
|
||||
// **What it names.** The one seat that serves the list (Seat and Verb), which ADR 0636 §4 fixes as the runtime's own
|
||||
// way in; no module, no other seat.
|
||||
package seatlist
|
||||
|
||||
import (
|
||||
@@ -22,43 +26,56 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
)
|
||||
|
||||
const (
|
||||
// Tool is the owner's tool that answers a seat definition.
|
||||
Tool = "indexer_seat_definition"
|
||||
// ChangedEvent is the owner's event that says a definition moved.
|
||||
// Seat is the seat that serves the seat list (ADR 0636 §4); the bootstrapped definitions name it (ADR 0636 §2).
|
||||
Seat = "mesh-catalog"
|
||||
// Verb is its verb: with no seat, every trunk seat with its digest; with a seat, that definition.
|
||||
Verb = "seat-definitions"
|
||||
// ChangedEvent and RemovedEvent are its events: a trunk's definition moved, or went.
|
||||
ChangedEvent = "seat-definition-changed"
|
||||
RemovedEvent = "seat-definition-removed"
|
||||
// SourceTrunk is the only source this reader holds.
|
||||
SourceTrunk = "trunk"
|
||||
// SourceBootstrap is what a definition is held from before the seat has named it.
|
||||
SourceBootstrap = "bootstrap"
|
||||
)
|
||||
|
||||
// Bus is what the reader needs of its host: a call of a tool by its key (`<module>.<tool>`) and a subscription to an
|
||||
// event pattern. StdioBus is the one a bundle has; the node-engine and the tool runner give theirs.
|
||||
// Key is the address a reader asks (`seat:<seat>.<verb>`, as stdio.Ask takes it); a host's manifest `invokes` lists the
|
||||
// verb. Pattern is what it subscribes to for an event (`<seat>.<event>`), which the host's manifest `consumes`.
|
||||
func Key() string { return "seat:" + Seat + "." + Verb }
|
||||
|
||||
// Pattern is the subscription of one of the seat's events.
|
||||
func Pattern(event string) string { return Seat + "." + event }
|
||||
|
||||
// Bus is what the reader needs of its host: a call by its address and a subscription to an event pattern. StdioBus is
|
||||
// the one a bundle has; the node-engine and the tool runner give theirs.
|
||||
type Bus interface {
|
||||
Ask(key string, body any) (json.RawMessage, error)
|
||||
Subscribe(pattern string, handle func(key string, body json.RawMessage) error) error
|
||||
}
|
||||
|
||||
// Entry is one definition as the owner answers it: the seat, its source, the digest of the file, and the definition
|
||||
// resolved with the parts it includes. It is the owner's SeatDefinitionEntry, read here by the fields it needs.
|
||||
// Entry is one definition as the seat answers it: the seat, its source, the digest of the file, and, in an answer for one
|
||||
// seat, the definition resolved with the parts it includes.
|
||||
type Entry struct {
|
||||
Seat string `json:"seat"`
|
||||
Source string `json:"source"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Digest string `json:"digest,omitempty"`
|
||||
Definition json.RawMessage `json:"definition,omitempty"`
|
||||
Unparsed string `json:"unparsed,omitempty"`
|
||||
}
|
||||
|
||||
// Listing is the owner's answer to the tool with no seat: every trunk definition and the revision of its state.
|
||||
// Listing is the seat's answer with no seat: every trunk seat with its digest, and the revision of its state.
|
||||
type Listing struct {
|
||||
Revision uint64 `json:"revision"`
|
||||
Definitions []Entry `json:"definitions"`
|
||||
Revision uint64 `json:"revision"`
|
||||
Seats []Entry `json:"seats"`
|
||||
}
|
||||
|
||||
// Answer is the owner's answer to the tool with one seat.
|
||||
// Answer is the seat's answer with one seat.
|
||||
type Answer struct {
|
||||
Seat string `json:"seat"`
|
||||
Found bool `json:"found"`
|
||||
@@ -66,20 +83,18 @@ type Answer struct {
|
||||
Revision uint64 `json:"revision"`
|
||||
}
|
||||
|
||||
// Changed is the event's body.
|
||||
// Changed is an event's body.
|
||||
type Changed struct {
|
||||
Seat string `json:"seat"`
|
||||
Source string `json:"source"`
|
||||
Digest string `json:"digest"`
|
||||
Source string `json:"source,omitempty"`
|
||||
Digest string `json:"digest,omitempty"`
|
||||
}
|
||||
|
||||
// Config is what a host gives Start.
|
||||
type Config struct {
|
||||
// Owner is the module that keeps the definitions, as the host's manifest `invokes` names it.
|
||||
Owner string
|
||||
// Bus is the host's way to the owner; StdioBus() for a bundle.
|
||||
// Bus is the host's way to the seat; StdioBus() for a bundle.
|
||||
Bus Bus
|
||||
// Bootstrap are the definitions known before the owner is heard (ADR 0635 §3); Bootstrapped() is the foundation's.
|
||||
// Bootstrap are the definitions known before the seat is heard (ADR 0636 §2); Bootstrapped() is the mesh's.
|
||||
Bootstrap []seatdef.Definition
|
||||
// OnChange, when set, is called after the held set changed, with the seats in reading order. It runs on the
|
||||
// goroutine that heard the change: it returns quickly.
|
||||
@@ -90,46 +105,69 @@ type Config struct {
|
||||
type Reader struct {
|
||||
cfg Config
|
||||
|
||||
mu sync.Mutex
|
||||
defs map[string]seatdef.Definition // by seat
|
||||
boot map[string]bool // seats held from the bootstrap, not yet from the owner
|
||||
// what the last ask of the owner came to (Status)
|
||||
mu sync.Mutex
|
||||
defs map[string]seatdef.Definition // by seat
|
||||
digests map[string]string // by seat: the digest of the trunk file held
|
||||
boot map[string]bool // seats held from the bootstrap, not yet from the seat
|
||||
refused map[string]string // seats whose trunk definition cannot be parsed, with why: the one that stands stays
|
||||
// what the last ask of the seat came to (Status)
|
||||
spoken bool
|
||||
revision uint64
|
||||
lastErr string
|
||||
}
|
||||
|
||||
// Status is what the reader holds and what it waits for (ADR 0635 §3), for the host's health and its tool: Heard says
|
||||
// the owner has answered, Waiting are the seats still held from the bootstrap alone (the owner has not named them),
|
||||
// LastError is why the last ask failed, and Revision is the owner's state revision of the last listing.
|
||||
// Held is one definition a reader holds: its seat, where from, and the digest of the trunk file it came from.
|
||||
type Held struct {
|
||||
Seat string `json:"seat"`
|
||||
Source string `json:"source"`
|
||||
Digest string `json:"digest,omitempty"`
|
||||
}
|
||||
|
||||
// Status is what the reader holds and what it waits for (ADR 0636 §1, §7), for the host's health and its tool: Heard says
|
||||
// the seat has answered, Waiting are the seats still held from the bootstrap alone (the seat has not named them), Refused
|
||||
// are the seats whose trunk definition did not parse (the one held stands), LastError is why the last ask failed, and
|
||||
// Revision is the seat's state revision of the last listing.
|
||||
type Status struct {
|
||||
Heard bool `json:"heard"`
|
||||
Held int `json:"held"`
|
||||
Waiting []string `json:"waiting,omitempty"`
|
||||
LastError string `json:"last-error,omitempty"`
|
||||
Revision uint64 `json:"revision,omitempty"`
|
||||
Heard bool `json:"heard"`
|
||||
Held []Held `json:"held"`
|
||||
Waiting []string `json:"waiting,omitempty"`
|
||||
Refused map[string]string `json:"refused,omitempty"`
|
||||
LastError string `json:"last-error,omitempty"`
|
||||
Revision uint64 `json:"revision,omitempty"`
|
||||
}
|
||||
|
||||
// Status is the reader's state of mind.
|
||||
func (r *Reader) Status() Status {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
st := Status{Heard: r.spoken, Held: len(r.defs), LastError: r.lastErr, Revision: r.revision}
|
||||
for seat := range r.boot {
|
||||
st.Waiting = append(st.Waiting, seat)
|
||||
st := Status{Heard: r.spoken, LastError: r.lastErr, Revision: r.revision, Held: []Held{}}
|
||||
for seat := range r.defs {
|
||||
h := Held{Seat: seat, Source: SourceTrunk, Digest: r.digests[seat]}
|
||||
if r.boot[seat] {
|
||||
h.Source = SourceBootstrap
|
||||
st.Waiting = append(st.Waiting, seat)
|
||||
}
|
||||
st.Held = append(st.Held, h)
|
||||
}
|
||||
sort.Slice(st.Held, func(i, j int) bool { return st.Held[i].Seat < st.Held[j].Seat })
|
||||
sort.Strings(st.Waiting)
|
||||
if len(r.refused) > 0 {
|
||||
st.Refused = map[string]string{}
|
||||
for k, v := range r.refused {
|
||||
st.Refused[k] = v
|
||||
}
|
||||
}
|
||||
return st
|
||||
}
|
||||
|
||||
// Health is Status as the one line a module's health says: empty when the owner has been heard and nothing waits.
|
||||
// Health is Status as the one line a module's health says: empty when the seat has been heard and nothing waits.
|
||||
func (st Status) Health() string {
|
||||
switch {
|
||||
case !st.Heard:
|
||||
return fmt.Sprintf("the seat list is held from the foundation's bootstrap (%d seats): the definitions' owner has not answered%s",
|
||||
st.Held, withError(st.LastError))
|
||||
return fmt.Sprintf("the seat list is held from the bootstrap (%d seats) and nothing new is acted on: the seat %s has not answered%s",
|
||||
len(st.Held), Seat, withError(st.LastError))
|
||||
case len(st.Waiting) > 0:
|
||||
return fmt.Sprintf("%d seats are held from the bootstrap alone, the owner names none of them: %s", len(st.Waiting), strings.Join(st.Waiting, ", "))
|
||||
return fmt.Sprintf("%d seats are held from the bootstrap alone, the seat %s names none of them: %s", len(st.Waiting), Seat, strings.Join(st.Waiting, ", "))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -141,24 +179,23 @@ func withError(e string) string {
|
||||
return " (" + e + ")"
|
||||
}
|
||||
|
||||
// Start returns a reader holding the bootstrap, asks the owner for every definition and follows its event. A first
|
||||
// ask the owner did not answer is no failure: the reader holds the bootstrap and reads again on the owner's next
|
||||
// event or on Reload. Start returns the subscription's error alone, which the host may ask again by calling Start again
|
||||
// on a new reader.
|
||||
// Start returns a reader holding the bootstrap, asks the seat for every definition and follows its two events. A first
|
||||
// ask the seat did not answer is no failure: the reader holds the bootstrap, acts on nothing new (MayAct) and reads again
|
||||
// on the seat's next event or on Reload. Start returns the subscription's error alone, which the host may ask again by
|
||||
// calling Start again on a new reader.
|
||||
func Start(cfg Config) (*Reader, error) {
|
||||
if cfg.Bus == nil {
|
||||
return nil, errors.New("seatlist: no bus to read the seat list through")
|
||||
}
|
||||
if cfg.Owner == "" || strings.ContainsAny(cfg.Owner, ". \t") {
|
||||
return nil, fmt.Errorf("seatlist: the owner %q is not a module name", cfg.Owner)
|
||||
}
|
||||
r := &Reader{cfg: cfg, defs: map[string]seatdef.Definition{}, boot: map[string]bool{}}
|
||||
r := &Reader{cfg: cfg, defs: map[string]seatdef.Definition{}, digests: map[string]string{}, boot: map[string]bool{}, refused: map[string]string{}}
|
||||
for _, d := range cfg.Bootstrap {
|
||||
r.defs[d.Seat] = d
|
||||
r.boot[d.Seat] = true
|
||||
}
|
||||
if err := cfg.Bus.Subscribe("*."+ChangedEvent, r.heard); err != nil {
|
||||
return r, fmt.Errorf("seatlist: following %s: %w", ChangedEvent, err)
|
||||
for _, ev := range []string{ChangedEvent, RemovedEvent} {
|
||||
if err := cfg.Bus.Subscribe(Pattern(ev), r.heard); err != nil {
|
||||
return r, fmt.Errorf("seatlist: following %s: %w", ev, err)
|
||||
}
|
||||
}
|
||||
_ = r.Reload()
|
||||
return r, nil
|
||||
@@ -182,6 +219,35 @@ func (r *Reader) Definitions() map[string]seatdef.Definition {
|
||||
return out
|
||||
}
|
||||
|
||||
// Has says whether the reader holds a definition of the seat: a claim, invoke or grant of a seat it does not hold is
|
||||
// refused, and nothing already running is stopped for it (ADR 0636 §3).
|
||||
func (r *Reader) Has(seat string) bool {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
_, ok := r.defs[seat]
|
||||
return ok
|
||||
}
|
||||
|
||||
// MayAct says whether a host may start, compose, grant or judge a module that claims, invokes or provides the given seats
|
||||
// (ADR 0636 §1): once the seat list has been read, any; before, only a module whose seats are all bootstrapped
|
||||
// ones (ADR 0636 §2), and at least one. It changes nothing a module already runs.
|
||||
func (r *Reader) MayAct(seats ...string) bool {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
if r.spoken {
|
||||
return true
|
||||
}
|
||||
if len(seats) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, s := range seats {
|
||||
if !r.boot[s] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (r *Reader) seatsLocked() []catalogue.Seat {
|
||||
ds := make([]seatdef.Definition, 0, len(r.defs))
|
||||
for _, d := range r.defs {
|
||||
@@ -190,80 +256,112 @@ func (r *Reader) seatsLocked() []catalogue.Seat {
|
||||
return catalogue.SeatsFromDefinitions(ds)
|
||||
}
|
||||
|
||||
// Reload asks the owner for every trunk definition and holds what it answers, a seat the owner holds replacing the
|
||||
// bootstrap's. An answer that cannot be read is returned and changes nothing.
|
||||
// Reload asks the seat for its trunk seats and digests and then for each definition it holds a different digest of,
|
||||
// and drops the trunk seats it no longer lists; a bootstrapped seat the seat does not list stays and waits. An answer that
|
||||
// cannot be read is returned and changes nothing; one definition that does not parse leaves the one held standing and is
|
||||
// said in Status.
|
||||
func (r *Reader) Reload() error {
|
||||
raw, err := r.cfg.Bus.Ask(r.cfg.Owner+"."+Tool, map[string]any{})
|
||||
raw, err := r.cfg.Bus.Ask(Key(), map[string]any{})
|
||||
if err != nil {
|
||||
return r.failed(fmt.Errorf("seatlist: asking %s.%s: %w", r.cfg.Owner, Tool, err))
|
||||
return r.failed(fmt.Errorf("seatlist: asking %s: %w", Key(), err))
|
||||
}
|
||||
var l Listing
|
||||
if err := json.Unmarshal(raw, &l); err != nil {
|
||||
return r.failed(fmt.Errorf("seatlist: %s.%s answered what is not a listing: %w", r.cfg.Owner, Tool, err))
|
||||
return r.failed(fmt.Errorf("seatlist: %s answered what is not a listing: %w", Key(), err))
|
||||
}
|
||||
parsed := map[string]seatdef.Definition{}
|
||||
for _, e := range l.Definitions {
|
||||
if e.Source != SourceTrunk || e.Unparsed != "" || len(e.Definition) == 0 {
|
||||
listed := map[string]bool{}
|
||||
var first error
|
||||
for _, e := range l.Seats {
|
||||
if e.Seat == "" || (e.Source != "" && e.Source != SourceTrunk) {
|
||||
continue
|
||||
}
|
||||
d, err := seatdef.Parse(e.Definition)
|
||||
if err != nil {
|
||||
return r.failed(fmt.Errorf("seatlist: the definition of %s does not parse: %w", e.Seat, err))
|
||||
listed[e.Seat] = true
|
||||
r.mu.Lock()
|
||||
same := !r.boot[e.Seat] && e.Digest != "" && r.digests[e.Seat] == e.Digest
|
||||
r.mu.Unlock()
|
||||
if same {
|
||||
continue
|
||||
}
|
||||
if err := r.fetch(e.Seat); err != nil && first == nil {
|
||||
first = err
|
||||
}
|
||||
parsed[d.Seat] = d
|
||||
}
|
||||
r.mu.Lock()
|
||||
r.spoken, r.revision, r.lastErr = true, l.Revision, ""
|
||||
for seat := range r.defs {
|
||||
if !r.boot[seat] {
|
||||
delete(r.defs, seat)
|
||||
}
|
||||
r.spoken, r.revision = true, l.Revision
|
||||
if first == nil {
|
||||
r.lastErr = ""
|
||||
}
|
||||
for seat, d := range parsed {
|
||||
r.defs[seat] = d
|
||||
delete(r.boot, seat)
|
||||
for seat := range r.defs {
|
||||
if !r.boot[seat] && !listed[seat] {
|
||||
r.dropLocked(seat)
|
||||
}
|
||||
}
|
||||
r.mu.Unlock()
|
||||
r.changed()
|
||||
if first != nil {
|
||||
return r.failed(first)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// heard takes the owner's event: a trunk definition moved, so that seat is asked again. An error is returned so the
|
||||
// bus offers the event again.
|
||||
func (r *Reader) heard(_ string, body json.RawMessage) error {
|
||||
// fetch asks the seat for one definition and holds it, or drops it when the seat holds none.
|
||||
func (r *Reader) fetch(seat string) error {
|
||||
raw, err := r.cfg.Bus.Ask(Key(), map[string]any{"seat": seat})
|
||||
if err != nil {
|
||||
return fmt.Errorf("seatlist: asking %s for %s: %w", Key(), seat, err)
|
||||
}
|
||||
var a Answer
|
||||
if err := json.Unmarshal(raw, &a); err != nil {
|
||||
return fmt.Errorf("seatlist: %s answered for %s what is not an answer: %w", Key(), seat, err)
|
||||
}
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
switch {
|
||||
case !a.Found || a.Entry == nil || (a.Entry.Source != "" && a.Entry.Source != SourceTrunk):
|
||||
r.dropLocked(seat)
|
||||
case a.Entry.Unparsed != "" || len(a.Entry.Definition) == 0:
|
||||
r.refused[seat] = a.Entry.Unparsed // the definition that stands stays: a trunk's that does not parse is never written over it
|
||||
default:
|
||||
d, err := seatdef.Parse(a.Entry.Definition)
|
||||
if err != nil {
|
||||
r.refused[seat] = err.Error()
|
||||
return nil
|
||||
}
|
||||
r.defs[seat], r.digests[seat] = d, a.Entry.Digest
|
||||
delete(r.boot, seat)
|
||||
delete(r.refused, seat)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// dropLocked forgets a seat: its definition is gone, so a new claim of it is refused (ADR 0636 §3).
|
||||
func (r *Reader) dropLocked(seat string) {
|
||||
delete(r.defs, seat)
|
||||
delete(r.digests, seat)
|
||||
delete(r.boot, seat)
|
||||
delete(r.refused, seat)
|
||||
}
|
||||
|
||||
// heard takes one of the seat's events: a trunk definition moved, so that seat is asked again, or went, so it is dropped.
|
||||
// An error is returned so the bus offers the event again.
|
||||
func (r *Reader) heard(key string, body json.RawMessage) error {
|
||||
var c Changed
|
||||
if err := json.Unmarshal(body, &c); err != nil || c.Seat == "" {
|
||||
return nil
|
||||
}
|
||||
if c.Source != SourceTrunk {
|
||||
if c.Source != "" && c.Source != SourceTrunk {
|
||||
return nil
|
||||
}
|
||||
raw, err := r.cfg.Bus.Ask(r.cfg.Owner+"."+Tool, map[string]any{"seat": c.Seat})
|
||||
if err != nil {
|
||||
if strings.HasSuffix(key, "."+RemovedEvent) {
|
||||
r.mu.Lock()
|
||||
r.dropLocked(c.Seat)
|
||||
r.mu.Unlock()
|
||||
r.changed()
|
||||
return nil
|
||||
}
|
||||
if err := r.fetch(c.Seat); err != nil {
|
||||
return r.failed(err)
|
||||
}
|
||||
var a Answer
|
||||
if err := json.Unmarshal(raw, &a); err != nil {
|
||||
return err
|
||||
}
|
||||
r.mu.Lock()
|
||||
switch {
|
||||
case !a.Found || a.Entry == nil || a.Entry.Source != SourceTrunk:
|
||||
if !r.boot[c.Seat] {
|
||||
delete(r.defs, c.Seat)
|
||||
}
|
||||
case a.Entry.Unparsed != "" || len(a.Entry.Definition) == 0:
|
||||
// the definition that stands stays: a trunk's that does not parse is never written over it
|
||||
default:
|
||||
d, err := seatdef.Parse(a.Entry.Definition)
|
||||
if err != nil {
|
||||
r.mu.Unlock()
|
||||
return fmt.Errorf("seatlist: the definition of %s does not parse: %w", c.Seat, err)
|
||||
}
|
||||
r.defs[c.Seat] = d
|
||||
delete(r.boot, c.Seat)
|
||||
}
|
||||
r.mu.Unlock()
|
||||
r.changed()
|
||||
return nil
|
||||
}
|
||||
@@ -280,3 +378,45 @@ func (r *Reader) changed() {
|
||||
r.cfg.OnChange(r.Seats())
|
||||
}
|
||||
}
|
||||
|
||||
// Tools are the reader's two runtime tools (ADR 0636 §7), for a host's bundle: read, which says what the reader holds
|
||||
// (every definition, its source and digest) and what it waits for; and again, which makes it read the list now, and
|
||||
// answers only to the callers the host's list names (the operator and the healers). The host names the tools and the list;
|
||||
// again records nothing but the read it asked.
|
||||
func Tools(r *Reader, read, again string, callers stdio.Callers) []stdio.Tool {
|
||||
return []stdio.Tool{
|
||||
{Name: read,
|
||||
Description: "The seat list this part holds (novox/hq ADR 0636): each seat's definition with its source (trunk or bootstrap) and " +
|
||||
"the digest of the file it came from, the seats still held from the bootstrap alone, definitions refused because they " +
|
||||
"do not parse, whether the seat " + Seat + " has answered and why not.",
|
||||
Input: map[string]any{"type": "object", "properties": map[string]any{}},
|
||||
Run: func(map[string]any) (any, error) {
|
||||
return map[string]any{"status": r.Status(), "health": r.Status().Health()}, nil
|
||||
}},
|
||||
{Name: again,
|
||||
Description: "Read the seat list again now, from the seat " + Seat + " (a hand act for a part that waits or holds a stale definition), " +
|
||||
"and answer what it holds afterwards. Answered only to the operator and the healers.",
|
||||
Input: map[string]any{"type": "object", "properties": map[string]any{}},
|
||||
Run: func(args map[string]any) (any, error) {
|
||||
if err := Refusal(again, callers, stdio.CallerOf(args)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err := r.Reload()
|
||||
st := r.Status()
|
||||
out := map[string]any{"status": st, "health": st.Health()}
|
||||
if err != nil {
|
||||
out["error"] = err.Error()
|
||||
}
|
||||
return out, nil
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
// Refusal is why caller may not make the reader read again, or nil: the caller's proof, never an argument, against the
|
||||
// list of the operator and the healers.
|
||||
func Refusal(tool string, callers stdio.Callers, caller string) error {
|
||||
if callers.Allows(caller) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("%s is refused to %q: it answers only %s (novox/hq ADR 0636 §7)", tool, caller, strings.Join(callers, ", "))
|
||||
}
|
||||
|
||||
+117
-54
@@ -3,50 +3,57 @@ package seatlist
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
||||
)
|
||||
|
||||
// fakeOwner is the definitions' owner: the tool answers from its table and the event is delivered by the test.
|
||||
// fakeOwner is the seat mesh-catalog: its verb answers from its table and the events are delivered by the test.
|
||||
type fakeOwner struct {
|
||||
table map[string]string // seat -> definition JSON, trunk
|
||||
down bool
|
||||
asked []string
|
||||
handler func(string, json.RawMessage) error
|
||||
pattern string
|
||||
table map[string]string // seat -> definition JSON, trunk
|
||||
down bool
|
||||
asked []string // "key seat"
|
||||
handlers map[string]func(string, json.RawMessage) error
|
||||
}
|
||||
|
||||
func (f *fakeOwner) Ask(key string, body any) (json.RawMessage, error) {
|
||||
f.asked = append(f.asked, key)
|
||||
args, _ := body.(map[string]any)
|
||||
seat, _ := args["seat"].(string)
|
||||
f.asked = append(f.asked, key+" "+seat)
|
||||
if f.down {
|
||||
return nil, errors.New("no answer")
|
||||
}
|
||||
args, _ := body.(map[string]any)
|
||||
if seat, _ := args["seat"].(string); seat != "" {
|
||||
if seat != "" {
|
||||
raw, ok := f.table[seat]
|
||||
if !ok {
|
||||
return json.Marshal(Answer{Seat: seat})
|
||||
}
|
||||
return json.Marshal(Answer{Seat: seat, Found: true, Entry: &Entry{Seat: seat, Source: SourceTrunk, Definition: json.RawMessage(raw)}})
|
||||
return json.Marshal(Answer{Seat: seat, Found: true, Entry: &Entry{Seat: seat, Source: SourceTrunk, Digest: digest(raw), Definition: json.RawMessage(raw)}})
|
||||
}
|
||||
l := Listing{Revision: 7}
|
||||
for seat, raw := range f.table {
|
||||
l.Definitions = append(l.Definitions, Entry{Seat: seat, Source: SourceTrunk, Definition: json.RawMessage(raw)})
|
||||
l.Seats = append(l.Seats, Entry{Seat: seat, Source: SourceTrunk, Digest: digest(raw)})
|
||||
}
|
||||
return json.Marshal(l)
|
||||
}
|
||||
|
||||
func digest(raw string) string { return fmt.Sprintf("%d", len(raw)) }
|
||||
|
||||
func (f *fakeOwner) Subscribe(pattern string, h func(string, json.RawMessage) error) error {
|
||||
f.pattern, f.handler = pattern, h
|
||||
if f.handlers == nil {
|
||||
f.handlers = map[string]func(string, json.RawMessage) error{}
|
||||
}
|
||||
f.handlers[pattern] = h
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeOwner) say(seat, source string) error {
|
||||
func (f *fakeOwner) say(event, seat, source string) error {
|
||||
b, _ := json.Marshal(Changed{Seat: seat, Source: source, Digest: "d"})
|
||||
return f.handler("mesh-owner."+ChangedEvent, b)
|
||||
return f.handlers[Pattern(event)](Pattern(event), b)
|
||||
}
|
||||
|
||||
func def(seat, scope, verb string) string {
|
||||
@@ -62,41 +69,51 @@ func names(r *Reader) string {
|
||||
return strings.Join(out, ",")
|
||||
}
|
||||
|
||||
// ADR 0635 §1, §3: the seat list comes from the owner on the bus, starting from the bootstrap, and follows its event.
|
||||
func TestTheSeatListIsReadFromTheOwnerAndFollowsItsEvent(t *testing.T) {
|
||||
// ADR 0636 §1, §3, §4: the seat list is read through the seat's verb, starting from the bootstrap, and follows its two events.
|
||||
func TestTheSeatListIsReadFromTheSeatAndFollowsItsEvents(t *testing.T) {
|
||||
boot, err := seatdef.Parse([]byte(def("boot-seat", "mesh", "go")))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
owner := &fakeOwner{down: true, table: map[string]string{
|
||||
"node-b": def("node-b", "node", "put"), "mesh-a": def("mesh-a", "mesh", "get"), "boot-seat": def("boot-seat", "mesh", "other")}}
|
||||
var told [][]catalogue.Seat
|
||||
r, err := Start(Config{Owner: "mesh-owner", Bus: owner, Bootstrap: []seatdef.Definition{boot},
|
||||
OnChange: func(s []catalogue.Seat) { told = append(told, s) }})
|
||||
var told int
|
||||
r, err := Start(Config{Bus: owner, Bootstrap: []seatdef.Definition{boot}, OnChange: func([]catalogue.Seat) { told++ }})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if owner.pattern != "*."+ChangedEvent {
|
||||
t.Fatalf("subscribed to %q", owner.pattern)
|
||||
if owner.handlers[Pattern(ChangedEvent)] == nil || owner.handlers[Pattern(RemovedEvent)] == nil || Pattern(ChangedEvent) != "mesh-catalog.seat-definition-changed" {
|
||||
t.Fatalf("subscribed to %v", owner.handlers)
|
||||
}
|
||||
if owner.asked[0] != "seat:mesh-catalog.seat-definitions " {
|
||||
t.Fatalf("first asked %q: the seat's verb, never a module", owner.asked[0])
|
||||
}
|
||||
steps := []struct {
|
||||
name string
|
||||
act func() error
|
||||
want string
|
||||
err string
|
||||
asks int // asks the step made
|
||||
}{
|
||||
{"the owner has not answered: the bootstrap stands", func() error { return nil }, "boot-seat", ""},
|
||||
{"an event while the owner is down is offered again", func() error { return owner.say("mesh-a", SourceTrunk) }, "boot-seat", "no answer"},
|
||||
{"the owner answers: its seats in reading order, mesh seats first, the owner's definition replacing the bootstrap's",
|
||||
func() error { owner.down = false; return r.Reload() }, "boot-seat,mesh-a,node-b", ""},
|
||||
{"the seat has not answered: the bootstrap stands", func() error { return nil }, "boot-seat", "", 0},
|
||||
{"an event while the seat is down is offered again", func() error { return owner.say(ChangedEvent, "mesh-a", SourceTrunk) }, "boot-seat", "no answer", 1},
|
||||
{"the seat answers: its seats in reading order, its definition replacing the bootstrap's, each fetched once",
|
||||
func() error { owner.down = false; return r.Reload() }, "boot-seat,mesh-a,node-b", "", 4},
|
||||
{"a read again fetches nothing whose digest it holds", func() error { return r.Reload() }, "boot-seat,mesh-a,node-b", "", 1},
|
||||
{"a moved definition is asked again", func() error {
|
||||
owner.table["mesh-a"] = def("mesh-a", "mesh", "renamed")
|
||||
return owner.say("mesh-a", SourceTrunk)
|
||||
}, "boot-seat,mesh-a,node-b", ""},
|
||||
{"a seat the owner holds no more goes", func() error { delete(owner.table, "node-b"); return owner.say("node-b", SourceTrunk) }, "boot-seat,mesh-a", ""},
|
||||
{"a pull request's definition is not held", func() error { owner.asked = nil; return owner.say("mesh-z", "pull") }, "boot-seat,mesh-a", ""},
|
||||
return owner.say(ChangedEvent, "mesh-a", SourceTrunk)
|
||||
}, "boot-seat,mesh-a,node-b", "", 1},
|
||||
{"a definition that does not parse leaves the one that stands", func() error {
|
||||
owner.table["mesh-a"] = `{"format":1,"seat":"mesh-a","verbs":"no"}`
|
||||
return owner.say(ChangedEvent, "mesh-a", SourceTrunk)
|
||||
}, "boot-seat,mesh-a,node-b", "", 1},
|
||||
{"a seat the seat removes goes", func() error { delete(owner.table, "node-b"); return owner.say(RemovedEvent, "node-b", SourceTrunk) }, "boot-seat,mesh-a", "", 0},
|
||||
{"a pull request's definition is not held and not asked", func() error { return owner.say(ChangedEvent, "mesh-z", "pull") }, "boot-seat,mesh-a", "", 0},
|
||||
{"a listing that drops a trunk seat drops it", func() error { delete(owner.table, "mesh-a"); return r.Reload() }, "boot-seat", "", 1},
|
||||
}
|
||||
for _, s := range steps {
|
||||
before := len(owner.asked)
|
||||
err := s.act()
|
||||
if (err == nil) != (s.err == "") || (err != nil && !strings.Contains(err.Error(), s.err)) {
|
||||
t.Fatalf("%s: error %v, want %q", s.name, err, s.err)
|
||||
@@ -104,47 +121,63 @@ func TestTheSeatListIsReadFromTheOwnerAndFollowsItsEvent(t *testing.T) {
|
||||
if got := names(r); got != s.want {
|
||||
t.Fatalf("%s: seats %q, want %q", s.name, got, s.want)
|
||||
}
|
||||
if got := len(owner.asked) - before; got != s.asks {
|
||||
t.Fatalf("%s: asked %d times (%v), want %d", s.name, got, owner.asked[before:], s.asks)
|
||||
}
|
||||
}
|
||||
if len(owner.asked) != 0 {
|
||||
t.Fatalf("a pull request's event asked the owner: %v", owner.asked)
|
||||
if st := r.Status(); len(st.Refused) != 0 || !r.Has("boot-seat") || r.Has("mesh-a") {
|
||||
t.Fatalf("status %+v, has mesh-a %v", st, r.Has("mesh-a"))
|
||||
}
|
||||
if v := r.Seats()[1].Serves[0]; v.Name != "renamed" || v.Input["type"] != "object" {
|
||||
t.Fatalf("the moved definition's verb is %+v, want renamed ", v)
|
||||
}
|
||||
if len(told) != 3 {
|
||||
t.Fatalf("OnChange was told %d times", len(told))
|
||||
if told < 5 {
|
||||
t.Fatalf("OnChange was told %d times", told)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStartRefusesWhatCannotRead(t *testing.T) {
|
||||
for name, cfg := range map[string]Config{
|
||||
"no bus": {Owner: "m"},
|
||||
"an owner with a dot": {Owner: "m.tool", Bus: &fakeOwner{}},
|
||||
"no owner": {Bus: &fakeOwner{}},
|
||||
} {
|
||||
if _, err := Start(cfg); err == nil {
|
||||
t.Fatalf("%s: started", name)
|
||||
}
|
||||
if _, err := Start(Config{}); err == nil {
|
||||
t.Fatal("started without a bus")
|
||||
}
|
||||
}
|
||||
|
||||
// ADR 0635 §3: before the owner is heard the reader holds the foundation's definitions and says it waits.
|
||||
func TestTheBootstrapParsesAndIsHeldUntilTheOwnerAnswers(t *testing.T) {
|
||||
// ADR 0636 §1, §2: before the seat is heard the reader holds the bootstrapped definitions, says it waits and lets a host act
|
||||
// on the modules those definitions name only.
|
||||
func TestTheBootstrapIsHeldAndNothingNewIsActedOnUntilTheSeatAnswers(t *testing.T) {
|
||||
boot := Bootstrapped()
|
||||
if len(boot) == 0 {
|
||||
t.Fatal("no bootstrapped definition")
|
||||
held := map[string]bool{}
|
||||
for _, d := range boot {
|
||||
held[d.Seat] = true
|
||||
}
|
||||
for _, seat := range []string{"mesh-catalog", "mesh-conditions", "operator-channel", "node-engine", "mesh-broker", "mesh-config"} {
|
||||
if !held[seat] {
|
||||
t.Fatalf("the bootstrap lacks %s (ADR 0636 §2)", seat)
|
||||
}
|
||||
}
|
||||
owner := &fakeOwner{down: true}
|
||||
r, err := Start(Config{Owner: "mesh-owner", Bus: owner, Bootstrap: boot})
|
||||
r, err := Start(Config{Bus: owner, Bootstrap: boot})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := len(r.Seats()); got != len(boot) {
|
||||
t.Fatalf("the reader holds %d seats, the bootstrap has %d", got, len(boot))
|
||||
if got := len(r.Definitions()); got != len(boot) {
|
||||
t.Fatalf("the reader holds %d definitions, the bootstrap has %d", got, len(boot))
|
||||
}
|
||||
st := r.Status()
|
||||
if st.Heard || len(st.Waiting) != len(boot) || st.LastError == "" {
|
||||
t.Fatalf("status %+v: want the owner not heard, every bootstrapped seat waiting and the error said", st)
|
||||
if st.Heard || len(st.Waiting) != len(boot) || st.LastError == "" || st.Health() == "" {
|
||||
t.Fatalf("status %+v: want the seat not heard, every bootstrapped seat waiting, the error and the health said", st)
|
||||
}
|
||||
acts := []struct {
|
||||
seats []string
|
||||
want bool
|
||||
}{
|
||||
{[]string{"node-engine"}, true},
|
||||
{[]string{"mesh-catalog", "mesh-config"}, true},
|
||||
{[]string{"node-engine", "node-files"}, false},
|
||||
{[]string{"node-files"}, false},
|
||||
{nil, false},
|
||||
}
|
||||
for _, a := range acts {
|
||||
if got := r.MayAct(a.seats...); got != a.want {
|
||||
t.Fatalf("waiting: MayAct(%v) = %v, want %v", a.seats, got, a.want)
|
||||
}
|
||||
}
|
||||
owner.down = false
|
||||
owner.table = map[string]string{"node-b": def("node-b", "node", "put")}
|
||||
@@ -152,6 +185,36 @@ func TestTheBootstrapParsesAndIsHeldUntilTheOwnerAnswers(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if st := r.Status(); !st.Heard || len(st.Waiting) != len(boot) || st.LastError != "" || st.Revision != 7 {
|
||||
t.Fatalf("status after the owner answered %+v: want heard, the bootstrap's seats the owner does not hold still waiting, no error, revision 7", st)
|
||||
t.Fatalf("status after the seat answered %+v: want heard, the bootstrap's seats it does not hold still waiting, no error, revision 7", st)
|
||||
}
|
||||
if !r.MayAct("node-files") || !r.MayAct() {
|
||||
t.Fatal("after the seat answered, a host acts on any module")
|
||||
}
|
||||
}
|
||||
|
||||
// ADR 0636 §7: the two tools read what is held and read again now, the second to its callers only.
|
||||
func TestTheTwoToolsReadTheHeldListAndReadItAgainForTheirCallers(t *testing.T) {
|
||||
owner := &fakeOwner{down: true, table: map[string]string{"node-b": def("node-b", "node", "put")}}
|
||||
r, err := Start(Config{Bus: owner, Bootstrap: Bootstrapped()})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
callers := stdio.Callers{"person.jo", "mesh-healers"}
|
||||
tools := Tools(r, "seat_list", "seat_list_again", callers)
|
||||
out, err := tools[0].Run(map[string]any{})
|
||||
if err != nil || out.(map[string]any)["health"] == "" || !strings.Contains(fmt.Sprint(out.(map[string]any)["status"]), "bootstrap") {
|
||||
t.Fatalf("read: %v %v", out, err)
|
||||
}
|
||||
for caller, allowed := range map[string]bool{"person.jo": true, "ace.mesh-healers": true, "ace.mesh-other": false, "person.al": false, stdio.Unproven: false} {
|
||||
if err := Refusal("seat_list_again", callers, caller); (err == nil) != allowed {
|
||||
t.Fatalf("%s: %v, allowed %v", caller, err, allowed)
|
||||
}
|
||||
}
|
||||
if _, err := tools[1].Run(map[string]any{}); err == nil || !strings.Contains(err.Error(), "refused") || len(owner.asked) != 1 {
|
||||
t.Fatalf("a call without a proven caller read the list again: %v, asks %v", err, owner.asked)
|
||||
}
|
||||
owner.down = false
|
||||
if err := r.Reload(); err != nil || !r.Status().Heard || len(r.Status().Held) != len(Bootstrapped())+1 {
|
||||
t.Fatalf("again: %v %+v", err, r.Status())
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+1
@@ -48,3 +48,4 @@ internal/beside/beside.go: mesh-catalog
|
||||
overlay/overlay.go: the-private-network
|
||||
provisions/provisions.go: mesh-provisioner
|
||||
provisions/provisions.go: mesh-vault
|
||||
seatlist/seatlist.go: mesh-catalog
|
||||
|
||||
Reference in New Issue
Block a user