Refuse paths with empty, dot or parent segments and a file that is also a directory; take an item only when its key says what it is; write the view under its lock; expand nodes all and check node names; merge the plugin's entries into the operator's own; render every file past one that fails; in the home, never take over the person's file, never write through a symbolic link, keep a deleted file deleted, keep the kind's directory; and refuse settings that would deny the console or the marketplace.
113 lines
8.5 KiB
Markdown
113 lines
8.5 KiB
Markdown
# claude-code
|
|
|
|
The operator's agent on a machine (novox/hq design 36): its package, its machine-wide managed
|
|
configuration, and the consumer side of the Anthropic licence manager (design 39, ADR 0183).
|
|
|
|
## What it owns
|
|
|
|
Two directories, declared, so the mesh refuses a second module owning either:
|
|
|
|
- `/etc/claude-code`, the agent's machine-wide managed directory, root's, `0755`.
|
|
- `~/.claude` under the operator account's home, the operator's, `0700`. The module owns the directory —
|
|
that it exists, who owns it, its mode — and of what is inside only what it writes. Everything else
|
|
in it (memory, history, projects, local settings, a person's own rules and skills) is the person's
|
|
and is never read or written (hq ADR 0182). Unassigned, the module leaves the directory: the host
|
|
removes a directory only when it is empty.
|
|
|
|
## What it writes
|
|
|
|
Under the agent's managed directory, `/etc/claude-code`, owned whole by this module and rewritten
|
|
whenever the node's tool runtime collects the module's tools:
|
|
|
|
| file | holds |
|
|
|---|---|
|
|
| `managed-mcp.json` | the tool servers every session loads: the mesh's console as `mesh`, and the servers set in this module's `mcp_servers` setting. **Exclusive**: a server not listed here does not load — not one added with `claude mcp add`, not a project's `.mcp.json`, not a plugin's |
|
|
| `managed-settings.json` | the keys set in this module's `managed_settings` setting, then the settings registered through this module (the mesh's, then this node's), under the mesh's own keys: the repositories' attribution convention, the claude.ai connectors kept beside the managed servers, the key-helper while the node holds an API-key licence, and the two that name the `nox-mesh` marketplace and enable its plugin |
|
|
| `CLAUDE.md` | how a session on this mesh works, this node's name and role, the conventions — then the instruction sections registered for every node and for this one |
|
|
| `marketplace/` | the `nox-mesh` plugin (hq ADR 0216): the skills, subagents, commands, hooks and output styles registered for every node and for this one, offered in a session as `nox-mesh:<name>`. Replaced whole, staged beside and swapped in |
|
|
|
|
Under the operator's home: `~/.claude/.credentials.json`, only when the licence manager hands this node a
|
|
subscription token; and what is registered at the **home** scope for this node — a skill, subagent,
|
|
command, output style, or instructions as a rule file — each path recorded in the module's state
|
|
(`home-placed.json`). It writes, changes and removes only those — never a path the person made, even one with the
|
|
same content, and never through a directory that is a symbolic link. A placed file changed by hand is left
|
|
alone, and one the person deleted stays deleted until the item is unregistered (hq ADR 0182). The status tool reads the rest of the home's
|
|
items to report them; nothing else is read or written.
|
|
|
|
## Over NATS
|
|
|
|
Everything between this module and the rest of the mesh is NATS, in three kinds: an **event** says that
|
|
something happened and carries no secret, because a stream keeps it; a **request** carries a token,
|
|
because nothing keeps it (hq design 32 §10); and **state** is the current value of something every node
|
|
must see, a node that joins later included — kept, so it carries no secret either (hq ADR 0201).
|
|
|
|
| what | how |
|
|
|---|---|
|
|
| what this node holds | the module's `holdings` state, one key for this node — the account, the kind, fingerprints and expiries, never a token — written at start and whenever the credentials file changes (hq ADR 0206) |
|
|
| a person ran `/login` here | the credentials file gains a refresh token this module never writes; its next report shows it, and the licence manager asks `claude_code_grant` for it, giving its key — the one time a refresh token leaves the node, for the manager to adopt by refreshing it |
|
|
| what this node should hold | the licence manager's `bindings` state, this node's key; on a newer generation this module asks `anthropic-licence-manager.current` for its token, sealed to the key it sends, and writes it access-token-only — so the agent here never refreshes. A node that was off reads its key when it is back |
|
|
| an MCP server registered through this module | a key in the module's `servers` state — `all.<server>` for every node, `<node>.<server>` for one; every node watches it and renders what applies to it, a node's own entry over the one for every node. A node that joins later, or was off, reads the whole current set at start; unregistering is a delete. An entry with a secret in its `env` or `headers` is refused by the runtime |
|
|
| the agent's configuration (hq ADR 0216) | a key in the module's `config` state per registration — `mesh.<kind>.<name>` for every node, `node.<node>.<kind>.<name>` for one, `home.<node>.<kind>.<name>` for one account's own directory — the item and its files in one value, at most 256 KiB. Every node watches it and renders what applies to it, a node item over a mesh item of the same kind and name |
|
|
|
|
## Tools
|
|
|
|
`claude_code_status`, `claude_code_render`, `claude_code_pull`, `claude_code_grant` (for the licence
|
|
manager), `claude_code_mcp_list`,
|
|
`claude_code_mcp_register` (this node by default; `nodes: "all"` or a list for more — called for this
|
|
node alone, its answer names the other nodes running claude-code), `claude_code_mcp_unregister`.
|
|
|
|
The agent's configuration (hq ADR 0216), each registered at a **scope** — `mesh` (the default), `node`
|
|
(`nodes`: a list, or `"all"` for every node running claude-code; absent is this node) or `home` (the
|
|
operator account's own `~/.claude` on those nodes):
|
|
|
|
- for each kind — `skill`, `agent`, `command`, `hook`, `output_style`, `instructions` —
|
|
`claude_code_<kind>_list`, `_register`, `_unregister`. A skill is its files (`files`, or `content` for a
|
|
lone SKILL.md); a hook is an `event`, a `matcher`, a `command` and its scripts as `files`, with
|
|
`${HOOK_DIR}` in the command naming their directory. Hooks and settings take no home scope;
|
|
- `claude_code_settings_get`, `_set` (merged into the scope, or `replace`), `_clear`;
|
|
`claude_code_permission_add` and `_remove` for one allow, ask or deny rule. The agent refuses to loosen
|
|
its own settings: these are the operator's to call;
|
|
- `claude_code_config_list`, `_show` (one registration in full), `_status` (what applies here, the plugin
|
|
as written, and the home's own items — which the mesh placed, which share a name with a mesh item, which
|
|
call a tool server not loaded here) and `_import` (an item of this node's home, registered at a scope;
|
|
the original stays).
|
|
|
|
A new session takes a change; a running one at `/reload-plugins`.
|
|
|
|
## Settings
|
|
|
|
Per node or for the whole mesh, through `mesh-controller.settings module=claude-code`:
|
|
|
|
- `role` — what this node is, in a few words; shown to every session.
|
|
- `mcp_servers` — extra tool servers, set by the operator for the mesh or a node, beside the ones
|
|
registered through the tools; keyed by name, in the vendor's `.mcp.json` entry shape
|
|
(`{"type":"http","url":…}` or `{"type":"stdio","command":…,"args":[…]}`). The name `mesh` is the
|
|
module's own and cannot be set. Put a person's own servers here, or they stop loading.
|
|
- `managed_settings` — keys of the agent's managed settings, in the vendor's `settings.json` shape:
|
|
`permissions` (allow, ask, deny), `autoMode` (environment, allow, soft_deny), `env`, hooks and so on.
|
|
Managed settings outrank every other scope, so a rule here holds in every session on the node. The
|
|
mesh's own keys (`attribution`, `allowAllClaudeAiMcps`, `apiKeyHelper`) are laid last and cannot be
|
|
set. A setting layer is replaced whole: setting `managed_settings` without `role` or `mcp_servers`
|
|
clears those in that layer.
|
|
|
|
## On a machine that carried the predecessor
|
|
|
|
Remove these by hand, once; the mesh removes nothing it did not make (ADR 0182):
|
|
|
|
- `~/.claude/CLAUDE.md`
|
|
- `~/.claude/rules/00-hal-mesh.md`, `~/.claude/rules/conventions.md`
|
|
- `~/.claude/skills/cleanup/`, `~/.claude/skills/hal-switch-license/`
|
|
- the hand-made console entry in `~/.claude.json` under `mcpServers` — it is ignored now anyway
|
|
|
|
## Escalation
|
|
|
|
Writing `/etc/claude-code` needs root. The runtime runs as the operator account, and the module uses
|
|
that account's passwordless `sudo`; on a machine without it, `claude_code_render` says so and nothing
|
|
is written.
|
|
|
|
## Code
|
|
|
|
Go, one binary (`cmd/claude-code`) the node's runtime launches. Tested with `go test ./...`; the managed
|
|
instruction file is held to the TypeScript renderer it replaced (`testdata/rendered-by-typescript.json`),
|
|
and the sealed box is the licence manager's own format.
|