ikigai-dev-server 0.4.0

A standalone, linkage-gated ikigai server for development tooling: git/gh/cargo + graphs + the browse family (persistent explanations/annotations), exec and llm rate-limited, over IPC.
docs.rs failed to build ikigai-dev-server-0.4.0
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.

ikigai-dev-server

A standalone, linkage-gated ikigai server for development tooling. Its Cargo.toml is the module manifest: the binary composes a curated space and links only what it serves.

ikigai-dev [socket] [flags]      # default: ~/.ikigai/dev.sock
ikigai --connect <socket>        # drive it from the REPL
ikigai-dev --help                # flags + the config-file grammar

What it serves

resource from
urn:system:exec, urn:repo:* ikigai-repo — git/gh/cargo as capability-gated resources
urn:rdf:* ikigai-rdf — graph union/diff/transrept
urn:sparql:* ikigai-sparql — query
urn:repo:{repo}:tree/file/state/hash/explain/…, urn:iki:annotation:* ikigai-browse — the browse family, when configured (see below)
urn:llm:* ikigai-llm — the explain seam's derivation engine, mounted only alongside browse

Run it in a project directory and it serves that project's git state (source urn:repo:status, :log, :branch), or pass dir= for another repo.

The browse family (opt-in)

This server owns the browse store on a machine (decision of record): the persistent explanation/annotation archive is RocksDB under an exclusive lock, so ONE process holds it and everyone else prefer-mounts this server's socket — which is why the default socket is the stable ~/.ikigai/dev.sock rather than something under $TMPDIR that churns across reboots.

Configure it in ~/.config/ikigai/dev.toml (flags override the file; there is deliberately no environment-variable channel):

socket = "~/.ikigai/dev.sock"                # this IS the default

# One line per root; the URN repo name is the directory's basename.
browse.root = "~/git-personal/ikigai-core"
browse.root = "~/git-personal/ikigai-cli"

# Optional once a root exists:
browse.store = "~/.ikigai/browse-store"      # this IS the default
browse.file_model = "coder"                  # urn:llm:coder:ask
browse.dir_model = "ask"                     # urn:llm:ask
browse.file_max_tokens = 400
browse.dir_max_tokens = 600
browse.review_model = "coder"                # review pass (urn:repo:{repo}:review:{path})
browse.review_max_tokens = 800               # findings carry quotes — raise for big files/PRs
browse.pr_model = "coder"                    # pull-request explain (the PR family)
browse.pr_max_tokens = 600
browse.review_model_label = "qwen3:30b"      # operator overrides folded into version tags;
browse.pr_model_label = "qwen3:30b"          # unset, the true model id resolves at derive time
browse.allow_model = "big"                   # ALSO selectable by an explain `provider=`
browse.allow_model = "ollama"                # (repeatable; same spellings; default: none)

browse.allow_model is the only key here that grants reach rather than tuning it. Browse's explain takes provider={iri} — derive THIS explanation against a named backend — and the option menu beside the explain button offers exactly the set this host accepts: the two configured tiers (file_model, dir_model) plus whatever is allow-listed. Anything else is Denied, never a silent fall back. The list is the operator's because explain declares one capability (urn:cap:net:*) and a capability cannot vary by argument value — it says "may derive", not "may derive against the metered vendor" — so a request argument must not be able to spend on the operator's behalf. Unset, the menu offers the two tiers alone and this server behaves exactly as it did before the key existed. Configuring the review or PR provider does not widen the set: those derive other actions, and granting explain authority as a side effect of an unrelated line is the surprise this key exists to avoid.

No browse.root ⇒ no browse family at all (and no urn:llm:*) — the original curated surface, unchanged. A browse.* tuning without a root fails loud at startup: half-configured must never come up looking healthy. With roots, the store opens (or fails loud — never a silent in-memory archive), the bundled vocabulary loads, and urn:sparql:* binds over the same store, so one query joins ik:Explanation and oa:Annotation rows live.

Every other process on the machine reaches the family through this socket (in the main host's ~/.config/ikigai/config.toml):

mount = "prefer urn:repo:=~/.ikigai/dev.sock"
mount = "prefer urn:iki:annotation=~/.ikigai/dev.sock"

⚠ The annotation line carries no trailing colon, deliberately. Mounts match by plain string prefix, and urn:iki:annotation covers the slug family (urn:iki:annotation:{id}) and the bare urn:iki:annotation a Sink mints under — the only write path the browse overlay has. Written urn:iki:annotation:=, reads keep working and every new annotation silently fails to route.

Upgrading a pre-0.3.0 store

ikigai-browse 0.3.0 moved the annotation namespace from urn:annotation: to urn:iki:annotation:. A store written before that upgrade does not migrate itself, and the failure is silent: browse's listing paths (list_annotations, list_annotations_for_target, included_for_ids) strip_prefix the stored subject IRI, so an old-prefix annotation is skipped rather than erroring — the panel comes back empty and nothing says why. Review passes lose their minted findings the same way, through the prov:generated objects that name them.

Count what a store holds before upgrading (this query runs over the store this process owns, so ask it through the socket rather than opening the RocksDB directory yourself — the lock is exclusive):

ikigai --connect ~/.ikigai/dev.sock --plain -c \
  'source urn:sparql:select query="SELECT (COUNT(*) AS ?c) WHERE { ?s ?p ?o . FILTER(STRSTARTS(STR(?s), \"urn:annotation:\")) }"'

A non-zero count means a one-time rename is owed, in three positions that must all move together: annotation subjects, their :selector:quote / :selector:position children, and every IRI object naming one (oa:hasSelector, prov:generated). That is an operator step with this server stopped — the store takes an exclusive lock, so nothing can rewrite it while the socket is up.

The LLM provider registry is the shared ~/.config/ikigai/llm.json (absent ⇒ a local Ollama default; present-but-unparseable ⇒ loud).

Why linkage-gating

ikigai mcp --grant dev on the omnibus binary config-gates — it hides the calendar tools, but their code is still linked and reachable in-process. This binary doesn't compile them in at all: EventKit and the calendar are not present, so a flaw in either cannot be reached. The security bound is the dependency list, not a runtime check.

ikigai-llm is linked — deliberately, and only mounted when browse is configured. The explain seam derives through urn:llm:*, and ikigai-llm is an outbound HTTP client to local inference servers — not the EventKit/TCC class of platform authority the gating posture exists to exclude. The rejected alternative (mounting urn:llm: from the main host) would couple every fresh explanation to the main host's uptime — the dev seam would degrade with the very process it exists to stand apart from.

On top of that, the expensive seams are wrapped in a RateLimit: 30 subprocess spawns/min on exec, 120 reads/min on urn:repo:, and 30 asks/min on urn:llm: (derivations are expensive; the limit governs explain's internal asks too, since subrequests resolve back through the overlay). Pure local graph ops pass unlimited.

Conformance, and what a mounting client actually gets

tests/conformance.rs runs ikigai-conformance over the kernel this binary serves — the same ikigai_dev_server::compose that main calls, not a re-creation of it. This crate binds zero endpoints of its own, so the test is not "does my module conform" but three questions a composing host is the only place to ask:

  • Is the served catalog exactly what the manifest composes? Every pattern is pinned with the crate that owns it, so linkage-gating is a checked fact rather than a paragraph, and a dependency that grows an endpoint puts it on this socket only after someone classifies it.
  • Does every entry answer Verb::Meta with a real contract? This process is the peer on the other end of everyone else's mount line, and a peer with no Meta renderer does not fail — it degrades. ForwardingEndpoint::describe is best-effort and falls back to Description::new("remote"), so a renderer-less peer shows up in a client's catalog as one anonymous, action-less row: a whole federated kernel reading as small rather than broken. The test pins the JSON Meta face specifically, because that is the one a mount parses.
  • What does a mount cost? Golden threads are #[serde(skip)] and do not cross a wire, so a cacheable representation arrives at a mounting client with nothing that can ever cut it. Five resources this server serves have a thread to lose, and the test enumerates them: urn:repo:style, which hangs on the layered a11y.toml files, and urn:llm:config / :models / :select / :ollama:model, which are config-derived and all hang on the one registry thread urn:llm:config. Everything the browse family reads out of a working tree is Expiry::Always, live by design: this process runs no filesystem watcher, and a server must not mint a thread no host keeps. ⚠ A named thread is not a cut thread — nothing in this process cuts any of the five today, so they are still served for the life of the daemon; what a name buys is something for a client to aim at.

Findings from the composed modules (ikigai-repo, ikigai-rdf, ikigai-sparql, ikigai-browse, ikigai-llm) are recorded and attributed, not fixed here — they belong to those repos. Today: 5. It was 103 when this test was written, and every one of the 98 that went away went away in another repo: each of those five crates adopted the suite itself, and this walk picks their fixes up on a fresh resolve. That is the composed-host dividend — a host that composes five modules gets five modules' conformance for the price of stating its catalog. Zero are this crate's, and the test fails if a finding ever names an endpoint the catalog table does not.

The declared capability gates are under test

A conformance walk fires endpoints, and this composition is a development seam: a bare walk really does execute git, shell out to gh, and POST to whatever is listening on the inference port. Those endpoints are waived — but ikigai-conformance 0.2.0 waives per check, so the waivers now name the checks that fire an endpoint under root and leave ENFORCED running. ENFORCED resolves under a capability holding no grants, so on an action that declares a requires the kernel refuses before dispatch and the endpoint is never entered. Twenty-one endpoints moved from "checked for nothing but their descriptions" to "their capability gate is exercised on every CI run": urn:system:exec, the four urn:repo:* git readers, the ten gh-backed PR facades, and the outbound inference family.

A typed Denied is not by itself proof that nothing ran first, so tests/enforced.rs is the licence under that: a separate test binary that puts shims for git and gh on PATH, points the LLM registry at a loopback listener it counts connections on, and watches the annotation store's quad count and the scratch tree. Every gated action is fired under no grants and must be refused with all four witnesses unmoved — and each witness is deliberately moved first, under a capability that grants the scope, so a witness that cannot see is a red test rather than a quiet pass. The complement is pinned too: an action declaring no requires is one ENFORCED resolves for real, so a module that grows an ungated action fails that test instead of making the next walk act.

Security posture (honest)

Three real bounds today — linkage (only these modules exist), rate-limiting (exec and llm are capped), and the transport's peercred check (owner-only socket). A per-scope capability ceiling enforced server-side is the "capability-on-the-wire" work; until then the local owner is trusted and the reachable surface is bounded by what is linked and limited. Browse's own capability story (urn:cap:browse:read:*, urn:cap:annotate) comes with the crate — and as of the conformance work above, those declarations are exercised rather than merely declared: every gated action in the composed catalog is refused under no grants, with witnesses proving nothing ran first. That is what makes the ceiling worth building on. An MCP face (so an agent connects under a grant) is a fast-follow.

License

MIT OR Apache-2.0.