Skill Security
Mayros enforces an 18-layer security architecture for semantic skills. Skills run in a WASM sandbox with no access to the file system, network, or process. Every graph operation is namespace-scoped, rate-limited, and audit-logged.
Security layers overview
| # | Layer | Component |
|---|---|---|
| 1 | Static scanner | 16 rules + anti-evasion preprocessing |
| 2 | Enrichment sanitizer | Unicode normalization + injection detection |
| 3 | QuickJS WASM sandbox | Isolated execution with resource limits |
| 4 | Namespace isolation | All queries forced to ${ns}: prefix |
| 5 | Tool allowlist | Intersection model across active skills |
| 6 | Permission resolver | Per-operation permission checks |
| 7 | Rate limiter | Sliding window per skill (calls/minute) |
| 8 | Query limits | Per-skill + global cap |
| 9 | Write limits | Per-sandbox session cap |
| 10 | Enrichment timeout | 2s Promise.race on skill enrichment |
| 11 | Path traversal protection | Reject .. + isPathInside() |
| 12 | Verify-then-promote | Temp extract, verify hashes, atomic promote |
| 13 | Hot-reload security | Atomic swap, manifest validation, downgrade block |
| 14 | Circuit breaker | 3-state with exponential backoff |
| 15 | Audit logging | Skill name + operation on all sandbox writes |
| 16 | No default assertion engine | skill_assert fails without declared engine |
| 17 | Per-request skill tracking | resolveCurrentSkill() round-robin |
| 18 | Integrity hashing | SHA-256 of skill directory contents |
Static scanner
The scanner (src/security/skill-scanner.ts) runs 16 rules against skill source code before any execution.
Line rules (12)
| Rule ID | Severity | Detects |
|---|---|---|
dangerous-exec | critical | exec, spawn, execSync with child_process context |
dynamic-code-execution | critical | eval(), new Function() |
crypto-mining | critical | stratum+tcp, coinhive, xmrig patterns |
suspicious-network | warn | WebSocket to non-standard ports |
semantic-unbounded-query | warn | patternQuery without limit |
semantic-unproven-assertion | warn | requireProof: false |
bracket-property-exec | critical | ["exec"], ["spawn"], ["eval"] |
dynamic-require | critical | require() with non-literal argument |
global-this-access | warn | globalThis[ bracket access |
process-env-bracket | critical | process["env"] with network context |
dynamic-import | critical | import() with non-literal argument |
Source rules (4)
| Rule ID | Severity | Detects |
|---|---|---|
potential-exfiltration | warn | readFile + fetch/http.request |
obfuscated-code | warn | Hex sequences (\x?? x6+) |
obfuscated-code | warn | Large base64 + atob/Buffer.from |
env-harvesting | critical | process.env + network send |
Anti-evasion preprocessing
Before scanning, source code is preprocessed to defeat common evasion techniques:
stripComments()— removes//and/* */while preserving string literalsjoinSplitStatements()— tracks paren balance across lines (catcheseval\n(...))countNetParens()— counts open/close parens outside string literals
A skill with any critical finding is rejected before sandbox instantiation.
Enrichment sanitizer
The sanitizer (enrichment-sanitizer.ts) prevents prompt injection via skill enrichment data.
Unicode normalization
Before pattern matching, all strings undergo:
- NFC normalization — canonical decomposition + composition
- Zero-width character stripping — removes
\u200B,\u200C,\u200D,\u2060,\uFEFF,\u00AD - Homoglyph mapping — Cyrillic and Greek lookalikes mapped to ASCII (e.g., Cyrillic
\u0410toA) - Fullwidth collapse —
U+FF01–U+FF5Emapped to ASCII0x21–0x7E
Injection patterns (11)
The sanitizer detects and blocks these injection patterns:
ignore/disregard previous instructionsyou are/act as/pretend to besystem:override/system messageexecute the following/run bashnew instructions/override instructionsimportant: you must/ignorecurl/wget/bash/evalcommandsrm -rfdo not follow the system/developerdeveloper message- HTML-like tags:
<system>,<assistant>,<developer>
Depth limits
| Limit | Value | Purpose |
|---|---|---|
MAX_DEPTH | 4 | Maximum nesting depth |
MAX_ARRAY_LENGTH | 50 | Maximum array elements |
MAX_STRING_LENGTH | 512 | Maximum string value length |
MAX_ENRICHMENT_CHARS | 4096 | Total output cap |
Output wrapping
Sanitized enrichment is wrapped in explicit tags for the LLM:
xml<skill-enrichment type="data"> { "key": "value" } </skill-enrichment>
QuickJS WASM sandbox
Skills execute inside QuickJS WASM ([email protected]) with no access to:
- File system (
fs) - Network (
net,fetch,http) - Process (
process,child_process) - Module system (
require,import) - Timers (
setTimeout,setInterval) - Workers (
Worker) - Dynamic code (
eval— harmless in WASM context)
Host functions (7 total)
Only these functions are exposed to sandboxed code:
graphClient (4 methods):
| Method | Security layers |
|---|---|
createTriple | Write limit check + namespace enforcement + audit log |
deleteTriple | Write limit check + audit log |
listTriples | Namespace enforcement on subject/predicate |
patternQuery | Namespace enforcement on subject/predicate |
logger (3 methods): info, warn, error — prefixed with [skill:name]
Resource limits
| Limit | Range | Default |
|---|---|---|
memoryLimitBytes | 1MB–256MB | 8MB |
maxStackSizeBytes | 64KB–8MB | 512KB |
executionTimeoutMs | 100–60,000 | 10,000 |
maxWritesPerSession | — | 50 |
The timeout is enforced via shouldInterruptAfterDeadline — the QuickJS runtime checks the deadline during execution and interrupts if exceeded.
Permission resolver
The PermissionResolver checks whether operations are allowed for a skill.
Operation kinds
typescripttype OperationKind = | "graph:read" | "graph:write" | "proofs:request" | "proofs:verify" | "proofs:publish" | "memory:recall" | "memory:remember";
Tool-to-permission mapping
| Tool | Required permissions |
|---|---|
skill_graph_query | graph:read |
skill_assert | graph:write |
skill_verify_assertion | graph:read, proofs:verify |
skill_request_zk_proof | proofs:request |
skill_verify_zk_proof | proofs:verify |
skill_memory_context | memory:recall |
Tool allowlist (intersection model)
When multiple skills are active, a tool is allowed only if all active skills include it in their allowedTools. This prevents a permissive skill from opening access for a restrictive one.
DEFAULT_ALLOWED_TOOLS = [
"skill_graph_query", "skill_assert", "skill_verify_assertion",
"skill_request_zk_proof", "skill_verify_zk_proof", "skill_memory_context",
"hub_search", "hub_verify", "mesh_request_knowledge"
]
- If a skill omits
allowedTools, the default restrictive list is applied ["*"]is an escape hatch for unrestricted tool access- The 6 semantic tools are always allowed when permissions match
Hot-reload security
When skills are reloaded at runtime:
- Atomic swap: new maps built in temp variables, then swapped in
- Manifest validation:
validateManifest()runs on every reload - Downgrade block: rejects reloads where
allowedToolswere removed - Diff logging:
diffManifests()logs changes toallowedTools, permissions, assertions, andmaxQueries
Related
- Semantic Skills — skill runtime and manifest format
- Cortex — AIngle Cortex knowledge graph
- Architecture — extension system overview