diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a5e70e9 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,189 @@ +# Agent Guidelines for Motr Enclave + +This document provides guidelines for AI coding agents working in this repository. + +## Project Overview + +Motr Enclave is an Extism WebAssembly plugin written in Go, compiled with Go 1.25+ for the `wasip1` target. It provides encrypted key storage for the Nebula wallet with an embedded SQLite database. + +## Build Commands + +```bash +# Build WASM plugin (primary build command) +make build + +# Build with debug symbols +make build-debug + +# Build optimized (requires wasm-opt) +make build-opt + +# Generate SQLC database code +make generate + +# Full rebuild +make clean && make generate && make build +``` + +## Test Commands + +```bash +# Run all tests +make test + +# Run tests with coverage +make test-cover + +# Run a single test +go test -v -run TestFunctionName ./... + +# Run tests in a specific package +go test -v ./db/... + +# Test the compiled plugin with Extism CLI +make test-plugin +``` + +## Lint and Format + +```bash +# Run all linters +make lint + +# Format code +make fmt + +# Run go vet +make vet + +# Run all checks (fmt, vet, lint, test) +make verify +``` + +## Code Style Guidelines + +### Imports + +Order imports in three groups separated by blank lines: +1. Standard library +2. External dependencies +3. Internal packages + +```go +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/extism/go-pdk" + + "enclave/db" +) +``` + +### Naming Conventions + +| Element | Convention | Example | +|---------|------------|---------| +| Exported types | PascalCase | `GenerateInput`, `QueryOutput` | +| Unexported types | PascalCase | `FilterParams` (internal use ok) | +| Struct fields | PascalCase | `CredentialID`, `ChainID` | +| JSON tags | snake_case | `json:"credential_id"` | +| Functions | camelCase for private, PascalCase for exported | `parseFilter`, `Generate` | +| Constants | PascalCase or ALL_CAPS | `MaxRetries` | +| Variables | camelCase | `credentialBytes`, `didDoc` | + +### Type Definitions + +Define input/output types for each exported function: + +```go +type GenerateInput struct { + Credential string `json:"credential"` +} + +type GenerateOutput struct { + DID string `json:"did"` + Database []byte `json:"database"` +} +``` + +### Extism Plugin Functions + +Exported functions must: +1. Use `//go:wasmexport` directive +2. Return `int32` (0 = success, 1 = error) +3. Use `pdk.InputJSON()` for input parsing +4. Use `pdk.OutputJSON()` for output +5. Use `pdk.SetError()` for error reporting +6. Log with `pdk.Log()` + +```go +//go:wasmexport functionName +func functionName() int32 { + pdk.Log(pdk.LogInfo, "functionName: starting") + + var input InputType + if err := pdk.InputJSON(&input); err != nil { + pdk.SetError(fmt.Errorf("functionName: failed to parse input: %w", err)) + return 1 + } + + // ... implementation ... + + if err := pdk.OutputJSON(output); err != nil { + pdk.SetError(fmt.Errorf("functionName: failed to output: %w", err)) + return 1 + } + return 0 +} +``` + +### Error Handling + +1. Wrap errors with context using `fmt.Errorf("context: %w", err)` +2. Prefix error messages with function name +3. Return early on errors +4. Use `errors.New()` for static errors + +```go +if err != nil { + pdk.SetError(fmt.Errorf("generate: failed to initialize: %w", err)) + return 1 +} +``` + +### SQL Queries (SQLC) + +- Schema in `db/schema.sql` +- Queries in `db/query.sql` +- Use SQLC annotations: `-- name: QueryName :one|:many|:exec` +- JSON columns use `json.RawMessage` type override + +### Comments + +- Only add comments for complex logic, security implications, or TODOs +- Avoid obvious comments +- Use `// TODO:` for planned implementations + +## File Structure + +``` +motr-enclave/ +├── main.go # Plugin entry point, exported functions +├── db/ +│ ├── schema.sql # Database schema +│ ├── query.sql # SQLC query definitions +│ └── *.go # Generated SQLC code +├── sqlc.yaml # SQLC configuration +├── Makefile # Build commands +└── go.mod # Go module +``` + +## Dependencies + +Install with `make deps`: +- `sqlc` - Database code generation +- `golangci-lint` - Linting +- `gofumpt` - Formatting +- Extism CLI - Plugin testing diff --git a/build/enclave.wasm b/build/enclave.wasm new file mode 100755 index 0000000..e27fb2f Binary files /dev/null and b/build/enclave.wasm differ diff --git a/example/index.html b/example/index.html new file mode 100644 index 0000000..70b9eb3 --- /dev/null +++ b/example/index.html @@ -0,0 +1,168 @@ + + + + + + Motr Enclave Test + + + +

Motr Enclave Plugin Test

+ +
+

Plugin Status

+
Loading plugin...
+ +
+ +
+

generate()

+

Initialize database with WebAuthn credential

+ + + +
+
+ +
+

load()

+

Load database from serialized buffer

+ + + + +
+
+ +
+

exec()

+

Execute action with GitHub-style filter syntax

+ + + + +
+ + + + + +
+
+
+ +
+

query()

+

Resolve DID to document with resources

+ + + +
+
+ +
+

Console Log

+ +
+
+ + + + diff --git a/example/test.js b/example/test.js new file mode 100644 index 0000000..91126c3 --- /dev/null +++ b/example/test.js @@ -0,0 +1,198 @@ +import createPlugin from 'https://esm.sh/@extism/extism@1.0.0/dist/browser/mod.js'; + +let plugin = null; +let generatedDatabase = null; + +function log(message, type = 'info') { + const consoleLog = document.getElementById('consoleLog'); + const timestamp = new Date().toISOString().split('T')[1].split('.')[0]; + const prefix = type === 'error' ? '[ERROR]' : type === 'success' ? '[OK]' : '[INFO]'; + consoleLog.textContent += `${timestamp} ${prefix} ${message}\n`; + consoleLog.scrollTop = consoleLog.scrollHeight; + console[type === 'error' ? 'error' : 'log'](message); +} + +function setStatus(message, type) { + const status = document.getElementById('status'); + status.textContent = message; + status.className = `status ${type}`; +} + +function formatOutput(data) { + try { + if (typeof data === 'string') { + const parsed = JSON.parse(data); + return JSON.stringify(parsed, null, 2); + } + return JSON.stringify(data, null, 2); + } catch { + return String(data); + } +} + +async function loadPlugin() { + setStatus('Loading plugin...', 'loading'); + log('Loading enclave.wasm...'); + + try { + plugin = await createPlugin('../build/enclave.wasm', { + useWasi: true, + logger: console + }); + + setStatus('Plugin loaded successfully', 'success'); + log('Plugin loaded successfully', 'success'); + } catch (error) { + setStatus(`Failed to load plugin: ${error.message}`, 'error'); + log(`Failed to load plugin: ${error.message}`, 'error'); + } +} + +async function testGenerate() { + if (!plugin) { + log('Plugin not loaded', 'error'); + return; + } + + const output = document.getElementById('generateOutput'); + const credential = document.getElementById('credentialInput').value; + + log(`Calling generate() with credential: ${credential.substring(0, 20)}...`); + output.textContent = 'Running...'; + + try { + const input = JSON.stringify({ credential }); + const result = await plugin.call('generate', input); + const data = result.json(); + + output.textContent = formatOutput(data); + log(`generate() completed. DID: ${data.did}`, 'success'); + + if (data.database) { + generatedDatabase = data.database; + log('Database buffer stored for load() test'); + } + } catch (error) { + output.textContent = `Error: ${error.message}`; + log(`generate() failed: ${error.message}`, 'error'); + } +} + +async function testLoad() { + if (!plugin) { + log('Plugin not loaded', 'error'); + return; + } + + const output = document.getElementById('loadOutput'); + const databaseInput = document.getElementById('databaseInput').value; + + if (!databaseInput) { + output.textContent = 'Error: Database buffer is required'; + log('load() requires database buffer', 'error'); + return; + } + + log('Calling load()...'); + output.textContent = 'Running...'; + + try { + const input = JSON.stringify({ + database: Array.from(atob(databaseInput), c => c.charCodeAt(0)) + }); + const result = await plugin.call('load', input); + const data = result.json(); + + output.textContent = formatOutput(data); + log(`load() completed. Success: ${data.success}`, data.success ? 'success' : 'error'); + } catch (error) { + output.textContent = `Error: ${error.message}`; + log(`load() failed: ${error.message}`, 'error'); + } +} + +function useGeneratedDb() { + if (generatedDatabase) { + const base64 = btoa(String.fromCharCode(...generatedDatabase)); + document.getElementById('databaseInput').value = base64; + log('Populated database input with generated database'); + } else { + log('No generated database available. Run generate() first.', 'error'); + } +} + +async function testExec() { + if (!plugin) { + log('Plugin not loaded', 'error'); + return; + } + + const output = document.getElementById('execOutput'); + const filter = document.getElementById('filterInput').value; + const token = document.getElementById('tokenInput').value; + + if (!filter) { + output.textContent = 'Error: Filter is required'; + log('exec() requires filter', 'error'); + return; + } + + log(`Calling exec() with filter: ${filter}`); + output.textContent = 'Running...'; + + try { + const input = JSON.stringify({ filter, token: token || undefined }); + const result = await plugin.call('exec', input); + const data = result.json(); + + output.textContent = formatOutput(data); + log(`exec() completed. Success: ${data.success}`, data.success ? 'success' : 'error'); + } catch (error) { + output.textContent = `Error: ${error.message}`; + log(`exec() failed: ${error.message}`, 'error'); + } +} + +function setFilter(filter) { + document.getElementById('filterInput').value = filter; +} + +async function testQuery() { + if (!plugin) { + log('Plugin not loaded', 'error'); + return; + } + + const output = document.getElementById('queryOutput'); + const did = document.getElementById('didInput').value; + + log(`Calling query() with DID: ${did || '(current)'}`); + output.textContent = 'Running...'; + + try { + const input = JSON.stringify({ did: did || '' }); + const result = await plugin.call('query', input); + const data = result.json(); + + output.textContent = formatOutput(data); + log(`query() completed. DID: ${data.did}`, 'success'); + } catch (error) { + output.textContent = `Error: ${error.message}`; + log(`query() failed: ${error.message}`, 'error'); + } +} + +function clearLog() { + document.getElementById('consoleLog').textContent = ''; +} + +window.loadPlugin = loadPlugin; +window.testGenerate = testGenerate; +window.testLoad = testLoad; +window.useGeneratedDb = useGeneratedDb; +window.testExec = testExec; +window.setFilter = setFilter; +window.testQuery = testQuery; +window.clearLog = clearLog; + +loadPlugin();