Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# MonkeyLLM agent guide

Knowledge forest navigable by an SLM: markdown + indexes, traversed through
**Vine**'s MCP primitives. `docs/monkeyllm-spec-v0.82.md` is normative
**Vine**'s MCP primitives. `docs/monkeyllm-spec-v0.83.md` is normative
(earlier versions are archived) **the spec is the truth**; any contract
change requires a new spec version before code.

Expand Down Expand Up @@ -77,6 +77,40 @@ Local models (llama.cpp on the 3090): see `docs/local-inference.md`.

## Conventions and pitfalls

- **The extension that could not be used (spec G.2 + J.8.5 + L.1 + L.6 r1
+ L.8 + L.11 + L.14, v0.83)**: an operator uploaded two extensions
through the console, enabled both, and could use neither. The ingest
console kept its own list of formats (`ACCEPT`/`BINARY` constants in
`Ingest.jsx`) so every `.pdf` stayed greyed out while the converter was
installed, enabled and loaded; the Models console kept its own list of
roles (`ROLES` in `Models.jsx`) and the bind route called `bind_model`
without `extra_roles` (`ROLES` in `registry.py` is the four built-ins), so
`transcribe` was listed on one console and refused on the other — the
shipped station test bound through the registry directly and hid it.
Nothing had failed; the surfaces had never been told what the mechanism
changed. Now ONE source per list: `gardener.supported_formats(config,
extra, registry)` answers what the chain claims in discovery order
(`hook` > `ext:<id>` > `describer` > `plugin:<name>` > `builtin`) and
rides `GET .../ingest` as `formats` (J.8.5); `roles_catalogue()` in the
Station's `extensions.py` answers every bindable role with `kind`, origin
and `description` (a new optional `RoleSpec` field) and rides `GET
/v1/admin/models` as `roles`, which the POST reads as `extra_roles`.
Both consoles render from those answers; `check-usable.mjs` (F.218)
asserts the constants are gone and runs against the v0.82.0 tag as the
negative control. L.8: the restart is about UNLOADING, so an id this
process never met is activated live at install (`Runtime.activate`,
offered only to ids not in `_attempted` — a failed load after import
keeps a module under the tree's name and a second load would silently
reuse it); the response says `activated`/`restart_required`, the listing
says `loaded`, and an update keeps the restart. L.11: the install is one
in-place act (choose → review → install, one primary control at a time,
`enable_on: <forest>` in the same request, the outcome naming what is
active, what is accepted and what is still to bind). Found on the way:
`load_all` popped `config_factory` INSIDE its loop, so only the first
extension on a volume read its settings live — the console's Save never
reached `whisper` when `pdf` sorted first. Left out, named: the console's
25 MB ceiling on an upload (a video by base64 JSON is a J.8 transport
question, not a constant). F.213-F.218 in `tests/test_v083_usable*.py`.
- **The ask that nobody could see (spec J.2.7 + J.10.5 + J.10.13 + J.10.8
+ J.1.2 r8 + J.10.9, v0.82)**: a consumer's MCP client filtered `answer`
out of its tool list and reported the product had no way to ask; the
Expand Down
143 changes: 117 additions & 26 deletions apps/station/monkeyllm_station/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3132,7 +3132,7 @@ def run_ingest_status(principal: str, forest: str,
whose scope is invisible — which is how v0.25 shipped, and how a
forest ingested the Station's own source tree.
"""
from monkeyllm.gardener import Gardener
from monkeyllm.gardener import Gardener, supported_formats

policy = registry.policy_for(principal, forest)
if policy is None:
Expand All @@ -3149,9 +3149,19 @@ def run_ingest_status(principal: str, forest: str,
# Built fresh rather than read off a cached Forest attribute: an
# adopt that just recorded a root must be visible to the next call,
# and this is the same loader the Gardener itself uses.
recorded = str(Gardener(vine, hooks=[]).config.get("source_root") or "").strip()
gardener = Gardener(vine, hooks=[])
recorded = str(gardener.config.get("source_root") or "").strip()
# J.8.5 (v0.83): what this forest converts, through the SAME
# discovery the next ingest runs — its command hooks, the extensions
# it enables, the describer the host injects, plugins, built-ins.
# The console derives its picker from this; it keeps no list.
describer = vision.image_converter(registry.binding(forest, "vision"))
formats = supported_formats(
gardener.config, extra=([describer] if describer else None),
registry=_ext_registry_for(forest))
return {
"source": recorded or None,
"formats": formats,
# Both halves matter and they fail for different reasons: no
# source at all, or a source this Station may no longer read
# because the roots were narrowed under it.
Expand Down Expand Up @@ -4392,7 +4402,11 @@ async def admin_models(request: Request) -> JSONResponse:
forest = request.query_params.get("forest")
if not is_admin(principal, forest, mask=mask_of(request)):
return _envelope(VineError(E_FORBIDDEN, "requires 'admin' on that forest"), 403)
return JSONResponse({"bindings": registry.bindings(forest)})
# L.6 rule 1 (v0.83): the bindable roles ride beside the
# bindings, so a console renders its cards from the host and
# never from a list of its own.
return JSONResponse({"bindings": registry.bindings(forest),
"roles": _bindable_roles()})
body = _json_object(await request.json())
forest = body.get("forest")
if not is_admin(principal, forest, mask=mask_of(request)):
Expand All @@ -4401,10 +4415,15 @@ async def admin_models(request: Request) -> JSONResponse:
if body.get("remove"):
registry.unbind_model(forest, body.get("role"))
else:
# L.6 rule 1: a role a loaded extension registers is
# bindable here — the v0.82 route refused it while the
# Extensions console listed it.
registry.bind_model(forest, body.get("role"), body.get("provider"),
body.get("model"),
max_tokens=body.get("max_tokens", 600),
reasoning=body.get("reasoning", "off"))
reasoning=body.get("reasoning", "off"),
extra_roles={r["role"] for r in
_bindable_roles()})
except (ValueError, KeyError, TypeError) as e:
return _envelope(VineError(E_SCHEMA, str(e)))
# Which model reads a forest's material is a property of that forest,
Expand Down Expand Up @@ -5830,6 +5849,45 @@ def _ext_store():
from monkeyllm.extensions.store import Store
return Store()

def _bindable_roles() -> list[dict]:
"""L.6 rule 1 (v0.83): one list for the bind route and the console."""
from monkeyllm_station.extensions import roles_catalogue
try:
store = _ext_store()
except Exception:
store = None
return roles_catalogue(ext_runtime, store)

def _enable_extension(principal: str, forest: str, ext_id: str,
enabled: bool) -> dict:
"""L.7 rule 2 — ONE write path for enablement, whether the operator
toggled it or asked for it in the same act as the install (L.11
rule 3). The forest's own `_meta/` first, the registry index second;
raises VineError with the reason when it cannot."""
from monkeyllm.extensions import forestcfg
root = _lock_root(forest)
if root is None:
raise VineError(E_NOT_FOUND, f"no such forest: {forest}")
_ext_store().require(ext_id)
if enabled:
forestcfg.enable(root, ext_id)
else:
forestcfg.disable(root, ext_id)
registry.ext_enablement.set(ext_id, forest, enabled, principal)
# The runtime caches which extensions a forest enables; the write
# just changed that answer.
ext_runtime.invalidate(forest)
registry.record(principal=principal, forest=forest,
primitive="extension.enable" if enabled
else "extension.disable",
args={"ext": ext_id}, result="ok")
return {"forest": forest, "ext": ext_id, "enabled": enabled,
# L.8 (v0.83): stated, and TRUE — an extension this process
# loaded serves the forest as soon as the write lands; one
# it did not load cannot serve it until a restart.
"restart_required": ext_id not in ext_runtime.loaded_ids(),
"enabled_now": forestcfg.enabled(root)}

def _ext_deployment_gate(principal: str, request: Request):
"""L.7 rule 1 — installing puts third-party code in this process.

Expand Down Expand Up @@ -5871,8 +5929,15 @@ def _ext_summary(record, store, *, detailed: bool) -> dict:
out["contributes"].append("panel")
out["registers_roles"] = [r.model_dump()
for r in manifest.models.registers]
# J.8.5: the file extensions its converters claim — what the
# ingest console will start accepting where it is enabled.
out["formats"] = sorted({ext for c in manifest.contributes.converters
for ext in c.extensions})
except VineError as exc:
out["broken"] = exc.message
# L.8 (v0.83): "installed" and "installed and serving" are two
# states, and an operator must be able to tell them apart.
out["loaded"] = record.id in ext_runtime.loaded_ids()
if detailed:
out.update({"source": record.source, "kind": record.kind,
"revision": record.revision,
Expand Down Expand Up @@ -6090,6 +6155,52 @@ async def admin_extensions(request: Request) -> JSONResponse:
args={"id": result["id"],
"tier": result["tier"]},
result="ok")
# L.8 (v0.83): an id this process never loaded has nothing
# to unload, so it is activated now; an update keeps the
# restart, because that is the case Python cannot honour.
# The response SAYS which happened.
activation = (ext_runtime.activate(result["id"])
if action == "install"
else {"activated": False,
"reason": "an update replaces code "
"that is loaded"})
result["activated"] = bool(activation.get("activated"))
result["restart_required"] = not result["activated"]
if activation.get("reason"):
result["activation_note"] = activation["reason"]
# L.11 rule 4: what it now takes and what it now offers,
# so the console can say what to do next.
try:
from monkeyllm.extensions.loader import read_manifest
manifest = read_manifest(store.tree(result["id"]))
result["formats"] = sorted({
ext for c in manifest.contributes.converters
for ext in c.extensions})
result["registers_roles"] = [
r.model_dump() for r in manifest.models.registers]
except Exception:
pass
# L.11 rule 3: enabling MAY ride the install — on the forest
# the console is open on, for a principal who administers
# it. A refusal here refuses the enablement, never the
# install that already landed.
enable_on = str(body.get("enable_on") or "")
if enable_on:
if not is_admin(principal, enable_on,
mask=mask_of(request)):
result["enable_error"] = (
f"requires the 'admin' capability on "
f"{enable_on!r}")
else:
try:
outcome = _enable_extension(
principal, enable_on, result["id"], True)
result["enabled_on"] = [enable_on]
result["restart_required"] = (
result["restart_required"]
or outcome["restart_required"])
except VineError as exc:
result["enable_error"] = exc.message
return JSONResponse(result, status_code=201)
if action == "remove":
from monkeyllm.extensions.installer import (dialect_impact,
Expand Down Expand Up @@ -6238,32 +6349,12 @@ async def admin_extension_enablement(request: Request) -> JSONResponse:
return _envelope(VineError(
E_READONLY, "this Station is read-only"), 403)
ext_id = str(body.get("ext") or "")
store = _ext_store()
try:
store.require(ext_id)
except VineError as exc:
return _envelope(exc)
enabled = body.get("enabled", True) is not False
try:
if enabled:
forestcfg.enable(root, ext_id)
else:
forestcfg.disable(root, ext_id)
return JSONResponse(_enable_extension(principal, forest, ext_id,
enabled))
except VineError as exc:
return _envelope(exc)
registry.ext_enablement.set(ext_id, forest, enabled, principal)
# The runtime caches which extensions a forest enables; the write
# just changed that answer.
ext_runtime.invalidate(forest)
registry.record(principal=principal, forest=forest,
primitive="extension.enable" if enabled
else "extension.disable",
args={"ext": ext_id}, result="ok")
return JSONResponse({"forest": forest, "ext": ext_id,
"enabled": enabled,
# L.8: stated, never implied.
"restart_required": True,
"enabled_now": forestcfg.enabled(root)})

async def admin_extension_config(request: Request) -> JSONResponse:
"""L.7 rule 4 — the host's custody, and a secret that never returns.
Expand Down
Loading
Loading