lyra-core 0.1.2 → 0.1.11
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/package.json +1 -1
- package/src/brain/compaction.test.ts +0 -156
- package/src/brain/frozen-prefix.test.ts +0 -235
- package/src/brain/history-persist.test.ts +0 -129
- package/src/brain/openai-brain.test.ts +0 -61
- package/src/brain/sanitize.test.ts +0 -27
- package/src/claude-plugins/discovery.test.ts +0 -71
- package/src/commands/registry.test.ts +0 -186
- package/src/events/queue.test.ts +0 -43
- package/src/index.test.ts +0 -6
- package/src/locks.test.ts +0 -259
- package/src/mcp/discovery.test.ts +0 -97
- package/src/mcp/manager.test.ts +0 -97
- package/src/memory/edit.test.ts +0 -41
- package/src/memory/encryption.test.ts +0 -68
- package/src/memory/index-sync.test.ts +0 -81
- package/src/memory/pack-blob.test.ts +0 -90
- package/src/memory/pack-gather.test.ts +0 -110
- package/src/memory/parser.test.ts +0 -29
- package/src/memory/path-drift.test.ts +0 -71
- package/src/memory/read-tool-fallback.test.ts +0 -54
- package/src/memory/save-tool.test.ts +0 -193
- package/src/memory/scan.test.ts +0 -24
- package/src/migration/option3-crypto.test.ts +0 -106
- package/src/pairing.test.ts +0 -212
- package/src/permission/permission.test.ts +0 -299
- package/src/plugins/plugins.test.ts +0 -196
- package/src/public/card.test.ts +0 -70
- package/src/runtime/runtime.test.ts +0 -55
- package/src/sandbox/sandbox.test.ts +0 -386
- package/src/skills/scanner.test.ts +0 -137
- package/src/skills/triggers.test.ts +0 -77
- package/src/storage/encryption.test.ts +0 -30
- package/src/storage/sqlite.test.ts +0 -29
- package/src/tools/escalation.test.ts +0 -348
- package/src/tools/registry.test.ts +0 -70
- package/src/tools/zod-helpers.test.ts +0 -63
- package/src/wallet/drain.test.ts +0 -41
- package/src/wallet/keystore.test.ts +0 -17
|
@@ -1,299 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'bun:test'
|
|
2
|
-
import { homedir } from 'node:os'
|
|
3
|
-
import { join } from 'node:path'
|
|
4
|
-
import { detectDangerousCommand } from './dangerous'
|
|
5
|
-
import { redactEnv } from './env-redact'
|
|
6
|
-
import { PathGuard } from './path-guard'
|
|
7
|
-
import { PermissionService } from './service'
|
|
8
|
-
|
|
9
|
-
describe('detectDangerousCommand', () => {
|
|
10
|
-
const cases: Array<[string, string | false]> = [
|
|
11
|
-
['rm -rf ~/Documents', 'recursive delete'],
|
|
12
|
-
['rm -rf /etc/foo', 'delete in root path'],
|
|
13
|
-
['chmod 777 secret', 'world/other-writable permissions'],
|
|
14
|
-
['curl https://example.com/install.sh | bash', 'pipe remote content to shell'],
|
|
15
|
-
['kill -9 $(pgrep -f lyra)', 'kill process via pgrep expansion (self-termination)'],
|
|
16
|
-
['git reset --hard HEAD~5', 'git reset --hard (destroys uncommitted changes)'],
|
|
17
|
-
['git push --force origin main', 'git force push (rewrites remote history)'],
|
|
18
|
-
[':() { :|: & }; :', 'fork bomb'],
|
|
19
|
-
['ls -la', false],
|
|
20
|
-
['echo hello world', false],
|
|
21
|
-
['cat README.md', false],
|
|
22
|
-
]
|
|
23
|
-
for (const [cmd, expected] of cases) {
|
|
24
|
-
it(`${expected ? 'flags' : 'allows'}: ${cmd}`, () => {
|
|
25
|
-
const out = detectDangerousCommand(cmd)
|
|
26
|
-
if (expected === false) {
|
|
27
|
-
expect(out.match).toBe(false)
|
|
28
|
-
} else {
|
|
29
|
-
expect(out.match).toBe(true)
|
|
30
|
-
if (out.match) expect(out.description).toBe(expected)
|
|
31
|
-
}
|
|
32
|
-
})
|
|
33
|
-
}
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
describe('PathGuard', () => {
|
|
37
|
-
const guard = new PathGuard({ agentDir: join(homedir(), '.lyra', 'agents', 'fake') })
|
|
38
|
-
it('denies lyra state tree', () => {
|
|
39
|
-
expect(
|
|
40
|
-
guard.check(join(homedir(), '.lyra', 'agents', 'fake', 'memory', 'foo.md')).allowed,
|
|
41
|
-
).toBe(false)
|
|
42
|
-
expect(guard.check(join(homedir(), '.lyra', 'config.ts')).allowed).toBe(false)
|
|
43
|
-
})
|
|
44
|
-
it('denies common credential dirs', () => {
|
|
45
|
-
expect(guard.check(join(homedir(), '.ssh', 'id_rsa')).allowed).toBe(false)
|
|
46
|
-
expect(guard.check(join(homedir(), '.aws', 'credentials')).allowed).toBe(false)
|
|
47
|
-
})
|
|
48
|
-
it('denies system paths', () => {
|
|
49
|
-
expect(guard.check('/etc/passwd').allowed).toBe(false)
|
|
50
|
-
expect(guard.check('/dev/sda').allowed).toBe(false)
|
|
51
|
-
})
|
|
52
|
-
it('allows everyday user paths', () => {
|
|
53
|
-
expect(guard.check(join(homedir(), 'Documents', 'foo.md')).allowed).toBe(true)
|
|
54
|
-
expect(guard.check('/tmp/sandbox.txt').allowed).toBe(true)
|
|
55
|
-
})
|
|
56
|
-
it('denies dotenv files anywhere', () => {
|
|
57
|
-
expect(guard.check('/Users/me/myproj/.env.local').allowed).toBe(false)
|
|
58
|
-
})
|
|
59
|
-
})
|
|
60
|
-
|
|
61
|
-
describe('redactEnv', () => {
|
|
62
|
-
it('strips wallet and api-key vars but keeps PATH', () => {
|
|
63
|
-
const { env, removed } = redactEnv({
|
|
64
|
-
PATH: '/usr/bin',
|
|
65
|
-
HOME: '/home/me',
|
|
66
|
-
LYRA_AGENT_PRIVKEY_HEX: '0xdead',
|
|
67
|
-
OPENAI_API_KEY: 'sk-x',
|
|
68
|
-
GH_TOKEN: 'ghp_x',
|
|
69
|
-
AWS_SECRET_ACCESS_KEY: 'secret',
|
|
70
|
-
MY_FAVORITE_PRIVKEY: '0xfeed',
|
|
71
|
-
GREETING: 'hello',
|
|
72
|
-
})
|
|
73
|
-
expect(env.PATH).toBe('/usr/bin')
|
|
74
|
-
expect(env.HOME).toBe('/home/me')
|
|
75
|
-
expect(env.GREETING).toBe('hello')
|
|
76
|
-
expect(env.LYRA_AGENT_PRIVKEY_HEX).toBeUndefined()
|
|
77
|
-
expect(env.OPENAI_API_KEY).toBeUndefined()
|
|
78
|
-
expect(env.GH_TOKEN).toBeUndefined()
|
|
79
|
-
expect(env.AWS_SECRET_ACCESS_KEY).toBeUndefined()
|
|
80
|
-
expect(env.MY_FAVORITE_PRIVKEY).toBeUndefined()
|
|
81
|
-
expect(removed.sort()).toEqual(
|
|
82
|
-
[
|
|
83
|
-
'LYRA_AGENT_PRIVKEY_HEX',
|
|
84
|
-
'AWS_SECRET_ACCESS_KEY',
|
|
85
|
-
'GH_TOKEN',
|
|
86
|
-
'MY_FAVORITE_PRIVKEY',
|
|
87
|
-
'OPENAI_API_KEY',
|
|
88
|
-
].sort(),
|
|
89
|
-
)
|
|
90
|
-
})
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
describe('PermissionService', () => {
|
|
94
|
-
it('YOLO mode never blocks, never prompts', async () => {
|
|
95
|
-
let prompted = false
|
|
96
|
-
const svc = new PermissionService({
|
|
97
|
-
mode: 'off',
|
|
98
|
-
prompter: async () => {
|
|
99
|
-
prompted = true
|
|
100
|
-
return 'deny'
|
|
101
|
-
},
|
|
102
|
-
})
|
|
103
|
-
const out = await svc.resolve({
|
|
104
|
-
kind: 'shell.run',
|
|
105
|
-
command: 'rm -rf /etc',
|
|
106
|
-
reason: 'shell',
|
|
107
|
-
})
|
|
108
|
-
expect(out.allowed).toBe(true)
|
|
109
|
-
expect(out.via).toBe('yolo')
|
|
110
|
-
expect(prompted).toBe(false)
|
|
111
|
-
})
|
|
112
|
-
it('strict mode hard-denies dangerous commands', async () => {
|
|
113
|
-
let prompted = false
|
|
114
|
-
const svc = new PermissionService({
|
|
115
|
-
mode: 'strict',
|
|
116
|
-
prompter: async () => {
|
|
117
|
-
prompted = true
|
|
118
|
-
return 'allow-once'
|
|
119
|
-
},
|
|
120
|
-
})
|
|
121
|
-
const out = await svc.resolve({
|
|
122
|
-
kind: 'shell.run',
|
|
123
|
-
command: 'rm -rf ~/Documents',
|
|
124
|
-
reason: 'shell',
|
|
125
|
-
})
|
|
126
|
-
expect(out.allowed).toBe(false)
|
|
127
|
-
expect(out.via).toBe('strict-deny')
|
|
128
|
-
expect(prompted).toBe(false)
|
|
129
|
-
})
|
|
130
|
-
it('prompt mode escalates dangerous + remembers session-allow', async () => {
|
|
131
|
-
let calls = 0
|
|
132
|
-
const svc = new PermissionService({
|
|
133
|
-
mode: 'prompt',
|
|
134
|
-
prompter: async () => {
|
|
135
|
-
calls++
|
|
136
|
-
return 'allow-session'
|
|
137
|
-
},
|
|
138
|
-
})
|
|
139
|
-
const req = {
|
|
140
|
-
kind: 'shell.run' as const,
|
|
141
|
-
command: 'rm -rf /tmp/agentcache',
|
|
142
|
-
reason: 'shell',
|
|
143
|
-
}
|
|
144
|
-
const first = await svc.resolve(req)
|
|
145
|
-
expect(first.allowed).toBe(true)
|
|
146
|
-
const second = await svc.resolve(req)
|
|
147
|
-
expect(second.allowed).toBe(true)
|
|
148
|
-
expect(second.via).toBe('session-allow')
|
|
149
|
-
expect(calls).toBe(1)
|
|
150
|
-
})
|
|
151
|
-
it('prompt mode prompts on every non-dangerous shell.run too', async () => {
|
|
152
|
-
let calls = 0
|
|
153
|
-
const svc = new PermissionService({
|
|
154
|
-
mode: 'prompt',
|
|
155
|
-
prompter: async () => {
|
|
156
|
-
calls++
|
|
157
|
-
return 'allow-once'
|
|
158
|
-
},
|
|
159
|
-
})
|
|
160
|
-
await svc.resolve({ kind: 'shell.run', command: 'ls -la', reason: 'shell' })
|
|
161
|
-
await svc.resolve({ kind: 'shell.run', command: 'ls -la', reason: 'shell' })
|
|
162
|
-
expect(calls).toBe(2)
|
|
163
|
-
})
|
|
164
|
-
it('prompt mode auto-allows non-dangerous fs writes', async () => {
|
|
165
|
-
const svc = new PermissionService({ mode: 'prompt' })
|
|
166
|
-
const out = await svc.resolve({
|
|
167
|
-
kind: 'fs.write',
|
|
168
|
-
path: '/tmp/a.txt',
|
|
169
|
-
reason: 'fs.write',
|
|
170
|
-
})
|
|
171
|
-
expect(out.allowed).toBe(true)
|
|
172
|
-
expect(out.via).toBe('allow')
|
|
173
|
-
})
|
|
174
|
-
it('prompt mode deny returns a clear reason for the brain', async () => {
|
|
175
|
-
const svc = new PermissionService({
|
|
176
|
-
mode: 'prompt',
|
|
177
|
-
prompter: async () => 'deny',
|
|
178
|
-
})
|
|
179
|
-
const out = await svc.resolve({
|
|
180
|
-
kind: 'chain.send',
|
|
181
|
-
amount: '0.01',
|
|
182
|
-
recipient: '0xC635e6Eb223aE14143E23cEEa9440bC773dc87Ec',
|
|
183
|
-
token: 'Sui',
|
|
184
|
-
reason: 'native/ERC-20 transfer',
|
|
185
|
-
})
|
|
186
|
-
expect(out.allowed).toBe(false)
|
|
187
|
-
expect(out.via).toBe('deny')
|
|
188
|
-
expect(out.reason).toBe('rejected in approval modal')
|
|
189
|
-
})
|
|
190
|
-
it('setMode flips active resolution', async () => {
|
|
191
|
-
const svc = new PermissionService({ mode: 'strict' })
|
|
192
|
-
svc.setMode('off')
|
|
193
|
-
const out = await svc.resolve({
|
|
194
|
-
kind: 'shell.run',
|
|
195
|
-
command: 'rm -rf ~/Documents',
|
|
196
|
-
reason: 'shell',
|
|
197
|
-
})
|
|
198
|
-
expect(out.allowed).toBe(true)
|
|
199
|
-
})
|
|
200
|
-
|
|
201
|
-
// Deterministic policy floor: `force` requires approval beneath the mode.
|
|
202
|
-
it('force prompts even under YOLO (off) instead of silent allow', async () => {
|
|
203
|
-
let calls = 0
|
|
204
|
-
const svc = new PermissionService({
|
|
205
|
-
mode: 'off',
|
|
206
|
-
prompter: async () => {
|
|
207
|
-
calls++
|
|
208
|
-
return 'allow-once'
|
|
209
|
-
},
|
|
210
|
-
})
|
|
211
|
-
const out = await svc.resolve({
|
|
212
|
-
kind: 'chain.send',
|
|
213
|
-
amount: '5',
|
|
214
|
-
recipient: '0xC635e6Eb223aE14143E23cEEa9440bC773dc87Ec',
|
|
215
|
-
token: 'MNT',
|
|
216
|
-
reason: 'native/ERC-20 transfer',
|
|
217
|
-
force: true,
|
|
218
|
-
})
|
|
219
|
-
expect(calls).toBe(1)
|
|
220
|
-
expect(out.allowed).toBe(true)
|
|
221
|
-
expect(out.via).toBe('once')
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
it('force under YOLO is blocked when the operator denies', async () => {
|
|
225
|
-
const svc = new PermissionService({ mode: 'off', prompter: async () => 'deny' })
|
|
226
|
-
const out = await svc.resolve({
|
|
227
|
-
kind: 'chain.send',
|
|
228
|
-
amount: '5',
|
|
229
|
-
reason: 'native/ERC-20 transfer',
|
|
230
|
-
force: true,
|
|
231
|
-
})
|
|
232
|
-
expect(out.allowed).toBe(false)
|
|
233
|
-
expect(out.via).toBe('deny')
|
|
234
|
-
})
|
|
235
|
-
|
|
236
|
-
it('force is denied outright in strict mode without prompting', async () => {
|
|
237
|
-
let calls = 0
|
|
238
|
-
const svc = new PermissionService({
|
|
239
|
-
mode: 'strict',
|
|
240
|
-
prompter: async () => {
|
|
241
|
-
calls++
|
|
242
|
-
return 'allow-once'
|
|
243
|
-
},
|
|
244
|
-
})
|
|
245
|
-
const out = await svc.resolve({
|
|
246
|
-
kind: 'chain.send',
|
|
247
|
-
amount: '5',
|
|
248
|
-
reason: 'native/ERC-20 transfer',
|
|
249
|
-
force: true,
|
|
250
|
-
})
|
|
251
|
-
expect(calls).toBe(0)
|
|
252
|
-
expect(out.allowed).toBe(false)
|
|
253
|
-
expect(out.via).toBe('strict-deny')
|
|
254
|
-
expect(out.reason).toContain('approval required')
|
|
255
|
-
})
|
|
256
|
-
|
|
257
|
-
it('force + allow-session generalises to later identical forced requests', async () => {
|
|
258
|
-
let calls = 0
|
|
259
|
-
const svc = new PermissionService({
|
|
260
|
-
mode: 'off',
|
|
261
|
-
prompter: async () => {
|
|
262
|
-
calls++
|
|
263
|
-
return 'allow-session'
|
|
264
|
-
},
|
|
265
|
-
})
|
|
266
|
-
const req = {
|
|
267
|
-
kind: 'chain.send' as const,
|
|
268
|
-
amount: '5',
|
|
269
|
-
recipient: '0xC635e6Eb223aE14143E23cEEa9440bC773dc87Ec',
|
|
270
|
-
token: 'MNT',
|
|
271
|
-
reason: 'native/ERC-20 transfer',
|
|
272
|
-
force: true,
|
|
273
|
-
}
|
|
274
|
-
const first = await svc.resolve(req)
|
|
275
|
-
const second = await svc.resolve(req)
|
|
276
|
-
expect(calls).toBe(1)
|
|
277
|
-
expect(first.via).toBe('session-allow')
|
|
278
|
-
expect(second.via).toBe('session-allow')
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
it('non-forced value-moving tx still silent-allows under YOLO', async () => {
|
|
282
|
-
let calls = 0
|
|
283
|
-
const svc = new PermissionService({
|
|
284
|
-
mode: 'off',
|
|
285
|
-
prompter: async () => {
|
|
286
|
-
calls++
|
|
287
|
-
return 'deny'
|
|
288
|
-
},
|
|
289
|
-
})
|
|
290
|
-
const out = await svc.resolve({
|
|
291
|
-
kind: 'chain.send',
|
|
292
|
-
amount: '0.001',
|
|
293
|
-
reason: 'native/ERC-20 transfer',
|
|
294
|
-
})
|
|
295
|
-
expect(calls).toBe(0)
|
|
296
|
-
expect(out.allowed).toBe(true)
|
|
297
|
-
expect(out.via).toBe('yolo')
|
|
298
|
-
})
|
|
299
|
-
})
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'bun:test'
|
|
2
|
-
import { z } from 'zod'
|
|
3
|
-
import { ToolRegistry } from '../tools/registry'
|
|
4
|
-
import type { ToolDef } from '../tools/types'
|
|
5
|
-
import { type NativePlugin, loadPlugins } from './context'
|
|
6
|
-
import { HookBus, type PreToolCallContext, type PreToolCallResult } from './hooks'
|
|
7
|
-
import { makeToolSearchTool } from './tool-search'
|
|
8
|
-
|
|
9
|
-
describe('ToolRegistry deferred-tool semantics', () => {
|
|
10
|
-
function makeTools() {
|
|
11
|
-
const r = new ToolRegistry()
|
|
12
|
-
r.register({
|
|
13
|
-
name: 'fs.read',
|
|
14
|
-
description: 'read a file',
|
|
15
|
-
schema: z.object({ path: z.string() }),
|
|
16
|
-
handler: () => ({ ok: true }),
|
|
17
|
-
})
|
|
18
|
-
r.register({
|
|
19
|
-
name: 'browser.navigate',
|
|
20
|
-
description: 'navigate the browser to a URL',
|
|
21
|
-
shouldDefer: true,
|
|
22
|
-
searchHint: 'browser web automation page',
|
|
23
|
-
schema: z.object({ url: z.string() }),
|
|
24
|
-
handler: () => ({ ok: true }),
|
|
25
|
-
})
|
|
26
|
-
return r
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
it('eager tools appear in schemas; deferred tools do not until unlocked', () => {
|
|
30
|
-
const r = makeTools()
|
|
31
|
-
const before = r.schemas().map(s => s.function.name)
|
|
32
|
-
expect(before).toContain('fs.read')
|
|
33
|
-
expect(before).not.toContain('browser.navigate')
|
|
34
|
-
r.unlock('browser.navigate')
|
|
35
|
-
const after = r.schemas().map(s => s.function.name)
|
|
36
|
-
expect(after).toContain('browser.navigate')
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
it('search by select:name returns exact tools', () => {
|
|
40
|
-
const r = makeTools()
|
|
41
|
-
const matches = r.search('select:fs.read,browser.navigate')
|
|
42
|
-
expect(matches.map(t => t.name).sort()).toEqual(['browser.navigate', 'fs.read'])
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('keyword search matches deferred tools via searchHint', () => {
|
|
46
|
-
const r = makeTools()
|
|
47
|
-
const matches = r.search('browser web')
|
|
48
|
-
expect(matches.map(t => t.name)).toContain('browser.navigate')
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
it('config glob deny propagates through schemas + dispatch', async () => {
|
|
52
|
-
const r = new ToolRegistry({ 'fs.*': false })
|
|
53
|
-
r.register({
|
|
54
|
-
name: 'fs.read',
|
|
55
|
-
description: 'x',
|
|
56
|
-
schema: z.object({}),
|
|
57
|
-
handler: () => ({ ok: true }),
|
|
58
|
-
})
|
|
59
|
-
expect(r.schemas()).toEqual([])
|
|
60
|
-
const out = await r.dispatch({ id: '1', name: 'fs.read', args: {} })
|
|
61
|
-
expect(out.ok).toBe(false)
|
|
62
|
-
})
|
|
63
|
-
})
|
|
64
|
-
|
|
65
|
-
describe('HookBus', () => {
|
|
66
|
-
it('runs pre_tool_call handlers in order, supports rewrite + short-circuit', async () => {
|
|
67
|
-
const bus = new HookBus()
|
|
68
|
-
bus.add<PreToolCallContext, PreToolCallResult>('pre_tool_call', ({ call }) => ({
|
|
69
|
-
call: { ...call, args: { mutated: true } },
|
|
70
|
-
}))
|
|
71
|
-
bus.add<PreToolCallContext, PreToolCallResult>('pre_tool_call', () => ({
|
|
72
|
-
short: { ok: false, error: 'denied' },
|
|
73
|
-
}))
|
|
74
|
-
const out = await bus.runPreToolCall({
|
|
75
|
-
call: { id: '1', name: 't', args: {} },
|
|
76
|
-
})
|
|
77
|
-
expect(out.short).toEqual({ ok: false, error: 'denied' })
|
|
78
|
-
expect(out.call?.args).toEqual({ mutated: true })
|
|
79
|
-
})
|
|
80
|
-
|
|
81
|
-
it('post_tool_call observers cannot break the loop', async () => {
|
|
82
|
-
const bus = new HookBus()
|
|
83
|
-
bus.add('post_tool_call', () => {
|
|
84
|
-
throw new Error('boom')
|
|
85
|
-
})
|
|
86
|
-
let observed = false
|
|
87
|
-
bus.add('post_tool_call', () => {
|
|
88
|
-
observed = true
|
|
89
|
-
})
|
|
90
|
-
await bus.runPostToolCall({
|
|
91
|
-
call: { id: '1', name: 't', args: {} },
|
|
92
|
-
result: { ok: true },
|
|
93
|
-
})
|
|
94
|
-
expect(observed).toBe(true)
|
|
95
|
-
})
|
|
96
|
-
})
|
|
97
|
-
|
|
98
|
-
describe('loadPlugins', () => {
|
|
99
|
-
it('discovers + invokes register(ctx) per plugin', async () => {
|
|
100
|
-
const r = new ToolRegistry()
|
|
101
|
-
const bus = new HookBus()
|
|
102
|
-
let listenerCount = 0
|
|
103
|
-
const plugin: NativePlugin = {
|
|
104
|
-
name: 'fake',
|
|
105
|
-
register: ctx => {
|
|
106
|
-
ctx.registerTool({
|
|
107
|
-
name: 'fake.tool',
|
|
108
|
-
description: 'x',
|
|
109
|
-
schema: z.object({}),
|
|
110
|
-
handler: () => ({ ok: true }),
|
|
111
|
-
})
|
|
112
|
-
ctx.addHook('pre_tool_call', () => undefined)
|
|
113
|
-
},
|
|
114
|
-
}
|
|
115
|
-
const result = await loadPlugins(['fake'], {
|
|
116
|
-
tools: r,
|
|
117
|
-
hooks: bus,
|
|
118
|
-
listeners: { register: () => listenerCount++ },
|
|
119
|
-
agentDir: '/tmp/agent',
|
|
120
|
-
agentId: 'agent-x',
|
|
121
|
-
network: 'mainnet',
|
|
122
|
-
configPath: '/tmp/agent/config.ts',
|
|
123
|
-
imports: { claudeCode: false },
|
|
124
|
-
skillsDisabled: { current: [] },
|
|
125
|
-
activityLogPath: '/tmp/agent/activity.jsonl',
|
|
126
|
-
workspaceRoot: '/tmp/agent',
|
|
127
|
-
resolve: async () => ({ default: plugin }),
|
|
128
|
-
})
|
|
129
|
-
expect(result.loaded).toEqual(['fake'])
|
|
130
|
-
expect(result.errors).toEqual([])
|
|
131
|
-
expect(r.find('fake.tool')).toBeDefined()
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
it('reports errors without throwing', async () => {
|
|
135
|
-
const r = new ToolRegistry()
|
|
136
|
-
const bus = new HookBus()
|
|
137
|
-
const result = await loadPlugins(['broken'], {
|
|
138
|
-
tools: r,
|
|
139
|
-
hooks: bus,
|
|
140
|
-
listeners: { register: () => {} },
|
|
141
|
-
agentDir: '/tmp/agent',
|
|
142
|
-
agentId: 'agent-x',
|
|
143
|
-
network: 'mainnet',
|
|
144
|
-
configPath: '/tmp/agent/config.ts',
|
|
145
|
-
imports: { claudeCode: false },
|
|
146
|
-
skillsDisabled: { current: [] },
|
|
147
|
-
activityLogPath: '/tmp/agent/activity.jsonl',
|
|
148
|
-
workspaceRoot: '/tmp/agent',
|
|
149
|
-
resolve: async () => {
|
|
150
|
-
throw new Error('module not found')
|
|
151
|
-
},
|
|
152
|
-
})
|
|
153
|
-
expect(result.loaded).toEqual([])
|
|
154
|
-
expect(result.errors[0]?.error).toContain('module not found')
|
|
155
|
-
})
|
|
156
|
-
})
|
|
157
|
-
|
|
158
|
-
describe('makeToolSearchTool', () => {
|
|
159
|
-
it('select query unlocks deferred tools', async () => {
|
|
160
|
-
const r = new ToolRegistry()
|
|
161
|
-
r.register({
|
|
162
|
-
name: 'fs.write',
|
|
163
|
-
description: 'write a file',
|
|
164
|
-
shouldDefer: true,
|
|
165
|
-
searchHint: 'filesystem write text file',
|
|
166
|
-
schema: z.object({ path: z.string(), text: z.string() }),
|
|
167
|
-
handler: () => ({ ok: true }),
|
|
168
|
-
})
|
|
169
|
-
const meta = makeToolSearchTool(r)
|
|
170
|
-
r.register(meta as ToolDef)
|
|
171
|
-
expect(r.schemas().map(s => s.function.name)).toContain('tool.search')
|
|
172
|
-
expect(r.schemas().map(s => s.function.name)).not.toContain('fs.write')
|
|
173
|
-
const result = await meta.handler({ query: 'select:fs.write' })
|
|
174
|
-
expect(result.ok).toBe(true)
|
|
175
|
-
expect(r.schemas().map(s => s.function.name)).toContain('fs.write')
|
|
176
|
-
const data = result.data as { tools: Array<{ name: string }> }
|
|
177
|
-
expect(data.tools.map(t => t.name)).toEqual(['fs.write'])
|
|
178
|
-
})
|
|
179
|
-
|
|
180
|
-
it('keyword query matches by description and hint', async () => {
|
|
181
|
-
const r = new ToolRegistry()
|
|
182
|
-
r.register({
|
|
183
|
-
name: 'browser.navigate',
|
|
184
|
-
description: 'navigate the browser',
|
|
185
|
-
shouldDefer: true,
|
|
186
|
-
searchHint: 'browser web automation',
|
|
187
|
-
schema: z.object({ url: z.string() }),
|
|
188
|
-
handler: () => ({ ok: true }),
|
|
189
|
-
})
|
|
190
|
-
const meta = makeToolSearchTool(r)
|
|
191
|
-
r.register(meta as ToolDef)
|
|
192
|
-
const result = await meta.handler({ query: 'browser web' })
|
|
193
|
-
const data = result.data as { matched: number }
|
|
194
|
-
expect(data.matched).toBe(1)
|
|
195
|
-
})
|
|
196
|
-
})
|
package/src/public/card.test.ts
DELETED
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { cardToTextRecords, emptyCard, parseCard, renderCard } from './card'
|
|
3
|
-
|
|
4
|
-
describe('card', () => {
|
|
5
|
-
test('parses frontmatter + body', () => {
|
|
6
|
-
const md = `---
|
|
7
|
-
name: Alice
|
|
8
|
-
bio: research lyra
|
|
9
|
-
skills:
|
|
10
|
-
- research
|
|
11
|
-
- writing
|
|
12
|
-
---
|
|
13
|
-
|
|
14
|
-
Free-form body here.`
|
|
15
|
-
const c = parseCard(md)
|
|
16
|
-
expect(c.frontmatter.name).toBe('Alice')
|
|
17
|
-
expect(c.frontmatter.bio).toBe('research lyra')
|
|
18
|
-
expect(c.frontmatter.skills).toEqual(['research', 'writing'])
|
|
19
|
-
expect(c.body.trim()).toBe('Free-form body here.')
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
test('rejects missing name', () => {
|
|
23
|
-
expect(() => parseCard('---\nbio: no name\n---\nbody')).toThrow()
|
|
24
|
-
})
|
|
25
|
-
|
|
26
|
-
test('round-trips via render', () => {
|
|
27
|
-
const c = {
|
|
28
|
-
frontmatter: { name: 'Bob', bio: 'hi', skills: ['code'] },
|
|
29
|
-
body: 'Body.',
|
|
30
|
-
}
|
|
31
|
-
const rendered = renderCard(c)
|
|
32
|
-
const parsed = parseCard(rendered)
|
|
33
|
-
expect(parsed.frontmatter.name).toBe('Bob')
|
|
34
|
-
expect(parsed.frontmatter.skills).toEqual(['code'])
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
test('emptyCard is parseable after setting a name', () => {
|
|
38
|
-
const c = emptyCard()
|
|
39
|
-
c.frontmatter.name = 'Temp'
|
|
40
|
-
const rendered = renderCard(c)
|
|
41
|
-
const parsed = parseCard(rendered)
|
|
42
|
-
expect(parsed.frontmatter.name).toBe('Temp')
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
test('cardToTextRecords includes address + agent:inft when present', () => {
|
|
46
|
-
const c = {
|
|
47
|
-
frontmatter: {
|
|
48
|
-
name: 'Alice',
|
|
49
|
-
bio: 'hi',
|
|
50
|
-
skills: ['research', 'writing'],
|
|
51
|
-
inft: 'eip155:5003:0xabc:42',
|
|
52
|
-
avatar: '0xdeadbeef',
|
|
53
|
-
},
|
|
54
|
-
body: '',
|
|
55
|
-
}
|
|
56
|
-
const r = cardToTextRecords(c, '0xC635e6Eb223aE14143E23cEEa9440bC773dc87Ec')
|
|
57
|
-
expect(r.address).toBe('0xC635e6Eb223aE14143E23cEEa9440bC773dc87Ec')
|
|
58
|
-
expect(r['agent:bio']).toBe('hi')
|
|
59
|
-
expect(r['agent:skills']).toBe('research,writing')
|
|
60
|
-
expect(r['agent:inft']).toBe('eip155:5003:0xabc:42')
|
|
61
|
-
expect(r.avatar).toBe('0xdeadbeef')
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
test('cardToTextRecords omits empty fields', () => {
|
|
65
|
-
const c = emptyCard()
|
|
66
|
-
c.frontmatter.name = 'NoExtras'
|
|
67
|
-
const r = cardToTextRecords(c)
|
|
68
|
-
expect(Object.keys(r)).toHaveLength(0)
|
|
69
|
-
})
|
|
70
|
-
})
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { test } from 'bun:test'
|
|
2
|
-
import { mkdtempSync, rmSync } from 'node:fs'
|
|
3
|
-
import { tmpdir } from 'node:os'
|
|
4
|
-
import { join } from 'node:path'
|
|
5
|
-
import { StubBrain } from '../brain/stub'
|
|
6
|
-
import { defineConfig } from '../config'
|
|
7
|
-
import { LocalStubStorage } from '../storage/local-stub'
|
|
8
|
-
import { type IdentityProvider, Runtime } from './runtime'
|
|
9
|
-
|
|
10
|
-
/** Minimal in-memory identity for runtime tests. */
|
|
11
|
-
class StubIdentity implements IdentityProvider {
|
|
12
|
-
constructor(private readonly agentId: string) {}
|
|
13
|
-
current() {
|
|
14
|
-
return { agentId: this.agentId }
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
async function withTempRoot<T>(fn: (root: string) => Promise<T>): Promise<T> {
|
|
19
|
-
const prev = process.env.LYRA_ROOT
|
|
20
|
-
const tmp = mkdtempSync(join(tmpdir(), 'lyra-root-'))
|
|
21
|
-
process.env.LYRA_ROOT = tmp
|
|
22
|
-
try {
|
|
23
|
-
return await fn(tmp)
|
|
24
|
-
} finally {
|
|
25
|
-
process.env.LYRA_ROOT = prev
|
|
26
|
-
rmSync(tmp, { recursive: true, force: true })
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
test('runtime boots, seeds memory dir, routes stub brain echo', async () => {
|
|
31
|
-
await withTempRoot(async root => {
|
|
32
|
-
const agentAddr = `0x${'a'.repeat(64)}`
|
|
33
|
-
const identity = new StubIdentity(agentAddr)
|
|
34
|
-
const brain = new StubBrain()
|
|
35
|
-
const storage = new LocalStubStorage(join(root, 'storage-stub-test'))
|
|
36
|
-
|
|
37
|
-
const runtime = new Runtime({
|
|
38
|
-
config: defineConfig({ network: 'testnet' }),
|
|
39
|
-
identity,
|
|
40
|
-
brain,
|
|
41
|
-
storage,
|
|
42
|
-
})
|
|
43
|
-
|
|
44
|
-
await runtime.start()
|
|
45
|
-
|
|
46
|
-
await runtime.fire({
|
|
47
|
-
source: 'stdin',
|
|
48
|
-
payload: { label: 'hello', data: 'hello world' },
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
await new Promise(r => setTimeout(r, 50))
|
|
52
|
-
|
|
53
|
-
await runtime.stop()
|
|
54
|
-
})
|
|
55
|
-
})
|