Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
faaba01
refactor(knowledge): extract generic CUE compiler core and projection…
ljahier Jul 16, 2026
5219ad1
feat(knowledge): add optional CUE metadata contract with explicit and…
ljahier Jul 16, 2026
1c22269
feat(runtime): add transport-independent knowledge runtime and route …
ljahier Jul 16, 2026
73285eb
feat(catalog): expose schema-aware domains, relations, and evaluation…
ljahier Jul 16, 2026
d946b63
feat(query): add bounded catalog-validated query API for agent-safe d…
ljahier Jul 16, 2026
3149780
chore(license): add MPL-2.0 headers to knowledge package source files
ljahier Jul 16, 2026
97e6fde
feat(eval): execute named CUE evaluations with validated JSON input o…
ljahier Jul 16, 2026
7ca9dd2
feat(knowledge): expose catalog/domains/query/eval runtime over HTTP …
ljahier Jul 18, 2026
16172a0
ci(backend): add GitHub Actions workflow for gofmt, build, and tests
ljahier Jul 19, 2026
88d6093
fix(knowledge): reject package selectors escaping the module root in …
ljahier Jul 19, 2026
bacd953
feat(examples): add service-catalog demo CUE module
ljahier Jul 19, 2026
84f1a1c
feat(assets): embed cue schema, demo module, and web UI; pin CI to so…
ljahier Jul 19, 2026
a4e2502
feat(server): extract shared HTTP serving loop with graceful shutdown
ljahier Jul 19, 2026
8e4a563
feat(projects): add Seed to create a project from an embedded module …
ljahier Jul 19, 2026
47bd8ba
feat(session): resolve the current project server-side via /session e…
ljahier Jul 19, 2026
4d62048
feat(cli): add serve, projects, and use commands with home-based modu…
ljahier Jul 19, 2026
64939c0
feat(frontend): bootstrap current project from server session instead…
ljahier Jul 19, 2026
278c536
ci: build cueto release binaries
ljahier Jul 19, 2026
53af16b
docs: rewrite README around cueto serve and drop the Makefile
ljahier Jul 19, 2026
a1a7ef7
feat(home): add cueto home root with config and project selection state
ljahier Jul 19, 2026
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
50 changes: 50 additions & 0 deletions .github/workflows/backend-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
name: backend-ci

on:
push:
branches: [main]
paths:
- backend/**
- .github/workflows/backend-ci.yml
pull_request:
paths:
- backend/**
- .github/workflows/backend-ci.yml

jobs:
test:
Comment thread
mendral-app[bot] marked this conversation as resolved.
Comment on lines +20 to +21

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (P3): No explicit permissions: block; the workflow inherits the repository default token scope. Adding a least-privilege block limits blast radius if a dependency or step is compromised.

Suggested change
Suggested change
jobs:
test:
jobs:
test:
runs-on: ubuntu-latest
permissions:
contents: read
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/backend-ci.yml, line 20:

<issue>
No explicit `permissions:` block; the workflow inherits the repository default token scope. Adding a least-privilege block limits blast radius if a dependency or step is compromised.
</issue>

runs-on: ubuntu-latest
Comment thread
mendral-app[bot] marked this conversation as resolved.
Comment on lines +21 to +22

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (P3): Adding an explicit permissions block limits the GITHUB_TOKEN scope, reducing blast radius if a step is compromised.

Suggested change
Suggested change
test:
runs-on: ubuntu-latest
test:
runs-on: ubuntu-latest
permissions:
contents: read
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At .github/workflows/backend-ci.yml, line 15:

<issue>
Adding an explicit `permissions` block limits the GITHUB_TOKEN scope, reducing blast radius if a step is compromised.
</issue>

defaults:
run:
working-directory: backend
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0
with:
go-version-file: backend/go.mod
cache-dependency-path: backend/go.sum

- name: gofmt check
run: |
unformatted="$(gofmt -l .)"
if [ -n "$unformatted" ]; then
echo "gofmt needed on:"
echo "$unformatted"
exit 1
fi

- name: build
run: go build ./...

- name: install gotestsum
run: go install gotest.tools/gotestsum@v1.13.0

- name: test
run: gotestsum --junitfile junit.xml --format testname -- ./...

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: junit
path: backend/junit.xml
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@

# Project store: registry and per-project versions (runtime data, default DATA_DIR).
/data/

# go build output for the CLI binary (e.g. `go build ./cmd/cueto`).
/cueto

# gotestsum JUnit report (backend-ci.yml).
/backend/junit.xml
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ The same property makes cueto a deterministic retrieval surface for agents. A qu
- **Workflow design**. The same model is edited two ways, a visual canvas and CUE code, kept in sync through a source map, then evaluated, validated, formatted, and saved to real files on disk in the user's own project, with git as the only history.
- **Knowledge model**. The schema separates rendering fields (`type`, `shape`, colors) from a free-form `data` payload, so the nodes you draw carry domain facts you can query.
- **Queryability**. A REPL pane with CUE stdlib introspection and autocompletion evaluates any expression against the live model in the editor.
- **Agent-safe retrieval**. Named domains, bounded Go-side queries, and schema-validated evaluations expose the same compiled value without ever handing an agent arbitrary CUE. See [Knowledge](#knowledge).
- **Observability**. Evaluation returns structured diagnostics with source positions and host paths scrubbed, plus provenance and hints, rather than opaque errors.
- **Production trade-offs**. Untrusted CUE is evaluated in-process under body-size, output-size, per-request deadline, and concurrency bounds, behind explicit server timeouts and graceful shutdown.

Expand Down Expand Up @@ -127,6 +128,44 @@ diagram: d.#Diagram & {

</details>

## Knowledge

The REPL answers with a CUE expression, which is expressive but not something to hand an agent: an agent should not be trusted with arbitrary CUE against your module. The knowledge runtime is a fixed set of named operations instead. It requires no import and no extra vocabulary: the same registry and evaluation shapes that drive inference and the REPL are enough. Add one plain `evaluations` field next to the `people` data above:

```cue
evaluations: isTraveler: {
description: "Check whether a person travels through time"
input: {personId: string}
result: {traveler: people[input.personId].role == "traveler"}
}
```

`cueto catalog` (or `GET /projects/:id/knowledge/catalog`) discovers `people` as a domain and `isTraveler` as a named evaluation, with their fields, types, and relations, so an agent can plan a call without ever reading the module's CUE:

```
$ cueto catalog -C cue
{
"domains": [{"name": "people", "kind": "registry", "fields": {"role": {"type": "string", "required": true}, ...}}],
"evaluations": [{"name": "isTraveler", "description": "Check whether a person travels through time", ...}]
}
```

`cueto query` (or `POST /projects/:id/knowledge/query`) runs a bounded, schema-checked filter, never a CUE expression:

```
$ echo '{"domain":"people","select":["name"],"where":[{"field":"role","operator":"eq","value":"traveler"}]}' | cueto query - -C cue
{"result": [{"id": "marty", "name": "Marty McFly"}], "count": 1}
```

`cueto eval` (or `POST /projects/:id/knowledge/eval/isTraveler`) runs one named, schema-validated evaluation against a JSON input:

```
$ echo '{"personId":"marty"}' | cueto eval isTraveler --input - -C cue
{"status": "success", "result": {"traveler": true}, "evaluation": "isTraveler", "revision": "..."}
```

Every answer comes from the same compiled value the canvas renders and the REPL queries, so there is no separate index to drift out of sync. `describe`, `get`, `provenance`, and `health` round out the same operation set (see [How it works](#how-it-works) above). Today only the catalog is wired into the app itself, in the Knowledge panel; the rest are CLI and HTTP only, for CI and agents.

## Authoring

The canvas and the CUE editor stay in sync through a source map, so a change in one appears in the other. Canvas edits are spliced back into CUE text through `/rewrite`, and `/format` normalizes the result with `cue fmt`, so the code and the picture never disagree.
Expand All @@ -142,7 +181,7 @@ flowchart LR
end

subgraph be["backend/ (Go + gin)"]
api["/config /cue/meta /format /rewrite /projects (list, create)\nper project: /projects/:id/{eval,repl,vet,tree,save,file,history}"]
api["/config /cue/meta /format /rewrite /projects (list, create)\nper project: /projects/:id/{eval,repl,vet,tree,save,file,history}\nknowledge: /projects/:id/knowledge/{catalog,domains,query,eval,provenance,health}"]
eval["CUE evaluator (bounded, in-process)"]
projectsdir[("projects root (each child: git repo + CUE module, git = history)")]
end
Expand Down Expand Up @@ -173,6 +212,7 @@ The CUE evaluator is a pure, adapter-independent core. It takes a prepared file
6. `/vet` validates every package in the module for validity, catching dangling references and schema and closedness violations, and returns structured diagnostics. It never requires concreteness, so an incomplete-but-valid module vets clean while `/eval` gates the rendered view. `make check` runs `cue vet ./...` plus `cueto vet` and `cueto check`, so an invalid committed diagram, or a broken file or URI reference, fails CI.
7. Persistence is git. The server is pointed at a **projects root**, and each child directory is a git repository with its own CUE module. `GET /projects` lists them and `POST /projects` creates one by git-initializing a new directory, scaffolding a minimal vocabulary-free module, and making one initial commit, the only time cueto ever writes git state. Every module-touching operation is scoped to a project, namely `/projects/:id/eval`, `/vet`, `/repl`, `/tree`, `/save`, `/file`, `/history`, and `DELETE /projects/:id/file`.
8. `/projects/:id/save` validates the buffer against the whole module and writes the real file on disk under a path guard, refusing a save when the file changed on disk since it was loaded and never staging, committing, or otherwise mutating git state. `/projects/:id/history` and `/projects/:id/file` read the git log and file blobs read-only to feed the history panel. cueto is not a version store, and git is the only history.
9. The knowledge runtime (`internal/knowledge`) generalizes the same discovery beyond diagrams: `/projects/:id/knowledge/catalog` lists every domain (declared or structurally inferred, same registry detector as inference) and every named `evaluations` entry; `domains/:domain` and `domains/:domain/:key` describe one domain and fetch one record; `query` runs a bounded, Go-side filter against a domain, never arbitrary CUE, so it stays safe for an agent to call; `eval/:name` runs one named, schema-validated evaluation against a JSON input overlay; `provenance` and `health` report where facts are declared and whether the module is valid. See [Knowledge](#knowledge) above. The `cueto catalog|describe|get|query|eval` CLI subcommands run the identical operations outside the server. Today only the catalog is wired into the app (the Knowledge panel lists domains and evaluations); query, eval, provenance, and health are CLI and HTTP only.

## Run locally

Expand Down
182 changes: 179 additions & 3 deletions backend/cmd/cueto/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (

"github.com/stratorys/cueto/backend/internal/diag"
"github.com/stratorys/cueto/backend/internal/evaluation"
"github.com/stratorys/cueto/backend/internal/knowledge"
)

// CLI evaluation bounds. Generous next to the server's per-request caps: a CI run is
Expand All @@ -58,6 +59,16 @@ func main() {
err = runCheck(args)
case "graph":
err = runGraph(args)
case "catalog":
err = runCatalog(args)
case "describe":
err = runDescribe(args)
case "get":
err = runGet(args)
case "query":
err = runQuery(args)
case "eval":
err = runEval(args)
case "-h", "--help", "help":
usage()
return
Expand All @@ -79,6 +90,11 @@ usage:
cueto vet -C <dir> validate the whole module (Layer 1, pure CUE)
cueto check -C <dir> run @file/@uri graph checks (Layer 2)
cueto graph -C <dir> [-view v] print the discovered/inferred diagram as JSON
cueto catalog -C <dir> print the knowledge catalog as JSON
cueto describe <domain> -C <dir> describe one catalog domain
cueto get <domain> <id> -C <dir> print one domain record
cueto query <query.json> -C <dir> run a safe knowledge query
cueto eval <name> --input <json> -C <dir> run a named evaluation

flags:
-C module root directory (contains cue.mod); default "."
Expand All @@ -87,6 +103,165 @@ flags:
`)
}

func runtimeFor(moduleDir, cueDir string) (*knowledge.CueRuntime, knowledge.ProjectRef, error) {
engine, src, err := setup(moduleDir, cueDir, "")
if err != nil {
return nil, knowledge.ProjectRef{}, err
}
return knowledge.NewRuntime(knowledge.New(engine)), knowledge.ProjectRef{ModuleDir: src.Dir}, nil
}

func commandFlags(name string, args []string) (*flag.FlagSet, *string, *string, error) {
fs := flag.NewFlagSet(name, flag.ContinueOnError)
dir := fs.String("C", ".", "module root directory")
cueDir := fs.String("cue", "../cue", "cueto schema directory")
if err := fs.Parse(normalizeFlags(args)); err != nil {
return nil, nil, nil, err
}
return fs, dir, cueDir, nil
}

// normalizeFlags permits the documented `command positional -C dir` form even
// though Go's flag package otherwise stops parsing at the first positional arg.
func normalizeFlags(args []string) []string {
flags, positional := []string{}, []string{}
for i := 0; i < len(args); i++ {
if args[i] == "-C" || args[i] == "-cue" || args[i] == "--input" {
flags = append(flags, args[i])
if i+1 < len(args) {
i++
flags = append(flags, args[i])
}
continue
}
positional = append(positional, args[i])
}
return append(flags, positional...)
}

func printJSON(value any) error {
out, err := json.MarshalIndent(value, "", " ")
if err == nil {
fmt.Println(string(out))
}
return err
}

func runCatalog(args []string) error {
_, dir, cueDir, err := commandFlags("catalog", args)
if err != nil {
return err
}
runtime, project, err := runtimeFor(*dir, *cueDir)
if err != nil {
return err
}
result, err := runtime.Catalog(context.Background(), project)
if err != nil {
return err
}
return printJSON(result)
}

func runDescribe(args []string) error {
fs, dir, cueDir, err := commandFlags("describe", args)
if err != nil {
return err
}
if fs.NArg() != 1 {
return errors.New("usage: cueto describe <domain> -C <dir>")
}
runtime, project, err := runtimeFor(*dir, *cueDir)
if err != nil {
return err
}
result, err := runtime.Describe(context.Background(), project, fs.Arg(0))
if err != nil {
return err
}
return printJSON(result)
}

func runGet(args []string) error {
fs, dir, cueDir, err := commandFlags("get", args)
if err != nil {
return err
}
if fs.NArg() != 2 {
return errors.New("usage: cueto get <domain> <id> -C <dir>")
}
runtime, project, err := runtimeFor(*dir, *cueDir)
if err != nil {
return err
}
result, err := runtime.Get(context.Background(), project, fs.Arg(0), fs.Arg(1))
if err != nil {
return err
}
fmt.Println(string(result))
return nil
}

func readJSONArg(name string) ([]byte, error) {
if name == "-" {
return os.ReadFile("/dev/stdin")
}
return os.ReadFile(name)
}

func runQuery(args []string) error {
fs, dir, cueDir, err := commandFlags("query", args)
if err != nil {
return err
}
if fs.NArg() != 1 {
return errors.New("usage: cueto query <query.json> -C <dir>")
}
raw, err := readJSONArg(fs.Arg(0))
if err != nil {
return err
}
var query knowledge.Query
if err := json.Unmarshal(raw, &query); err != nil {
return err
}
runtime, project, err := runtimeFor(*dir, *cueDir)
if err != nil {
return err
}
result, err := runtime.Query(context.Background(), project, query)
if err != nil {
return err
}
return printJSON(result)
}

func runEval(args []string) error {
fs := flag.NewFlagSet("eval", flag.ContinueOnError)
dir := fs.String("C", ".", "module root directory")
cueDir := fs.String("cue", "../cue", "cueto schema directory")
input := fs.String("input", "", "input JSON file or - for stdin")
if err := fs.Parse(normalizeFlags(args)); err != nil {
return err
}
if fs.NArg() != 1 || *input == "" {
return errors.New("usage: cueto eval <name> --input <json> -C <dir>")
}
raw, err := readJSONArg(*input)
if err != nil {
return err
}
runtime, project, err := runtimeFor(*dir, *cueDir)
if err != nil {
return err
}
result, err := runtime.Eval(context.Background(), project, knowledge.EvalRequest{Evaluation: fs.Arg(0), Input: raw})
if err != nil {
return err
}
return printJSON(result)
}

// runVet validates the whole module and exits nonzero on any diagnostic. It never
// gates concreteness: an incomplete-but-valid module vets clean.
func runVet(args []string) error {
Expand All @@ -100,12 +275,13 @@ func runVet(args []string) error {
if err != nil {
return err
}
diags, err := engine.Vet(context.Background(), src)
runtime := knowledge.NewRuntime(knowledge.New(engine))
health, err := runtime.Health(context.Background(), knowledge.ProjectRef{ModuleDir: src.Dir, Package: src.Package})
if err != nil {
return err
}
if len(diags) > 0 {
return errors.New(formatDiags("module is not valid:", diags))
if !health.Valid {
return errors.New(formatDiags("module is not valid:", health.Diagnostics))
}
fmt.Println("OK: module is valid.")
return nil
Expand Down
Loading