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.
Files changed (109) hide show
  1. package/README.md +24 -0
  2. package/package.json +69 -0
  3. package/src/brain/compaction.ts +131 -0
  4. package/src/brain/frozen-prefix.ts +320 -0
  5. package/src/brain/history-persist.ts +154 -0
  6. package/src/brain/index.ts +43 -0
  7. package/src/brain/openai-brain.ts +533 -0
  8. package/src/brain/sanitize.ts +23 -0
  9. package/src/brain/stub.ts +20 -0
  10. package/src/brain/types.ts +129 -0
  11. package/src/chain.ts +75 -0
  12. package/src/claude-plugins/discovery.ts +152 -0
  13. package/src/claude-plugins/index.ts +6 -0
  14. package/src/claude-plugins/types.ts +38 -0
  15. package/src/commands/index.ts +16 -0
  16. package/src/commands/registry.ts +255 -0
  17. package/src/config.ts +213 -0
  18. package/src/economy/index.ts +6 -0
  19. package/src/events/index.ts +4 -0
  20. package/src/events/listeners.ts +37 -0
  21. package/src/events/queue.ts +63 -0
  22. package/src/events/router.ts +42 -0
  23. package/src/events/types.ts +28 -0
  24. package/src/format.ts +12 -0
  25. package/src/identity/agent-card.ts +110 -0
  26. package/src/identity/deployments.ts +20 -0
  27. package/src/identity/erc8004.ts +161 -0
  28. package/src/identity/index.ts +29 -0
  29. package/src/identity/keystore-blob.ts +60 -0
  30. package/src/identity/receipt.ts +27 -0
  31. package/src/identity/stub.ts +29 -0
  32. package/src/identity/types.ts +20 -0
  33. package/src/index.ts +372 -0
  34. package/src/locks.ts +233 -0
  35. package/src/mcp/discovery.ts +150 -0
  36. package/src/mcp/index.ts +10 -0
  37. package/src/mcp/manager.ts +110 -0
  38. package/src/mcp/stdio-client.ts +154 -0
  39. package/src/mcp/types.ts +44 -0
  40. package/src/memory/edit.ts +53 -0
  41. package/src/memory/encryption.ts +88 -0
  42. package/src/memory/fs-util.ts +15 -0
  43. package/src/memory/index-file.ts +74 -0
  44. package/src/memory/index-sync.ts +99 -0
  45. package/src/memory/index.ts +58 -0
  46. package/src/memory/list-tool.ts +105 -0
  47. package/src/memory/pack-blob.ts +120 -0
  48. package/src/memory/pack-gather.ts +112 -0
  49. package/src/memory/parser.ts +20 -0
  50. package/src/memory/read-tool.ts +198 -0
  51. package/src/memory/save-tool.ts +189 -0
  52. package/src/memory/scan.ts +63 -0
  53. package/src/memory/topic.ts +32 -0
  54. package/src/memory/types.ts +49 -0
  55. package/src/migration/index.ts +6 -0
  56. package/src/migration/option3-crypto.ts +127 -0
  57. package/src/operator/index.ts +9 -0
  58. package/src/operator/keychain.ts +53 -0
  59. package/src/operator/keystore-file.ts +33 -0
  60. package/src/operator/privkey-base.ts +60 -0
  61. package/src/operator/raw-privkey.ts +39 -0
  62. package/src/operator/signer.ts +46 -0
  63. package/src/operator/walletconnect.ts +454 -0
  64. package/src/pairing.ts +285 -0
  65. package/src/paths.ts +70 -0
  66. package/src/permission/dangerous.ts +108 -0
  67. package/src/permission/env-redact.ts +54 -0
  68. package/src/permission/index.ts +16 -0
  69. package/src/permission/path-guard.ts +114 -0
  70. package/src/permission/service.ts +191 -0
  71. package/src/plugins/context.ts +225 -0
  72. package/src/plugins/hooks.ts +81 -0
  73. package/src/plugins/index.ts +24 -0
  74. package/src/plugins/tool-search.ts +49 -0
  75. package/src/public/card.ts +67 -0
  76. package/src/runtime/activity.ts +29 -0
  77. package/src/runtime/index.ts +2 -0
  78. package/src/runtime/runtime.ts +113 -0
  79. package/src/sandbox/credentials.ts +25 -0
  80. package/src/sandbox/docker.ts +396 -0
  81. package/src/sandbox/factory.ts +99 -0
  82. package/src/sandbox/index.ts +15 -0
  83. package/src/sandbox/linux.ts +141 -0
  84. package/src/sandbox/local.ts +19 -0
  85. package/src/sandbox/macos.ts +71 -0
  86. package/src/sandbox/seatbelt-profile.ts +139 -0
  87. package/src/sandbox/types.ts +129 -0
  88. package/src/skills/index.ts +8 -0
  89. package/src/skills/scanner.ts +257 -0
  90. package/src/skills/triggers.ts +78 -0
  91. package/src/skills/types.ts +37 -0
  92. package/src/storage/encryption.ts +87 -0
  93. package/src/storage/factory.ts +31 -0
  94. package/src/storage/index.ts +11 -0
  95. package/src/storage/local-stub.ts +70 -0
  96. package/src/storage/sqlite.ts +95 -0
  97. package/src/storage/types.ts +21 -0
  98. package/src/tools/escalation.ts +200 -0
  99. package/src/tools/index.ts +11 -0
  100. package/src/tools/registry.ts +152 -0
  101. package/src/tools/types.ts +65 -0
  102. package/src/tools/zod-helpers.ts +36 -0
  103. package/src/tools/zod-schema.ts +99 -0
  104. package/src/wallet/drain.ts +79 -0
  105. package/src/wallet/eoa.ts +51 -0
  106. package/src/wallet/index.ts +47 -0
  107. package/src/wallet/keystore.ts +50 -0
  108. package/src/wallet/operator-keystore-crypto.ts +530 -0
  109. package/src/wallet/operator-session.ts +344 -0
@@ -0,0 +1,127 @@
1
+ import { createCipheriv, createDecipheriv, hkdfSync, randomBytes } from 'node:crypto'
2
+ import { secp256k1 } from '@noble/curves/secp256k1.js'
3
+ import { type Hex, bytesToHex, hexToBytes } from 'viem'
4
+
5
+ /**
6
+ * Phase 6.6 Option 3: TEE → TEE migration ECIES.
7
+ *
8
+ * The local gateway (which holds the plaintext agent privkey in its RAM)
9
+ * encrypts the privkey to the sandbox container's bootstrap pubkey. The CLI
10
+ * only ever relays ciphertext — the operator's laptop never sees the
11
+ * plaintext during a Local → Sandbox migration.
12
+ *
13
+ * Wire shape (envelope, base64-of):
14
+ * ephPubKey(33 bytes, compressed) || iv(12) || tag(16) || ct(N)
15
+ *
16
+ * Encryption:
17
+ * 1. Generate ephemeral secp256k1 keypair (ekPriv, ekPub).
18
+ * 2. Compute shared = ECDH(ekPriv, recipientPub) — uncompressed 64-byte point's
19
+ * x-coordinate (32 bytes).
20
+ * 3. Derive 32-byte AEAD key via HKDF-SHA256(shared, salt=ekPub, info='nebula-option3-v1').
21
+ * 4. AES-256-GCM(key, iv, plaintext).
22
+ *
23
+ * Decryption:
24
+ * 1. Recompute shared = ECDH(recipientPriv, ekPub).
25
+ * 2. Same HKDF derivation.
26
+ * 3. AES-256-GCM decrypt.
27
+ *
28
+ * Where this is wired:
29
+ * - `POST /migration/encrypt-to` on the local gateway: takes a container
30
+ * bootstrap pubkey + operator-signed migration request, returns the
31
+ * envelope here. Sandbox harness Phase 11 lands the gateway endpoint.
32
+ * - `POST /bootstrap/provision` on the sandbox container: receives the
33
+ * envelope and decrypts inside its sealed memory. Phase 11 lands the
34
+ * container endpoint.
35
+ *
36
+ * MVP caveat: in unsealed sandbox mode the local gateway can't
37
+ * cryptographically verify the container's bootstrap pubkey, so a network
38
+ * MITM could substitute a pubkey it controls and harvest the plaintext.
39
+ * Sealed sandbox mode (Phase 11 stretch) closes the gap via TDX attestation;
40
+ * the gateway verifies the attestation report before encrypting.
41
+ */
42
+ const HKDF_INFO = Buffer.from('nebula-option3-v1', 'utf8')
43
+
44
+ export interface Option3Envelope {
45
+ /** Ephemeral compressed secp256k1 pubkey (33 bytes), hex-encoded. */
46
+ ephPubkeyHex: Hex
47
+ /** Random 12-byte IV, hex-encoded. */
48
+ ivHex: Hex
49
+ /** AES-GCM 16-byte auth tag, hex-encoded. */
50
+ tagHex: Hex
51
+ /** AES-GCM ciphertext, hex-encoded. */
52
+ ciphertextHex: Hex
53
+ }
54
+
55
+ /**
56
+ * Encrypt a plaintext payload to the container's bootstrap pubkey.
57
+ *
58
+ * @param recipientPubkey 33-byte compressed or 65-byte uncompressed secp256k1 pubkey hex.
59
+ * @param plaintext bytes to encrypt (typically the agent privkey, 32 bytes).
60
+ */
61
+ export function encryptToPubkey(opts: {
62
+ recipientPubkey: Hex
63
+ plaintext: Uint8Array
64
+ }): Option3Envelope {
65
+ const recipientPubBytes = hexToBytes(opts.recipientPubkey)
66
+ if (recipientPubBytes.length !== 33 && recipientPubBytes.length !== 65) {
67
+ throw new Error(
68
+ `Invalid recipient pubkey length: ${recipientPubBytes.length} (expected 33 or 65 bytes)`,
69
+ )
70
+ }
71
+
72
+ const eph = secp256k1.keygen()
73
+ const ekPriv = eph.secretKey
74
+ const ekPubCompressed = secp256k1.getPublicKey(ekPriv, true)
75
+
76
+ const shared = secp256k1.getSharedSecret(ekPriv, recipientPubBytes, true)
77
+ const ikm = Buffer.from(shared.subarray(1)) // strip 0x02/0x03 sign byte → 32-byte x-coord
78
+ const aeadKey = Buffer.from(hkdfSync('sha256', ikm, Buffer.from(ekPubCompressed), HKDF_INFO, 32))
79
+
80
+ const iv = randomBytes(12)
81
+ const cipher = createCipheriv('aes-256-gcm', aeadKey, iv)
82
+ const ct = Buffer.concat([cipher.update(opts.plaintext), cipher.final()])
83
+ const tag = cipher.getAuthTag()
84
+
85
+ return {
86
+ ephPubkeyHex: bytesToHex(ekPubCompressed),
87
+ ivHex: bytesToHex(new Uint8Array(iv)),
88
+ tagHex: bytesToHex(new Uint8Array(tag)),
89
+ ciphertextHex: bytesToHex(new Uint8Array(ct)),
90
+ }
91
+ }
92
+
93
+ export function decryptWithPrivkey(opts: {
94
+ recipientPrivkey: Hex
95
+ envelope: Option3Envelope
96
+ }): Uint8Array {
97
+ const ekPubBytes = hexToBytes(opts.envelope.ephPubkeyHex)
98
+ const recipientPrivBytes = hexToBytes(opts.recipientPrivkey)
99
+
100
+ const shared = secp256k1.getSharedSecret(recipientPrivBytes, ekPubBytes, true)
101
+ const ikm = Buffer.from(shared.subarray(1))
102
+ const aeadKey = Buffer.from(hkdfSync('sha256', ikm, Buffer.from(ekPubBytes), HKDF_INFO, 32))
103
+
104
+ const iv = hexToBytes(opts.envelope.ivHex)
105
+ const tag = hexToBytes(opts.envelope.tagHex)
106
+ const ct = hexToBytes(opts.envelope.ciphertextHex)
107
+
108
+ const decipher = createDecipheriv('aes-256-gcm', aeadKey, iv)
109
+ decipher.setAuthTag(Buffer.from(tag))
110
+ return new Uint8Array(Buffer.concat([decipher.update(ct), decipher.final()]))
111
+ }
112
+
113
+ /** Convenience: derive a fresh container bootstrap keypair (used by sandbox harness). */
114
+ export function generateBootstrapKeypair(): {
115
+ privkeyHex: Hex
116
+ pubkeyHexCompressed: Hex
117
+ pubkeyHexUncompressed: Hex
118
+ } {
119
+ const { secretKey } = secp256k1.keygen()
120
+ const pubC = secp256k1.getPublicKey(secretKey, true)
121
+ const pubU = secp256k1.getPublicKey(secretKey, false)
122
+ return {
123
+ privkeyHex: bytesToHex(secretKey),
124
+ pubkeyHexCompressed: bytesToHex(pubC),
125
+ pubkeyHexUncompressed: bytesToHex(pubU),
126
+ }
127
+ }
@@ -0,0 +1,9 @@
1
+ export type { OperatorSigner } from './signer'
2
+ export { KeychainOperatorSigner } from './keychain'
3
+ export { KeystoreFileOperatorSigner } from './keystore-file'
4
+ export { RawPrivkeyOperatorSigner } from './raw-privkey'
5
+ export {
6
+ WalletConnectOperatorSigner,
7
+ NEBULA_WC_PROJECT_ID,
8
+ } from './walletconnect'
9
+ export type { WalletConnectOperatorSignerOptions } from './walletconnect'
@@ -0,0 +1,53 @@
1
+ import { spawnSync } from 'node:child_process'
2
+ import type { Hex } from 'viem'
3
+ import { PrivkeyOperatorSigner } from './privkey-base'
4
+
5
+ /** Safe subset of characters allowed in a keychain service name. Rejects
6
+ * shell metacharacters so user-supplied service names can never inject.
7
+ */
8
+ const SERVICE_NAME_RE = /^[a-zA-Z0-9._-]{1,128}$/
9
+
10
+ /**
11
+ * Loads the operator privkey from the macOS Keychain under a service name.
12
+ *
13
+ * First-class operator wallet source on macOS. Same trust model as a password
14
+ * manager: the key is encrypted at rest by the OS, unlocked by the user's
15
+ * login password, accessible to the process the user is running. Keychain
16
+ * entries can optionally be gated by Touch ID via the biometric helper
17
+ * (shipped as a separate Swift binary, see Phase 6.5b).
18
+ *
19
+ * Linux and Windows equivalents (libsecret, Credential Manager) are post-MVP.
20
+ * For now non-macOS users pick one of the other OperatorSigner implementations
21
+ * (WalletConnect, keystore file, raw privkey).
22
+ *
23
+ * Service name is user-chosen: we default to `nebula.operator` but the caller
24
+ * can pass any string. Existing dev setups may use `dev.deployer` etc.
25
+ */
26
+ export class KeychainOperatorSigner extends PrivkeyOperatorSigner {
27
+ readonly source: string
28
+
29
+ constructor(private readonly keychainService: string = 'dev.deployer') {
30
+ super()
31
+ if (!SERVICE_NAME_RE.test(keychainService)) {
32
+ throw new Error(
33
+ `Invalid keychain service name. Allowed: alphanumerics, dot, underscore, hyphen (max 128). Got: ${keychainService}`,
34
+ )
35
+ }
36
+ this.source = `keychain:${keychainService}`
37
+ }
38
+
39
+ protected async loadPrivkey(): Promise<Hex> {
40
+ const result = spawnSync(
41
+ 'security',
42
+ ['find-generic-password', '-s', this.keychainService, '-w'],
43
+ { encoding: 'utf8' },
44
+ )
45
+ if (result.status !== 0) {
46
+ throw new Error(
47
+ `security find-generic-password failed for service '${this.keychainService}': ${result.stderr?.trim() || `exit ${result.status}`}`,
48
+ )
49
+ }
50
+ const raw = result.stdout.trim()
51
+ return (raw.startsWith('0x') ? raw : `0x${raw}`) as Hex
52
+ }
53
+ }
@@ -0,0 +1,33 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { Wallet as EthersWallet } from 'ethers'
3
+ import type { Hex } from 'viem'
4
+ import { PrivkeyOperatorSigner } from './privkey-base'
5
+
6
+ /**
7
+ * Operator source backed by a standard geth-format encrypted JSON keystore.
8
+ * Portable across machines, no network dependency, no OS-specific keystore.
9
+ *
10
+ * Caller is responsible for prompting the user for the passphrase; the signer
11
+ * just decrypts lazily on first use and caches the privkey in memory.
12
+ */
13
+ export class KeystoreFileOperatorSigner extends PrivkeyOperatorSigner {
14
+ readonly source: string
15
+
16
+ constructor(
17
+ private readonly opts: {
18
+ /** Absolute path to the encrypted JSON keystore (geth format). */
19
+ path: string
20
+ /** Pre-collected passphrase. CLI prompts; core decrypts. */
21
+ passphrase: string
22
+ },
23
+ ) {
24
+ super()
25
+ this.source = `keystore:${opts.path}`
26
+ }
27
+
28
+ protected async loadPrivkey(): Promise<Hex> {
29
+ const json = await readFile(this.opts.path, 'utf8')
30
+ const wallet = await EthersWallet.fromEncryptedJson(json, this.opts.passphrase)
31
+ return wallet.privateKey as Hex
32
+ }
33
+ }
@@ -0,0 +1,60 @@
1
+ import {
2
+ http,
3
+ type Address,
4
+ type Chain,
5
+ type Hex,
6
+ type PublicClient,
7
+ type WalletClient,
8
+ createPublicClient,
9
+ } from 'viem'
10
+ import { type PrivateKeyAccount, privateKeyToAccount } from 'viem/accounts'
11
+ import { makeViemClients, mantleChain } from '../chain'
12
+ import { NETWORK_RPC, type NebulaNetwork } from '../config'
13
+ import type { OperatorSigner } from './signer'
14
+
15
+ /**
16
+ * Shared base for privkey-backed operator sources. Subclasses only need to
17
+ * implement `loadPrivkey()` — everything else (viem account/wallet/public
18
+ * clients, caching) is identical across keychain / keystore-file / raw
19
+ * privkey, and that shared plumbing lives here.
20
+ *
21
+ * WalletConnect does NOT extend this base: its signing happens on a paired
22
+ * phone, there's no local privkey, so it has its own custom `walletClient`.
23
+ */
24
+ export abstract class PrivkeyOperatorSigner implements OperatorSigner {
25
+ abstract readonly source: string
26
+ private cachedPrivkey: Hex | null = null
27
+ private cachedAccount: PrivateKeyAccount | null = null
28
+
29
+ /** Subclass hook: yield a 32-byte hex privkey. Caller invoked at most once. */
30
+ protected abstract loadPrivkey(): Promise<Hex>
31
+
32
+ protected async getPrivkey(): Promise<Hex> {
33
+ if (!this.cachedPrivkey) this.cachedPrivkey = await this.loadPrivkey()
34
+ return this.cachedPrivkey
35
+ }
36
+
37
+ async account(): Promise<PrivateKeyAccount> {
38
+ if (!this.cachedAccount) this.cachedAccount = privateKeyToAccount(await this.getPrivkey())
39
+ return this.cachedAccount
40
+ }
41
+
42
+ async address(): Promise<Address> {
43
+ return (await this.account()).address
44
+ }
45
+
46
+ async walletClient(network: NebulaNetwork): Promise<WalletClient> {
47
+ return makeViemClients({ network, privkeyHex: await this.getPrivkey() }).walletClient
48
+ }
49
+
50
+ async publicClient(network: NebulaNetwork): Promise<PublicClient> {
51
+ return createPublicClient({
52
+ transport: http(NETWORK_RPC[network]),
53
+ chain: mantleChain(network),
54
+ })
55
+ }
56
+
57
+ chain(network: NebulaNetwork): Chain {
58
+ return mantleChain(network)
59
+ }
60
+ }
@@ -0,0 +1,39 @@
1
+ import type { Hex } from 'viem'
2
+ import { PrivkeyOperatorSigner } from './privkey-base'
3
+
4
+ /**
5
+ * Operator source backed by a raw private key supplied as a hex string.
6
+ *
7
+ * CLI layer collects the hex (stdin prompt, `--privkey` flag, or
8
+ * `NEBULA_OPERATOR_PRIVKEY` env var) and passes it in. The signer just wraps.
9
+ * Intended for CI/scripting and for users who prefer no-on-disk secrets.
10
+ *
11
+ * The hex may be passed with or without the `0x` prefix; the signer normalizes.
12
+ */
13
+ export class RawPrivkeyOperatorSigner extends PrivkeyOperatorSigner {
14
+ readonly source: string
15
+ private readonly privkeyHex: Hex
16
+
17
+ constructor(opts: {
18
+ /** Raw private key hex, with or without `0x` prefix. */
19
+ privkey: string
20
+ /**
21
+ * Optional label for logs (e.g. `"env:NEBULA_OPERATOR_PRIVKEY"` or
22
+ * `"stdin"`). Defaults to `"raw-privkey"` which tells the user nothing.
23
+ */
24
+ sourceLabel?: string
25
+ }) {
26
+ super()
27
+ const raw = opts.privkey.trim()
28
+ const withPrefix = raw.startsWith('0x') ? raw : `0x${raw}`
29
+ if (!/^0x[0-9a-fA-F]{64}$/.test(withPrefix)) {
30
+ throw new Error('RawPrivkeyOperatorSigner: privkey must be 32 bytes hex (with or without 0x)')
31
+ }
32
+ this.privkeyHex = withPrefix as Hex
33
+ this.source = opts.sourceLabel ? `raw-privkey:${opts.sourceLabel}` : 'raw-privkey'
34
+ }
35
+
36
+ protected async loadPrivkey(): Promise<Hex> {
37
+ return this.privkeyHex
38
+ }
39
+ }
@@ -0,0 +1,46 @@
1
+ import type { Address, Chain, PublicClient, WalletClient } from 'viem'
2
+ import type { LocalAccount } from 'viem/accounts'
3
+ import type { NebulaNetwork } from '../config'
4
+
5
+ /**
6
+ * The operator is the human (or organization) behind an nebula agent. Per
7
+ * project-nebula.md section 22.1, the operator's wallet is what OWNS the
8
+ * ERC-7857 iNFT. A separate agent EOA (generated by `nebula init`) is a
9
+ * pay-for-infra key that the operator approves via `setApprovalForAll`.
10
+ *
11
+ * Operator signing material can come from many sources. First-class sources:
12
+ * - WalletConnect v2 (QR pair to any WC-compatible mobile wallet)
13
+ * - OS keychain (macOS Keychain, later libsecret and Credential Manager)
14
+ * - Encrypted keystore file (geth-format JSON)
15
+ * - Raw private key (CLI/scripting, env var, or stdin prompt)
16
+ *
17
+ * This interface is the abstraction a caller uses without caring which source
18
+ * is behind it. All four implementations satisfy the same contract, so init
19
+ * flow, topup flow, and future governance flows can pick at runtime.
20
+ */
21
+ export interface OperatorSigner {
22
+ /** Source label for logs + UI ("keychain:<service>", "walletconnect:<peer>", "keystore:<path>", ...). */
23
+ readonly source: string
24
+ /** Operator's on-chain address. Becomes the iNFT owner. */
25
+ address(): Promise<Address>
26
+ /**
27
+ * viem LocalAccount for composing with wallet clients. For privkey-based
28
+ * sources (keychain, keystore file, raw) this is a `PrivateKeyAccount`. For
29
+ * WalletConnect it's a custom local account whose sign methods delegate to
30
+ * the paired mobile wallet over the WC relay. Keeping the return type as
31
+ * `LocalAccount` (not the broader `Account`) guarantees `signTypedData` is
32
+ * reachable, which Phase 6.6 needs for the sign-derived-key keystore.
33
+ */
34
+ account(): Promise<LocalAccount>
35
+ /** A wallet client bound to `network` that signs from the operator's address. */
36
+ walletClient(network: NebulaNetwork): Promise<WalletClient>
37
+ /** A public client for the same network (reads only). */
38
+ publicClient(network: NebulaNetwork): Promise<PublicClient>
39
+ /** Chain metadata as viem Chain, per network. */
40
+ chain(network: NebulaNetwork): Chain
41
+ /**
42
+ * Optional teardown. WalletConnect uses this to close the session cleanly;
43
+ * privkey-based sources have nothing to release. Safe to call unconditionally.
44
+ */
45
+ close?(): Promise<void>
46
+ }