31 — Capability Static Analyzer
The static analyzer is what makes “secure by default” credible at submission time. It scans an app’s source code and verifies that the code only uses capabilities the manifest declares.
This document specifies the analyzer’s architecture, its rules, its limitations, and how it interacts with the rest of the trust pipeline.
What the analyzer is for
Three independent layers enforce capabilities (see 03-capabilities.md):
- macOS App Sandbox — kernel-level deny.
- Locara runtime — Rust-side capability checks at every primitive call.
- Static analyzer — pre-runtime check that the source code only attempts what the manifest declares.
Layers 1 and 2 are runtime enforcement. The analyzer is build-time — it catches mismatches before an app ships, so we don’t rely on runtime denial as the first line of defense.
The analyzer’s job:
- Find every capability-touching call in the source.
- Match against the manifest’s declarations.
- Block submission if the code uses a capability the manifest doesn’t grant.
- Warn for ambiguous cases (dead code, dynamic dispatch).
What the analyzer is NOT
- Not a sandbox. Static analysis can be evaded; runtime enforcement is the real defense.
- Not a malware detector. We catch capability mismatches, not “is this app secretly evil.”
- Not a correctness checker. It doesn’t verify the app does what it says it does.
- Not a substitute for review. Human review still happens for high-risk capability profiles.
Architecture
┌──────────────────────────────────────────────────────────┐
│ src/**/*.{ts,tsx} ← App's source │
│ │ │
│ ▼ │
│ TypeScript AST parser (TypeScript Compiler API) │
│ │ │
│ ▼ │
│ Capability-touching call detector │
│ (matches against rule catalog) │
│ │ │
│ ▼ │
│ Capability use graph │
│ (each call mapped to required capability) │
│ │ │
│ ▼ │
│ Manifest matcher │
│ (compares graph to declared capabilities) │
│ │ │
│ ▼ │
│ Diagnostics │
│ (errors, warnings, suggested manifest edits) │
└──────────────────────────────────────────────────────────┘
Implemented in TypeScript using the TypeScript Compiler API. Lives in crates/locara-cli/src/analyzer/ (Rust binary) or packages/manifest/src/analyzer/ (TypeScript) — TBD whether we run analyzer in Rust via deno_ast or in TS via tsc.
(open: A vs B) Rust-via-deno_ast for performance + single-binary install vs TypeScript-via-tsc for tighter ecosystem integration. Leaning TypeScript because the rule catalog evolves with the SDK and they should stay in sync.
Rule catalog
The analyzer has a published rule catalog. Each rule defines:
- Pattern — what the AST looks like.
- Required capability — what the manifest must declare.
- Severity — error, warning, or info.
Examples:
// Rule: locara-net-fetch
{
id: "locara-net-fetch",
pattern: { type: "CallExpression", callee: { name: "fetch" } },
requiredCapability: "net",
severity: "error",
rationale: "fetch() makes network requests; declare net capability."
}
// Rule: locara-fs-user-selected
{
id: "locara-fs-user-selected",
pattern: { type: "CallExpression", callee: { object: { name: "fs" }, property: { name: "pick" } } },
requiredCapability: "fs.user-selected",
severity: "error"
}
// Rule: locara-llm-model-not-declared
{
id: "locara-llm-model-not-declared",
pattern: {
type: "CallExpression",
callee: { object: { name: "llm" }, property: { name: "chat" } },
args: [{ properties: { model: { type: "Literal" } } }]
},
requiredCapability: "models[<extracted-from-arg>]",
severity: "error",
rationale: "Model used in llm.chat must be declared in capabilities.models[]"
}
Detected categories
Network
fetch(),XMLHttpRequest,WebSocket,EventSource- Dynamic
import()of remote URLs - Library calls known to make network requests
- → Requires
netcapability (true or scoped allowlist)
Filesystem
@locara/sdkfs.*calls@locara/sdkdb.*calls (requirefs.app-data)- Direct file reads in Tauri (rare; usually flagged as suspicious)
- Drag-drop event handlers consuming
Fileobjects (requirefs.user-selected) - → Requires
fs.user-selectedorfs.app-dataorfs.user-paths
Device APIs
navigator.mediaDevices.getUserMedia()@locara/sdkaudio.record(),image.capture()- → Requires
device.microphone/device.camera
LLM / models
- Every
llm.*,embed.*,transcribe.*,vlm.*,ocr.*call - Model argument is extracted; verified against
capabilities.models[] - → Requires the specific model in
models[](with hash)
Tools
- Every
tools.invoke('name', ...)call - Tool name extracted; verified against
capabilities.tools[] - → Requires the tool in
tools[]
IPC
- Every reference to
$SHARED/...paths - Verified against
ipc.shared_folders[]
Dynamic / suspicious patterns
eval(),Function(string),new Function(...)— flagged as ERROR (forbidden in v1).- Dynamic property access into
@locara/sdk(locara[someVar]) — flagged as warning. - String concatenation building network URLs — flagged as warning if
netis allowlist-mode.
Resolution: declared vs used
For each capability in the use graph, the analyzer compares to the manifest:
| Use graph | Manifest | Outcome |
|---|---|---|
| Used + declared | ✓ | Pass |
| Used + not declared | ✗ | ERROR — submission blocked |
| Used + declared but scope mismatch (e.g., wrong host) | ✗ | ERROR |
| Not used + declared | ⚠ | Warning — “unused capability” |
| Used in dead code branch + not declared | ⚠ | Warning — flagged for human review |
Handling library calls
The hardest case: app calls @locara/sdk function, which internally calls fetch. Whose responsibility?
Resolution rule: the SDK function declares its own capability requirements in its TypeScript JSDoc. The analyzer reads those declarations and propagates the requirement to the caller.
Example:
// In @locara/sdk/llm.ts
/**
* @locara-capability llm.chat
* @locara-capability-extract model from args.model
*/
export async function chat(args: ChatArgs): Promise<ChatResponse> {
// ...
}
When app code calls llm.chat({ model: 'qwen-3b-q4', ... }), the analyzer sees the JSDoc, extracts model = 'qwen-3b-q4', and requires that model to be in capabilities.models[].
This means the SDK is the authoritative source for capability requirements. Third-party libraries that don’t have capability annotations are treated suspiciously; the analyzer warns and human review escalates.
Handling third-party npm packages
(committed) Apps can use third-party npm packages. But the analyzer treats them as “opaque” by default:
- A package’s runtime behavior is unknown to the analyzer.
- If the app imports from a package, all capabilities the package’s code might use must be declared.
Mitigation: a curated “vetted packages” list (small) that the analyzer trusts. Adding to the vetted list requires a published capability manifest from the package author + Locara review.
For now, popular utility packages (lodash, date-fns, zod, react, etc.) are pre-vetted as capability-free.
This is conservative but safe. Apps that want fancy npm dependencies pay the trust tax.
Output
locara verify runs the analyzer and produces:
$ locara verify
✓ Manifest schema valid
✓ Identity matches local git
✗ src/lib/transcribe.ts:47:3 — capability "net" used but not declared
code:
const response = await fetch('https://api.openai.com/v1/...')
^
capability used: net (host: api.openai.com)
fix:
either remove the fetch() call,
or add to manifest:
"capabilities": {
"net": { "allowed_hosts": ["api.openai.com"] }
}
(note: doing so loses the "fully local" badge)
⚠ src/lib/utils.ts:23 — file read in dead code branch
capability would be: fs.user-selected (currently declared)
2 issues (1 error, 1 warning)
For machine consumption, --json output:
{
"errors": [
{
"rule": "locara-net-fetch",
"severity": "error",
"file": "src/lib/transcribe.ts",
"line": 47,
"column": 3,
"capability_required": { "type": "net", "host": "api.openai.com" },
"fix_hint": "..."
}
],
"warnings": [...]
}
False positive handling
Static analysis is imprecise. False positives waste developer time. Mitigations:
- Configurable rule severity in a
locara.config.json(similar to ESLint). Devs can downgrade specific rules with justification. - Inline pragma comments to suppress specific lines:
// @locara-allow: net (this fetch is fired only in dev mode) if (DEV_MODE) { await fetch('http://localhost:3000/debug'); } - Reviewer override at the registry level for cases where the analyzer is provably wrong.
But: ALL suppressions are visible in the manifest review; abuse is caught.
Performance
The analyzer must run fast enough that locara verify is sub-second for typical apps:
- Target: < 1 second for an app with 100 source files.
- Strategy: incremental analysis (only re-analyze changed files); cache rule matches.
Adversarial robustness
The analyzer is part of the trust pipeline, so we test it adversarially. A separate document in tests/security/analyzer-evasion-corpus/ maintains attempted bypasses:
- Eval-like dynamic dispatch
- String concatenation building API URLs
- Imports from packages that themselves call sensitive APIs
- Source code with deliberate mis-formatting
Each evasion attempt becomes a permanent regression test.
Limitations
The analyzer cannot catch:
- Capability use that only happens at runtime via dynamically-fetched code (which we ban via the no-
evalrule). - Capability use inside opaque third-party packages (which we mitigate via the vetted-packages list).
- Capability use through native code (Rust shouldn’t expose anything not gated by the manifest).
- Capability use through the wasm tool runtime (which has its own capability composition).
When a limitation is hit, the runtime layer catches it. Defense in depth.
Versioning
The analyzer rule catalog is versioned. When new capabilities ship, the catalog updates in lockstep with the SDK. Apps can pin a rule catalog version in their locara.json for reproducibility.
Open questions
- (open) Should the analyzer also run via LSP (live in editors) for instant feedback? Probably yes, phase 2+.
- (open) Should the analyzer enforce style (e.g., always use
@locara/sdkinstead of barefetch)? Maybe; would need community consensus. - (open) Metadata for vetted packages — where does the trust score / capability declaration live? Probably in a Locara-curated registry alongside the model + tool registries.
Cross-references
- Capability model being verified: 03-capabilities.md
- Modalities + tooling that affect what’s checked: 04-modalities.md
- SDK that the analyzer understands: 05-sdk.md
- CLI that runs the analyzer: 06-cli.md
- Testing strategy for the analyzer itself: 30-testing-strategy.md
- Trust + safety mechanics: 13-security-privacy.md, 14-trust-safety.md