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,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Phase 9.1 skills surface. Mirrors Claude Code's SKILL.md frontmatter so
|
|
3
|
+
* imports.claudeCode picks up the entire ~/.claude ecosystem free.
|
|
4
|
+
*/
|
|
5
|
+
export type SkillSource = 'lyra' | 'lyra-plugin' | 'claude-code' | 'claude-plugin'
|
|
6
|
+
|
|
7
|
+
export interface SkillFrontmatter {
|
|
8
|
+
/** Unique name used by the brain to reference this skill. Required. */
|
|
9
|
+
name: string
|
|
10
|
+
/** One-line summary the brain sees in the skill index. Required. */
|
|
11
|
+
description: string
|
|
12
|
+
version?: string
|
|
13
|
+
license?: string
|
|
14
|
+
/** Comma-separated globs (e.g. `*.test.ts,*.spec.ts`) that auto-trigger the skill on fs.* paths. */
|
|
15
|
+
filePattern?: string
|
|
16
|
+
/** Regex (string) that auto-triggers the skill on shell.run commands. */
|
|
17
|
+
bashPattern?: string
|
|
18
|
+
/**
|
|
19
|
+
* Claude Code commands set this to distinguish slash-only invocations from
|
|
20
|
+
* model-invokable skills. Skills omit it; commands set it (any value).
|
|
21
|
+
*/
|
|
22
|
+
argumentHint?: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface SkillRef {
|
|
26
|
+
/** `<source-prefix>:<dir-name>` (e.g. `lyra:dogfood`, `claude-code:tmux`). */
|
|
27
|
+
id: string
|
|
28
|
+
/** Display name from frontmatter (falls back to directory name). */
|
|
29
|
+
name: string
|
|
30
|
+
description: string
|
|
31
|
+
/** Absolute path to SKILL.md. */
|
|
32
|
+
path: string
|
|
33
|
+
source: SkillSource
|
|
34
|
+
/** When set, marketplace > plugin > version triple from `~/.claude/plugins/cache/...` paths. */
|
|
35
|
+
pluginCoord?: { marketplace: string; plugin: string; version: string }
|
|
36
|
+
frontmatter: SkillFrontmatter
|
|
37
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { decrypt, encrypt, packEnvelope, unpackEnvelope } from './encryption'
|
|
3
|
+
|
|
4
|
+
describe('encryption', () => {
|
|
5
|
+
test('round-trips a short message', () => {
|
|
6
|
+
const plaintext = new TextEncoder().encode('hello lyra')
|
|
7
|
+
const env = encrypt(plaintext, 'testpass1234')
|
|
8
|
+
const out = decrypt(env, 'testpass1234')
|
|
9
|
+
expect(new TextDecoder().decode(out)).toBe('hello lyra')
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
test('wrong passphrase fails', () => {
|
|
13
|
+
const env = encrypt(new TextEncoder().encode('secret'), 'right-password')
|
|
14
|
+
expect(() => decrypt(env, 'wrong-password')).toThrow()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
test('packs + unpacks round-trip', () => {
|
|
18
|
+
const env = encrypt(new TextEncoder().encode('pack me'), 'pw')
|
|
19
|
+
const packed = packEnvelope(env)
|
|
20
|
+
const unpacked = unpackEnvelope(packed)
|
|
21
|
+
expect(Array.from(unpacked.salt)).toEqual(Array.from(env.salt))
|
|
22
|
+
expect(Array.from(unpacked.iv)).toEqual(Array.from(env.iv))
|
|
23
|
+
expect(Array.from(unpacked.tag)).toEqual(Array.from(env.tag))
|
|
24
|
+
expect(Array.from(unpacked.ciphertext)).toEqual(Array.from(env.ciphertext))
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
test('unpack throws on truncated input', () => {
|
|
28
|
+
expect(() => unpackEnvelope(new Uint8Array(20))).toThrow('envelope shorter than header')
|
|
29
|
+
})
|
|
30
|
+
})
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto'
|
|
2
|
+
|
|
3
|
+
const SCRYPT_N = 1 << 15
|
|
4
|
+
const SCRYPT_R = 8
|
|
5
|
+
const SCRYPT_P = 1
|
|
6
|
+
const SCRYPT_MAXMEM = 64 * 1024 * 1024
|
|
7
|
+
const KEY_LEN = 32
|
|
8
|
+
const IV_LEN = 12
|
|
9
|
+
const TAG_LEN = 16
|
|
10
|
+
const SALT_LEN = 16
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* AES-256-GCM symmetric encryption, scrypt-derived key from a passphrase.
|
|
14
|
+
* MVP pattern: each agent has one symmetric key derived from the operator
|
|
15
|
+
* passphrase. Same scrypt parameters as the wallet keystore for consistency.
|
|
16
|
+
*
|
|
17
|
+
* Post-MVP: replace with TEE-sealed key for /agent/ partition + ECIES to
|
|
18
|
+
* operator pubkey for /user/ partition (section 22 wallet architecture).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export interface EncryptedEnvelope {
|
|
22
|
+
/** Random 16-byte salt used to derive the symmetric key. */
|
|
23
|
+
salt: Uint8Array
|
|
24
|
+
/** Random 12-byte GCM IV. */
|
|
25
|
+
iv: Uint8Array
|
|
26
|
+
/** 16-byte GCM auth tag. */
|
|
27
|
+
tag: Uint8Array
|
|
28
|
+
/** Ciphertext. */
|
|
29
|
+
ciphertext: Uint8Array
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function encrypt(plaintext: Uint8Array, passphrase: string): EncryptedEnvelope {
|
|
33
|
+
const salt = randomBytes(SALT_LEN)
|
|
34
|
+
const iv = randomBytes(IV_LEN)
|
|
35
|
+
const key = scryptSync(passphrase, salt, KEY_LEN, {
|
|
36
|
+
N: SCRYPT_N,
|
|
37
|
+
r: SCRYPT_R,
|
|
38
|
+
p: SCRYPT_P,
|
|
39
|
+
maxmem: SCRYPT_MAXMEM,
|
|
40
|
+
})
|
|
41
|
+
const cipher = createCipheriv('aes-256-gcm', key, iv)
|
|
42
|
+
const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()])
|
|
43
|
+
const tag = cipher.getAuthTag()
|
|
44
|
+
return {
|
|
45
|
+
salt,
|
|
46
|
+
iv,
|
|
47
|
+
tag: new Uint8Array(tag),
|
|
48
|
+
ciphertext: new Uint8Array(ciphertext),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function decrypt(envelope: EncryptedEnvelope, passphrase: string): Uint8Array {
|
|
53
|
+
const key = scryptSync(passphrase, envelope.salt, KEY_LEN, {
|
|
54
|
+
N: SCRYPT_N,
|
|
55
|
+
r: SCRYPT_R,
|
|
56
|
+
p: SCRYPT_P,
|
|
57
|
+
maxmem: SCRYPT_MAXMEM,
|
|
58
|
+
})
|
|
59
|
+
const decipher = createDecipheriv('aes-256-gcm', key, envelope.iv)
|
|
60
|
+
decipher.setAuthTag(envelope.tag)
|
|
61
|
+
const plaintext = Buffer.concat([decipher.update(envelope.ciphertext), decipher.final()])
|
|
62
|
+
return new Uint8Array(plaintext)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Pack envelope into a single byte buffer for storage: salt || iv || tag || ciphertext. */
|
|
66
|
+
export function packEnvelope(envelope: EncryptedEnvelope): Uint8Array {
|
|
67
|
+
const total = SALT_LEN + IV_LEN + TAG_LEN + envelope.ciphertext.length
|
|
68
|
+
const out = new Uint8Array(total)
|
|
69
|
+
out.set(envelope.salt, 0)
|
|
70
|
+
out.set(envelope.iv, SALT_LEN)
|
|
71
|
+
out.set(envelope.tag, SALT_LEN + IV_LEN)
|
|
72
|
+
out.set(envelope.ciphertext, SALT_LEN + IV_LEN + TAG_LEN)
|
|
73
|
+
return out
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Unpack a packed envelope back into its fields. */
|
|
77
|
+
export function unpackEnvelope(packed: Uint8Array): EncryptedEnvelope {
|
|
78
|
+
if (packed.length < SALT_LEN + IV_LEN + TAG_LEN) {
|
|
79
|
+
throw new Error('envelope shorter than header')
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
salt: packed.slice(0, SALT_LEN),
|
|
83
|
+
iv: packed.slice(SALT_LEN, SALT_LEN + IV_LEN),
|
|
84
|
+
tag: packed.slice(SALT_LEN + IV_LEN, SALT_LEN + IV_LEN + TAG_LEN),
|
|
85
|
+
ciphertext: packed.slice(SALT_LEN + IV_LEN + TAG_LEN),
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
import { agentPaths } from '../paths'
|
|
3
|
+
import { SqliteStorage } from './sqlite'
|
|
4
|
+
import type { Storage } from './types'
|
|
5
|
+
|
|
6
|
+
let singleton: SqliteStorage | null = null
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Shared, content-addressed local store at `~/.lyra/storage.sqlite`.
|
|
10
|
+
* Blobs are addressed by their `0x`+sha256 CID, so a single store serves all
|
|
11
|
+
* agents (a blob put by one is fetchable by hash from any). KV/log entries are
|
|
12
|
+
* namespaced by streamId. Replaces the prior decentralized storage backend.
|
|
13
|
+
*/
|
|
14
|
+
export function getStorage(): Storage {
|
|
15
|
+
if (!singleton) {
|
|
16
|
+
singleton = new SqliteStorage(join(agentPaths.root, 'storage.sqlite'))
|
|
17
|
+
}
|
|
18
|
+
return singleton
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Back-compat shim for the old "download blob by on-chain root hash" call.
|
|
23
|
+
* Blobs are content-addressed (rootHash === CID), so this is just `getBlob`.
|
|
24
|
+
* The network arg is ignored; kept so existing call sites need no reshaping.
|
|
25
|
+
*/
|
|
26
|
+
export async function downloadBlobByRoot(
|
|
27
|
+
_network: unknown,
|
|
28
|
+
rootHash: string,
|
|
29
|
+
): Promise<Uint8Array | null> {
|
|
30
|
+
return getStorage().getBlob(rootHash)
|
|
31
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type { Storage } from './types'
|
|
2
|
+
export { LocalStubStorage } from './local-stub'
|
|
3
|
+
export { SqliteStorage } from './sqlite'
|
|
4
|
+
export { getStorage, downloadBlobByRoot } from './factory'
|
|
5
|
+
export {
|
|
6
|
+
encrypt,
|
|
7
|
+
decrypt,
|
|
8
|
+
packEnvelope,
|
|
9
|
+
unpackEnvelope,
|
|
10
|
+
type EncryptedEnvelope,
|
|
11
|
+
} from './encryption'
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { appendFile, mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
import type { Storage } from './types'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Local-disk Storage stub. Layout under `${root}/storage-stub`:
|
|
8
|
+
* kv/<streamId>/<url-encoded-key> — latest value
|
|
9
|
+
* log/<streamId>.jsonl — append-only JSONL
|
|
10
|
+
* blob/<cid> — immutable by content hash
|
|
11
|
+
*/
|
|
12
|
+
export class LocalStubStorage implements Storage {
|
|
13
|
+
constructor(private readonly root: string) {}
|
|
14
|
+
|
|
15
|
+
private kvPath(stream: string, key: string): string {
|
|
16
|
+
return join(this.root, 'storage-stub', 'kv', stream, encodeURIComponent(key))
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
private logPath(stream: string): string {
|
|
20
|
+
return join(this.root, 'storage-stub', 'log', `${stream}.jsonl`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private blobPath(cid: string): string {
|
|
24
|
+
return join(this.root, 'storage-stub', 'blob', cid)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async putKV(stream: string, key: string, value: Uint8Array): Promise<void> {
|
|
28
|
+
const p = this.kvPath(stream, key)
|
|
29
|
+
await mkdir(join(p, '..'), { recursive: true })
|
|
30
|
+
await writeFile(p, value)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async getKV(stream: string, key: string): Promise<Uint8Array | null> {
|
|
34
|
+
return await readOrNull(this.kvPath(stream, key))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async appendLog(stream: string, entry: Uint8Array): Promise<string> {
|
|
38
|
+
const p = this.logPath(stream)
|
|
39
|
+
await mkdir(join(p, '..'), { recursive: true })
|
|
40
|
+
const cid = cidOf(entry)
|
|
41
|
+
const line = JSON.stringify({ cid, hex: Buffer.from(entry).toString('hex'), ts: Date.now() })
|
|
42
|
+
await appendFile(p, `${line}\n`)
|
|
43
|
+
return cid
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async putBlob(bytes: Uint8Array): Promise<string> {
|
|
47
|
+
const cid = cidOf(bytes)
|
|
48
|
+
const p = this.blobPath(cid)
|
|
49
|
+
await mkdir(join(p, '..'), { recursive: true })
|
|
50
|
+
await writeFile(p, bytes)
|
|
51
|
+
return cid
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async getBlob(cid: string): Promise<Uint8Array | null> {
|
|
55
|
+
return await readOrNull(this.blobPath(cid))
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function readOrNull(path: string): Promise<Uint8Array | null> {
|
|
60
|
+
try {
|
|
61
|
+
return new Uint8Array(await readFile(path))
|
|
62
|
+
} catch (e) {
|
|
63
|
+
if ((e as NodeJS.ErrnoException).code === 'ENOENT') return null
|
|
64
|
+
throw e
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function cidOf(bytes: Uint8Array): string {
|
|
69
|
+
return `0x${createHash('sha256').update(bytes).digest('hex')}`
|
|
70
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import { SqliteStorage } from './sqlite'
|
|
3
|
+
|
|
4
|
+
const enc = (s: string) => new TextEncoder().encode(s)
|
|
5
|
+
const dec = (b: Uint8Array | null) => (b ? new TextDecoder().decode(b) : null)
|
|
6
|
+
|
|
7
|
+
describe('SqliteStorage', () => {
|
|
8
|
+
it('round-trips a content-addressed blob', async () => {
|
|
9
|
+
const s = new SqliteStorage(':memory:')
|
|
10
|
+
const cid = await s.putBlob(enc('hello sui'))
|
|
11
|
+
expect(cid).toMatch(/^0x[0-9a-f]{64}$/)
|
|
12
|
+
// Same bytes -> same CID (content addressed, idempotent).
|
|
13
|
+
expect(await s.putBlob(enc('hello sui'))).toBe(cid)
|
|
14
|
+
expect(dec(await s.getBlob(cid))).toBe('hello sui')
|
|
15
|
+
expect(await s.getBlob('0xdeadbeef')).toBeNull()
|
|
16
|
+
s.close()
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('stores + overwrites KV and appends to the log', async () => {
|
|
20
|
+
const s = new SqliteStorage(':memory:')
|
|
21
|
+
await s.putKV('stream1', 'k', enc('v1'))
|
|
22
|
+
await s.putKV('stream1', 'k', enc('v2'))
|
|
23
|
+
expect(dec(await s.getKV('stream1', 'k'))).toBe('v2')
|
|
24
|
+
expect(await s.getKV('stream1', 'missing')).toBeNull()
|
|
25
|
+
const cid = await s.appendLog('stream1', enc('entry'))
|
|
26
|
+
expect(cid).toMatch(/^0x[0-9a-f]{64}$/)
|
|
27
|
+
s.close()
|
|
28
|
+
})
|
|
29
|
+
})
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { Database } from 'bun:sqlite'
|
|
2
|
+
import { createHash } from 'node:crypto'
|
|
3
|
+
import { mkdirSync } from 'node:fs'
|
|
4
|
+
import { dirname } from 'node:path'
|
|
5
|
+
import type { Storage } from './types'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* SQLite-backed Storage (via `bun:sqlite`). Replaces the prior decentralized
|
|
9
|
+
* blob backend with a local, zero-infra store — ideal for the agent's
|
|
10
|
+
* encrypted memory and easy to demo. Implements the same three primitives:
|
|
11
|
+
* - KV: mutable value per (stream, key)
|
|
12
|
+
* - Log: append-only entries, each addressed by content CID
|
|
13
|
+
* - Blob: immutable, content-addressed bytes
|
|
14
|
+
*
|
|
15
|
+
* CID convention matches LocalStubStorage: `0x` + sha256(bytes) hex.
|
|
16
|
+
*/
|
|
17
|
+
export class SqliteStorage implements Storage {
|
|
18
|
+
private readonly db: Database
|
|
19
|
+
|
|
20
|
+
/** @param path SQLite file path (defaults to in-memory). Parent dir is created. */
|
|
21
|
+
constructor(path = ':memory:') {
|
|
22
|
+
if (path !== ':memory:') {
|
|
23
|
+
mkdirSync(dirname(path), { recursive: true })
|
|
24
|
+
}
|
|
25
|
+
this.db = new Database(path, { create: true })
|
|
26
|
+
this.db.run('PRAGMA journal_mode = WAL;')
|
|
27
|
+
this.db.run(
|
|
28
|
+
`CREATE TABLE IF NOT EXISTS kv (
|
|
29
|
+
stream TEXT NOT NULL,
|
|
30
|
+
key TEXT NOT NULL,
|
|
31
|
+
value BLOB NOT NULL,
|
|
32
|
+
PRIMARY KEY (stream, key)
|
|
33
|
+
);`,
|
|
34
|
+
)
|
|
35
|
+
this.db.run(
|
|
36
|
+
`CREATE TABLE IF NOT EXISTS log (
|
|
37
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
38
|
+
stream TEXT NOT NULL,
|
|
39
|
+
cid TEXT NOT NULL,
|
|
40
|
+
entry BLOB NOT NULL,
|
|
41
|
+
ts INTEGER NOT NULL
|
|
42
|
+
);`,
|
|
43
|
+
)
|
|
44
|
+
this.db.run('CREATE INDEX IF NOT EXISTS log_stream_idx ON log (stream, id);')
|
|
45
|
+
this.db.run(
|
|
46
|
+
`CREATE TABLE IF NOT EXISTS blob (
|
|
47
|
+
cid TEXT PRIMARY KEY,
|
|
48
|
+
bytes BLOB NOT NULL
|
|
49
|
+
);`,
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async putKV(stream: string, key: string, value: Uint8Array): Promise<void> {
|
|
54
|
+
this.db
|
|
55
|
+
.query('INSERT OR REPLACE INTO kv (stream, key, value) VALUES (?, ?, ?)')
|
|
56
|
+
.run(stream, key, value)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async getKV(stream: string, key: string): Promise<Uint8Array | null> {
|
|
60
|
+
const row = this.db
|
|
61
|
+
.query('SELECT value FROM kv WHERE stream = ? AND key = ?')
|
|
62
|
+
.get(stream, key) as { value: Uint8Array } | null
|
|
63
|
+
return row ? new Uint8Array(row.value) : null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async appendLog(stream: string, entry: Uint8Array): Promise<string> {
|
|
67
|
+
const cid = cidOf(entry)
|
|
68
|
+
this.db
|
|
69
|
+
.query('INSERT INTO log (stream, cid, entry, ts) VALUES (?, ?, ?, ?)')
|
|
70
|
+
.run(stream, cid, entry, Date.now())
|
|
71
|
+
return cid
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async putBlob(bytes: Uint8Array): Promise<string> {
|
|
75
|
+
const cid = cidOf(bytes)
|
|
76
|
+
this.db.query('INSERT OR IGNORE INTO blob (cid, bytes) VALUES (?, ?)').run(cid, bytes)
|
|
77
|
+
return cid
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async getBlob(cid: string): Promise<Uint8Array | null> {
|
|
81
|
+
const row = this.db.query('SELECT bytes FROM blob WHERE cid = ?').get(cid) as {
|
|
82
|
+
bytes: Uint8Array
|
|
83
|
+
} | null
|
|
84
|
+
return row ? new Uint8Array(row.bytes) : null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Close the underlying database handle. */
|
|
88
|
+
close(): void {
|
|
89
|
+
this.db.close()
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function cidOf(bytes: Uint8Array): string {
|
|
94
|
+
return `0x${createHash('sha256').update(bytes).digest('hex')}`
|
|
95
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage interface abstracting Sui Storage's three primitives as used by lyra:
|
|
3
|
+
* - KV: mutable key→value per namespace
|
|
4
|
+
* - Log: append-only, returns CID per entry
|
|
5
|
+
* - Blob: immutable bytes, content-addressed
|
|
6
|
+
*
|
|
7
|
+
* Phase 1 ships a local-disk stub. Phase 5 ships the real @0gfoundation/0g-ts-sdk
|
|
8
|
+
* backend + on-chain-event replay for KV reads (per verified architecture).
|
|
9
|
+
*/
|
|
10
|
+
export interface Storage {
|
|
11
|
+
/** Put a value into a named stream under a key. */
|
|
12
|
+
putKV(streamId: string, key: string, value: Uint8Array): Promise<void>
|
|
13
|
+
/** Get the latest value for (streamId, key) or null. */
|
|
14
|
+
getKV(streamId: string, key: string): Promise<Uint8Array | null>
|
|
15
|
+
/** Append an entry to a stream's log. Returns CID (rootHash) of the entry. */
|
|
16
|
+
appendLog(streamId: string, entry: Uint8Array): Promise<string>
|
|
17
|
+
/** Upload immutable bytes, returns content CID. */
|
|
18
|
+
putBlob(bytes: Uint8Array): Promise<string>
|
|
19
|
+
/** Retrieve bytes by CID. */
|
|
20
|
+
getBlob(cid: string): Promise<Uint8Array | null>
|
|
21
|
+
}
|