Headless app framework for ENC protocol. Spec-driven development — JSON config to any platform.
Headless app framework for the ENC protocol. Spec-driven development from JSON config to any platform.
JSON config (agent writes)
|
+-- validate() --> instant correctness checks (JS, no Lean needed)
|
+-- loadConfig() --> runtime config for EncApp
|
+-- toLean() --> complete Lean 4 file (proven by native_decide)
|
+-- EncApp --> headless runtime --> platform renderer
|
+-- TUI (ink)
+-- Web (React/Svelte/vanilla)
+-- Native (React Native/SwiftUI/Jetpack)
+-- Desktop (Electron/Tauri)One config. Validated instantly. Proven formally. Runs anywhere.
enc-app interprets an AppModel -- the declarative spec for an ENC app -- and provides a headless runtime. It handles state management, command execution, page navigation, RBAC enforcement, and reactive subscriptions. Platforms only render and capture input.
The AppModel defines:
enclave:, kv_shared:, local:, smt_rbac)The runtime enforces:
import { EncApp, Emulator, loadConfig, validate, toLean } from '@enc-protocol/enc-app'import { EncApp, Emulator } from '@enc-protocol/enc-app'
import { config } from './apps/group/config.js'
const emu = new Emulator()
emu.initFromConfig(config)
const app = new EncApp(config.model, {
emulator: emu,
enclave: { identity: emu.ownerIdentity },
})
await app.start()
app.navigate('Chat')
const state = await app.state() // { messages: [...], topic: '...' }
await app.exec('send', { content: 'hello' })
app.on('state', (s) => render(s)) // reactive updatesconst app = new EncApp(model, {
enclave: {
nodeUrl: 'https://node1.enc.dev',
enclaveId: 'abc...',
identity: { pubHex, privateKey, publicKey },
},
})
await app.start()import { EncApp } from '@enc-protocol/enc-app'
const app = new EncApp(model, {
adapter: myNativeAdapter, // implements fetch, submit, subscribe, connect, disconnect
})import { loadConfig, validate, toLean } from '@enc-protocol/enc-app'
import config from './my-app.json'
// 1. Validate (instant, no Lean compiler needed)
const result = validate(config)
// { checks: { ownerCanTerminate: true, ... }, errors: [], allPass: true }
// 2. Load into runtime format
const appConfig = loadConfig(config)
// { name, states, traits, schema, model, ... }
// 3. Transpile to Lean (deterministic, write to .lean file)
const lean = toLean(config)
// Complete Lean 4 file with types, validation theorem, codegen blocksEncApp (orchestrator)
|-- EncRouter (navigation state machine)
|-- EncProvider (local state + adapter routing)
| +-- Adapter (platform-specific)
| |-- EmulatorAdapter (testing/TUI)
| |-- NetworkAdapter (web)
| |-- NativeAdapter (future)
| +-- DesktopAdapter (future)
+-- Emulator (optional, in-memory Node with real crypto)| Module | Purpose |
|---|---|
app.js | Headless runtime -- state, commands, navigation, events |
router.js | Page navigation state machine with history |
provider.js | Data access layer routing through adapters |
adapter.js | Platform adapter interface + built-in adapters |
| Module | Purpose |
|---|---|
validate.js | 12 runtime checks mirroring Lean's Validate.lean |
config.js | JSON config -> runtime config (flatten manifest, derive roles) |
transpile.js | JSON -> Lean 4 transpiler (deterministic, pure function) |
| Module | Purpose |
|---|---|
emulator.js | In-memory enclave with real crypto, real RBAC, real SMT |
multi-user.js | Multiple users sharing one emulator |
const app = new EncApp(model, config)
await app.start() // connect + fetch initial state
app.stop() // disconnect
// State
const state = await app.state() // { fieldName: value, ... }
app.on('state', callback) // reactive updates
// Commands
const result = await app.exec('send', { content: 'hello' })
// { ok: true, event, receipt } or { ok: false, error: '...' }
// Navigation
app.navigate('Settings') // -> true/false
app.back() // -> true/false
app.currentPage // 'Chat'
app.availableTargets // ['Settings', 'Members']
app.availableCommands // [{ name, event, role, params }]
app.components // current page components
// Events
app.on('state', (s) => {}) // state changed
app.on('navigate', (e) => {}) // page changed
app.on('command', (e) => {}) // command executed
app.on('error', (e) => {}) // error occurredAny platform adapter implements:
{
fetch(field) // -> Promise<data>
submit(identity, type, content) // -> Promise<{ ok, error?, event?, receipt? }>
subscribe(events, callback) // register event listener
connect() // establish persistent connection
disconnect() // tear down
}| Check | What it verifies |
|---|---|
ownerCanTerminate | Some operator has C on Terminate |
statesReachable | Every state reachable via moves or init |
traitsManageable | Every trait has Grant/Revoke paths |
validRanks | Traits use name(rank) syntax |
completeStates | Move references only declared states |
validOperators | Rule operators are states/traits/contexts |
noEncryptedDataView | Encrypted events not read from DataView |
dataFieldsValid | Data fields use valid store types |
commandsAuthorized | Commands reference authorized events |
pagesConsistent | Components reference existing data/commands/subscriptions |
hasEntryPage | At least one page marked as entry |
initDefined | At least one init entry exists |
Agents build apps by outputting JSON configs that match manifests/schema.json. They never touch Lean.
User describes app
|
Agent outputs config.json
|
validate(json) -- instant check (milliseconds)
|
loadConfig(json) -- runtime config
|
EncApp runs it -- any platform
|
toLean(json) -- formal proof (write .lean, lake build proves it)The agent stays in JSON-land where it is reliable. Lean stays in proof-land where it is deterministic. The transpiler is a pure function -- no AI, no ambiguity.
AppModel defines:
data: [{ name, store, encrypt }] -- what exists
commands: [{ name, event, role, params }] -- what can happen
subscriptions: [{ name, source, events }] -- what to watch
pages: [{
name, entry, role,
components: [{
state: [fieldName], -- reads from data
commands: [cmdName], -- dispatches commands
subscriptions: [subName], -- listens for updates
nav: [{ trigger, target }] -- edges to other pages
}]
}]State fetch: app.state() -> collects fields from active components -> provider.fetch(field) -> adapter routes by store type -> returns data.
Command exec: app.exec(name, params) -> validates access -> adapter.submit(identity, event, params) -> emits state update.
Navigation: app.navigate(page) -> router validates -> emits navigate + state events.
| Prefix | Source | Returns |
|---|---|---|
enclave:EventType | Event log | Array of events with parsed content |
kv_shared:key | Shared KV | Last value (last-write-wins) |
kv_own:key | Per-identity KV | Last value filtered by identity |
smt_rbac | RBAC state | Array of members with roles |
local:key | Client memory | Anything (never hits network) |
ENC Protocol