nebula-ai-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +24 -0
- package/package.json +69 -0
- package/src/brain/compaction.ts +131 -0
- package/src/brain/frozen-prefix.ts +320 -0
- package/src/brain/history-persist.ts +154 -0
- package/src/brain/index.ts +43 -0
- package/src/brain/openai-brain.ts +533 -0
- package/src/brain/sanitize.ts +23 -0
- package/src/brain/stub.ts +20 -0
- package/src/brain/types.ts +129 -0
- package/src/chain.ts +75 -0
- package/src/claude-plugins/discovery.ts +152 -0
- package/src/claude-plugins/index.ts +6 -0
- package/src/claude-plugins/types.ts +38 -0
- package/src/commands/index.ts +16 -0
- package/src/commands/registry.ts +255 -0
- package/src/config.ts +213 -0
- package/src/economy/index.ts +6 -0
- package/src/events/index.ts +4 -0
- package/src/events/listeners.ts +37 -0
- package/src/events/queue.ts +63 -0
- package/src/events/router.ts +42 -0
- package/src/events/types.ts +28 -0
- package/src/format.ts +12 -0
- package/src/identity/agent-card.ts +110 -0
- package/src/identity/deployments.ts +20 -0
- package/src/identity/erc8004.ts +161 -0
- package/src/identity/index.ts +29 -0
- package/src/identity/keystore-blob.ts +60 -0
- package/src/identity/receipt.ts +27 -0
- package/src/identity/stub.ts +29 -0
- package/src/identity/types.ts +20 -0
- package/src/index.ts +372 -0
- package/src/locks.ts +233 -0
- package/src/mcp/discovery.ts +150 -0
- package/src/mcp/index.ts +10 -0
- package/src/mcp/manager.ts +110 -0
- package/src/mcp/stdio-client.ts +154 -0
- package/src/mcp/types.ts +44 -0
- package/src/memory/edit.ts +53 -0
- package/src/memory/encryption.ts +88 -0
- package/src/memory/fs-util.ts +15 -0
- package/src/memory/index-file.ts +74 -0
- package/src/memory/index-sync.ts +99 -0
- package/src/memory/index.ts +58 -0
- package/src/memory/list-tool.ts +105 -0
- package/src/memory/pack-blob.ts +120 -0
- package/src/memory/pack-gather.ts +112 -0
- package/src/memory/parser.ts +20 -0
- package/src/memory/read-tool.ts +198 -0
- package/src/memory/save-tool.ts +189 -0
- package/src/memory/scan.ts +63 -0
- package/src/memory/topic.ts +32 -0
- package/src/memory/types.ts +49 -0
- package/src/migration/index.ts +6 -0
- package/src/migration/option3-crypto.ts +127 -0
- package/src/operator/index.ts +9 -0
- package/src/operator/keychain.ts +53 -0
- package/src/operator/keystore-file.ts +33 -0
- package/src/operator/privkey-base.ts +60 -0
- package/src/operator/raw-privkey.ts +39 -0
- package/src/operator/signer.ts +46 -0
- package/src/operator/walletconnect.ts +454 -0
- package/src/pairing.ts +285 -0
- package/src/paths.ts +70 -0
- package/src/permission/dangerous.ts +108 -0
- package/src/permission/env-redact.ts +54 -0
- package/src/permission/index.ts +16 -0
- package/src/permission/path-guard.ts +114 -0
- package/src/permission/service.ts +191 -0
- package/src/plugins/context.ts +225 -0
- package/src/plugins/hooks.ts +81 -0
- package/src/plugins/index.ts +24 -0
- package/src/plugins/tool-search.ts +49 -0
- package/src/public/card.ts +67 -0
- package/src/runtime/activity.ts +29 -0
- package/src/runtime/index.ts +2 -0
- package/src/runtime/runtime.ts +113 -0
- package/src/sandbox/credentials.ts +25 -0
- package/src/sandbox/docker.ts +396 -0
- package/src/sandbox/factory.ts +99 -0
- package/src/sandbox/index.ts +15 -0
- package/src/sandbox/linux.ts +141 -0
- package/src/sandbox/local.ts +19 -0
- package/src/sandbox/macos.ts +71 -0
- package/src/sandbox/seatbelt-profile.ts +139 -0
- package/src/sandbox/types.ts +129 -0
- package/src/skills/index.ts +8 -0
- package/src/skills/scanner.ts +257 -0
- package/src/skills/triggers.ts +78 -0
- package/src/skills/types.ts +37 -0
- package/src/storage/encryption.ts +87 -0
- package/src/storage/factory.ts +31 -0
- package/src/storage/index.ts +11 -0
- package/src/storage/local-stub.ts +70 -0
- package/src/storage/sqlite.ts +95 -0
- package/src/storage/types.ts +21 -0
- package/src/tools/escalation.ts +200 -0
- package/src/tools/index.ts +11 -0
- package/src/tools/registry.ts +152 -0
- package/src/tools/types.ts +65 -0
- package/src/tools/zod-helpers.ts +36 -0
- package/src/tools/zod-schema.ts +99 -0
- package/src/wallet/drain.ts +79 -0
- package/src/wallet/eoa.ts +51 -0
- package/src/wallet/index.ts +47 -0
- package/src/wallet/keystore.ts +50 -0
- package/src/wallet/operator-keystore-crypto.ts +530 -0
- package/src/wallet/operator-session.ts +344 -0
package/src/config.ts
ADDED
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* User-facing configuration shape for `nebula.config.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Example:
|
|
5
|
+
*
|
|
6
|
+
* import { defineConfig } from 'nebula-ai-core'
|
|
7
|
+
*
|
|
8
|
+
* export default defineConfig({
|
|
9
|
+
* network: 'mantle-mainnet', // or 'mantle-testnet'
|
|
10
|
+
* storage: { network: 'mantle-mainnet' },
|
|
11
|
+
* brain: { model: 'gpt-4o-mini' }, // chosen at `nebula init`
|
|
12
|
+
* plugins: ['onchain', 'system'],
|
|
13
|
+
* tools: { 'defi.*': false, 'shell.run': false },
|
|
14
|
+
* imports: { claudeCode: true },
|
|
15
|
+
* })
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
export type NebulaNetwork = 'mantle-mainnet' | 'mantle-testnet'
|
|
19
|
+
|
|
20
|
+
export type NebulaPlugin = 'onchain' | 'comms' | 'system' | 'telegram'
|
|
21
|
+
|
|
22
|
+
export type OperatorSourceKind = 'walletconnect' | 'keychain' | 'keystore-file' | 'raw-privkey'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Persisted hint about which operator source to use when commands like
|
|
26
|
+
* `nebula` (chat) and `nebula drain` need to talk to the operator wallet
|
|
27
|
+
* again. Stores enough metadata to reconstruct the signer without re-prompting
|
|
28
|
+
* the user from scratch (passphrases / QR scans still happen per-session).
|
|
29
|
+
*/
|
|
30
|
+
export interface OperatorSourceHint {
|
|
31
|
+
source: OperatorSourceKind
|
|
32
|
+
/** Only for `keychain`: the macOS Keychain service name to read. */
|
|
33
|
+
keychainService?: string
|
|
34
|
+
/** Only for `keystore-file`: absolute or `~`-prefixed path to the JSON keystore. */
|
|
35
|
+
keystorePath?: string
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface NebulaConfig {
|
|
39
|
+
identity: {
|
|
40
|
+
/** Operator wallet address that encrypts (and can recover) the agent keystore. */
|
|
41
|
+
operator: string | null
|
|
42
|
+
/** Agent EOA address (separate key, signs + pays gas). */
|
|
43
|
+
agent: string | null
|
|
44
|
+
}
|
|
45
|
+
network: NebulaNetwork
|
|
46
|
+
storage: {
|
|
47
|
+
network: NebulaNetwork
|
|
48
|
+
}
|
|
49
|
+
brain: {
|
|
50
|
+
provider: string | null
|
|
51
|
+
model: string | null
|
|
52
|
+
/** Max assistant output tokens per turn. Default 4096. */
|
|
53
|
+
maxOutputTokens?: number
|
|
54
|
+
/**
|
|
55
|
+
* Model context window. Used for auto-compaction trigger. Default
|
|
56
|
+
* 1_000_000. Override for smaller models.
|
|
57
|
+
*/
|
|
58
|
+
contextWindow?: number
|
|
59
|
+
/**
|
|
60
|
+
* Pre-flight summarize-fold of older history when the running estimate
|
|
61
|
+
* breaches `threshold * contextWindow`. Set to `null` to disable.
|
|
62
|
+
* Default: { threshold: 0.5, keepRecent: 8 }.
|
|
63
|
+
*/
|
|
64
|
+
compaction?: {
|
|
65
|
+
threshold?: number
|
|
66
|
+
keepRecent?: number
|
|
67
|
+
} | null
|
|
68
|
+
/**
|
|
69
|
+
* Persist channel histories to JSONL under
|
|
70
|
+
* `~/.nebula/agents/<id>/conversations/`. Loaded on boot, appended per
|
|
71
|
+
* turn, atomically rewritten on compaction. Default true.
|
|
72
|
+
*/
|
|
73
|
+
persistConversations?: boolean
|
|
74
|
+
}
|
|
75
|
+
plugins: NebulaPlugin[]
|
|
76
|
+
/** Glob-level tool allow/deny. Right-most match wins. */
|
|
77
|
+
tools: Record<string, boolean>
|
|
78
|
+
imports: {
|
|
79
|
+
claudeCode: boolean
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Which operator source to use when reconnecting. Optional so legacy configs
|
|
83
|
+
* still parse; commands fall back to the interactive picker when missing.
|
|
84
|
+
*/
|
|
85
|
+
operator?: OperatorSourceHint | null
|
|
86
|
+
/**
|
|
87
|
+
* Permission system. `prompt` (default) prompts on dangerous commands;
|
|
88
|
+
* `strict` always denies them; `off` is YOLO (no prompts). The `--yolo` CLI
|
|
89
|
+
* flag and `/yolo` TUI slash both flip the active service to 'off' for the
|
|
90
|
+
* current session without rewriting the file.
|
|
91
|
+
*/
|
|
92
|
+
approvals?: {
|
|
93
|
+
mode: 'strict' | 'prompt' | 'off'
|
|
94
|
+
/** Always-approved patterns (regex against `kind|command|path` signature). */
|
|
95
|
+
allowlist?: string[]
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Skills system. `disabled` is the persistent list of skill ids that should
|
|
99
|
+
* never auto-load or appear in the index.
|
|
100
|
+
*/
|
|
101
|
+
skills?: {
|
|
102
|
+
disabled?: string[]
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Operator-supplied additions to the system prompt. `append` is concatenated
|
|
106
|
+
* under a `# Operator instructions` header AFTER nebula's built-in safety +
|
|
107
|
+
* tool-use scaffolding. Can NOT replace the base prompt; use it for personal
|
|
108
|
+
* rules ("always reply in Indonesian", "prefer Bun over npm").
|
|
109
|
+
*/
|
|
110
|
+
prompt?: {
|
|
111
|
+
append?: string | null
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Multimodal vision routing. Vision limbs (vision.analyze, browser.vision)
|
|
115
|
+
* call this OpenAI-compatible provider; the brain stays on `brain.provider`.
|
|
116
|
+
* Set `null` to disable; tools then return a clear "not configured" error.
|
|
117
|
+
*/
|
|
118
|
+
vision?: {
|
|
119
|
+
provider?: string | null
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Structural sandbox for limb spawns. Defense-in-depth BENEATH the permission
|
|
123
|
+
* floor — even when `s` (allow session) or yolo grants a destructive command,
|
|
124
|
+
* the sandbox profile prevents writes outside an allowlist (agentDir +
|
|
125
|
+
* workspaceRoot + /tmp/nebula-* + /var/folders).
|
|
126
|
+
*
|
|
127
|
+
* - `none` (default): passthrough. Permission floor only.
|
|
128
|
+
* - `os`: native OS sandbox. macOS = sandbox-exec wrapper. Linux = bubblewrap.
|
|
129
|
+
* - `docker`: long-lived container per session, every spawn through `docker exec`.
|
|
130
|
+
*/
|
|
131
|
+
sandbox?: {
|
|
132
|
+
mode?: 'none' | 'os' | 'docker'
|
|
133
|
+
/**
|
|
134
|
+
* docker mode only: container image. Default `oven/bun:1`. Compatible with
|
|
135
|
+
* Docker Desktop AND Podman. Override for custom tooling.
|
|
136
|
+
*/
|
|
137
|
+
dockerImage?: string
|
|
138
|
+
/**
|
|
139
|
+
* docker mode only: bind-mount the host's workspaceRoot into the container
|
|
140
|
+
* at /workspace. Default `false` for max isolation.
|
|
141
|
+
*/
|
|
142
|
+
dockerMountWorkspace?: boolean
|
|
143
|
+
/**
|
|
144
|
+
* docker mode only: force a specific container runtime binary. Auto-detect
|
|
145
|
+
* by default (tries docker, then podman).
|
|
146
|
+
*/
|
|
147
|
+
dockerRuntimePath?: string
|
|
148
|
+
/** docker mode only: CPU cores cap (`--cpus`). Default unlimited. */
|
|
149
|
+
dockerCpu?: number
|
|
150
|
+
/** docker mode only: memory cap in MB (`--memory <N>m`). Default unlimited. */
|
|
151
|
+
dockerMemoryMb?: number
|
|
152
|
+
/**
|
|
153
|
+
* docker mode only: per-container writable-layer disk cap in MB. Linux +
|
|
154
|
+
* overlay2 with pquota only — silently dropped on macOS / podman.
|
|
155
|
+
*/
|
|
156
|
+
dockerDiskMb?: number
|
|
157
|
+
/**
|
|
158
|
+
* docker mode only: block all network access from inside the container
|
|
159
|
+
* (`--network=none`). Default false.
|
|
160
|
+
*/
|
|
161
|
+
dockerNoNetwork?: boolean
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export type NebulaConfigInput = Partial<NebulaConfig> & Pick<NebulaConfig, 'network'>
|
|
166
|
+
|
|
167
|
+
const DEFAULT_CONFIG: Omit<NebulaConfig, 'network' | 'storage'> = {
|
|
168
|
+
identity: { operator: null, agent: null },
|
|
169
|
+
brain: { provider: null, model: null },
|
|
170
|
+
plugins: ['onchain', 'system'],
|
|
171
|
+
tools: {},
|
|
172
|
+
imports: { claudeCode: true },
|
|
173
|
+
operator: null,
|
|
174
|
+
approvals: { mode: 'prompt', allowlist: [] },
|
|
175
|
+
skills: { disabled: [] },
|
|
176
|
+
prompt: { append: null },
|
|
177
|
+
vision: { provider: undefined },
|
|
178
|
+
sandbox: { mode: 'none' },
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export function defineConfig(input: NebulaConfigInput): NebulaConfig {
|
|
182
|
+
return {
|
|
183
|
+
...DEFAULT_CONFIG,
|
|
184
|
+
identity: input.identity ?? DEFAULT_CONFIG.identity,
|
|
185
|
+
network: input.network,
|
|
186
|
+
storage: input.storage ?? { network: input.network },
|
|
187
|
+
brain: input.brain ?? DEFAULT_CONFIG.brain,
|
|
188
|
+
plugins: input.plugins ?? DEFAULT_CONFIG.plugins,
|
|
189
|
+
tools: input.tools ?? DEFAULT_CONFIG.tools,
|
|
190
|
+
imports: input.imports ?? DEFAULT_CONFIG.imports,
|
|
191
|
+
operator: input.operator ?? DEFAULT_CONFIG.operator,
|
|
192
|
+
approvals: input.approvals ?? DEFAULT_CONFIG.approvals,
|
|
193
|
+
skills: input.skills ?? DEFAULT_CONFIG.skills,
|
|
194
|
+
prompt: input.prompt ?? DEFAULT_CONFIG.prompt,
|
|
195
|
+
vision: input.vision ?? DEFAULT_CONFIG.vision,
|
|
196
|
+
sandbox: input.sandbox ?? DEFAULT_CONFIG.sandbox,
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export const NETWORK_RPC: Record<NebulaNetwork, string> = {
|
|
201
|
+
'mantle-mainnet': 'https://rpc.mantle.xyz',
|
|
202
|
+
'mantle-testnet': 'https://rpc.sepolia.mantle.xyz',
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export const NETWORK_CHAIN_ID: Record<NebulaNetwork, number> = {
|
|
206
|
+
'mantle-mainnet': 5000,
|
|
207
|
+
'mantle-testnet': 5003,
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export function networkFromChainId(id: number): NebulaNetwork | null {
|
|
211
|
+
return (Object.entries(NETWORK_CHAIN_ID).find(([, cid]) => cid === id)?.[0] ??
|
|
212
|
+
null) as NebulaNetwork | null
|
|
213
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { EventQueue } from './queue'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A Listener watches some source (stdin, chain, a2a, ...) and pushes events
|
|
5
|
+
* onto the queue. Plugins contribute listeners via `registerListener()`.
|
|
6
|
+
*/
|
|
7
|
+
export interface Listener {
|
|
8
|
+
name: string
|
|
9
|
+
source: string
|
|
10
|
+
start(queue: EventQueue): Promise<void>
|
|
11
|
+
stop(): Promise<void>
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
class ListenerRegistry {
|
|
15
|
+
private registered: Listener[] = []
|
|
16
|
+
|
|
17
|
+
register(l: Listener): void {
|
|
18
|
+
if (this.registered.some(x => x.name === l.name)) {
|
|
19
|
+
throw new Error(`Listener already registered: ${l.name}`)
|
|
20
|
+
}
|
|
21
|
+
this.registered.push(l)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
list(): readonly Listener[] {
|
|
25
|
+
return this.registered
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async startAll(queue: EventQueue): Promise<void> {
|
|
29
|
+
await Promise.all(this.registered.map(l => l.start(queue)))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async stopAll(): Promise<void> {
|
|
33
|
+
await Promise.all(this.registered.map(l => l.stop()))
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const listeners = new ListenerRegistry()
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { NebulaEvent } from './types'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Minimal in-memory FIFO queue. Async-iterable so consumers `for await` over
|
|
5
|
+
* incoming events. Enqueue resolves immediately; dequeue awaits the next event.
|
|
6
|
+
*/
|
|
7
|
+
export class EventQueue {
|
|
8
|
+
private buffer: NebulaEvent[] = []
|
|
9
|
+
private waiters: Array<(ev: NebulaEvent) => void> = []
|
|
10
|
+
private closed = false
|
|
11
|
+
|
|
12
|
+
enqueue(ev: NebulaEvent): void {
|
|
13
|
+
if (this.closed) throw new Error('EventQueue closed')
|
|
14
|
+
const w = this.waiters.shift()
|
|
15
|
+
if (w) {
|
|
16
|
+
w(ev)
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
this.buffer.push(ev)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async dequeue(): Promise<NebulaEvent> {
|
|
23
|
+
const head = this.buffer.shift()
|
|
24
|
+
if (head) return head
|
|
25
|
+
if (this.closed) throw new Error('EventQueue closed')
|
|
26
|
+
return new Promise<NebulaEvent>(resolve => {
|
|
27
|
+
this.waiters.push(resolve)
|
|
28
|
+
})
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Close and wake all waiters with error. */
|
|
32
|
+
close(): void {
|
|
33
|
+
this.closed = true
|
|
34
|
+
for (const w of this.waiters) {
|
|
35
|
+
Promise.resolve().then(() => w({} as NebulaEvent))
|
|
36
|
+
}
|
|
37
|
+
this.waiters = []
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get length(): number {
|
|
41
|
+
return this.buffer.length
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get isClosed(): boolean {
|
|
45
|
+
return this.closed
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async *[Symbol.asyncIterator](): AsyncGenerator<NebulaEvent> {
|
|
49
|
+
while (!this.closed) {
|
|
50
|
+
try {
|
|
51
|
+
yield await this.dequeue()
|
|
52
|
+
} catch {
|
|
53
|
+
return
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
let counter = 0
|
|
60
|
+
export function newEventId(): string {
|
|
61
|
+
counter += 1
|
|
62
|
+
return `${Date.now().toString(36)}-${counter.toString(36)}`
|
|
63
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Brain, BrainTurn } from '../brain/types'
|
|
2
|
+
import type { ToolRegistry } from '../tools/registry'
|
|
3
|
+
import type { EventQueue } from './queue'
|
|
4
|
+
import type { NebulaEvent } from './types'
|
|
5
|
+
|
|
6
|
+
export interface RouterDeps {
|
|
7
|
+
brain: Brain
|
|
8
|
+
tools: ToolRegistry
|
|
9
|
+
onTurn?: (ev: NebulaEvent, turn: BrainTurn) => void | Promise<void>
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Pulls events from the queue, assembles a prompt via the brain, executes any
|
|
14
|
+
* returned tool_calls until the brain produces a final message, and yields
|
|
15
|
+
* the turn back via `onTurn`.
|
|
16
|
+
*/
|
|
17
|
+
export async function routeLoop(queue: EventQueue, deps: RouterDeps): Promise<void> {
|
|
18
|
+
for await (const ev of queue) {
|
|
19
|
+
if (!ev.source) continue // closed sentinel
|
|
20
|
+
await handleOne(ev, deps)
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function handleOne(ev: NebulaEvent, deps: RouterDeps): Promise<void> {
|
|
25
|
+
// Seed conversation with the triggering event (stub does echo; real brain
|
|
26
|
+
// in phase 3 will load memory, assemble frozen prefix, etc.)
|
|
27
|
+
const turn = await deps.brain.infer({ event: ev })
|
|
28
|
+
|
|
29
|
+
// Resolve any tool calls in a single iteration for MVP — phase 3 extends
|
|
30
|
+
// this to a proper multi-turn loop.
|
|
31
|
+
for (const call of turn.toolCalls ?? []) {
|
|
32
|
+
const tool = deps.tools.find(call.name)
|
|
33
|
+
if (!tool) continue
|
|
34
|
+
try {
|
|
35
|
+
await tool.handler(call.args)
|
|
36
|
+
} catch {
|
|
37
|
+
// Tool errors are surfaced in activity log by the runtime, not here.
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await deps.onTurn?.(ev, turn)
|
|
42
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/** Sources that can enqueue events into the runtime. */
|
|
2
|
+
export type EventSource =
|
|
3
|
+
| 'stdin'
|
|
4
|
+
| 'cron'
|
|
5
|
+
| 'webhook'
|
|
6
|
+
| 'a2a'
|
|
7
|
+
| 'marketplace'
|
|
8
|
+
| 'chain'
|
|
9
|
+
| 'internal'
|
|
10
|
+
| 'telegram'
|
|
11
|
+
|
|
12
|
+
export interface EventPayload {
|
|
13
|
+
/** Short human-readable label for logs/status. */
|
|
14
|
+
label: string
|
|
15
|
+
/** Arbitrary structured data. Listener-specific shape. */
|
|
16
|
+
data: unknown
|
|
17
|
+
/** Any peer address (ECIES pubkey or .0g name) that originated this event. */
|
|
18
|
+
peer?: string
|
|
19
|
+
/** Per-listener hint about which memory topics are relevant for this event. */
|
|
20
|
+
memoryHint?: string[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface NebulaEvent {
|
|
24
|
+
id: string
|
|
25
|
+
source: EventSource
|
|
26
|
+
payload: EventPayload
|
|
27
|
+
ts: number
|
|
28
|
+
}
|
package/src/format.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { formatEther } from 'viem'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Render a wei bigint as a 6-decimal Mantle string. Matches the statusline,
|
|
5
|
+
* `nebula ledger balance`, and `nebula balance` output styles. Always emits
|
|
6
|
+
* exactly 6 decimal places (zero-padded) so columns align.
|
|
7
|
+
*/
|
|
8
|
+
export function formatMnt(wei: bigint): string {
|
|
9
|
+
const raw = formatEther(wei)
|
|
10
|
+
const [whole, frac = ''] = raw.split('.')
|
|
11
|
+
return `${whole}.${frac.padEnd(6, '0').slice(0, 6)}`
|
|
12
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ERC-8004 / A2A "agent card" — the JSON identity document an agent publishes
|
|
3
|
+
* so other agents and systems can discover what it is, where to reach it, and
|
|
4
|
+
* which on-chain identity backs it. Referenced by the Identity Registry's
|
|
5
|
+
* tokenURI (see erc8004.ts).
|
|
6
|
+
*/
|
|
7
|
+
import type { Address } from 'viem'
|
|
8
|
+
import type { NebulaNetwork } from '../config'
|
|
9
|
+
import { NETWORK_CHAIN_ID } from '../config'
|
|
10
|
+
|
|
11
|
+
export interface AgentCardSkill {
|
|
12
|
+
id: string
|
|
13
|
+
name: string
|
|
14
|
+
description: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface AgentCardRegistration {
|
|
18
|
+
agentId: string
|
|
19
|
+
registry: Address
|
|
20
|
+
chainId: number
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AgentCard {
|
|
24
|
+
/** A2A protocol version this card conforms to. */
|
|
25
|
+
protocolVersion: string
|
|
26
|
+
name: string
|
|
27
|
+
description: string
|
|
28
|
+
/** Service endpoint (optional; e.g. a gateway URL). */
|
|
29
|
+
url?: string
|
|
30
|
+
version: string
|
|
31
|
+
/** The agent's operational EOA — signs + pays gas. */
|
|
32
|
+
agentAddress: Address
|
|
33
|
+
network: NebulaNetwork
|
|
34
|
+
chainId: number
|
|
35
|
+
/** What this agent is built to do well. */
|
|
36
|
+
capabilities: {
|
|
37
|
+
policyAware: boolean
|
|
38
|
+
simulation: boolean
|
|
39
|
+
approvals: boolean
|
|
40
|
+
auditable: boolean
|
|
41
|
+
}
|
|
42
|
+
skills: AgentCardSkill[]
|
|
43
|
+
/** ERC-8004 on-chain identity registrations backing this card. */
|
|
44
|
+
registrations: AgentCardRegistration[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Nebula's default skill set — the defensible treasury-operator surface. */
|
|
48
|
+
export const DEFAULT_AGENT_SKILLS: AgentCardSkill[] = [
|
|
49
|
+
{
|
|
50
|
+
id: 'treasury-ops',
|
|
51
|
+
name: 'Policy-aware treasury operations',
|
|
52
|
+
description:
|
|
53
|
+
'Transfers, swaps (Agni + Merchant Moe best-execution), wrap/unwrap, and Aave V3 lending on Mantle — every write policy-checked, simulated, and approval-gated.',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'risk-analysis',
|
|
57
|
+
name: 'Pre-trade risk + counterparty intel',
|
|
58
|
+
description:
|
|
59
|
+
'Token risk vetting (exit/liquidity/restricted-RWA) and Nansen counterparty labels before any value-moving action.',
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
id: 'yield-discovery',
|
|
63
|
+
name: 'Yield discovery',
|
|
64
|
+
description:
|
|
65
|
+
'DeFiLlama analytics: Mantle pools ranked by APY/TVL with risk + RWA flags (read-only).',
|
|
66
|
+
},
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
export function buildAgentCard(opts: {
|
|
70
|
+
name: string
|
|
71
|
+
agentAddress: Address
|
|
72
|
+
network: NebulaNetwork
|
|
73
|
+
description?: string
|
|
74
|
+
url?: string
|
|
75
|
+
version?: string
|
|
76
|
+
skills?: AgentCardSkill[]
|
|
77
|
+
registration?: { agentId: bigint; registry: Address }
|
|
78
|
+
}): AgentCard {
|
|
79
|
+
const chainId = NETWORK_CHAIN_ID[opts.network]
|
|
80
|
+
return {
|
|
81
|
+
protocolVersion: '0.3.0',
|
|
82
|
+
name: opts.name,
|
|
83
|
+
description:
|
|
84
|
+
opts.description ??
|
|
85
|
+
'A Mantle-native, policy-aware AI treasury assistant. The AI advises; deterministic code enforces the fund controls.',
|
|
86
|
+
url: opts.url,
|
|
87
|
+
version: opts.version ?? '0.1.0',
|
|
88
|
+
agentAddress: opts.agentAddress,
|
|
89
|
+
network: opts.network,
|
|
90
|
+
chainId,
|
|
91
|
+
capabilities: { policyAware: true, simulation: true, approvals: true, auditable: true },
|
|
92
|
+
skills: opts.skills ?? DEFAULT_AGENT_SKILLS,
|
|
93
|
+
registrations: opts.registration
|
|
94
|
+
? [
|
|
95
|
+
{
|
|
96
|
+
agentId: opts.registration.agentId.toString(),
|
|
97
|
+
registry: opts.registration.registry,
|
|
98
|
+
chainId,
|
|
99
|
+
},
|
|
100
|
+
]
|
|
101
|
+
: [],
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Encode a card as an on-chain `data:` URI so it can be the tokenURI without external hosting. */
|
|
106
|
+
export function cardToDataUri(card: AgentCard): string {
|
|
107
|
+
const json = JSON.stringify(card)
|
|
108
|
+
const b64 = Buffer.from(json, 'utf8').toString('base64')
|
|
109
|
+
return `data:application/json;base64,${b64}`
|
|
110
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { NebulaNetwork } from '../config'
|
|
2
|
+
|
|
3
|
+
export const EXPLORER_BASE: Record<NebulaNetwork, string> = {
|
|
4
|
+
'mantle-mainnet': 'https://mantlescan.xyz',
|
|
5
|
+
'mantle-testnet': 'https://sepolia.mantlescan.xyz',
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export type NetworkName = NebulaNetwork
|
|
9
|
+
|
|
10
|
+
export function explorerTxUrl(network: NebulaNetwork, txHash: string): string {
|
|
11
|
+
return `${EXPLORER_BASE[network]}/tx/${txHash}`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function explorerTokenUrl(
|
|
15
|
+
network: NebulaNetwork,
|
|
16
|
+
contract: string,
|
|
17
|
+
tokenId: bigint,
|
|
18
|
+
): string {
|
|
19
|
+
return `${EXPLORER_BASE[network]}/token/${contract}/${tokenId}`
|
|
20
|
+
}
|