Skip to content

Feat/managed namespace - #131

Merged
naerymdan merged 9 commits into
mainfrom
feat/managed-namespace
Oct 30, 2026
Merged

naerymdan merged 9 commits into
mainfrom
feat/managed-namespace

Conversation

@naerymdan

@naerymdan naerymdan commented Oct 30, 2026 •

Copy link
Copy Markdown
Collaborator

A client-neutral managed namespace (protocol 2), with kandev frozen on protocol 1

Why

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 keeps msbctl kandev (protocol 1) exactly as it was.

What changes

  • msbctl managed health|provision|agentctl|start|expiry|lease (protocol 2), run through host serve --client NAME. The client is the forced command's; a --client in argv naming another is refused (126).
  • [managed] for host-wide keys and optional totals over every client; [clients.NAME] per client, including body_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]; doctor reports client tables that don't check out. Configure refuses a new name_prefix while the client still has sandboxes under its old one (purge them first). host authorize --client NAME warns when that client's table is missing, broken or disabled.
  • The proxy serves every enabled client's sandboxes, and the reaper reaps every client's, each by its own label.
  • The callback is optional for a client (unless its require_callback is set) and checked when given. A given callback is egress to one address and port, so a client's must be admitted by its own allow_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's private_names, so a pattern gets no more than the same name in egress_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 needs private_names or an address/CIDR entry. A name that no entry could admit is refused before it is looked up. Otherwise the provision is invalid_config naming callback, before anything is created. Kandev's kandev_api remains its trusted API and is not checked against allow_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's kandev_api stays required in both protocols, with protocol 1's message unchanged.
  • kandev health gains the managed_namespace feature string.
  • k- is Kandev's on every host: a client asking for it (or defaulting to it: kube now defaults to ku-) is that client's own invalid_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.
  • A problem under [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 as ci and cd defaulting to c-, 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) is invalid_config naming the exception's type; at the host its traceback goes to stderr (the journal), never under a key. doctor names the problem, and at the host it also warns when a client's prefix already covers a sandbox of the operator's.
  • Isolation hardening: a MSB_RESTRICTED that host serve never sets fails closed (no verb runs); a client's key hears no other client's name or prefix in a prefix clash (the operator's doctor names them), and a client's provision refused for a taken name doesn't name the sandbox; the Kandev key's managed provision refuses guest ports other than Kandev's four.

Back-compat

  • tests/test_protocol1_golden.py pins protocol 1 byte for byte (stdout, exit, msb argv) for every kandev verb, the generic verbs under the Kandev key, the --kandev authorized_keys line, host kandev configure and 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 to features in the four health documents (commit 5).
  • Every existing test passes unedited.
  • One protocol-1 error text changes on purpose (commit 3): the allow_extra examples in the invalid_config message of kandev provision / host kandev configure use 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.
  • A wrong-typed value in [kandev] (rule_groups = [["base"]]) or a base_url that does not parse used to answer unavailable ("internal error") and kill the proxy; it is now invalid_config, like any other bad value (CHANGELOG, Fixed).
  • A problem in [kandev]'s own (client) keys used to stop host 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-reap is the success case).
  • Golden 23-self-update no longer depends on running from a git checkout (the harness fixes IS_CHECKOUT).
  • Lock, state dirs, lease key and unit names are unchanged, so an old proxy or reaper never races a new verb during an upgrade.
  • Downgrade: an older msbctl fails on source = "managed" entries; purge managed clients' sandboxes first (CHANGELOG says so).

Manual check (plugin contract)

  • Against a dev host: the Kandev plugin's Test and Setup (health, configure, install), then provision, agentctl, lease and purge, with the plugin unchanged.
  • A second client on the same host: managed health, provision, lease through the proxy, and ls --json from each key showing only its own sandbox.

Commits

  1. tests: pin protocol 1 with golden fixtures
  2. refactor: read client identity in one place
  3. refactor: split managed settings into host and per-client scopes
  4. refactor: a Dialect, and the Kandev machinery generalized to any client
  5. feat: msbctl managed (protocol 2) and client keys
  6. feat: host managed configure, and doctor reports client tables
  7. docs: managed clients

naerymdan and others added 7 commits October 30, 2026 15:31
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>
Comment thread tests/test_managed.py Fixed
Comment thread tests/test_managed.py Fixed
Comment thread tests/test_managed.py Fixed
naerymdan and others added 2 commits October 30, 2026 18:12
- 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.
@naerymdan
naerymdan merged commit 2cc99f5 into main Oct 30, 2026
4 checks passed
@naerymdan
naerymdan deleted the feat/managed-namespace branch October 30, 2026 22:57
@naerymdan naerymdan mentioned this pull request Oct 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants