Repository navigation
Feat/managed namespace - #131
Merged
Merged
Conversation
Add tests/test_protocol1_golden.py and tests/golden/protocol1/*.json, captured on main as it stands (after #130, the Kandev proxy observability change) with no code change. Each step runs one Kandev-key or host-side verb in the harness with the clock, randomness, versions, machine facts, port binding, tool lookup and whether msbctl runs from a git checkout fixed (so 23-self-update answers the same from a git clone or a plain copy), and compares the exit code, the exact stdout and the argv of every `msb` call it made: every `kandev` verb (success and the main error codes), `ls`/`show`/`stop`/`purge`/`self-update`/scoped `doctor --json` under the Kandev key, the `--kandev` authorized_keys line and its `--replace`, `host kandev configure --json` with the file it writes, the units `host kandev install` writes, and one reaper pass. Fixtures are rewritten only by hand (MSBCTL_GOLDEN_WRITE=1), so any later diff to them is a visible change to what the Kandev plugin was built against. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add `key_client()` (MSB_RESTRICTED "kandev" or "client:NAME"), `entry_client()`/`entry_env()` for registry entries, and `client_name_problem()`; `kandev_key()` now goes through `key_client()`. Any non-empty MSB_RESTRICTED is `restricted()`, and a value `host serve` never sets (a malformed `client:` name, anything else) fails closed: `key_client()` refuses it, and `main()` asks it first, so such a session runs no verb at all rather than the everyday key's. `Sandbox` accepts `source = "managed"` with `client`, sets `client` and `is_managed` (`is_kandev` is its alias, since every such site means "a client owns it"), and shows `location` as `<client>:<env>`. Purge's scoping, the port-claim scan, the pool-overlap count and the version probe's skip use the parser instead of comparing `source`. The protocol-1 fixtures do not change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`managed_host_settings()` reads the host-wide keys (serve, proxy_listen, base_url, the two pools) from the operator's `[managed]`, else `[kandev]`, else what ships, refuses one key in both, records which table each is set in, and adds optional host totals from `[managed]`. `client_settings()` reads one client's table over the shipped `[kandev]` defaults: Kandev's from `[kandev]` or `[clients.kandev]` (not both) with its prefixes, ports, callback rule and body-secret hosts fixed, and any other client's from `[clients.NAME]` with validated `name_prefix`, `env_prefix`, `guest_ports`, `require_callback` and `body_secret_hosts` (each reachable through the client's `allow_extra`, at any port of an address entry, an `allow@` rule of its `rule_groups` or `base`, or its `private_names`). A host key in any `[clients.*]` table, `[clients.kandev]` included, is refused rather than silently dropped. `k-` is Kandev's on every host: a client asking for it gets its own `invalid_config`, never Kandev, and the default prefix (first letter and "-") takes a second letter for a name starting with k (`kube`: `ku-`). `clients_settings()` never raises: every problem is the `invalid_config` of the clients it concerns, and Kandev and every other client go on. That covers a table that does not check out, a table whose name is not a client name, an entry under `[clients]` that is not a table, and a prefix two clients share, which marks each of them (counting the prefix a broken table asks for, so no one takes it meanwhile). Under a client's key a clash names no other client. A value of a type no check foresaw (`rule_groups = [["base"]]`, an inline table, a `base_url` that does not parse as a URL) is `invalid_config` too, of the client or host table it is in, naming the exception's type, never an internal error: in Kandev's own `[kandev]` it used to be `unavailable` ("internal error"), and is now `invalid_config`. Since that may as well be a bug in a check, at the host (the operator's terminal, the reaper's and the proxy's journal) its traceback goes to stderr, once per process; never under a key, where stderr crosses ssh, and never into the JSON. The table is named however the client's name is passed (positionally or by keyword). A `[clients]` that is not a table at all names no client but Kandev. Conflicts are read from the operator layer, since defaults.toml ships a full `[kandev]`. `kandev_settings()` is now their composition and returns exactly the keys it always did; `host kandev configure` writes each key into the table that already holds it. Fixtures unchanged, but one protocol-1 error text changes on purpose: the `allow_extra` example addresses in the invalid_config message of `kandev provision` and `host kandev configure` are now documentation addresses (192.0.2.6, 192.0.2.0/24) instead of a LAN address. No fixture covered that message; a test now pins it, and the plugin does not parse messages. Config tests added, including every client, `[kandev]` and `[managed]` key set to a value of each wrong TOML type.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add the frozen `Dialect` dataclass, `KANDEV_DIALECT` (protocol 1) and `managed_dialect()` (protocol 2; for Kandev its guest ports stay protocol 1's four, since its agentctl is the first of them), and generalize under it: - `managed_name()`, `managed_entries()`, `managed_lookup()`/`managed_need()`, `managed_usage()`, `managed_labelled()`/`owner_labels()`, and `alloc_ports()`/`alloc_proxy_ports()` for any count. - `managed_provision`/`_agentctl`/`_start`/`_expiry`/`_lease`: per-request guest ports, the client's env prefix, agentctl path and nonce variable, its `body_secret_hosts` as the secret's passthrough list, client and host caps, and `registry-managed.toml.in` for non-Kandev entries. Every `kandev_*` name stays as a thin wrapper. Messages about a host key (pools exhausted, base_url or proxy_listen unset) name the table it is in through `host_table()`; Kandev's keep saying `[kandev]`, as protocol 1 pins. A provision whose sandbox name is taken by a sandbox the client does not own answers `conflict` without naming it for a non-Kandev client; Kandev's keeps protocol 1's text. Lock, state dirs, lease key and unit names are deliberately unchanged. Fixtures unchanged. Callback: - Optional for a non-Kandev client unless its `require_callback` is set, with `kandev_api` as its alias, and checked as `kandev_api` is when given. A client that is never called back sends none: then no egress rule names it and nothing is pinned in the guest's /etc/hosts (a pinned name gets no secret substituted, since msb substitutes only on an address it resolved). - A given callback is egress to one address and port, so a non-Kandev client's must be admitted by its own `allow_extra`. An address is admitted by an address or CIDR entry (any port), an address:port entry (that port only), or `*`, which admits any address given as one, private and reserved included (it trusts the client with everything). A name is resolved on the host and admitted when an address, CIDR or address:port entry covers the address it resolves to, the operator's explicit choice; or else by a hostname pattern (any port), and for a name `*` counts as a pattern, but then only if that address is public (`is_global`, not multicast, outside the reserved ranges msb's rebind filter drops) or the name is in the client's `private_names`. That is no more than the same name gets through `egress_allow` and `private_names`: the rule pins the host's answer, so msb's DNS rebind protection never sees it. A name that no entry could admit (no matching pattern or `*`, and no IPv4 address, CIDR or address:port entry at its port that could hold its answer, since a name resolves to IPv4 only) is refused before it is looked up, so a client key cannot make the host resolve arbitrary names or learn which resolve. Otherwise the provision is `invalid_config` naming `callback` (or `kandev_api`), before anything is created. - Kandev's stays required and is its trusted API, not checked against `allow_extra`, as in protocol 1. Reaper: removal checks the entry's own client label, and the reaper passes over every client, gated only on the host's keys (`managed_host_settings()`, no longer Kandev's whole `kandev_settings()`). So a table that does not check out, `[kandev]`'s own keys included, stops no one's expiry or stuck-create cleanup, its own sandboxes' neither. It reaps orphans only of configured clients whose tables check out. Proxy: - It serves every enabled client's entries and checks each connection against its listener's client. - A client whose table does not check out keeps exactly the proxy ports it has open and gets none from the registry, as a broken `[kandev]` always has: a client disabled stays closed, after a proxy restart it has none until fixed, and a port whose sandbox is removed meanwhile (purged, or reaped) closes on the next scan instead of being listed in another client's `health`. Kandev's ports behave as on a host where it is the only client (where a broken `[kandev]` now drops such a port too). The proxy says so once each time a table breaks. - Whatever else a scan raises from the config or the registry, it keeps the ports it has open (only those still backed when the registry can be read, all of them when it cannot, each connection still checked) and says so once while the reason reads the same, rather than ending the proxy (and every client's ports with it). An entry's `proxy_ports` that is not a list is no one's port. - Its summary on a shared host speaks of clients and managed sandboxes rather than of Kandev's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`host serve --client NAME` and `host authorize KEY --client NAME` (`--client kandev` is `--kandev`, byte for byte; `--admin` with either is refused; it warns when `[clients.NAME]` is missing, does not check out or is not enabled, and writes the line all the same); `restricted_problem(argv, client)` admits a client key's own `managed` verbs and ls/show/stop/purge/doctor with --json, refuses a `--client` naming another client, keeps self-update, `tls install` and `restart-proxy` with the Kandev key, and keeps both namespaces from the everyday key. Add `msbctl managed health|provision|agentctl|start|expiry|lease` through `run_client_verb()` (`cmd_kandev` now uses it too), protocol-2 health (`protocols`, `client`, `client_settings`, `host_capacity`), `record`/`client` in ls and show rows for non-Kandev entries, `managed_guard` scoping show/stop to the key's client, `json_verb` checking the key's client is enabled, scoped doctor for any client key, `name_problem` refusing every prefix a client table asks for (its table checking out or not), health's proxy ports never another client's (and its reasons naming the host key's own table), Kandev's `managed provision` refusing guest ports other than its four (protocol 1 never read them, and still does not), and the `managed_namespace` feature: the only change to the protocol-1 fixtures (four health documents). Tests: tests/test_managed.py, new cases in test_host_serve.py and a proxy case in test_kandev_tls.py, the proxy's freeze of a broken client's ports (including a sandbox purged while its table is broken, Kandev's on a Kandev-only host too), and wrong-typed values in another client's table, in `[kandev]` and `[managed]`, and in an entry's `proxy_ports`, under which Kandev's protocol 1 and the other clients answer as before and the proxy keeps every port it holds; the reaper expiring every client's sandboxes while `[kandev]` (a wrong value or a wrong type) or ci's table does not check out, and stopping only on the host's keys; a failure no check foresaw named by its type, traced on stderr at the host once and never under a key; commands.md documents the verbs and flags. Tests pin the callback rules end to end: a client's provision with no callback has no rule or argv naming it and runs no `msb exec` for /etc/hosts at provision or start; with one, the rule and the pin are as for Kandev; a malformed one, or one ci's `allow_extra` admits by neither host nor address (`192.0.2.9:22`, the alias `kandev_api`, a name resolving outside it), is `invalid_config` before anything is created, while one admitted by host, by address:port (at that port only) or by a CIDR around its resolved address is accepted with its rule; a name admitted only by `*.example.org` that resolves on the host to 10.0.0.5, 127.0.0.1, 169.254.169.254, 100.64.0.5 or 192.88.99.1 is refused, and accepted once the name is in ci's `private_names` or an address, CIDR or address:port entry covers that address (rebind protection still on); with `allow_extra = ["*"]`, a literal 10.0.0.5 is accepted while a name resolving to 10.0.0.5 is refused until it is in `private_names`; a name no entry could admit is refused without the host resolving it, also when the only address entries are at another port or IPv6; and a provision without `kandev_api` is still refused, by `kandev provision` with protocol 1's exact message and by the Kandev key's `managed provision`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
`msbctl host managed configure [--client NAME] [--json]` sets `[managed]` keys, or a client's (`--enabled`, caps, `--allow-extra`, `--private-name`, `--name-prefix`, `--env-prefix`, `--guest-port`, `--[no-]require-callback`, `--body-secret-host`), each into the table that already holds it, and only if the host's keys and, with `--client`, that client's own table (its prefix shared with no other client's) still check out; it also refuses a `name_prefix` that an existing sandbox the client does not own already starts with, and a new `name_prefix` while the client still has sandboxes under its old one, which its verbs would no longer find (purge them first). Another client's table that does not check out refuses nothing and is listed under `problems`. Host-only, like `host kandev configure`. `msbctl doctor` gains a `clients` check: every table at the host, the key's own (and what all share, naming no other client) under a client key; a broken table's warning says only the clients named are stopped, and that until they are fixed the proxy keeps the ports their sandboxes have open and opens no new ones (none after it restarts), with leases already issued valid until they expire. "No client's verbs work" is said only for a problem in the host's keys; a `[clients]` that is not a table is reported as naming no client. At the host, a `client-prefixes` warning names each client whose prefix is already the start of a sandbox it does not own (a table written by hand). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
New docs/content/managed.md (setting a client up, `[clients.NAME]` and the keys it refuses, `k-` and the default prefix, which clients a problem in `[clients]` stops (only those it concerns) and which proxy ports a broken table keeps open, `[managed]` and what `host_capacity` shows, secrets in request bodies and exactly which hosts `body_secret_hosts` accepts, protocol 2's differences, removing a client and downgrading), with kandev.md saying Kandev is one client, which of its keys may move to `[clients.kandev]` and which to `[managed]`, and that the proxy and reaper cover all clients; configuration.md pointing at both tables, commented `[managed]`/`[clients.<name>]` examples in defaults.toml, the msb-kandev skill, the AGENTS.md rows, and kandev.md's reaper section saying only the host's keys stop it, and CHANGELOG entries (Added: managed and --client, the managed_namespace feature, the tables; Changed: client prefixes are reserved and `k-` is no client's, the proxy serves every enabled client's sandboxes and the reaper every client's, and the downgrade warning; Fixed: a wrong-typed `[kandev]` value or an unparsable `base_url` is `invalid_config`, not `unavailable`, and no longer stops the proxy, and a client key that does not check out no longer stops the reaper). managed.md, commands.md and the CHANGELOG say why a client that is never called back should send no callback (the pin would stop secret substitution for that name). managed.md (the `allow_extra` and `require_callback` rows and protocol 2's differences), commands.md, defaults.toml's `require_callback` comment and the CHANGELOG say a client's callback must be admitted by its `allow_extra`: by an address, CIDR or address:port entry covering its address, or by a hostname pattern only when the address the host resolves it to is public or the name is in the client's `private_names`, with a name nothing could admit refused unresolved; `*` admits any address given as one, private included, and for a name counts as a pattern; Kandev's is not checked. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- tests/test_managed.py: drop an unused reassignment of `found`, patch tests.test_kandev.API_ADDR by name instead of importing the module twice, and suppress the expected KandevError with a comment, not an empty except - msbctl: managed_host_settings decides base_url's validity in one expression, so no variable is set that is never read; cmd_ls and cmd_show return None explicitly on every path Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GitHub's runner has lingering on for its user, so `host kandev install` left out the enable-linger hint the fixture expects.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A client-neutral
managednamespace (protocol 2), withkandevfrozen on protocol 1Why
A second orchestrator that wants sandboxes on a Kandev host today has to pose as Kandev: its sandboxes are named
k-…, count against Kandev's limits, share its egress and are visible to (and removable by) the Kandev plugin's key. This adds a client namespace: each client gets its own[clients.NAME]table, its own key (host authorize KEY --client NAME), its own name prefix, msb label, limits, egress, guest ports and agentctl variable prefix, and sees nothing of anyone else's sandboxes. Kandev keepsmsbctl kandev(protocol 1) exactly as it was.What changes
msbctl managed health|provision|agentctl|start|expiry|lease(protocol 2), run throughhost serve --client NAME. The client is the forced command's; a--clientin argv naming another is refused (126).[managed]for host-wide keys and optional totals over every client;[clients.NAME]per client, includingbody_secret_hosts(hosts a secret's placeholder may reach in a request body, e.g. a client's LLM router).[kandev]keeps working; its client keys may move to[clients.kandev]and its host keys to[managed]. A host key in any[clients.*]table is refused, not silently dropped.msbctl host managed configure [--client NAME];doctorreports client tables that don't check out. Configure refuses a newname_prefixwhile the client still has sandboxes under its old one (purge them first).host authorize --client NAMEwarns when that client's table is missing, broken or disabled.require_callbackis set) and checked when given. A given callback is egress to one address and port, so a client's must be admitted by its ownallow_extra: by an address, CIDR or address:port entry (that port only) covering its address (for a name, the one it resolves to on the host), or by a hostname pattern only if that resolved address is public or the name is in the client'sprivate_names, so a pattern gets no more than the same name inegress_allow(the rule pins the host's answer, out of reach of msb's DNS rebind protection).*admits any address given as one, private and reserved included; for a name it counts as a pattern, so a private answer needsprivate_namesor an address/CIDR entry. A name that no entry could admit is refused before it is looked up. Otherwise the provision isinvalid_confignamingcallback, before anything is created. Kandev'skandev_apiremains its trusted API and is not checked againstallow_extra. A client that is never called back, such as an orchestrator that only drives its sandboxes, sends none: then no egress rule names it and nothing is pinned in the guest's/etc/hosts, since a pinned name gets a connection through but no secret substituted (msb substitutes only on an address it resolved; see docs "Names on your LAN"). Kandev'skandev_apistays required in both protocols, with protocol 1's message unchanged.kandev healthgains themanaged_namespacefeature string.k-is Kandev's on every host: a client asking for it (or defaulting to it:kubenow defaults toku-) is that client's owninvalid_config, never Kandev's. A table that doesn't check out keeps the prefix it asks for, so no other client or operator sandbox takes it meanwhile.[clients]stops only the clients it concerns. That covers a broken table (a value of the wrong type included), a bad table name, a non-table entry, and two clients sharing a prefix, such asciandcddefaulting toc-, which stops only those two. Kandev and every other client keep working. The proxy keeps the ports a broken client's sandboxes have open and opens no new ones (none after a restart, and none for a sandbox removed meanwhile), so a client you disabled stays closed, and Kandev's proxy ports behave exactly as on a Kandev-only host. The reaper reads only the host's keys, so every client's sandboxes still expire while any client's table (Kandev's[kandev]included) is broken; only that client's orphans wait. Only a problem in the host's keys stops every client. Nothing the config or registry holds can end the proxy: a scan that raises keeps the ports it has open (when the registry can be read, only those still backed; when it cannot, all of them, each connection still checked) and logs it once while the reason reads the same. A failure no check foresaw (possibly a bug in a check) isinvalid_confignaming the exception's type; at the host its traceback goes to stderr (the journal), never under a key.doctornames the problem, and at the host it also warns when a client's prefix already covers a sandbox of the operator's.MSB_RESTRICTEDthathost servenever sets fails closed (no verb runs); a client's key hears no other client's name or prefix in a prefix clash (the operator'sdoctornames them), and a client's provision refused for a taken name doesn't name the sandbox; the Kandev key'smanaged provisionrefuses guest ports other than Kandev's four.Back-compat
tests/test_protocol1_golden.pypins protocol 1 byte for byte (stdout, exit,msbargv) for everykandevverb, the generic verbs under the Kandev key, the--kandevauthorized_keys line,host kandev configureand its file, the installed units and a reap pass. Captured before any refactor (commit 1); commits 2–4 leave them unchanged; the only fixture diff in the PR is"managed_namespace"appended tofeaturesin the four health documents (commit 5).allow_extraexamples in the invalid_config message ofkandev provision/host kandev configureuse documentation addresses (192.0.2.x) instead of a LAN address. No fixture covered it; a test now pins it, and the plugin does not parse messages.[kandev](rule_groups = [["base"]]) or abase_urlthat does not parse used to answerunavailable("internal error") and kill the proxy; it is nowinvalid_config, like any other bad value (CHANGELOG, Fixed).[kandev]'s own (client) keys used to stophost kandev reap(exit 3) until fixed; the reaper now stops only on the host's keys, so expiry goes on. No fixture pins a failed reap (30-reapis the success case).23-self-updateno longer depends on running from a git checkout (the harness fixesIS_CHECKOUT).source = "managed"entries; purge managed clients' sandboxes first (CHANGELOG says so).Manual check (plugin contract)
managed health, provision, lease through the proxy, andls --jsonfrom each key showing only its own sandbox.Commits