Rename Installation to Instance

Follows the portal: the noun is "instance" everywhere the app says it —
UI strings, CLI output, error details, docs and comments. The wire keys
(`instance_id`, `instance_token`) and the hub route this calls move with it.

An existing cloud.json is adopted rather than refused: without the key
alias the dataclass fails to parse, which the caller swallows and reads as
"never enrolled" instead of "reconnect".

`instance_key` on a node type becomes `target_key`. It means the outside
thing a node points at, which is a different sense of the word, and keeping
both would put two meanings of "instance" in one codebase.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015YrQnKV3bnQd4K342y8tKj
This commit is contained in:
2026-08-31 10:12:01 +02:00
co-authored by Claude Opus 5
parent 6534855492
commit d01a8dad37
101 changed files with 374 additions and 375 deletions
+13 -9
View File
@@ -1,4 +1,4 @@
"""What this installation knows about the portal it is enrolled with.
"""What this instance knows about the portal it is enrolled with.
One file on the data volume, beside the OAuth keypair and for the same reason:
it is a credential, it must survive a rebuild, and it must never be in the
@@ -9,7 +9,7 @@ portal still believes.
The portal's public keys are stored here rather than fetched: they are pinned
at enrolment, when a person was holding a claim code they had just read off the
portal's own screen. A hub whose DNS or TLS is later hijacked cannot re-key an
installation that already enrolled; it can only fail to verify.
instance that already enrolled; it can only fail to verify.
"""
from __future__ import annotations
@@ -42,11 +42,11 @@ VIA_PORTAL = secrets.token_urlsafe(16)
class CloudConfig:
portal_url: str
ws_url: str
installation_id: str
instance_id: str
token: str
issuer: str
jwks: dict[str, Any]
#: The local account this installation acts as on its own behalf: what the
#: The local account this instance acts as on its own behalf: what the
#: health summary is collected as, and what a screen paired through the
#: portal borrows for want of a person. Recorded at enrolment from whoever
#: performed it. Portal *sessions* no longer come through here — they name
@@ -72,6 +72,10 @@ def _read() -> CloudConfig | None:
return _cache[1]
try:
data = json.loads(path.read_text())
# A file written before the installation/instance rename. Adopt it
# rather than failing to parse, which would read as "never enrolled".
if "installation_id" in data:
data["instance_id"] = data.pop("installation_id")
config = CloudConfig(**data)
except (OSError, ValueError, TypeError):
logger.exception("Could not read %s; remote access stays off", path)
@@ -109,15 +113,15 @@ def delete() -> None:
def decode_portal_token(token: str) -> dict[str, Any]:
"""Validate a token the portal minted for this installation.
"""Validate a token the portal minted for this instance.
Raises ``InvalidTokenError`` for everything else, including the ordinary
case of not being enrolled at all — which is what makes this branch free
for an installation nobody connected.
for an instance nobody connected.
"""
config = _read()
if config is None:
raise InvalidTokenError("this installation is not enrolled with a portal")
raise InvalidTokenError("this instance is not enrolled with a portal")
try:
key = jwt.PyJWKSet.from_dict(config.jwks).keys[0]
@@ -128,9 +132,9 @@ def decode_portal_token(token: str) -> dict[str, Any]:
token,
key,
algorithms=["RS256"],
# The audience is this installation's own id, so a token the portal
# The audience is this instance's own id, so a token the portal
# minted for somebody else's machine fails here.
audience=config.installation_id,
audience=config.instance_id,
issuer=config.issuer,
)
scope = claims.get("scope")