Vyasa KG — ACL (viewer-mask) Engine + UI Handoff Prompt
5-min readUpdated Sep 03, 2026
Paste this whole file to a fresh LLM as the task brief.
Role & goal URL copied
You are extending the Vyasa knowledge-graph engine to enforce access control (ACL). The data layer is already authored; your job is the generator support, query-engine mask, and UI lens. The design is locked — implement it, don't redesign it. Discuss trade-offs only inside the locked model.
What Vyasa is (context) URL copied
- A KG is a datom fact-log: every line is
e=<entity> a=<attribute> v=<value> [c=<context>]. An edge is just a fact whosev=@ref. Schema, nodes, attrs, contexts all compile to this. - A pack (authored
.schema/.nodes/.attrs/.contextfiles) is compiled by a generator into a flatkg.index. A DSL query engine reads the index (nodes | where … | follow … | diff …). - The reference pack is SIFT — a 19-day project snapshot (130 nodes, 19 daily contexts).
File locations URL copied
Engine (Python, stdlib only) — /Users/yeshwanth/Code/Personal/vyasa/scripts/
gen_kg_index.py— generator: pack dir →kg.index. Parses@-sections.kg_query.py— query engine:Index(parse,fold,nodes),run()(the DSL stages).
UI / renderer (NOT yet read — read these first) — /Users/yeshwanth/Code/Personal/vyasa/
core.py,main.py,extensions_builtin/tasks/static/tasks.js— the served visualizer and the View dropdown (lenses). Find where@viewsare rendered; the ACL lens hangs off the same place.
SIFT pack — /Users/yeshwanth/Code/Divami/sift-tech/snapshot/sift.kg/
kg.schema— has the@aclblock (classes, role grants, person→role). Authored, inert.kg.attrs— has the multi-labelcls:block (each node tagged 1–3 classes).kg.nodes,day01.context … day19.context,kg.index(generated, 3184 facts).sift.md— pack README.
Run after any pack edit:
PY=/Users/yeshwanth/.venv/bin/python
$PY .agents/skills/vyasa/scripts/gen_kg_index.py <pack-dir> <pack-dir>/kg.index
$PY .agents/skills/vyasa/scripts/kg_query.py <pack-dir>/kg.index '<pipeline>'
The ACL model (locked) URL copied
- Default-deny. An untagged fact is visible to no one. Every context should carry a blanket default class so the common case isn't hand-tagged. (SIFT tags per-node instead, for now.)
- Classes & grants are themselves facts (no parallel ACL system):
e=cls_eng a=kind v=acl_class e=role_lead a=can_see v=@cls_internal e=role_lead a=can_see v=@cls_eng e=Rajesh Illuri a=role v=@role_lead - Multi-label: a node may carry several
clsfacts (e.g.d_brd= eng + internal + external). - Visibility predicate: viewer sees a fact iff
closure(can_see(viewer)) ∩ cls(fact) ≠ ∅.closure= transitive overcan_seeedges (role hierarchy falls out for free). - Mask is applied FIRST — filter the fact-set to the viewer's visible subset before any
fold/follow/incoming/diff. Filtering last leaks: traversal already crossed the boundary. Masking first makes invisible facts non-existent for the query → traversal cannot leak.
Roles in SIFT (@acl in kg.schema) URL copied
cls_external, cls_internal, cls_eng. Grants: cxo→all; biz→external+internal;
lead→internal+eng; dev→eng; ext→external. 12 people mapped to roles by full name.
What to build URL copied
- Generator (
gen_kg_index.py): parse the@aclblock → emit facts:e=<class> a=kind v=acl_class,e=<role> a=can_see v=@<class>(one per class),e=<person> a=role v=@<role>. The block syntax (already authored):classes=cls_a,cls_b,cls_cgrant <role> <class> <class> …person <Full Name> = <role>← note the=delimiter (names contain spaces).
- Query engine (
kg_query.py):- Make
clsfold as a SET (union), not latest-wins scalar — add it to the relation-like union path, or special-case it. (Today it collapses to one class — see bugs.) - Add a viewer parameter to the query/
Index. Computeclosure(can_see(viewer)). - Apply the mask as the first operation: drop every fact whose
clsset doesn't intersect the viewer's visible classes. A fact with nocls→ denied (default-deny). - Verify no leak: as
role_ext,follow/incoming/diffmust never surface an internal/eng fact, even transitively.
- Make
- UI: add a "View as <role/person>" entry to the existing View dropdown (same surface as State / Open Items / Persona). On select → set viewer → re-render the masked graph. Hidden nodes/edges are silently omitted (default-deny; no tombstone in consumption views). A flat dropdown entry is enough — no nested menu needed.
Acceptance checks URL copied
role_dev(Satya) sees onlycls_engnodes; no decisions/questions.role_ext(Rajeev) sees only the 22cls_externalnodes; provenance traversal from an external node never reaches an internal/eng fact.role_cxosees everything (3184 facts).d_brdis visible to cxo, lead (via eng+internal), biz (internal+external), dev (eng), ext (external).
Bugs / gaps encountered (real, verified) URL copied
clsfolds to one value.foldtreats non-relation attrs as scalar latest-wins, so a multi-class node collapses. Verified:nodes | where id=d_brd | select cls→cls_engonly, thoughfacts | where a=cls | where e=d_brd→ all 3. The mask must read rawclsfacts.- Entity key in fold record is
id, note.nodes | where e=<id>returns 0 rows. Usewhere id=<id>. (eonly exists on the rawfacts/diffstreams.) - DSL filters tokenize on whitespace.
where owner=Rajesh IlluriparsesIllurias a stray token → 0 rows. No quoting support. Use~(contains):where owner~Rajesh. This is why the@aclperson syntax uses=as an explicit delimiter — preserve that when parsing. @aclis currently inert. The generator skips unknown@-sections, so the authored block compiles to nothing yet. Safe, but step 1 above is what activates it.- Raw
diffis noisy (~92 changed facts between adjacent days). Not an ACL bug, but any per-viewer "what changed for me" view must filter by attribute (stage/status) after masking. - Sandbox quirk: a leading
cdin a shell command triggers a noisy directory listing in tool output. Use absolute paths / shell vars instead.
History note URL copied
A prior runbook/workflow modelling experiment on this pack was reverted (it mixed prescriptive
Layer-3 knowledge into the observed-state pool and was partly fabricated). It still exists in commit
2096014 "add snapshots" in the sift-tech repo if you need to see what was tried. Do not
reintroduce it; the pool is observed-state only.