Keep every seat's holder under a placement, and hold a user list that would lose a grant (issue 1945)

The user list of 2026-10-27 09:21 granted no holder for nine seats: the seeded
placements hold only the seats the controller had on record, and the grants part
read those holds as every holder (records.go heldWherePlaced). A placement's
holds are now the holders on record, as catalogue.World's Holdings are: a mesh
seat no placement holds is held by derivation, wherever its claimant is placed.

The membership loss guard (Losses) did not cover the user list: after take-over
the list that lost those seats went out. UserLosses applies the same guard per
user against the loaded list, and holdsBack holds a list that would lose a grant,
or whose loaded grants cannot be read, raising the held warning.

Quick fix chosen over moving the on-record rule into mesh-sdk: mesh-grants is the
only part reading placement holds itself; the others pass them to the catalogue.
This commit is contained in:
2026-10-27 20:54:30 +01:00
parent 24cb379434
commit bc1978ec67
8 changed files with 734 additions and 22 deletions
+184
View File
@@ -0,0 +1,184 @@
{
"module": "claude-licence-manager",
"version": "1",
"slug": "licmgr",
"requires": [
"postgres-database",
"secret"
],
"contributes": {
"postgres-database": {
"name": "claude_licences",
"readable": [
"audit",
"binding"
]
}
},
"binds": {
"postgres-database": "${dir:state}/database.json"
},
"secrets": {
"postgres-database": "${dir:state}/database.secret",
"secret": {
"grant-key": "${dir:state}/grant.key"
}
},
"seats": [
{
"name": "anthropic-licence-manager",
"scope": "mesh",
"serves": [
"licences",
"bindings",
"bind",
"switch",
"release",
"refresh",
"usage",
"adopt",
"current",
"public-key"
]
}
],
"claims": [
{
"name": "anthropic-licence-manager",
"scope": "mesh",
"serves": [
"licences",
"bindings",
"bind",
"switch",
"release",
"refresh",
"usage",
"adopt",
"current",
"public-key"
]
}
],
"emits": [
"licence.adopted",
"licence.refused",
"licence.bound",
"licence.switched",
"licence.released",
"licence.rotated",
"licence.failing",
"usage.read"
],
"state": [
"bindings"
],
"reads": [
"claude-code.holdings"
],
"config": {
"cadence_minutes": {
"type": "number",
"description": "How old a licence's grant may grow before it is rotated, in minutes; 0 keeps 240.",
"minimum": 0,
"trusted": true,
"default": 240
},
"floor_minutes": {
"type": "number",
"description": "How little time a grant may have left before it is refreshed in any case, in minutes; 0 keeps 60.",
"minimum": 0,
"trusted": true,
"default": 60
},
"failures_to_notify": {
"type": "number",
"description": "How many refreshes of a licence fail in a row before the operator is told, a count; 0 keeps 3.",
"minimum": 0,
"trusted": true,
"default": 3
},
"cooldown_hours": {
"type": "number",
"description": "How long after telling the operator about a licence nothing more is said about it, in hours; 0 keeps 24.",
"minimum": 0,
"trusted": true,
"default": 24
},
"refresh_warn_days": {
"type": "number",
"description": "How long before a licence's refresh token itself expires the operator is warned, in days; 0 keeps 3.",
"minimum": 0,
"trusted": true,
"default": 3
}
},
"resources": [
{
"id": "state",
"type": "directory",
"mode": "0700",
"place": ".",
"unknown": "program"
},
{
"id": "database-url",
"type": "file",
"path": "${dir:state}/database.url",
"mode": "0600",
"template": "files/database.url"
},
{
"id": "settings",
"type": "file",
"path": "${dir:state}/settings.json",
"mode": "0600",
"takes-config": ["cadence_minutes", "floor_minutes", "failures_to_notify", "cooldown_hours", "refresh_warn_days"],
"merge": "json",
"template": "files/settings.json"
},
{
"id": "prepare",
"type": "process",
"name": "claude-licence-manager-prepare",
"artifact": "code",
"run": [
"./claude-licence-manager",
"prepare"
],
"run-once": true,
"env": {
"DATABASE_URL_FILE": "${dir:state}/database.url"
},
"restart-on": [
"database-url"
]
}
],
"build": {
"artifacts": [
{
"name": "code",
"kind": "bundle",
"language": "go",
"system": "arch",
"from": "cmd/claude-licence-manager",
"binary": "claude-licence-manager",
"loads": [
"claude-licence-manager"
],
"env": {
"DATABASE_URL_FILE": "${dir:state}/database.url",
"MESH_LICENCE_STATE": "${dir:state}",
"MESH_LICENCE_KEY_FILE": "${dir:state}/grant.key",
"MESH_LICENCE_SETTINGS": "${dir:state}/settings.json"
}
},
{
"name": "files",
"kind": "archive",
"from": "files"
}
]
}
}
@@ -0,0 +1,35 @@
{
"users": [
{
"user": "novox.node-tools",
"subscribe": [
"$SRV.INFO.anthropic-licence-manager",
"$SRV.INFO.anthropic-licence-manager.>",
"$SRV.PING.anthropic-licence-manager",
"$SRV.PING.anthropic-licence-manager.>",
"$SRV.STATS.anthropic-licence-manager",
"$SRV.STATS.anthropic-licence-manager.>",
"mesh.seat.anthropic-licence-manager.call.adopt.*",
"mesh.seat.anthropic-licence-manager.call.bind.*",
"mesh.seat.anthropic-licence-manager.call.bindings.*",
"mesh.seat.anthropic-licence-manager.call.current.*",
"mesh.seat.anthropic-licence-manager.call.licences.*",
"mesh.seat.anthropic-licence-manager.call.public-key.*",
"mesh.seat.anthropic-licence-manager.call.refresh.*",
"mesh.seat.anthropic-licence-manager.call.release.*",
"mesh.seat.anthropic-licence-manager.call.switch.*",
"mesh.seat.anthropic-licence-manager.call.usage.*",
"mesh.seat.anthropic-licence-manager.tool.adopt",
"mesh.seat.anthropic-licence-manager.tool.bind",
"mesh.seat.anthropic-licence-manager.tool.bindings",
"mesh.seat.anthropic-licence-manager.tool.current",
"mesh.seat.anthropic-licence-manager.tool.licences",
"mesh.seat.anthropic-licence-manager.tool.public-key",
"mesh.seat.anthropic-licence-manager.tool.refresh",
"mesh.seat.anthropic-licence-manager.tool.release",
"mesh.seat.anthropic-licence-manager.tool.switch",
"mesh.seat.anthropic-licence-manager.tool.usage"
]
}
]
}
@@ -0,0 +1,326 @@
{
"v": 1,
"node": "novox",
"modules": [
{
"module": "artifact-store-tools"
},
{
"module": "avahi"
},
{
"module": "ca-trust"
},
{
"module": "claude-code"
},
{
"module": "claude-licence-manager"
},
{
"module": "dbus"
},
{
"module": "de-spiegel"
},
{
"module": "desk-channel"
},
{
"module": "dev-tools"
},
{
"module": "disk-load"
},
{
"module": "distribution"
},
{
"module": "dnsmasq"
},
{
"module": "docker"
},
{
"module": "fail2ban"
},
{
"module": "gitea"
},
{
"module": "go"
},
{
"module": "hostname"
},
{
"module": "invoicing"
},
{
"module": "kernel"
},
{
"module": "keycloak"
},
{
"module": "link2pay"
},
{
"module": "localization"
},
{
"module": "logrotate"
},
{
"module": "mailu"
},
{
"module": "mesh-build-go"
},
{
"module": "mesh-build-node"
},
{
"module": "mesh-catalog"
},
{
"module": "mesh-cli"
},
{
"module": "mesh-config"
},
{
"module": "mesh-controller"
},
{
"module": "mesh-delivery-checker"
},
{
"module": "mesh-delivery-forge-bridge"
},
{
"module": "mesh-delivery-gate"
},
{
"module": "mesh-delivery-indexer"
},
{
"module": "mesh-delivery-teller"
},
{
"module": "mesh-delivery-watchman"
},
{
"module": "mesh-files"
},
{
"module": "mesh-grants"
},
{
"module": "mesh-host"
},
{
"module": "mesh-issues"
},
{
"module": "mesh-knowledge"
},
{
"module": "mesh-mail"
},
{
"module": "mesh-mail-reader"
},
{
"module": "mesh-mail-web"
},
{
"module": "mesh-provisioner"
},
{
"module": "mesh-provisioner-client"
},
{
"module": "mesh-reads"
},
{
"module": "mesh-vault"
},
{
"module": "mesh-wireguard"
},
{
"module": "messenger"
},
{
"module": "minio"
},
{
"module": "mongodb"
},
{
"module": "mssql"
},
{
"module": "nats"
},
{
"module": "netcheck"
},
{
"module": "nextcloud"
},
{
"module": "nftables"
},
{
"module": "node-env"
},
{
"module": "node-tools"
},
{
"module": "nodejs"
},
{
"module": "only-office"
},
{
"module": "pacman"
},
{
"module": "photos"
},
{
"module": "postgres"
},
{
"module": "power"
},
{
"module": "powerlevel10k"
},
{
"module": "records"
},
{
"module": "restic"
},
{
"module": "route-proxy"
},
{
"module": "ssh-client"
},
{
"module": "sshd"
},
{
"module": "step-ca"
},
{
"module": "sudo"
},
{
"module": "systemd"
},
{
"module": "systemd-networkd"
},
{
"module": "telegram"
},
{
"module": "time-sync"
},
{
"module": "umami"
},
{
"module": "website"
},
{
"module": "zsh"
},
{
"module": "zsh-autosuggestions"
},
{
"module": "zsh-syntax-highlighting"
}
],
"holds": [
{
"claim": "git",
"scope": "mesh",
"module": "gitea",
"site": "hosting"
},
{
"claim": "mesh-artifact-store",
"scope": "mesh",
"module": "distribution",
"site": "hosting"
},
{
"claim": "mesh-broker",
"scope": "mesh",
"module": "nats",
"site": "hosting"
},
{
"claim": "mesh-catalog",
"scope": "mesh",
"module": "mesh-catalog",
"site": "hosting"
},
{
"claim": "mesh-config",
"scope": "mesh",
"module": "mesh-config",
"site": "hosting"
},
{
"claim": "mesh-controller",
"scope": "mesh",
"module": "mesh-controller",
"site": "hosting"
},
{
"claim": "mesh-delivery",
"scope": "mesh",
"module": "mesh-delivery-gate",
"site": "hosting"
},
{
"claim": "mesh-dns-resolver",
"scope": "mesh",
"module": "dnsmasq",
"site": "hosting"
},
{
"claim": "mesh-provisioner",
"scope": "mesh",
"module": "mesh-provisioner",
"site": "hosting"
},
{
"claim": "mesh-store",
"scope": "mesh",
"module": "postgres",
"site": "hosting"
},
{
"claim": "mesh-vault",
"scope": "mesh",
"module": "mesh-vault",
"site": "hosting"
},
{
"claim": "npm-package-registry",
"scope": "mesh",
"module": "gitea",
"site": "hosting"
}
]
}