Chio/Docs
LOGIN · JOIN

BuildPolicy

Editor Integrations

Use VS Code or Zed to edit Chio YAML with diagnostics, completion, hover, and go-to-definition.


What Ships

Chio includes two extensions and a shared snippet pack. The VS Code extension is a TypeScript package; the Zed extension is a Rust crate that compiles to wasm32 for the editor and to a plain rlib on host targets so the workspace build exercises its manifest contract. The snippet source is shared and each editor receives its native format.

EditorPackageIdentity
Visual Studio Codevscode-chioPublisher backbay, display name Chio, Apache-2.0.
Zedzed-chioExtension id chio, language server Chio Language Server.

Supported Documents

Both editors target the same three Chio document types, keyed by filename or suffix. A chio.yaml is a project's config file: it points at a HushSpec policy file or inline block, lists capabilities, and activates guards. Manifests describe tools and capabilities; guard DSL files describe guard pipelines.

Language IDMatchesPurpose
chio-yamlchio.yamlProject config: policy reference, capabilities, guards, manifest path.
chio-manifest*.chio-manifest.yamlTool and capability manifest.
chio-guard*.chio-guard.yamlGuard DSL.

VS Code contributes the three IDs above; Zed contributes a single Chio language scoped to the same suffixes (chio.yaml, chio-manifest.yaml, chio-guard.yaml). Both reuse a YAML tree-sitter grammar for highlighting and scope it to the Chio constructs: capability, policy, and guard keys, and urn:chio:error:* references.


Prerequisites

  • A Chio source checkout and a Rust toolchain, to build chio-lsp. Both extensions spawn that binary; neither bundles it.
  • For VS Code, Node.js and npm, to compile the TypeScript client in integrations/editors/vscode-chio.
  • For the verification steps, Python 3 to speak LSP over stdio, and chio for the doctor cross-check.
  • No running kernel, receipt store, or trust-control service. The language server stops at the document boundary.

Install

Both extensions depend on the chio-lsp binary. Build it first and put it on your PATH, or pin an absolute path in the editor settings below.

editor-lsp · buildtranscript
$ cargo build --release -p chio-lsp
    Finished `release` profile [optimized] target(s) in 0.55s
exit 0in ../../../home/connor/backbay/arc

The binary lands at target/release/chio-lsp. Cargo writes its progress to stderr, and a workspace whose dependency tree is already built prints the one line above; from cold it compiles the graph first.

Load the VS Code extension from its package directory:

bash
$ cd integrations/editors/vscode-chio
$ npm install
$ npm run compile
# Launch via "Run Extension" (F5) inside VS Code, or package it:
$ npx @vscode/vsce package

Load the Zed extension as a dev extension: open the command palette and run Zed: Install Dev Extension, then point it at integrations/editors/zed-chio.

The language server validates documents

The extensions forward edits and requests; chio-lsp owns the document cache, validation, and completion, hover, and definition catalogs. The client forwards diagnostic codes and completion results without interpreting them.

What the language server provides

chio-lsp classifies each open document by language ID or file suffix and serves four request classes from a shared document cache. It stops at the document boundary: it does not run the kernel, evaluate policy, or read arbitrary project files.

FeatureDocumentsBehavior
Diagnosticsall threeStructural required-key and shape checks over parsed YAML on didOpen / didChange, one provider per language.
Completionchio.yaml onlyCapability scopes under scopes / tools / capabilities, guard identifiers under guards, and top-level policy keys.
Hoverall threeRegistry help on urn:chio:scope:*, urn:chio:guard:*, and urn:chio:error:* identifiers.
Go to definitionall threeResolves a urn:chio:scope:* / urn:chio:guard:* reference to a linked manifest first, then the first occurrence in the open document.

The chio.yaml top-level keys the completion catalog offers are version, policy, capabilities, guards, and manifest. Of these, version and policy are required: a chio.yaml missing either surfaces a diagnostic carrying urn:chio:error:cli:doctor-chio-yaml-invalid, the same code the chio doctor probe emits, so the editor and the CLI agree on what counts as valid.


Verify the result

An editor that silently fails to start its language server looks exactly like a project with no problems. Speak LSP to the binary directly and you get an answer either way. This script sends framed JSON-RPC over stdio, the same transport the extensions use, and prints one reply. It runs chio-lsp from CHIO_LSP when that is set and from PATH otherwise, which is the resolution order both extensions use.

lsp-probe.pypython
import json, os, subprocess, sys, time

def frame(msg):
    body = json.dumps(msg).encode()
    return b"Content-Length: %d\r\n\r\n%s" % (len(body), body)

def replies(messages):
    server = os.environ.get("CHIO_LSP", "chio-lsp")
    proc = subprocess.Popen([server], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
    for msg in messages:
        proc.stdin.write(frame(msg))
        proc.stdin.flush()
        time.sleep(0.5)
    proc.stdin.close()
    raw, out = proc.stdout.read().decode(), []
    while "Content-Length: " in raw:
        head = raw.index("Content-Length: ")
        gap = raw.index("\r\n\r\n", head)
        size = int(raw[head + 16 : gap])
        out.append(json.loads(raw[gap + 4 : gap + 4 + size]))
        raw = raw[gap + 4 + size :]
    return out

OPEN = {"jsonrpc": "2.0", "id": 1, "method": "initialize",
        "params": {"processId": None, "rootUri": None, "capabilities": {}}}
READY = {"jsonrpc": "2.0", "method": "initialized", "params": {}}
BAD_CONFIG = {"jsonrpc": "2.0", "method": "textDocument/didOpen", "params": {"textDocument": {
    "uri": "file:///work/chio.yaml", "languageId": "chio-yaml", "version": 1,
    "text": "version: 1\ncapabilities: []\n"}}}

if sys.argv[1] == "handshake":
    for reply in replies([OPEN]):
        if "result" in reply:
            print(json.dumps(reply["result"], indent=2))
else:
    for reply in replies([OPEN, READY, BAD_CONFIG]):
        if reply.get("method") == "textDocument/publishDiagnostics":
            print(json.dumps(reply["params"]["diagnostics"], indent=2))

The handshake is the first check. A working server answers initialize with the four capabilities the feature table describes and identifies itself by name and version:

editor-lsp · handshaketranscript
$ python3 lsp-probe.py handshake
{
  "capabilities": {
    "completionProvider": {
      "triggerCharacters": [
        ":",
        " ",
        "-"
      ]
    },
    "definitionProvider": true,
    "hoverProvider": true,
    "textDocumentSync": 1
  },
  "serverInfo": {
    "name": "chio-lsp",
    "version": "0.1.0"
  }
}
exit 0

textDocumentSync: 1 is the LSP full-sync kind: every edit resends the whole document, which is why the server can validate from the cache without reading the file from disk. The completion trigger characters are ":", " ", and "-", so completions fire after a key and inside a list item. Nothing in the reply advertises a pull-diagnostics provider: diagnostics are pushed through textDocument/publishDiagnostics when a document opens or changes.

The second check is the refusal. Open a chio.yaml that has version but no policy and the server publishes a diagnostic for it unprompted:

editor-lsp · diagnostictranscript
$ python3 lsp-probe.py diagnostic
[
  {
    "code": "urn:chio:error:cli:doctor-chio-yaml-invalid",
    "message": "chio.yaml is missing required key `policy`.",
    "range": {
      "end": {
        "character": 0,
        "line": 0
      },
      "start": {
        "character": 0,
        "line": 0
      }
    },
    "severity": 1,
    "source": "chio-lsp"
  }
]
exit 0

That is the whole contract an editor renders: code is the registry URN, source is chio-lsp, and severity: 1 is the LSP error level. A missing required key has no token to point at, so its range collapses to the start of the document; a parse error carries the line and column the YAML parser reported.

Configuration

By default both editors resolve chio-lsp on PATH and spawn it with no extra arguments. Override the binary path or forward flags when the server lives somewhere non-standard.

VS Code

The extension contributes three settings under the chio namespace.

SettingDefaultDescription
chio.lsp.pathchio-lspPath to the server binary. Resolved on PATH when left as the default.
chio.lsp.args[]Extra arguments forwarded to the server verbatim on spawn.
chio.trace.serveroffLSP trace verbosity: off, messages, or verbose.
settings.jsonjson
{
  "chio.lsp.path": "/opt/chio/bin/chio-lsp",
  "chio.lsp.args": [],
  "chio.trace.server": "off"
}

Zed

Zed reads the standard lsp.<server>.binary block. The server id is chio-lsp. An empty or whitespace-only path falls back to the bare chio-lsp invocation, so the default PATH lookup still works.

settings.jsonjson
{
  "lsp": {
    "chio-lsp": {
      "binary": {
        "path": "/opt/chio/bin/chio-lsp",
        "arguments": ["--verbose"]
      }
    }
  }
}

Both editors let you override the binary path and launch arguments.


Snippets

The extensions include four snippets for common authoring patterns. Type a prefix and expand it; each snippet uses LSP-standard ${1:label} placeholders so both editors accept the same source.

PrefixScaffoldScope
capability-allowlistCapability allowlist with explicit scope bounds.chio-yaml, chio-manifest
scope-boundScope-bound capability fragment with TTL and tenant restriction.chio-yaml, chio-manifest
guard-pipelineGuard pipeline composing input, decision, and output stages.chio-yaml, chio-guard
manifest-skeletonMinimal manifest with policy, capabilities, and guard references.chio-manifest

Expanding manifest-skeleton drops in a complete starting manifest with the default values already filled:

app.chio-manifest.yamlyaml
version: 1
name: manifest_name
policy:
  default: deny
capabilities:
  - id: capability_id
    scope:
      - scope.action
guards:
  - ref: guard.identifier

Snippet sources are tool-neutral YAML at integrations/editors/snippets/*.snippet.yaml, validated against a JSON schema. A build task renders them into each editor's native format; the generated files carry a generated by cargo xtask snippets regen header and are checked for drift in CI.

bash
$ cargo xtask snippets regen          # regenerate native files
$ cargo xtask snippets regen --check  # CI drift check (no writes)

The check form prints one line and exits zero when the generated files match their sources. Anything else is drift. This is the same invocation the .github/workflows/spec-drift.yml job runs, so a local pass and a CI pass are the same check:

editor-lsp · snippets-checktranscript
$ cargo xtask snippets regen --check
snippets in sync (4 snippet sources)
exit 0in ../../../home/connor/backbay/arc

Diagnostics and registry codes

Every diagnostic carries a registry code in its LSP code field, formatted as urn:chio:error:<domain>:<code>, for example urn:chio:error:capability:scope-mismatch. The editors render the URN unchanged; downstream tooling matches on the code field programmatically. The source is chio-lsp. Every range is zero-width, so an editor renders a caret rather than a span: a parse error sits at the line and column the YAML parser reported, and a structural complaint with no token to blame sits at the start of the document.

One vocabulary of errors

The URN codes on editor diagnostics are the same registry codes the CLI and the runtime emit. See Schemas & Errors for the registry that backs both the diagnostic codes and their hover help text.

Failures and recovery

SymptomCause and recovery
No diagnostics, no completions, no hover, in any file.The client could not spawn the server. Run the handshake probe: if it fails too, the binary is missing from PATH. Pin an absolute path in chio.lsp.path or the Zed binary.path.
The server starts, but one file gets no diagnostics.That document classified as Other. Classification takes the reported language id first and the path suffix second, so a policy file named something other than chio.yaml, *.chio-manifest.yaml, or *.chio-guard.yaml is cached and left alone. Rename it or set the language id by hand.
Completions appear in chio.yaml but nowhere else.Working as built. Completion serves chio.yaml only; manifests and guard documents get diagnostics, hover, and go-to-definition.
A capability or guard completion list looks short.The catalogs are curated, not derived from your project. They offer a starting vocabulary, not an inventory of what you have declared.
cargo xtask snippets regen --check fails.A generated editor snippet file drifted from its source. Run the command without --check and commit what it writes; the sources are the four *.snippet.yaml files, and the editor files are outputs.

When the editor and the CLI disagree about a chio.yaml, they should not: both check the same required keys and emit the same registry code. chio doctor on the same file the diagnostic capture used:

editor-lsp · doctortranscript
$ chio doctor --skip-network | grep -E 'chio_yaml|missing:'
  [error] chio_yaml (urn:chio:error:cli:doctor-chio-yaml-invalid): ~/chio/chio.yaml is missing required keys: policy
    missing: policy
exit 1

Other editors

Any LSP-capable editor (Neovim, Helix, JetBrains, Emacs lsp-mode) can use chio-lsp without a first-party extension. Configure:

  • Binary: chio-lsp, resolved on PATH by default, pinnable to an absolute path.
  • Transport: LSP over stdio, no environment variables required.
  • Selectors: map the client to chio.yaml, *.chio-manifest.yaml, and *.chio-guard.yaml.
  • Diagnostics: read the urn:chio:error:* code from each diagnostic's code field and render it as-is.

Next steps