docs(AGENTS): add agent guidelines for Motr Enclave
This commit is contained in:
189
AGENTS.md
Normal file
189
AGENTS.md
Normal file
@@ -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
|
||||
BIN
build/enclave.wasm
Executable file
BIN
build/enclave.wasm
Executable file
Binary file not shown.
168
example/index.html
Normal file
168
example/index.html
Normal file
@@ -0,0 +1,168 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Motr Enclave Test</title>
|
||||
<style>
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
h1 {
|
||||
color: #333;
|
||||
}
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
}
|
||||
.card h2 {
|
||||
margin-top: 0;
|
||||
color: #555;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
font-weight: 500;
|
||||
color: #666;
|
||||
}
|
||||
input, textarea {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
button {
|
||||
background: #4a90d9;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
margin-right: 10px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
button:hover {
|
||||
background: #357abd;
|
||||
}
|
||||
button:disabled {
|
||||
background: #ccc;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.output {
|
||||
background: #1e1e1e;
|
||||
color: #d4d4d4;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 13px;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.status {
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.status.success {
|
||||
background: #d4edda;
|
||||
color: #155724;
|
||||
}
|
||||
.status.error {
|
||||
background: #f8d7da;
|
||||
color: #721c24;
|
||||
}
|
||||
.status.loading {
|
||||
background: #fff3cd;
|
||||
color: #856404;
|
||||
}
|
||||
.btn-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Motr Enclave Plugin Test</h1>
|
||||
|
||||
<div class="card">
|
||||
<h2>Plugin Status</h2>
|
||||
<div id="status" class="status loading">Loading plugin...</div>
|
||||
<button id="loadPluginBtn" onclick="loadPlugin()">Reload Plugin</button>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>generate()</h2>
|
||||
<p>Initialize database with WebAuthn credential</p>
|
||||
<label for="credentialInput">Credential (Base64):</label>
|
||||
<input type="text" id="credentialInput" value="dGVzdC1jcmVkZW50aWFsLWRhdGEtZm9yLXRlc3Rpbmc=" placeholder="Base64-encoded PublicKeyCredential">
|
||||
<button onclick="testGenerate()">Run generate()</button>
|
||||
<div id="generateOutput" class="output"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>load()</h2>
|
||||
<p>Load database from serialized buffer</p>
|
||||
<label for="databaseInput">Database Buffer (Base64):</label>
|
||||
<input type="text" id="databaseInput" placeholder="Base64-encoded database buffer">
|
||||
<button onclick="testLoad()">Run load()</button>
|
||||
<button onclick="useGeneratedDb()">Use Generated DB</button>
|
||||
<div id="loadOutput" class="output"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>exec()</h2>
|
||||
<p>Execute action with GitHub-style filter syntax</p>
|
||||
<label for="filterInput">Filter:</label>
|
||||
<input type="text" id="filterInput" value="resource:accounts action:list" placeholder="resource:accounts action:sign subject:did:sonr:abc">
|
||||
<label for="tokenInput">UCAN Token (optional):</label>
|
||||
<input type="text" id="tokenInput" placeholder="UCAN token for authorization">
|
||||
<div class="btn-group">
|
||||
<button onclick="testExec()">Run exec()</button>
|
||||
<button onclick="setFilter('resource:accounts action:list')">List Accounts</button>
|
||||
<button onclick="setFilter('resource:credentials action:list')">List Credentials</button>
|
||||
<button onclick="setFilter('resource:sessions action:list')">List Sessions</button>
|
||||
<button onclick="setFilter('resource:accounts action:sign')">Sign</button>
|
||||
</div>
|
||||
<div id="execOutput" class="output"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>query()</h2>
|
||||
<p>Resolve DID to document with resources</p>
|
||||
<label for="didInput">DID:</label>
|
||||
<input type="text" id="didInput" placeholder="did:sonr:abc123 (leave empty for current DID)">
|
||||
<button onclick="testQuery()">Run query()</button>
|
||||
<div id="queryOutput" class="output"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Console Log</h2>
|
||||
<button onclick="clearLog()">Clear</button>
|
||||
<div id="consoleLog" class="output"></div>
|
||||
</div>
|
||||
|
||||
<script type="module" src="test.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
198
example/test.js
Normal file
198
example/test.js
Normal file
@@ -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();
|
||||
Reference in New Issue
Block a user