lyra-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 +65 -0
- package/src/brain/compaction.test.ts +156 -0
- package/src/brain/compaction.ts +131 -0
- package/src/brain/demo-endpoint.ts +13 -0
- package/src/brain/frozen-prefix.test.ts +235 -0
- package/src/brain/frozen-prefix.ts +320 -0
- package/src/brain/history-persist.test.ts +129 -0
- package/src/brain/history-persist.ts +154 -0
- package/src/brain/index.ts +44 -0
- package/src/brain/openai-brain.test.ts +61 -0
- package/src/brain/openai-brain.ts +544 -0
- package/src/brain/sanitize.test.ts +27 -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 +35 -0
- package/src/claude-plugins/discovery.test.ts +71 -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.test.ts +186 -0
- package/src/commands/registry.ts +255 -0
- package/src/config.ts +201 -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.test.ts +43 -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 +13 -0
- package/src/index.test.ts +6 -0
- package/src/index.ts +314 -0
- package/src/locks.test.ts +259 -0
- package/src/locks.ts +233 -0
- package/src/mcp/discovery.test.ts +97 -0
- package/src/mcp/discovery.ts +150 -0
- package/src/mcp/index.ts +10 -0
- package/src/mcp/manager.test.ts +97 -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.test.ts +41 -0
- package/src/memory/edit.ts +53 -0
- package/src/memory/encryption.test.ts +68 -0
- package/src/memory/encryption.ts +94 -0
- package/src/memory/fs-util.ts +15 -0
- package/src/memory/index-file.ts +74 -0
- package/src/memory/index-sync.test.ts +81 -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.test.ts +90 -0
- package/src/memory/pack-blob.ts +120 -0
- package/src/memory/pack-gather.test.ts +110 -0
- package/src/memory/pack-gather.ts +112 -0
- package/src/memory/parser.test.ts +29 -0
- package/src/memory/parser.ts +20 -0
- package/src/memory/path-drift.test.ts +71 -0
- package/src/memory/read-tool-fallback.test.ts +54 -0
- package/src/memory/read-tool.ts +198 -0
- package/src/memory/save-tool.test.ts +193 -0
- package/src/memory/save-tool.ts +189 -0
- package/src/memory/scan.test.ts +24 -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.test.ts +106 -0
- package/src/migration/option3-crypto.ts +143 -0
- package/src/pairing.test.ts +212 -0
- package/src/pairing.ts +285 -0
- package/src/paths.ts +70 -0
- package/src/permission/dangerous.ts +105 -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/permission.test.ts +299 -0
- package/src/permission/service.ts +191 -0
- package/src/plugins/context.ts +226 -0
- package/src/plugins/hooks.ts +81 -0
- package/src/plugins/index.ts +24 -0
- package/src/plugins/plugins.test.ts +196 -0
- package/src/plugins/tool-search.ts +49 -0
- package/src/public/card.test.ts +70 -0
- package/src/public/card.ts +67 -0
- package/src/runtime/activity.ts +29 -0
- package/src/runtime/index.ts +7 -0
- package/src/runtime/runtime.test.ts +55 -0
- package/src/runtime/runtime.ts +126 -0
- package/src/sandbox/credentials.ts +25 -0
- package/src/sandbox/docker.ts +389 -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/sandbox.test.ts +386 -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.test.ts +137 -0
- package/src/skills/scanner.ts +257 -0
- package/src/skills/triggers.test.ts +77 -0
- package/src/skills/triggers.ts +78 -0
- package/src/skills/types.ts +37 -0
- package/src/storage/encryption.test.ts +30 -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.test.ts +29 -0
- package/src/storage/sqlite.ts +95 -0
- package/src/storage/types.ts +21 -0
- package/src/tools/escalation.test.ts +348 -0
- package/src/tools/escalation.ts +200 -0
- package/src/tools/index.ts +11 -0
- package/src/tools/registry.test.ts +70 -0
- package/src/tools/registry.ts +152 -0
- package/src/tools/types.ts +65 -0
- package/src/tools/zod-helpers.test.ts +63 -0
- package/src/tools/zod-helpers.ts +36 -0
- package/src/tools/zod-schema.ts +99 -0
- package/src/wallet/drain.test.ts +41 -0
- package/src/wallet/drain.ts +76 -0
- package/src/wallet/eoa.ts +61 -0
- package/src/wallet/index.ts +14 -0
- package/src/wallet/keystore.test.ts +17 -0
- package/src/wallet/keystore.ts +50 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import matter from 'gray-matter'
|
|
2
|
+
|
|
3
|
+
export interface CardFrontmatter {
|
|
4
|
+
/** Display name, e.g. "Alice". */
|
|
5
|
+
name: string
|
|
6
|
+
/** Short one-line bio, <= 140 chars. */
|
|
7
|
+
bio?: string
|
|
8
|
+
/** Skills / domains the agent is competent in. */
|
|
9
|
+
skills?: string[]
|
|
10
|
+
/** Endpoints the agent exposes (URLs). */
|
|
11
|
+
endpoints?: string[]
|
|
12
|
+
/** Avatar: either a Sui Storage CID or an absolute URL. */
|
|
13
|
+
avatar?: string
|
|
14
|
+
/** Fully-qualified .0g subname, e.g. "alice.lyra.0g". */
|
|
15
|
+
subname?: string
|
|
16
|
+
/** iNFT pointer, CAIP-10-ish: eip155:<chainId>:<contract>:<tokenId> */
|
|
17
|
+
inft?: string
|
|
18
|
+
[key: string]: unknown
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface Card {
|
|
22
|
+
frontmatter: CardFrontmatter
|
|
23
|
+
body: string
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const DEFAULT_CARD: Card = {
|
|
27
|
+
frontmatter: {
|
|
28
|
+
name: '',
|
|
29
|
+
bio: '',
|
|
30
|
+
skills: [],
|
|
31
|
+
endpoints: [],
|
|
32
|
+
},
|
|
33
|
+
body: '',
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function parseCard(markdown: string): Card {
|
|
37
|
+
const parsed = matter(markdown)
|
|
38
|
+
const fm = (parsed.data ?? {}) as CardFrontmatter
|
|
39
|
+
if (typeof fm.name !== 'string') {
|
|
40
|
+
throw new Error('CARD.md requires a "name" frontmatter field')
|
|
41
|
+
}
|
|
42
|
+
return { frontmatter: fm, body: parsed.content ?? '' }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function renderCard(card: Card): string {
|
|
46
|
+
return matter.stringify(card.body, card.frontmatter as Record<string, unknown>)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function emptyCard(): Card {
|
|
50
|
+
return {
|
|
51
|
+
frontmatter: { ...DEFAULT_CARD.frontmatter },
|
|
52
|
+
body: DEFAULT_CARD.body,
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Map a Card to the text-record key/value pairs we publish to .0g. */
|
|
57
|
+
export function cardToTextRecords(card: Card, agentEoa?: string): Record<string, string> {
|
|
58
|
+
const rec: Record<string, string> = {}
|
|
59
|
+
const fm = card.frontmatter
|
|
60
|
+
if (agentEoa) rec.address = agentEoa
|
|
61
|
+
if (fm.bio) rec['agent:bio'] = fm.bio
|
|
62
|
+
if (fm.skills?.length) rec['agent:skills'] = fm.skills.join(',')
|
|
63
|
+
if (fm.endpoints?.length) rec['agent:endpoints'] = fm.endpoints.join(',')
|
|
64
|
+
if (fm.avatar) rec.avatar = fm.avatar
|
|
65
|
+
if (fm.inft) rec['agent:inft'] = fm.inft
|
|
66
|
+
return rec
|
|
67
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { appendFile, mkdir } from 'node:fs/promises'
|
|
2
|
+
import { dirname } from 'node:path'
|
|
3
|
+
|
|
4
|
+
export interface ActivityEntry {
|
|
5
|
+
ts: number
|
|
6
|
+
kind:
|
|
7
|
+
| 'wake'
|
|
8
|
+
| 'tool-call'
|
|
9
|
+
| 'tool-result'
|
|
10
|
+
| 'brain-response'
|
|
11
|
+
| 'error'
|
|
12
|
+
| 'context-compacted'
|
|
13
|
+
| 'auto-topup'
|
|
14
|
+
data: unknown
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class ActivityLog {
|
|
18
|
+
private dirEnsured = false
|
|
19
|
+
|
|
20
|
+
constructor(private readonly path: string) {}
|
|
21
|
+
|
|
22
|
+
async append(entry: ActivityEntry): Promise<void> {
|
|
23
|
+
if (!this.dirEnsured) {
|
|
24
|
+
await mkdir(dirname(this.path), { recursive: true })
|
|
25
|
+
this.dirEnsured = true
|
|
26
|
+
}
|
|
27
|
+
await appendFile(this.path, `${JSON.stringify(entry)}\n`, 'utf8')
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
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
|
+
})
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { mkdir } from 'node:fs/promises'
|
|
2
|
+
import type { Brain } from '../brain/types'
|
|
3
|
+
import type { LyraConfig } from '../config'
|
|
4
|
+
import { EventQueue, listeners, newEventId, routeLoop } from '../events'
|
|
5
|
+
import type { LyraEvent } from '../events/types'
|
|
6
|
+
import { addEntryLine, readIndexFile, writeIndexFile } from '../memory/index-file'
|
|
7
|
+
import { agentPaths } from '../paths'
|
|
8
|
+
import type { Storage } from '../storage/types'
|
|
9
|
+
import { ToolRegistry } from '../tools/registry'
|
|
10
|
+
import { type ActivityEntry, ActivityLog } from './activity'
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Minimal agent identity the runtime needs: a stable on-chain id used to root
|
|
14
|
+
* the per-agent filesystem. On Sui this is the agent's address (or iNFT object
|
|
15
|
+
* id). Provided by the gateway/cli at construction time.
|
|
16
|
+
*/
|
|
17
|
+
export interface AgentIdentity {
|
|
18
|
+
agentId: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Source of the current agent identity. */
|
|
22
|
+
export interface IdentityProvider {
|
|
23
|
+
current(): Promise<AgentIdentity> | AgentIdentity
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RuntimeDeps {
|
|
27
|
+
config: LyraConfig
|
|
28
|
+
identity: IdentityProvider
|
|
29
|
+
brain: Brain
|
|
30
|
+
storage: Storage
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class Runtime {
|
|
34
|
+
readonly queue: EventQueue
|
|
35
|
+
readonly tools: ToolRegistry
|
|
36
|
+
private activity?: ActivityLog
|
|
37
|
+
private running = false
|
|
38
|
+
private routeTask?: Promise<void>
|
|
39
|
+
|
|
40
|
+
constructor(private readonly deps: RuntimeDeps) {
|
|
41
|
+
this.queue = new EventQueue()
|
|
42
|
+
this.tools = new ToolRegistry(deps.config.tools)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Ensure per-agent filesystem exists and boot the event loop. */
|
|
46
|
+
async start(): Promise<void> {
|
|
47
|
+
if (this.running) return
|
|
48
|
+
const id = (await this.deps.identity.current()).agentId
|
|
49
|
+
const paths = agentPaths.agent(id)
|
|
50
|
+
|
|
51
|
+
await mkdir(paths.memoryDir, { recursive: true })
|
|
52
|
+
await mkdir(paths.agentMemoryDir, { recursive: true })
|
|
53
|
+
await mkdir(paths.userMemoryDir, { recursive: true })
|
|
54
|
+
await mkdir(paths.publicDir, { recursive: true })
|
|
55
|
+
await mkdir(paths.cache, { recursive: true })
|
|
56
|
+
|
|
57
|
+
this.activity = new ActivityLog(paths.activityLog)
|
|
58
|
+
|
|
59
|
+
// Initialize MEMORY.md if missing.
|
|
60
|
+
let index = await readIndexFile(paths.memoryIndex)
|
|
61
|
+
if (index.lines.length === 0) {
|
|
62
|
+
index = {
|
|
63
|
+
lines: [
|
|
64
|
+
`# ${id} — Memory Index`,
|
|
65
|
+
'',
|
|
66
|
+
'Self-contained memory for this agent. Topic files live under `agent/` (transfers with iNFT) and `user/` (purges on transfer).',
|
|
67
|
+
'',
|
|
68
|
+
'## Memories',
|
|
69
|
+
'',
|
|
70
|
+
],
|
|
71
|
+
entries: new Map(),
|
|
72
|
+
}
|
|
73
|
+
index = addEntryLine(index, {
|
|
74
|
+
file: 'agent/identity.md',
|
|
75
|
+
title: 'Agent identity',
|
|
76
|
+
hook: 'Seed record of this agent — tokenId, creation block, operator history.',
|
|
77
|
+
})
|
|
78
|
+
await writeIndexFile(paths.memoryIndex, index)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
this.routeTask = routeLoop(this.queue, {
|
|
82
|
+
brain: this.deps.brain,
|
|
83
|
+
tools: this.tools,
|
|
84
|
+
onTurn: async (ev, turn) => {
|
|
85
|
+
await this.activity?.append({
|
|
86
|
+
ts: Date.now(),
|
|
87
|
+
kind: 'brain-response',
|
|
88
|
+
data: {
|
|
89
|
+
event: { id: ev.id, source: ev.source },
|
|
90
|
+
content: turn.content,
|
|
91
|
+
toolCalls: turn.toolCalls,
|
|
92
|
+
finishReason: turn.finishReason,
|
|
93
|
+
usage: turn.usage,
|
|
94
|
+
},
|
|
95
|
+
})
|
|
96
|
+
},
|
|
97
|
+
})
|
|
98
|
+
this.running = true
|
|
99
|
+
|
|
100
|
+
await listeners.startAll(this.queue)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Push an event onto the queue from outside the listener system. */
|
|
104
|
+
async fire(event: Omit<LyraEvent, 'id' | 'ts'>): Promise<string> {
|
|
105
|
+
const ev: LyraEvent = { ...event, id: newEventId(), ts: Date.now() }
|
|
106
|
+
await this.activity?.append({
|
|
107
|
+
ts: ev.ts,
|
|
108
|
+
kind: 'wake',
|
|
109
|
+
data: { id: ev.id, source: ev.source, label: ev.payload.label },
|
|
110
|
+
})
|
|
111
|
+
this.queue.enqueue(ev)
|
|
112
|
+
return ev.id
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async logActivity(entry: ActivityEntry): Promise<void> {
|
|
116
|
+
await this.activity?.append(entry)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async stop(): Promise<void> {
|
|
120
|
+
if (!this.running) return
|
|
121
|
+
this.running = false
|
|
122
|
+
this.queue.close()
|
|
123
|
+
await listeners.stopAll()
|
|
124
|
+
await this.routeTask
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared credential-dir blocklist used by every sandbox backend (macOS
|
|
3
|
+
* seatbelt, Linux bubblewrap). Centralized so the platforms don't drift —
|
|
4
|
+
* earlier the bwrap profile included `~/.config/anthropic` + `~/.gnupg`
|
|
5
|
+
* while the seatbelt profile didn't. Centralizing closes that gap.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Cross-platform credential paths to blackhole. Relative to homedir; backends
|
|
10
|
+
* format the absolute path. `Library/Keychains` is macOS-only but keeping it
|
|
11
|
+
* here is harmless on Linux (the path won't exist; `--tmpfs` no-ops).
|
|
12
|
+
*/
|
|
13
|
+
export const CREDENTIAL_DIR_RELATIVE_PATHS: readonly string[] = [
|
|
14
|
+
'.ssh',
|
|
15
|
+
'.aws',
|
|
16
|
+
'Library/Keychains',
|
|
17
|
+
'.config/gcloud',
|
|
18
|
+
'.config/anthropic', // claude-code config
|
|
19
|
+
'.gnupg',
|
|
20
|
+
] as const
|
|
21
|
+
|
|
22
|
+
/** Build the absolute paths of credential dirs to deny under `homedir`. */
|
|
23
|
+
export function credentialDirs(homedir: string): string[] {
|
|
24
|
+
return CREDENTIAL_DIR_RELATIVE_PATHS.map(rel => `${homedir}/${rel}`)
|
|
25
|
+
}
|