Install
$ agentstack add skill-itsalt-nacl-nacl-sa-roles ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
/nacl-sa-roles --- System Roles & Permissions (Graph)
Purpose
Define system roles, build the CRUD permission matrix against DomainEntities, and create BA-to-SA role mapping edges. All data lives in Neo4j -- no markdown artifacts.
Shared references: nacl-core/SKILL.md
Neo4j Tools
| Tool | Usage | |------|-------| | mcp__neo4j__read-cypher | Read-only queries (fetch BA roles, existing SystemRoles, DomainEntities) | | mcp__neo4j__write-cypher | Create/update/delete nodes and edges | | mcp__neo4j__get-schema | Introspect current graph schema |
Modes
Mode IMPORT_BA
Import BA business roles from Neo4j graph as SystemRole candidates with N:M mapping and full handoff traceability.
When: BA layer populated in Neo4j (BusinessRole nodes exist), SA role model not yet created.
Mode CREATE
Create a single new SystemRole interactively with permissions.
When: User asks to add a new system role not sourced from BA (e.g. Admin, System).
Parameter: role_name -- name of the role to create.
Mode MODIFY
Modify an existing SystemRole: change permissions, add/remove MAPPED_TO edges, update properties.
When: User asks to change permissions or properties of an existing role.
Parameter: role_name -- name of the role to modify.
Mode FULL
Build the complete role model: all roles, permission matrix, data scope rules.
When: After nacl-sa-architect and nacl-sa-domain, when building the full role model from scratch.
Workflow
+-----------------+ +-----------------+ +-----------------+ +-----------------+
| Phase 1 | | Phase 2 | | Phase 3 | | Phase 4 |
| Identify |--->| CRUD |--->| Data scope |--->| Workflow |
| roles | | permission | | constraints | | permissions |
| | | matrix | | | | |
+-----------------+ +-----------------+ +-----------------+ +-----------------+
Each phase ends with:
- Summary -- what was understood
- Confirmation -- user approval
- Graph writes -- create/update nodes and edges in Neo4j
Do not proceed to the next phase without explicit user confirmation!
Pre-check
- Verify DomainEntity nodes exist (needed for permission matrix):
// mcp__neo4j__read-cypher
MATCH (de:DomainEntity)
OPTIONAL MATCH (m:Module)-[:CONTAINS_ENTITY]->(de)
RETURN de.id AS id, de.name AS name, m.name AS module
ORDER BY m.name, de.name
If no DomainEntities found -- suggest running nacl-sa-domain first.
- Check existing SystemRole nodes:
// mcp__neo4j__read-cypher
MATCH (sr:SystemRole)
OPTIONAL MATCH (br:BusinessRole)-[:MAPPED_TO]->(sr)
OPTIONAL MATCH (sr)-[p:HAS_PERMISSION]->(de:DomainEntity)
RETURN sr.id AS id, sr.name AS name, sr.type AS type, sr.description AS description,
collect(DISTINCT br.full_name) AS mapped_from_ba,
count(DISTINCT p) AS permission_count
ORDER BY sr.id
If SystemRoles already exist -- warn about possible overwrite and suggest MODIFY mode.
- Check for available BA roles (for IMPORT_BA suggestion):
// mcp__neo4j__read-cypher
MATCH (br:BusinessRole)
WHERE NOT (br)-[:MAPPED_TO]->(:SystemRole)
RETURN count(br) AS unmapped_ba_roles
If unmapped BA roles exist -- suggest starting with IMPORT_BA mode.
Mode: IMPORT_BA
Step 1: Read BA roles from graph
Goal: Fetch all BusinessRole nodes and their process associations.
Cypher -- fetch all BA roles with context:
// mcp__neo4j__read-cypher
MATCH (br:BusinessRole)
OPTIONAL MATCH (br)-[:OWNS]->(owned:BusinessProcess)
OPTIONAL MATCH (br)-[:PARTICIPATES_IN]->(part:BusinessProcess)
OPTIONAL MATCH (br)-[:PERFORMED_BY]-(ws:WorkflowStep)
RETURN br.id AS id, br.full_name AS name, br.description AS description,
collect(DISTINCT owned.name) AS owns_processes,
collect(DISTINCT part.name) AS participates_in,
collect(DISTINCT ws.function_name) AS performs_steps
ORDER BY br.id
Cypher -- check already-mapped BA roles:
// mcp__neo4j__read-cypher
MATCH (br:BusinessRole)-[:MAPPED_TO]->(sr:SystemRole)
RETURN br.id AS ba_id, br.full_name AS ba_name,
sr.id AS sa_id, sr.name AS sa_name
If all BA roles already mapped -- tell the user. Suggest CREATE or MODIFY mode.
Cypher -- fetch DomainEntities for permission context:
// mcp__neo4j__read-cypher
MATCH (de:DomainEntity)
OPTIONAL MATCH (m:Module)-[:CONTAINS_ENTITY]->(de)
RETURN de.id AS id, de.name AS name, m.name AS module
ORDER BY m.name, de.name
Cypher -- fetch UseCases for actor assignment context:
// mcp__neo4j__read-cypher
MATCH (uc:UseCase)
OPTIONAL MATCH (m:Module)-[:CONTAINS_UC]->(uc)
RETURN uc.id AS id, uc.name AS name, m.name AS module
ORDER BY m.name, uc.id
Step 2: Propose N:M mapping BA -> SA
Goal: For each BusinessRole, propose one or more SystemRoles. Multiple BA roles may merge into one SA role. One BA role may split into multiple SA roles.
Mapping rules:
| Situation | Action | Example | |-----------|--------|---------| | 1 BA role = 1 SA role | Direct mapping (1:1) | "Менеджер" -> "Manager" | | 1 BA role -> N SA roles | Split by access level (1:N) | "Руководитель" -> "TeamLead", "DepartmentHead" | | N BA roles -> 1 SA role | Merge similar system rights (N:1) | "Кладовщик", "Приёмщик" -> "WarehouseOperator" | | No BA source | System-only role (new) | -- -> "Admin", "System" |
Always consider adding these system-only roles if not present in BA:
- Admin -- full system access, user management, configuration
- System -- automated/background processes (if the system has scheduled tasks, integrations, etc.)
Present to user:
**BA -> SA Role Mapping**
Found {N} BA business roles -> proposing {M} SA system roles:
| # | BA Role(s) | SA Role (candidate) | Type | Mapping |
|---|-----------|--------------------|----|---------|
| 1 | {BA Role 1} | {SA Role 1} | internal | 1:1 |
| 2 | {BA Role 2}, {BA Role 3} | {SA Role 2} | internal | N:1 (merge) |
| 3 | {BA Role 4} | {SA Role 3a}, {SA Role 3b} | external | 1:N (split) |
| 4 | -- | Admin | internal | New SA role |
| 5 | -- | System | internal | New SA role |
Questions:
1. Is the proposed BA -> SA mapping correct?
2. Should any roles be split or merged differently?
3. Which additional system-only roles are needed?
4. Role types: internal (employee) or external (client, partner)?
Step 3: Create SystemRole nodes and MAPPED_TO edges
Goal: Write SystemRole nodes and BA->SA handoff edges to Neo4j.
Cypher -- get next SystemRole ID:
// mcp__neo4j__read-cypher
MATCH (sr:SystemRole)
WITH max(toInteger(replace(sr.id, 'SR-', ''))) AS maxNum
RETURN 'SR-' + apoc.text.lpad(toString(coalesce(maxNum, 0) + 1), 2, '0') AS nextId
If apoc is not available, compute the next ID in the agent and pass it as a parameter.
Cypher -- create SystemRole:
// mcp__neo4j__write-cypher
MERGE (sr:SystemRole {id: $id})
SET sr.name = $name,
sr.type = $type,
sr.description = $description,
sr.responsibilities = $responsibilities,
sr.system_only = $systemOnly,
sr.status = 'draft',
sr.created = datetime()
Parameters:
$id-- formatSR-NN(e.g. "SR-01")$name-- English PascalCase (e.g. "OrderManager")$type-- "internal" or "external"$description-- Russian description of the role$responsibilities-- semicolon-separated list of main responsibilities (Russian)$systemOnly-- boolean.truefor infrastructure-only roles that never appear in BA mapping (e.g.,System,Worker,Cron);falsefor end-user-facing roles (Teacher, Student, OrderManager, etc.). Used by validator XL8.2 to skip BA-mapping requirements on infrastructure roles. If forgotten,nacl-sa-flags backfill-alldefaults it tofalseand the user can flip it later via/nacl-sa-flags set-system-only --role true.
Cypher -- create MAPPED_TO handoff edge (BusinessRole -> SystemRole):
// mcp__neo4j__write-cypher
MATCH (br:BusinessRole {id: $baRoleId}), (sr:SystemRole {id: $saRoleId})
MERGE (br)-[:MAPPED_TO]->(sr)
Execute for each BA-to-SA pair in the confirmed mapping table.
After all roles created, verify:
// mcp__neo4j__read-cypher
MATCH (sr:SystemRole)
OPTIONAL MATCH (br:BusinessRole)-[:MAPPED_TO]->(sr)
RETURN sr.id AS id, sr.name AS name, sr.type AS type,
collect(DISTINCT {ba_id: br.id, ba_name: br.full_name}) AS mapped_from
ORDER BY sr.id
Present summary:
**Step 3 complete: SystemRole nodes created**
| SA Role | ID | Type | Mapped from BA |
|---------|-----|------|---------------|
| {name} | {id} | {type} | {ba_names or "New SA role"} |
Total: {N} SystemRole nodes, {M} MAPPED_TO edges
After confirmation -> proceed to Phase 2 (CRUD matrix).
Phase 1: Identify Roles (FULL / CREATE modes)
Goal: Determine all system roles.
Mode FULL
If BA roles exist in the graph, start with IMPORT_BA Steps 1-3 to pre-populate. Then continue here to review and add system-only roles.
If no BA roles -- gather roles from user.
Cypher -- load context for role proposal:
// mcp__neo4j__read-cypher
MATCH (m:Module)
OPTIONAL MATCH (m)-[:CONTAINS_UC]->(uc:UseCase)
OPTIONAL MATCH (m)-[:CONTAINS_ENTITY]->(de:DomainEntity)
RETURN m.name AS module,
collect(DISTINCT uc.name) AS use_cases,
collect(DISTINCT de.name) AS entities
ORDER BY m.name
Present to user:
Based on the system architecture and domain model I propose the following roles:
1. **{Role 1}** ({type: internal/external}) -- {description, what the role does}
2. **{Role 2}** ({type}) -- {description}
3. **Admin** (internal) -- full system access, configuration, user management
Questions:
1. Are the proposed roles correct?
2. Any additional roles?
3. Can one user hold multiple roles?
4. Is there a role hierarchy (permission inheritance)?
Mode CREATE
Ask user for role details:
**New SystemRole: {name}**
1. English name (PascalCase): {suggestion}
2. Type: internal / external?
3. Description: ?
4. Main responsibilities (3-5 items): ?
5. Map to existing BA role? (optional)
Confirm?
Create the SystemRole node using the Cypher from IMPORT_BA Step 3.
Rules
- Minimum 2 roles (otherwise no role model needed)
- Maximum 7 roles (otherwise reconsider decomposition)
- Each role must differ from others by at least one module or access level
- Guest / unauthenticated user -- separate role if public access exists
- Admin -- always present if the system requires configuration
Phase 2: CRUD Permission Matrix
Goal: Define access for each role to each DomainEntity (Create, Read, Update, Delete).
Step 2.1: Fetch all DomainEntities
// mcp__neo4j__read-cypher
MATCH (de:DomainEntity)
OPTIONAL MATCH (m:Module)-[:CONTAINS_ENTITY]->(de)
RETURN de.id AS id, de.name AS name, m.name AS module
ORDER BY m.name, de.name
Step 2.2: Fetch all SystemRoles
// mcp__neo4j__read-cypher
MATCH (sr:SystemRole)
RETURN sr.id AS id, sr.name AS name, sr.type AS type
ORDER BY sr.id
Step 2.3: Propose CRUD matrix
Build a table: rows = DomainEntities, columns = SystemRoles. For each cell determine: C/R/U/D or combination.
Use BA context to inform proposals:
- If a BA role OWNS a process that PRODUCES an entity -> C (create)
- If a BA role PARTICIPATES_IN a process that READS an entity -> R (read)
- If a BA role performs steps that MODIFY an entity -> U (update)
- Admin gets CRUD on everything
Cypher -- BA role-entity interaction (for informed proposals):
// mcp__neo4j__read-cypher
MATCH (br:BusinessRole)-[:PERFORMED_BY]-(ws:WorkflowStep)
MATCH (ws)-[rel:READS|PRODUCES|MODIFIES]->(be:BusinessEntity)
MATCH (be)-[:REALIZED_AS]->(de:DomainEntity)
OPTIONAL MATCH (br)-[:MAPPED_TO]->(sr:SystemRole)
RETURN sr.name AS system_role,
de.name AS domain_entity,
collect(DISTINCT type(rel)) AS interactions
ORDER BY sr.name, de.name
Present to user:
**CRUD Permission Matrix**
| DomainEntity | {Role 1} | {Role 2} | {Role 3} | Admin |
|-------------|----------|----------|----------|-------|
| {Entity 1} | CR | R | -- | CRUD |
| {Entity 2} | CRUD | R | R(own) | CRUD |
| {Entity 3} | -- | CRU | R | CRUD |
Legend:
- C = Create, R = Read, U = Update, D = Delete
- R(own) = read only own records
- CRU(own) = create, read, update only own records
- -- = no access
Questions:
1. Are the access levels correct?
2. Any entities that should be restricted further?
3. Should we distinguish "list view" vs "detail view"?
Step 2.4: Write HAS_PERMISSION edges to Neo4j
Cypher -- create HAS_PERMISSION edge:
// mcp__neo4j__write-cypher
MATCH (sr:SystemRole {id: $roleId}), (de:DomainEntity {id: $entityId})
MERGE (sr)-[p:HAS_PERMISSION]->(de)
SET p.crud = $crud,
p.scope = $scope
Parameters:
$roleId-- SystemRole.id (e.g. "SR-01")$entityId-- DomainEntity.id (e.g. "DE-Order")$crud-- combination of C/R/U/D letters (e.g. "CRUD", "CR", "R")$scope-- data visibility scope: "all", "own", "department", "assigned" (default "all")
Execute for each confirmed (role, entity) pair from the matrix.
CRUD notation rules:
"CRUD"-- full access"CR"-- create and read"R"-- read only"RU"-- read and update""(empty string) or no edge -- no access (do not create HAS_PERMISSION edge for no-access pairs)
Step 2.5: Verify permission matrix
// mcp__neo4j__read-cypher
MATCH (sr:SystemRole)-[p:HAS_PERMISSION]->(de:DomainEntity)
RETURN sr.name AS role, de.name AS entity, p.crud AS crud, p.scope AS scope
ORDER BY sr.name, de.name
Present summary:
**Phase 2 complete: CRUD matrix written to graph**
| DomainEntity | {Role 1} | {Role 2} | ... | Admin |
|-------------|----------|----------|-----|-------|
| {Entity} | {crud} | {crud} | ... | CRUD |
Total: {N} HAS_PERMISSION edges
Phase 3: Data Scope Constraints
Goal: Define which records each role can see (row-level security).
Questions
For each role, let's define data visibility:
1. **{Role 1}**: Sees {all data / only own / only department}?
2. **{Role 2}**: Sees {all data / only own}?
Questions:
1. Should data be filtered by ownership?
2. Is there an organizational hierarchy (departments, branches)?
3. Should a manager see other managers' data?
Scope Values
| Scope | Description | Filter Rule | |-------|-------------|-------------| | all | Sees all records | No filter | | own | Only records where user = creator/owner | entity.owner_id = current_user.id | | department | Records from user's department | entity.department_id = current_user.department_id | | assigned | Records explicitly assigned to user | entity.assignee_id = current_user.id |
Update HAS_PERMISSION scope
For each (role, entity) pair that needs scope restriction:
// mcp__neo4j__write-cypher
MATCH (sr:SystemRole {id: $roleId})-[p:HAS_PERMISSION]->(de:DomainEntity {id: $entityId})
SET p.scope = $scope,
p.scope_rule = $scopeRule
Parameters:
$scope-- "all", "own", "department", "assigned"$scopeRule-- human-readable filter rule (e.g. "order.clientid = currentuser.id")
Verify data scope
// mcp__neo4j__read-cypher
MATCH (sr:SystemRole)-[p:HAS_PERMISSION]->(de:DomainEntity)
WHERE p.scope <> 'all'
RETURN sr.name AS role, de.name AS entity, p.crud AS crud,
p.scope AS scope, p.scope_rule AS scope_rule
ORDER BY sr.name, de.name
Present summary:
**Phase 3 complete: Data scope constraints**
| Role | Entity | CRUD | Scope | Rule |
|------|--------|------|-------|------|
| {role} | {entity} | {crud} | {scope}
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [ITSalt](https://github.com/ITSalt)
- **Source:** [ITSalt/NaCl](https://github.com/ITSalt/NaCl)
- **License:** MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.