@elisym/sdk 0.15.2 → 0.17.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/dist/node.cjs.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/primitives/encryption.ts","../src/agent-store/writer.ts","../src/config/global-schema.ts","../src/config/global.ts"],"names":["randomBytes","scryptSync","createCipheriv","Buffer","createDecipheriv","path","mkdir","dirname","writeFile","rename","z","readFile","YAML"],"mappings":"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","file":"node.cjs","sourcesContent":["/**\n * Secret encryption/decryption for agent config files.\n * Uses scrypt (KDF) + AES-256-GCM (cipher).\n * Format: \"encrypted:v1:\" + base64(salt[16] + iv[12] + ciphertext + tag[16])\n *\n * scrypt params: N=2^17, r=8, p=1 (~128 MB RAM per derivation).\n *\n * Node.js/Bun only - not available in browsers. Reachable only via the\n * '@elisym/sdk/node' subpath, which browser bundlers will not resolve.\n */\n\nimport { Buffer } from 'node:buffer';\nimport { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';\n\nconst PREFIX = 'encrypted:v1:';\nconst SALT_LENGTH = 16;\nconst IV_LENGTH = 12;\nconst TAG_LENGTH = 16;\nconst KEY_LENGTH = 32; // AES-256\n// v1: N=2^17 (OWASP minimum). v2 will use N=2^20 with format migration.\nconst SCRYPT_N = 2 ** 17;\nconst SCRYPT_R = 8;\nconst SCRYPT_P = 1;\nconst SCRYPT_MAXMEM = 128 * SCRYPT_N * SCRYPT_R * 2; // 2x the minimum required memory\n\n/** Check if a value is encrypted (has the encrypted:v1: prefix). */\nexport function isEncrypted(value: string): boolean {\n return value.startsWith(PREFIX);\n}\n\n/** Encrypt a plaintext secret with a passphrase. Returns \"encrypted:v1:base64...\". Node.js/Bun only. */\nexport function encryptSecret(plaintext: string, passphrase: string): string {\n if (!passphrase) {\n throw new Error('Passphrase must not be empty.');\n }\n\n const salt = randomBytes(SALT_LENGTH);\n const key = scryptSync(passphrase, salt, KEY_LENGTH, {\n N: SCRYPT_N,\n r: SCRYPT_R,\n p: SCRYPT_P,\n maxmem: SCRYPT_MAXMEM,\n });\n const iv = randomBytes(IV_LENGTH);\n\n const cipher = createCipheriv('aes-256-gcm', key, iv);\n const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);\n const tag = cipher.getAuthTag();\n\n const payload = Buffer.concat([salt, iv, encrypted, tag]);\n return PREFIX + payload.toString('base64');\n}\n\n/** Decrypt an encrypted secret with a passphrase. Throws on wrong passphrase or corrupted data. Node.js/Bun only. */\nexport function decryptSecret(encrypted: string, passphrase: string): string {\n if (!isEncrypted(encrypted)) {\n throw new Error('Value is not encrypted (missing encrypted:v1: prefix).');\n }\n if (!passphrase) {\n throw new Error('Passphrase must not be empty.');\n }\n\n const payload = Buffer.from(encrypted.slice(PREFIX.length), 'base64');\n if (payload.length < SALT_LENGTH + IV_LENGTH + TAG_LENGTH) {\n throw new Error('Encrypted payload is too short.');\n }\n\n const salt = payload.subarray(0, SALT_LENGTH);\n const iv = payload.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);\n const tag = payload.subarray(payload.length - TAG_LENGTH);\n const ciphertext = payload.subarray(SALT_LENGTH + IV_LENGTH, payload.length - TAG_LENGTH);\n\n const key = scryptSync(passphrase, salt, KEY_LENGTH, {\n N: SCRYPT_N,\n r: SCRYPT_R,\n p: SCRYPT_P,\n maxmem: SCRYPT_MAXMEM,\n });\n\n const decipher = createDecipheriv('aes-256-gcm', key, iv);\n decipher.setAuthTag(tag);\n\n try {\n const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);\n return decrypted.toString('utf8');\n } catch {\n throw new Error('Decryption failed. Wrong passphrase or corrupted data.');\n }\n}\n","/**\n * Write agent files: elisym.yaml, .secrets.json, .gitignore, and create agent dirs.\n */\n\nimport { randomBytes } from 'node:crypto';\nimport { mkdir, rename, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport YAML from 'yaml';\nimport { encryptSecret, isEncrypted } from '../primitives/encryption';\nimport { agentPaths, type AgentPaths } from './paths';\nimport { elisymRootFor, type AgentSource } from './resolver';\nimport { ElisymYamlSchema, SecretsSchema, type ElisymYaml, type Secrets } from './schema';\nimport { renderInitialYaml } from './template';\n\nconst GITIGNORE_CONTENT = [\n '# elisym private state - do not commit.',\n '.secrets.json',\n '.media-cache.json',\n '.jobs.json',\n '.jobs.json.corrupt.*',\n '.customer-history.json',\n '.contacts.json',\n '',\n].join('\\n');\n\nexport interface CreateAgentDirOptions {\n target: AgentSource;\n name: string;\n cwd: string;\n /**\n * For `target: 'project'`: if no .elisym/ dir exists above cwd,\n * where should we create one? Defaults to cwd.\n */\n projectRoot?: string;\n}\n\nexport interface CreatedAgentDir {\n dir: string;\n paths: AgentPaths;\n source: AgentSource;\n createdNewElisymRoot: boolean;\n}\n\n/**\n * Create (or reuse) the directory layout for a new agent. Idempotent: if the\n * agent directory already exists, returns its paths without overwriting.\n * Writes `.gitignore` in project-local .elisym/ on first creation.\n */\nexport async function createAgentDir(options: CreateAgentDirOptions): Promise<CreatedAgentDir> {\n const { target, name, cwd, projectRoot } = options;\n\n const existingRoot = elisymRootFor(target, cwd);\n let elisymRoot: string;\n let createdNewElisymRoot = false;\n\n if (existingRoot) {\n elisymRoot = existingRoot;\n } else if (target === 'project') {\n elisymRoot = join(projectRoot ?? cwd, '.elisym');\n createdNewElisymRoot = true;\n } else {\n throw new Error('homeElisymDir should always exist conceptually - this is unreachable');\n }\n\n const agentDir = join(elisymRoot, name);\n const mode = target === 'home' ? 0o700 : 0o755;\n await mkdir(agentDir, { recursive: true, mode });\n await mkdir(join(agentDir, 'skills'), { recursive: true, mode });\n\n if (target === 'project') {\n const gitignorePath = join(elisymRoot, '.gitignore');\n await writeFileIfMissing(gitignorePath, GITIGNORE_CONTENT, 0o644);\n }\n\n return {\n dir: agentDir,\n paths: agentPaths(agentDir),\n source: target,\n createdNewElisymRoot,\n };\n}\n\n/** Write elisym.yaml atomically. Validates via Zod before writing. */\nexport async function writeYaml(agentDir: string, yaml: ElisymYaml): Promise<void> {\n const validated = ElisymYamlSchema.parse(yaml);\n const body = YAML.stringify(validated);\n const target = agentPaths(agentDir).yaml;\n await writeFileAtomic(target, body, 0o644);\n}\n\n/**\n * Write a brand-new elisym.yaml with descriptive header comments and\n * commented-out examples for unset optional fields. Use only at agent\n * creation time (CLI `init`, MCP `create_agent`). Subsequent edits go\n * through `writeYaml`, which discards comments.\n */\nexport async function writeYamlInitial(agentDir: string, yaml: ElisymYaml): Promise<void> {\n const validated = ElisymYamlSchema.parse(yaml);\n const body = renderInitialYaml(validated);\n const target = agentPaths(agentDir).yaml;\n await writeFileAtomic(target, body, 0o644);\n}\n\n/**\n * Write .secrets.json atomically. If `passphrase` is given, encrypts all\n * plaintext secret fields (already-encrypted values are left as-is).\n * Generic over `llm_api_keys` so any registered provider's key is\n * encrypted without per-provider plumbing here.\n */\nexport async function writeSecrets(\n agentDir: string,\n secrets: Secrets,\n passphrase?: string,\n): Promise<void> {\n const validated = SecretsSchema.parse(secrets);\n let encryptedLlmKeys: Record<string, string> | undefined;\n if (validated.llm_api_keys) {\n encryptedLlmKeys = {};\n for (const [providerId, value] of Object.entries(validated.llm_api_keys)) {\n if (value) {\n encryptedLlmKeys[providerId] = maybeEncrypt(value, passphrase);\n }\n }\n if (Object.keys(encryptedLlmKeys).length === 0) {\n encryptedLlmKeys = undefined;\n }\n }\n const finalSecrets: Secrets = {\n nostr_secret_key: maybeEncrypt(validated.nostr_secret_key, passphrase),\n solana_secret_key: validated.solana_secret_key\n ? maybeEncrypt(validated.solana_secret_key, passphrase)\n : undefined,\n llm_api_keys: encryptedLlmKeys,\n };\n const body = JSON.stringify(finalSecrets, null, 2) + '\\n';\n const target = agentPaths(agentDir).secrets;\n await writeFileAtomic(target, body, 0o600);\n}\n\nfunction maybeEncrypt(value: string, passphrase: string | undefined): string {\n if (!passphrase) {\n return value;\n }\n if (isEncrypted(value)) {\n return value;\n }\n return encryptSecret(value, passphrase);\n}\n\n/** Atomic write: temp file + rename. Preserves mode. */\nexport async function writeFileAtomic(\n path: string,\n data: string | Buffer,\n mode: number,\n): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n const tmpPath = `${path}.tmp.${randomBytes(6).toString('hex')}`;\n await writeFile(tmpPath, data, { mode });\n try {\n await rename(tmpPath, path);\n } catch (e) {\n // Best-effort cleanup of temp file on rename failure.\n try {\n const { unlink } = await import('node:fs/promises');\n await unlink(tmpPath);\n } catch {\n /* ignore */\n }\n throw e;\n }\n}\n\nasync function writeFileIfMissing(path: string, data: string, mode: number): Promise<void> {\n try {\n await writeFile(path, data, { mode, flag: 'wx' });\n } catch (e: unknown) {\n // wx fails with EEXIST if file exists - that's fine.\n if (!isEexist(e)) {\n throw e;\n }\n }\n}\n\nfunction isEexist(e: unknown): boolean {\n return (\n typeof e === 'object' && e !== null && 'code' in e && (e as { code: string }).code === 'EEXIST'\n );\n}\n","/**\n * Zod schemas and types for `~/.elisym/config.yaml`.\n *\n * Split from `./global` so the schemas can be re-exported from the\n * browser-safe `@elisym/sdk` entry point without dragging in `node:fs/promises`\n * (which the loader/writer in `./global` needs).\n */\n\nimport { z } from 'zod';\n\nexport const SessionSpendLimitEntrySchema = z\n .object({\n chain: z.enum(['solana']),\n token: z\n .string()\n .min(1)\n .max(16)\n .regex(/^[a-z0-9]+$/, 'token must be lowercase alphanumeric'),\n mint: z.string().min(1).max(64).optional(),\n amount: z.number().positive().finite(),\n })\n .strict();\n\nexport const GlobalConfigSchema = z\n .object({\n session_spend_limits: z.array(SessionSpendLimitEntrySchema).max(16).optional(),\n })\n .strict();\n\nexport type SessionSpendLimitEntry = z.infer<typeof SessionSpendLimitEntrySchema>;\nexport type GlobalConfig = z.infer<typeof GlobalConfigSchema>;\n","/**\n * Global (not per-agent) config stored at `~/.elisym/config.yaml`.\n *\n * Node.js/Bun only - reads and writes the filesystem. Browser code must not\n * import this module; import the schemas from `./global-schema` instead, or\n * the loader/writer from `@elisym/sdk/node`.\n */\n\nimport { readFile } from 'node:fs/promises';\nimport YAML from 'yaml';\nimport { writeFileAtomic } from '../agent-store/writer';\nimport { GlobalConfigSchema, type GlobalConfig } from './global-schema';\n\nexport {\n GlobalConfigSchema,\n SessionSpendLimitEntrySchema,\n type GlobalConfig,\n type SessionSpendLimitEntry,\n} from './global-schema';\n\nfunction isEnoent(e: unknown): boolean {\n return (\n typeof e === 'object' && e !== null && 'code' in e && (e as { code: string }).code === 'ENOENT'\n );\n}\n\n/**\n * Read and validate `~/.elisym/config.yaml`. Returns `{}` if missing. Throws\n * on malformed YAML or schema violations — the MCP server treats these as fatal\n * at startup rather than silently ignoring bad overrides.\n */\nexport async function loadGlobalConfig(path: string): Promise<GlobalConfig> {\n let raw: string;\n try {\n raw = await readFile(path, 'utf-8');\n } catch (e) {\n if (isEnoent(e)) {\n return {};\n }\n throw e;\n }\n if (raw.trim() === '') {\n return {};\n }\n const parsed: unknown = YAML.parse(raw);\n return GlobalConfigSchema.parse(parsed ?? {});\n}\n\n/** Write the config YAML atomically. Validates via Zod before writing. */\nexport async function writeGlobalConfig(path: string, config: GlobalConfig): Promise<void> {\n const validated = GlobalConfigSchema.parse(config);\n const body = YAML.stringify(validated);\n await writeFileAtomic(path, body, 0o644);\n}\n"]}
1
+ {"version":3,"sources":["../src/primitives/encryption.ts","../src/agent-store/writer.ts","../src/config/global-schema.ts","../src/config/global.ts"],"names":["randomBytes","scryptSync","createCipheriv","Buffer","createDecipheriv","path","mkdir","dirname","writeFile","rename","z","readFile","YAML"],"mappings":"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","file":"node.cjs","sourcesContent":["/**\n * Secret encryption/decryption for agent config files.\n * Uses scrypt (KDF) + AES-256-GCM (cipher).\n * Format: \"encrypted:v1:\" + base64(salt[16] + iv[12] + ciphertext + tag[16])\n *\n * scrypt params: N=2^17, r=8, p=1 (~128 MB RAM per derivation).\n *\n * Node.js/Bun only - not available in browsers. Reachable only via the\n * '@elisym/sdk/node' subpath, which browser bundlers will not resolve.\n */\n\nimport { Buffer } from 'node:buffer';\nimport { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';\n\nconst PREFIX = 'encrypted:v1:';\nconst SALT_LENGTH = 16;\nconst IV_LENGTH = 12;\nconst TAG_LENGTH = 16;\nconst KEY_LENGTH = 32; // AES-256\n// v1: N=2^17 (OWASP minimum). v2 will use N=2^20 with format migration.\nconst SCRYPT_N = 2 ** 17;\nconst SCRYPT_R = 8;\nconst SCRYPT_P = 1;\nconst SCRYPT_MAXMEM = 128 * SCRYPT_N * SCRYPT_R * 2; // 2x the minimum required memory\n\n/** Check if a value is encrypted (has the encrypted:v1: prefix). */\nexport function isEncrypted(value: string): boolean {\n return value.startsWith(PREFIX);\n}\n\n/** Encrypt a plaintext secret with a passphrase. Returns \"encrypted:v1:base64...\". Node.js/Bun only. */\nexport function encryptSecret(plaintext: string, passphrase: string): string {\n if (!passphrase) {\n throw new Error('Passphrase must not be empty.');\n }\n\n const salt = randomBytes(SALT_LENGTH);\n const key = scryptSync(passphrase, salt, KEY_LENGTH, {\n N: SCRYPT_N,\n r: SCRYPT_R,\n p: SCRYPT_P,\n maxmem: SCRYPT_MAXMEM,\n });\n const iv = randomBytes(IV_LENGTH);\n\n const cipher = createCipheriv('aes-256-gcm', key, iv);\n const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);\n const tag = cipher.getAuthTag();\n\n const payload = Buffer.concat([salt, iv, encrypted, tag]);\n return PREFIX + payload.toString('base64');\n}\n\n/** Decrypt an encrypted secret with a passphrase. Throws on wrong passphrase or corrupted data. Node.js/Bun only. */\nexport function decryptSecret(encrypted: string, passphrase: string): string {\n if (!isEncrypted(encrypted)) {\n throw new Error('Value is not encrypted (missing encrypted:v1: prefix).');\n }\n if (!passphrase) {\n throw new Error('Passphrase must not be empty.');\n }\n\n const payload = Buffer.from(encrypted.slice(PREFIX.length), 'base64');\n if (payload.length < SALT_LENGTH + IV_LENGTH + TAG_LENGTH) {\n throw new Error('Encrypted payload is too short.');\n }\n\n const salt = payload.subarray(0, SALT_LENGTH);\n const iv = payload.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);\n const tag = payload.subarray(payload.length - TAG_LENGTH);\n const ciphertext = payload.subarray(SALT_LENGTH + IV_LENGTH, payload.length - TAG_LENGTH);\n\n const key = scryptSync(passphrase, salt, KEY_LENGTH, {\n N: SCRYPT_N,\n r: SCRYPT_R,\n p: SCRYPT_P,\n maxmem: SCRYPT_MAXMEM,\n });\n\n const decipher = createDecipheriv('aes-256-gcm', key, iv);\n decipher.setAuthTag(tag);\n\n try {\n const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);\n return decrypted.toString('utf8');\n } catch {\n throw new Error('Decryption failed. Wrong passphrase or corrupted data.');\n }\n}\n","/**\n * Write agent files: elisym.yaml, .secrets.json, .gitignore, and create agent dirs.\n */\n\nimport { randomBytes } from 'node:crypto';\nimport { mkdir, rename, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport YAML from 'yaml';\nimport { encryptSecret, isEncrypted } from '../primitives/encryption';\nimport { agentPaths, type AgentPaths } from './paths';\nimport { elisymRootFor, type AgentSource } from './resolver';\nimport { ElisymYamlSchema, SecretsSchema, type ElisymYaml, type Secrets } from './schema';\nimport { renderInitialYaml } from './template';\n\nconst GITIGNORE_CONTENT = [\n '# elisym private state - do not commit.',\n '.secrets.json',\n '.media-cache.json',\n '.jobs.json',\n '.jobs.json.corrupt.*',\n '.customer-history.json',\n '.contacts.json',\n '',\n].join('\\n');\n\nexport interface CreateAgentDirOptions {\n target: AgentSource;\n name: string;\n cwd: string;\n /**\n * For `target: 'project'`: if no .elisym/ dir exists above cwd,\n * where should we create one? Defaults to cwd.\n */\n projectRoot?: string;\n}\n\nexport interface CreatedAgentDir {\n dir: string;\n paths: AgentPaths;\n source: AgentSource;\n createdNewElisymRoot: boolean;\n}\n\n/**\n * Create (or reuse) the directory layout for a new agent. Idempotent: if the\n * agent directory already exists, returns its paths without overwriting.\n * Writes `.gitignore` in project-local .elisym/ on first creation.\n */\nexport async function createAgentDir(options: CreateAgentDirOptions): Promise<CreatedAgentDir> {\n const { target, name, cwd, projectRoot } = options;\n\n const existingRoot = elisymRootFor(target, cwd);\n let elisymRoot: string;\n let createdNewElisymRoot = false;\n\n if (existingRoot) {\n elisymRoot = existingRoot;\n } else if (target === 'project') {\n elisymRoot = join(projectRoot ?? cwd, '.elisym');\n createdNewElisymRoot = true;\n } else {\n throw new Error('homeElisymDir should always exist conceptually - this is unreachable');\n }\n\n const agentDir = join(elisymRoot, name);\n const mode = target === 'home' ? 0o700 : 0o755;\n await mkdir(agentDir, { recursive: true, mode });\n await mkdir(join(agentDir, 'skills'), { recursive: true, mode });\n\n if (target === 'project') {\n const gitignorePath = join(elisymRoot, '.gitignore');\n await writeFileIfMissing(gitignorePath, GITIGNORE_CONTENT, 0o644);\n }\n\n return {\n dir: agentDir,\n paths: agentPaths(agentDir),\n source: target,\n createdNewElisymRoot,\n };\n}\n\n/** Write elisym.yaml atomically. Validates via Zod before writing. */\nexport async function writeYaml(agentDir: string, yaml: ElisymYaml): Promise<void> {\n const validated = ElisymYamlSchema.parse(yaml);\n const body = YAML.stringify(validated);\n const target = agentPaths(agentDir).yaml;\n await writeFileAtomic(target, body, 0o644);\n}\n\n/**\n * Write a brand-new elisym.yaml with descriptive header comments and\n * commented-out examples for unset optional fields. Use only at agent\n * creation time (CLI `init`, MCP `create_agent`). Subsequent edits go\n * through `writeYaml`, which discards comments.\n */\nexport async function writeYamlInitial(agentDir: string, yaml: ElisymYaml): Promise<void> {\n const validated = ElisymYamlSchema.parse(yaml);\n const body = renderInitialYaml(validated);\n const target = agentPaths(agentDir).yaml;\n await writeFileAtomic(target, body, 0o644);\n}\n\n/**\n * Create a skills directory placeholder file with a commented-out SKILL.md\n * template covering every supported field, so operators have a reference\n * for what they can declare without having to read source. The file is\n * named `EXAMPLE.md` (not `SKILL.md`) and lives directly in `skills/`\n * (not in a subdirectory), so the skill loader skips it - it's reference\n * material, not an active skill. To turn it into a real skill: copy it\n * into `skills/<your-skill-name>/SKILL.md` and uncomment the lines you\n * need.\n *\n * Idempotent: written with `wx` flag so we never overwrite an operator's\n * edits on re-run of `init`.\n */\nexport async function writeExampleSkillTemplate(agentDir: string): Promise<void> {\n const target = join(agentDir, 'skills', 'EXAMPLE.md');\n await writeFileIfMissing(target, EXAMPLE_SKILL_TEMPLATE, 0o644);\n}\n\nconst EXAMPLE_SKILL_TEMPLATE = `# elisym skill template\n#\n# This is reference material, not an active skill. The agent runtime\n# only loads skills from \\`skills/<name>/SKILL.md\\` (one folder per\n# skill). To turn this template into a real skill:\n#\n# 1. mkdir skills/my-skill\n# 2. cp skills/EXAMPLE.md skills/my-skill/SKILL.md\n# 3. uncomment the fields you need and fill in real values\n# 4. delete this comment block from the new file\n#\n# Full reference: see packages/cli/SKILLS.md in the elisym monorepo.\n\n# ---\n# # Required fields ---------------------------------------------------\n#\n# # Skill name. Must be unique within the agent. Used as the d-tag for\n# # routing incoming jobs (NIP-89/NIP-90).\n# name: My Skill\n#\n# # One-line description shown to customers in discovery UIs. Keep it\n# # short and concrete - this is the \"elevator pitch\" for the skill.\n# description: Send a prompt, get back a poem about it.\n#\n# # Capability tags. Customers filter and discover skills by these.\n# # At least one entry required. Use lowercase kebab-case.\n# capabilities:\n# - poetry\n# - text-generation\n#\n# # Price the customer pays per job. Number or numeric string. Free\n# # skills (price: 0) are allowed only when the agent runtime is\n# # configured with \\`allowFreeSkills\\`.\n# price: 0.05\n#\n# # Asset the price is denominated in. Defaults to \"sol\" for back-compat\n# # but USDC is the canonical paid-skill currency for examples.\n# token: usdc\n# # Optional explicit mint (base58). Resolved automatically for known\n# # tokens, so usually omit this.\n# # mint: 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU\n#\n# # Execution mode. Defaults to \"llm\" if omitted.\n# # - llm: feed input to an LLM with the system prompt below.\n# # - static-file: return the contents of a fixed file (no input read).\n# # - static-script: spawn a script with no stdin (no input read).\n# # - dynamic-script: spawn a script and pipe the customer's input to stdin.\n# mode: llm\n#\n# # ---\n# # LLM configuration / dependency -----------------------------------\n# #\n# # For mode: 'llm', \\`provider\\` + \\`model\\` override the agent default\n# # for runtime LLM execution. \\`max_tokens\\` overrides the default cap.\n# #\n# # For script modes (static-script / dynamic-script / static-file):\n# # \\`provider\\` + \\`model\\` declare which LLM API key the script\n# # depends on. The agent registers the (provider, model) pair with\n# # the health monitor so it can:\n# # - probe the key at startup (refuse to start on invalid/billing-out)\n# # - reactively flip the pair to unhealthy if the script exits with\n# # SCRIPT_EXIT_BILLING_EXHAUSTED (= 42), refusing future jobs\n# # before payment until the key recovers\n# # - run a 5-min lazy recovery probe loop that flips the pair back\n# # to healthy as soon as the key works again\n# # \\`max_tokens\\` is rejected for script modes (the script controls\n# # its own token limits).\n# # provider: anthropic\n# # model: claude-haiku-4-5-20251001\n# # max_tokens: 4096\n#\n# # ---\n# # mode-specific fields -------------------------------------------\n#\n# # Required when mode === 'static-file'.\n# # output_file: ./output.txt\n#\n# # Required when mode === 'static-script' | 'dynamic-script'.\n# # script: ./scripts/run.sh\n# # script_args:\n# # - --flag\n# # - value\n# # script_timeout_ms: 60000\n#\n# # ---\n# # mode === 'llm' extras ------------------------------------------\n# #\n# # External tools the LLM can invoke during a job. Each tool is a\n# # named subprocess; the LLM decides whether/when to call it.\n# # tools:\n# # - name: lookup\n# # description: Look up a record by id.\n# # command:\n# # - ./tools/lookup.sh\n# # parameters:\n# # - name: id\n# # description: Record identifier (UUID).\n# # required: true\n#\n# # Cap on tool-use rounds (LLM <-> tools loop). Default 10.\n# # max_tool_rounds: 10\n#\n# # ---\n# # Per-skill rate limit (any mode) --------------------------------\n# # Snake-case in YAML, applied by the runtime regardless of mode.\n# # rate_limit:\n# # per_window_secs: 60\n# # max_per_window: 30\n#\n# # ---\n# # Imagery ---------------------------------------------------------\n# # Either a local file path (uploaded on first start) or an absolute\n# # URL. Local paths must stay inside the skill directory.\n# # image_file: ./skill-icon.png\n# # image: https://example.com/icon.png\n# ---\n#\n# Markdown body below the frontmatter is the system prompt for\n# mode === 'llm'. For other modes it's ignored.\n#\n# You are a helpful assistant. Reply concisely.\n`;\n\n/**\n * Write .secrets.json atomically. If `passphrase` is given, encrypts all\n * plaintext secret fields (already-encrypted values are left as-is).\n * Generic over `llm_api_keys` so any registered provider's key is\n * encrypted without per-provider plumbing here.\n */\nexport async function writeSecrets(\n agentDir: string,\n secrets: Secrets,\n passphrase?: string,\n): Promise<void> {\n const validated = SecretsSchema.parse(secrets);\n let encryptedLlmKeys: Record<string, string> | undefined;\n if (validated.llm_api_keys) {\n encryptedLlmKeys = {};\n for (const [providerId, value] of Object.entries(validated.llm_api_keys)) {\n if (value) {\n encryptedLlmKeys[providerId] = maybeEncrypt(value, passphrase);\n }\n }\n if (Object.keys(encryptedLlmKeys).length === 0) {\n encryptedLlmKeys = undefined;\n }\n }\n const finalSecrets: Secrets = {\n nostr_secret_key: maybeEncrypt(validated.nostr_secret_key, passphrase),\n solana_secret_key: validated.solana_secret_key\n ? maybeEncrypt(validated.solana_secret_key, passphrase)\n : undefined,\n llm_api_keys: encryptedLlmKeys,\n };\n const body = JSON.stringify(finalSecrets, null, 2) + '\\n';\n const target = agentPaths(agentDir).secrets;\n await writeFileAtomic(target, body, 0o600);\n}\n\nfunction maybeEncrypt(value: string, passphrase: string | undefined): string {\n if (!passphrase) {\n return value;\n }\n if (isEncrypted(value)) {\n return value;\n }\n return encryptSecret(value, passphrase);\n}\n\n/** Atomic write: temp file + rename. Preserves mode. */\nexport async function writeFileAtomic(\n path: string,\n data: string | Buffer,\n mode: number,\n): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n const tmpPath = `${path}.tmp.${randomBytes(6).toString('hex')}`;\n await writeFile(tmpPath, data, { mode });\n try {\n await rename(tmpPath, path);\n } catch (e) {\n // Best-effort cleanup of temp file on rename failure.\n try {\n const { unlink } = await import('node:fs/promises');\n await unlink(tmpPath);\n } catch {\n /* ignore */\n }\n throw e;\n }\n}\n\nasync function writeFileIfMissing(path: string, data: string, mode: number): Promise<void> {\n try {\n await writeFile(path, data, { mode, flag: 'wx' });\n } catch (e: unknown) {\n // wx fails with EEXIST if file exists - that's fine.\n if (!isEexist(e)) {\n throw e;\n }\n }\n}\n\nfunction isEexist(e: unknown): boolean {\n return (\n typeof e === 'object' && e !== null && 'code' in e && (e as { code: string }).code === 'EEXIST'\n );\n}\n","/**\n * Zod schemas and types for `~/.elisym/config.yaml`.\n *\n * Split from `./global` so the schemas can be re-exported from the\n * browser-safe `@elisym/sdk` entry point without dragging in `node:fs/promises`\n * (which the loader/writer in `./global` needs).\n */\n\nimport { z } from 'zod';\n\nexport const SessionSpendLimitEntrySchema = z\n .object({\n chain: z.enum(['solana']),\n token: z\n .string()\n .min(1)\n .max(16)\n .regex(/^[a-z0-9]+$/, 'token must be lowercase alphanumeric'),\n mint: z.string().min(1).max(64).optional(),\n amount: z.number().positive().finite(),\n })\n .strict();\n\nexport const GlobalConfigSchema = z\n .object({\n session_spend_limits: z.array(SessionSpendLimitEntrySchema).max(16).optional(),\n })\n .strict();\n\nexport type SessionSpendLimitEntry = z.infer<typeof SessionSpendLimitEntrySchema>;\nexport type GlobalConfig = z.infer<typeof GlobalConfigSchema>;\n","/**\n * Global (not per-agent) config stored at `~/.elisym/config.yaml`.\n *\n * Node.js/Bun only - reads and writes the filesystem. Browser code must not\n * import this module; import the schemas from `./global-schema` instead, or\n * the loader/writer from `@elisym/sdk/node`.\n */\n\nimport { readFile } from 'node:fs/promises';\nimport YAML from 'yaml';\nimport { writeFileAtomic } from '../agent-store/writer';\nimport { GlobalConfigSchema, type GlobalConfig } from './global-schema';\n\nexport {\n GlobalConfigSchema,\n SessionSpendLimitEntrySchema,\n type GlobalConfig,\n type SessionSpendLimitEntry,\n} from './global-schema';\n\nfunction isEnoent(e: unknown): boolean {\n return (\n typeof e === 'object' && e !== null && 'code' in e && (e as { code: string }).code === 'ENOENT'\n );\n}\n\n/**\n * Read and validate `~/.elisym/config.yaml`. Returns `{}` if missing. Throws\n * on malformed YAML or schema violations — the MCP server treats these as fatal\n * at startup rather than silently ignoring bad overrides.\n */\nexport async function loadGlobalConfig(path: string): Promise<GlobalConfig> {\n let raw: string;\n try {\n raw = await readFile(path, 'utf-8');\n } catch (e) {\n if (isEnoent(e)) {\n return {};\n }\n throw e;\n }\n if (raw.trim() === '') {\n return {};\n }\n const parsed: unknown = YAML.parse(raw);\n return GlobalConfigSchema.parse(parsed ?? {});\n}\n\n/** Write the config YAML atomically. Validates via Zod before writing. */\nexport async function writeGlobalConfig(path: string, config: GlobalConfig): Promise<void> {\n const validated = GlobalConfigSchema.parse(config);\n const body = YAML.stringify(validated);\n await writeFileAtomic(path, body, 0o644);\n}\n"]}
package/dist/node.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/primitives/encryption.ts","../src/agent-store/writer.ts","../src/config/global-schema.ts","../src/config/global.ts"],"names":["randomBytes","YAML"],"mappings":"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","file":"node.js","sourcesContent":["/**\n * Secret encryption/decryption for agent config files.\n * Uses scrypt (KDF) + AES-256-GCM (cipher).\n * Format: \"encrypted:v1:\" + base64(salt[16] + iv[12] + ciphertext + tag[16])\n *\n * scrypt params: N=2^17, r=8, p=1 (~128 MB RAM per derivation).\n *\n * Node.js/Bun only - not available in browsers. Reachable only via the\n * '@elisym/sdk/node' subpath, which browser bundlers will not resolve.\n */\n\nimport { Buffer } from 'node:buffer';\nimport { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';\n\nconst PREFIX = 'encrypted:v1:';\nconst SALT_LENGTH = 16;\nconst IV_LENGTH = 12;\nconst TAG_LENGTH = 16;\nconst KEY_LENGTH = 32; // AES-256\n// v1: N=2^17 (OWASP minimum). v2 will use N=2^20 with format migration.\nconst SCRYPT_N = 2 ** 17;\nconst SCRYPT_R = 8;\nconst SCRYPT_P = 1;\nconst SCRYPT_MAXMEM = 128 * SCRYPT_N * SCRYPT_R * 2; // 2x the minimum required memory\n\n/** Check if a value is encrypted (has the encrypted:v1: prefix). */\nexport function isEncrypted(value: string): boolean {\n return value.startsWith(PREFIX);\n}\n\n/** Encrypt a plaintext secret with a passphrase. Returns \"encrypted:v1:base64...\". Node.js/Bun only. */\nexport function encryptSecret(plaintext: string, passphrase: string): string {\n if (!passphrase) {\n throw new Error('Passphrase must not be empty.');\n }\n\n const salt = randomBytes(SALT_LENGTH);\n const key = scryptSync(passphrase, salt, KEY_LENGTH, {\n N: SCRYPT_N,\n r: SCRYPT_R,\n p: SCRYPT_P,\n maxmem: SCRYPT_MAXMEM,\n });\n const iv = randomBytes(IV_LENGTH);\n\n const cipher = createCipheriv('aes-256-gcm', key, iv);\n const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);\n const tag = cipher.getAuthTag();\n\n const payload = Buffer.concat([salt, iv, encrypted, tag]);\n return PREFIX + payload.toString('base64');\n}\n\n/** Decrypt an encrypted secret with a passphrase. Throws on wrong passphrase or corrupted data. Node.js/Bun only. */\nexport function decryptSecret(encrypted: string, passphrase: string): string {\n if (!isEncrypted(encrypted)) {\n throw new Error('Value is not encrypted (missing encrypted:v1: prefix).');\n }\n if (!passphrase) {\n throw new Error('Passphrase must not be empty.');\n }\n\n const payload = Buffer.from(encrypted.slice(PREFIX.length), 'base64');\n if (payload.length < SALT_LENGTH + IV_LENGTH + TAG_LENGTH) {\n throw new Error('Encrypted payload is too short.');\n }\n\n const salt = payload.subarray(0, SALT_LENGTH);\n const iv = payload.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);\n const tag = payload.subarray(payload.length - TAG_LENGTH);\n const ciphertext = payload.subarray(SALT_LENGTH + IV_LENGTH, payload.length - TAG_LENGTH);\n\n const key = scryptSync(passphrase, salt, KEY_LENGTH, {\n N: SCRYPT_N,\n r: SCRYPT_R,\n p: SCRYPT_P,\n maxmem: SCRYPT_MAXMEM,\n });\n\n const decipher = createDecipheriv('aes-256-gcm', key, iv);\n decipher.setAuthTag(tag);\n\n try {\n const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);\n return decrypted.toString('utf8');\n } catch {\n throw new Error('Decryption failed. Wrong passphrase or corrupted data.');\n }\n}\n","/**\n * Write agent files: elisym.yaml, .secrets.json, .gitignore, and create agent dirs.\n */\n\nimport { randomBytes } from 'node:crypto';\nimport { mkdir, rename, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport YAML from 'yaml';\nimport { encryptSecret, isEncrypted } from '../primitives/encryption';\nimport { agentPaths, type AgentPaths } from './paths';\nimport { elisymRootFor, type AgentSource } from './resolver';\nimport { ElisymYamlSchema, SecretsSchema, type ElisymYaml, type Secrets } from './schema';\nimport { renderInitialYaml } from './template';\n\nconst GITIGNORE_CONTENT = [\n '# elisym private state - do not commit.',\n '.secrets.json',\n '.media-cache.json',\n '.jobs.json',\n '.jobs.json.corrupt.*',\n '.customer-history.json',\n '.contacts.json',\n '',\n].join('\\n');\n\nexport interface CreateAgentDirOptions {\n target: AgentSource;\n name: string;\n cwd: string;\n /**\n * For `target: 'project'`: if no .elisym/ dir exists above cwd,\n * where should we create one? Defaults to cwd.\n */\n projectRoot?: string;\n}\n\nexport interface CreatedAgentDir {\n dir: string;\n paths: AgentPaths;\n source: AgentSource;\n createdNewElisymRoot: boolean;\n}\n\n/**\n * Create (or reuse) the directory layout for a new agent. Idempotent: if the\n * agent directory already exists, returns its paths without overwriting.\n * Writes `.gitignore` in project-local .elisym/ on first creation.\n */\nexport async function createAgentDir(options: CreateAgentDirOptions): Promise<CreatedAgentDir> {\n const { target, name, cwd, projectRoot } = options;\n\n const existingRoot = elisymRootFor(target, cwd);\n let elisymRoot: string;\n let createdNewElisymRoot = false;\n\n if (existingRoot) {\n elisymRoot = existingRoot;\n } else if (target === 'project') {\n elisymRoot = join(projectRoot ?? cwd, '.elisym');\n createdNewElisymRoot = true;\n } else {\n throw new Error('homeElisymDir should always exist conceptually - this is unreachable');\n }\n\n const agentDir = join(elisymRoot, name);\n const mode = target === 'home' ? 0o700 : 0o755;\n await mkdir(agentDir, { recursive: true, mode });\n await mkdir(join(agentDir, 'skills'), { recursive: true, mode });\n\n if (target === 'project') {\n const gitignorePath = join(elisymRoot, '.gitignore');\n await writeFileIfMissing(gitignorePath, GITIGNORE_CONTENT, 0o644);\n }\n\n return {\n dir: agentDir,\n paths: agentPaths(agentDir),\n source: target,\n createdNewElisymRoot,\n };\n}\n\n/** Write elisym.yaml atomically. Validates via Zod before writing. */\nexport async function writeYaml(agentDir: string, yaml: ElisymYaml): Promise<void> {\n const validated = ElisymYamlSchema.parse(yaml);\n const body = YAML.stringify(validated);\n const target = agentPaths(agentDir).yaml;\n await writeFileAtomic(target, body, 0o644);\n}\n\n/**\n * Write a brand-new elisym.yaml with descriptive header comments and\n * commented-out examples for unset optional fields. Use only at agent\n * creation time (CLI `init`, MCP `create_agent`). Subsequent edits go\n * through `writeYaml`, which discards comments.\n */\nexport async function writeYamlInitial(agentDir: string, yaml: ElisymYaml): Promise<void> {\n const validated = ElisymYamlSchema.parse(yaml);\n const body = renderInitialYaml(validated);\n const target = agentPaths(agentDir).yaml;\n await writeFileAtomic(target, body, 0o644);\n}\n\n/**\n * Write .secrets.json atomically. If `passphrase` is given, encrypts all\n * plaintext secret fields (already-encrypted values are left as-is).\n * Generic over `llm_api_keys` so any registered provider's key is\n * encrypted without per-provider plumbing here.\n */\nexport async function writeSecrets(\n agentDir: string,\n secrets: Secrets,\n passphrase?: string,\n): Promise<void> {\n const validated = SecretsSchema.parse(secrets);\n let encryptedLlmKeys: Record<string, string> | undefined;\n if (validated.llm_api_keys) {\n encryptedLlmKeys = {};\n for (const [providerId, value] of Object.entries(validated.llm_api_keys)) {\n if (value) {\n encryptedLlmKeys[providerId] = maybeEncrypt(value, passphrase);\n }\n }\n if (Object.keys(encryptedLlmKeys).length === 0) {\n encryptedLlmKeys = undefined;\n }\n }\n const finalSecrets: Secrets = {\n nostr_secret_key: maybeEncrypt(validated.nostr_secret_key, passphrase),\n solana_secret_key: validated.solana_secret_key\n ? maybeEncrypt(validated.solana_secret_key, passphrase)\n : undefined,\n llm_api_keys: encryptedLlmKeys,\n };\n const body = JSON.stringify(finalSecrets, null, 2) + '\\n';\n const target = agentPaths(agentDir).secrets;\n await writeFileAtomic(target, body, 0o600);\n}\n\nfunction maybeEncrypt(value: string, passphrase: string | undefined): string {\n if (!passphrase) {\n return value;\n }\n if (isEncrypted(value)) {\n return value;\n }\n return encryptSecret(value, passphrase);\n}\n\n/** Atomic write: temp file + rename. Preserves mode. */\nexport async function writeFileAtomic(\n path: string,\n data: string | Buffer,\n mode: number,\n): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n const tmpPath = `${path}.tmp.${randomBytes(6).toString('hex')}`;\n await writeFile(tmpPath, data, { mode });\n try {\n await rename(tmpPath, path);\n } catch (e) {\n // Best-effort cleanup of temp file on rename failure.\n try {\n const { unlink } = await import('node:fs/promises');\n await unlink(tmpPath);\n } catch {\n /* ignore */\n }\n throw e;\n }\n}\n\nasync function writeFileIfMissing(path: string, data: string, mode: number): Promise<void> {\n try {\n await writeFile(path, data, { mode, flag: 'wx' });\n } catch (e: unknown) {\n // wx fails with EEXIST if file exists - that's fine.\n if (!isEexist(e)) {\n throw e;\n }\n }\n}\n\nfunction isEexist(e: unknown): boolean {\n return (\n typeof e === 'object' && e !== null && 'code' in e && (e as { code: string }).code === 'EEXIST'\n );\n}\n","/**\n * Zod schemas and types for `~/.elisym/config.yaml`.\n *\n * Split from `./global` so the schemas can be re-exported from the\n * browser-safe `@elisym/sdk` entry point without dragging in `node:fs/promises`\n * (which the loader/writer in `./global` needs).\n */\n\nimport { z } from 'zod';\n\nexport const SessionSpendLimitEntrySchema = z\n .object({\n chain: z.enum(['solana']),\n token: z\n .string()\n .min(1)\n .max(16)\n .regex(/^[a-z0-9]+$/, 'token must be lowercase alphanumeric'),\n mint: z.string().min(1).max(64).optional(),\n amount: z.number().positive().finite(),\n })\n .strict();\n\nexport const GlobalConfigSchema = z\n .object({\n session_spend_limits: z.array(SessionSpendLimitEntrySchema).max(16).optional(),\n })\n .strict();\n\nexport type SessionSpendLimitEntry = z.infer<typeof SessionSpendLimitEntrySchema>;\nexport type GlobalConfig = z.infer<typeof GlobalConfigSchema>;\n","/**\n * Global (not per-agent) config stored at `~/.elisym/config.yaml`.\n *\n * Node.js/Bun only - reads and writes the filesystem. Browser code must not\n * import this module; import the schemas from `./global-schema` instead, or\n * the loader/writer from `@elisym/sdk/node`.\n */\n\nimport { readFile } from 'node:fs/promises';\nimport YAML from 'yaml';\nimport { writeFileAtomic } from '../agent-store/writer';\nimport { GlobalConfigSchema, type GlobalConfig } from './global-schema';\n\nexport {\n GlobalConfigSchema,\n SessionSpendLimitEntrySchema,\n type GlobalConfig,\n type SessionSpendLimitEntry,\n} from './global-schema';\n\nfunction isEnoent(e: unknown): boolean {\n return (\n typeof e === 'object' && e !== null && 'code' in e && (e as { code: string }).code === 'ENOENT'\n );\n}\n\n/**\n * Read and validate `~/.elisym/config.yaml`. Returns `{}` if missing. Throws\n * on malformed YAML or schema violations — the MCP server treats these as fatal\n * at startup rather than silently ignoring bad overrides.\n */\nexport async function loadGlobalConfig(path: string): Promise<GlobalConfig> {\n let raw: string;\n try {\n raw = await readFile(path, 'utf-8');\n } catch (e) {\n if (isEnoent(e)) {\n return {};\n }\n throw e;\n }\n if (raw.trim() === '') {\n return {};\n }\n const parsed: unknown = YAML.parse(raw);\n return GlobalConfigSchema.parse(parsed ?? {});\n}\n\n/** Write the config YAML atomically. Validates via Zod before writing. */\nexport async function writeGlobalConfig(path: string, config: GlobalConfig): Promise<void> {\n const validated = GlobalConfigSchema.parse(config);\n const body = YAML.stringify(validated);\n await writeFileAtomic(path, body, 0o644);\n}\n"]}
1
+ {"version":3,"sources":["../src/primitives/encryption.ts","../src/agent-store/writer.ts","../src/config/global-schema.ts","../src/config/global.ts"],"names":["randomBytes","YAML"],"mappings":"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","file":"node.js","sourcesContent":["/**\n * Secret encryption/decryption for agent config files.\n * Uses scrypt (KDF) + AES-256-GCM (cipher).\n * Format: \"encrypted:v1:\" + base64(salt[16] + iv[12] + ciphertext + tag[16])\n *\n * scrypt params: N=2^17, r=8, p=1 (~128 MB RAM per derivation).\n *\n * Node.js/Bun only - not available in browsers. Reachable only via the\n * '@elisym/sdk/node' subpath, which browser bundlers will not resolve.\n */\n\nimport { Buffer } from 'node:buffer';\nimport { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';\n\nconst PREFIX = 'encrypted:v1:';\nconst SALT_LENGTH = 16;\nconst IV_LENGTH = 12;\nconst TAG_LENGTH = 16;\nconst KEY_LENGTH = 32; // AES-256\n// v1: N=2^17 (OWASP minimum). v2 will use N=2^20 with format migration.\nconst SCRYPT_N = 2 ** 17;\nconst SCRYPT_R = 8;\nconst SCRYPT_P = 1;\nconst SCRYPT_MAXMEM = 128 * SCRYPT_N * SCRYPT_R * 2; // 2x the minimum required memory\n\n/** Check if a value is encrypted (has the encrypted:v1: prefix). */\nexport function isEncrypted(value: string): boolean {\n return value.startsWith(PREFIX);\n}\n\n/** Encrypt a plaintext secret with a passphrase. Returns \"encrypted:v1:base64...\". Node.js/Bun only. */\nexport function encryptSecret(plaintext: string, passphrase: string): string {\n if (!passphrase) {\n throw new Error('Passphrase must not be empty.');\n }\n\n const salt = randomBytes(SALT_LENGTH);\n const key = scryptSync(passphrase, salt, KEY_LENGTH, {\n N: SCRYPT_N,\n r: SCRYPT_R,\n p: SCRYPT_P,\n maxmem: SCRYPT_MAXMEM,\n });\n const iv = randomBytes(IV_LENGTH);\n\n const cipher = createCipheriv('aes-256-gcm', key, iv);\n const encrypted = Buffer.concat([cipher.update(plaintext, 'utf8'), cipher.final()]);\n const tag = cipher.getAuthTag();\n\n const payload = Buffer.concat([salt, iv, encrypted, tag]);\n return PREFIX + payload.toString('base64');\n}\n\n/** Decrypt an encrypted secret with a passphrase. Throws on wrong passphrase or corrupted data. Node.js/Bun only. */\nexport function decryptSecret(encrypted: string, passphrase: string): string {\n if (!isEncrypted(encrypted)) {\n throw new Error('Value is not encrypted (missing encrypted:v1: prefix).');\n }\n if (!passphrase) {\n throw new Error('Passphrase must not be empty.');\n }\n\n const payload = Buffer.from(encrypted.slice(PREFIX.length), 'base64');\n if (payload.length < SALT_LENGTH + IV_LENGTH + TAG_LENGTH) {\n throw new Error('Encrypted payload is too short.');\n }\n\n const salt = payload.subarray(0, SALT_LENGTH);\n const iv = payload.subarray(SALT_LENGTH, SALT_LENGTH + IV_LENGTH);\n const tag = payload.subarray(payload.length - TAG_LENGTH);\n const ciphertext = payload.subarray(SALT_LENGTH + IV_LENGTH, payload.length - TAG_LENGTH);\n\n const key = scryptSync(passphrase, salt, KEY_LENGTH, {\n N: SCRYPT_N,\n r: SCRYPT_R,\n p: SCRYPT_P,\n maxmem: SCRYPT_MAXMEM,\n });\n\n const decipher = createDecipheriv('aes-256-gcm', key, iv);\n decipher.setAuthTag(tag);\n\n try {\n const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);\n return decrypted.toString('utf8');\n } catch {\n throw new Error('Decryption failed. Wrong passphrase or corrupted data.');\n }\n}\n","/**\n * Write agent files: elisym.yaml, .secrets.json, .gitignore, and create agent dirs.\n */\n\nimport { randomBytes } from 'node:crypto';\nimport { mkdir, rename, writeFile } from 'node:fs/promises';\nimport { dirname, join } from 'node:path';\nimport YAML from 'yaml';\nimport { encryptSecret, isEncrypted } from '../primitives/encryption';\nimport { agentPaths, type AgentPaths } from './paths';\nimport { elisymRootFor, type AgentSource } from './resolver';\nimport { ElisymYamlSchema, SecretsSchema, type ElisymYaml, type Secrets } from './schema';\nimport { renderInitialYaml } from './template';\n\nconst GITIGNORE_CONTENT = [\n '# elisym private state - do not commit.',\n '.secrets.json',\n '.media-cache.json',\n '.jobs.json',\n '.jobs.json.corrupt.*',\n '.customer-history.json',\n '.contacts.json',\n '',\n].join('\\n');\n\nexport interface CreateAgentDirOptions {\n target: AgentSource;\n name: string;\n cwd: string;\n /**\n * For `target: 'project'`: if no .elisym/ dir exists above cwd,\n * where should we create one? Defaults to cwd.\n */\n projectRoot?: string;\n}\n\nexport interface CreatedAgentDir {\n dir: string;\n paths: AgentPaths;\n source: AgentSource;\n createdNewElisymRoot: boolean;\n}\n\n/**\n * Create (or reuse) the directory layout for a new agent. Idempotent: if the\n * agent directory already exists, returns its paths without overwriting.\n * Writes `.gitignore` in project-local .elisym/ on first creation.\n */\nexport async function createAgentDir(options: CreateAgentDirOptions): Promise<CreatedAgentDir> {\n const { target, name, cwd, projectRoot } = options;\n\n const existingRoot = elisymRootFor(target, cwd);\n let elisymRoot: string;\n let createdNewElisymRoot = false;\n\n if (existingRoot) {\n elisymRoot = existingRoot;\n } else if (target === 'project') {\n elisymRoot = join(projectRoot ?? cwd, '.elisym');\n createdNewElisymRoot = true;\n } else {\n throw new Error('homeElisymDir should always exist conceptually - this is unreachable');\n }\n\n const agentDir = join(elisymRoot, name);\n const mode = target === 'home' ? 0o700 : 0o755;\n await mkdir(agentDir, { recursive: true, mode });\n await mkdir(join(agentDir, 'skills'), { recursive: true, mode });\n\n if (target === 'project') {\n const gitignorePath = join(elisymRoot, '.gitignore');\n await writeFileIfMissing(gitignorePath, GITIGNORE_CONTENT, 0o644);\n }\n\n return {\n dir: agentDir,\n paths: agentPaths(agentDir),\n source: target,\n createdNewElisymRoot,\n };\n}\n\n/** Write elisym.yaml atomically. Validates via Zod before writing. */\nexport async function writeYaml(agentDir: string, yaml: ElisymYaml): Promise<void> {\n const validated = ElisymYamlSchema.parse(yaml);\n const body = YAML.stringify(validated);\n const target = agentPaths(agentDir).yaml;\n await writeFileAtomic(target, body, 0o644);\n}\n\n/**\n * Write a brand-new elisym.yaml with descriptive header comments and\n * commented-out examples for unset optional fields. Use only at agent\n * creation time (CLI `init`, MCP `create_agent`). Subsequent edits go\n * through `writeYaml`, which discards comments.\n */\nexport async function writeYamlInitial(agentDir: string, yaml: ElisymYaml): Promise<void> {\n const validated = ElisymYamlSchema.parse(yaml);\n const body = renderInitialYaml(validated);\n const target = agentPaths(agentDir).yaml;\n await writeFileAtomic(target, body, 0o644);\n}\n\n/**\n * Create a skills directory placeholder file with a commented-out SKILL.md\n * template covering every supported field, so operators have a reference\n * for what they can declare without having to read source. The file is\n * named `EXAMPLE.md` (not `SKILL.md`) and lives directly in `skills/`\n * (not in a subdirectory), so the skill loader skips it - it's reference\n * material, not an active skill. To turn it into a real skill: copy it\n * into `skills/<your-skill-name>/SKILL.md` and uncomment the lines you\n * need.\n *\n * Idempotent: written with `wx` flag so we never overwrite an operator's\n * edits on re-run of `init`.\n */\nexport async function writeExampleSkillTemplate(agentDir: string): Promise<void> {\n const target = join(agentDir, 'skills', 'EXAMPLE.md');\n await writeFileIfMissing(target, EXAMPLE_SKILL_TEMPLATE, 0o644);\n}\n\nconst EXAMPLE_SKILL_TEMPLATE = `# elisym skill template\n#\n# This is reference material, not an active skill. The agent runtime\n# only loads skills from \\`skills/<name>/SKILL.md\\` (one folder per\n# skill). To turn this template into a real skill:\n#\n# 1. mkdir skills/my-skill\n# 2. cp skills/EXAMPLE.md skills/my-skill/SKILL.md\n# 3. uncomment the fields you need and fill in real values\n# 4. delete this comment block from the new file\n#\n# Full reference: see packages/cli/SKILLS.md in the elisym monorepo.\n\n# ---\n# # Required fields ---------------------------------------------------\n#\n# # Skill name. Must be unique within the agent. Used as the d-tag for\n# # routing incoming jobs (NIP-89/NIP-90).\n# name: My Skill\n#\n# # One-line description shown to customers in discovery UIs. Keep it\n# # short and concrete - this is the \"elevator pitch\" for the skill.\n# description: Send a prompt, get back a poem about it.\n#\n# # Capability tags. Customers filter and discover skills by these.\n# # At least one entry required. Use lowercase kebab-case.\n# capabilities:\n# - poetry\n# - text-generation\n#\n# # Price the customer pays per job. Number or numeric string. Free\n# # skills (price: 0) are allowed only when the agent runtime is\n# # configured with \\`allowFreeSkills\\`.\n# price: 0.05\n#\n# # Asset the price is denominated in. Defaults to \"sol\" for back-compat\n# # but USDC is the canonical paid-skill currency for examples.\n# token: usdc\n# # Optional explicit mint (base58). Resolved automatically for known\n# # tokens, so usually omit this.\n# # mint: 4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU\n#\n# # Execution mode. Defaults to \"llm\" if omitted.\n# # - llm: feed input to an LLM with the system prompt below.\n# # - static-file: return the contents of a fixed file (no input read).\n# # - static-script: spawn a script with no stdin (no input read).\n# # - dynamic-script: spawn a script and pipe the customer's input to stdin.\n# mode: llm\n#\n# # ---\n# # LLM configuration / dependency -----------------------------------\n# #\n# # For mode: 'llm', \\`provider\\` + \\`model\\` override the agent default\n# # for runtime LLM execution. \\`max_tokens\\` overrides the default cap.\n# #\n# # For script modes (static-script / dynamic-script / static-file):\n# # \\`provider\\` + \\`model\\` declare which LLM API key the script\n# # depends on. The agent registers the (provider, model) pair with\n# # the health monitor so it can:\n# # - probe the key at startup (refuse to start on invalid/billing-out)\n# # - reactively flip the pair to unhealthy if the script exits with\n# # SCRIPT_EXIT_BILLING_EXHAUSTED (= 42), refusing future jobs\n# # before payment until the key recovers\n# # - run a 5-min lazy recovery probe loop that flips the pair back\n# # to healthy as soon as the key works again\n# # \\`max_tokens\\` is rejected for script modes (the script controls\n# # its own token limits).\n# # provider: anthropic\n# # model: claude-haiku-4-5-20251001\n# # max_tokens: 4096\n#\n# # ---\n# # mode-specific fields -------------------------------------------\n#\n# # Required when mode === 'static-file'.\n# # output_file: ./output.txt\n#\n# # Required when mode === 'static-script' | 'dynamic-script'.\n# # script: ./scripts/run.sh\n# # script_args:\n# # - --flag\n# # - value\n# # script_timeout_ms: 60000\n#\n# # ---\n# # mode === 'llm' extras ------------------------------------------\n# #\n# # External tools the LLM can invoke during a job. Each tool is a\n# # named subprocess; the LLM decides whether/when to call it.\n# # tools:\n# # - name: lookup\n# # description: Look up a record by id.\n# # command:\n# # - ./tools/lookup.sh\n# # parameters:\n# # - name: id\n# # description: Record identifier (UUID).\n# # required: true\n#\n# # Cap on tool-use rounds (LLM <-> tools loop). Default 10.\n# # max_tool_rounds: 10\n#\n# # ---\n# # Per-skill rate limit (any mode) --------------------------------\n# # Snake-case in YAML, applied by the runtime regardless of mode.\n# # rate_limit:\n# # per_window_secs: 60\n# # max_per_window: 30\n#\n# # ---\n# # Imagery ---------------------------------------------------------\n# # Either a local file path (uploaded on first start) or an absolute\n# # URL. Local paths must stay inside the skill directory.\n# # image_file: ./skill-icon.png\n# # image: https://example.com/icon.png\n# ---\n#\n# Markdown body below the frontmatter is the system prompt for\n# mode === 'llm'. For other modes it's ignored.\n#\n# You are a helpful assistant. Reply concisely.\n`;\n\n/**\n * Write .secrets.json atomically. If `passphrase` is given, encrypts all\n * plaintext secret fields (already-encrypted values are left as-is).\n * Generic over `llm_api_keys` so any registered provider's key is\n * encrypted without per-provider plumbing here.\n */\nexport async function writeSecrets(\n agentDir: string,\n secrets: Secrets,\n passphrase?: string,\n): Promise<void> {\n const validated = SecretsSchema.parse(secrets);\n let encryptedLlmKeys: Record<string, string> | undefined;\n if (validated.llm_api_keys) {\n encryptedLlmKeys = {};\n for (const [providerId, value] of Object.entries(validated.llm_api_keys)) {\n if (value) {\n encryptedLlmKeys[providerId] = maybeEncrypt(value, passphrase);\n }\n }\n if (Object.keys(encryptedLlmKeys).length === 0) {\n encryptedLlmKeys = undefined;\n }\n }\n const finalSecrets: Secrets = {\n nostr_secret_key: maybeEncrypt(validated.nostr_secret_key, passphrase),\n solana_secret_key: validated.solana_secret_key\n ? maybeEncrypt(validated.solana_secret_key, passphrase)\n : undefined,\n llm_api_keys: encryptedLlmKeys,\n };\n const body = JSON.stringify(finalSecrets, null, 2) + '\\n';\n const target = agentPaths(agentDir).secrets;\n await writeFileAtomic(target, body, 0o600);\n}\n\nfunction maybeEncrypt(value: string, passphrase: string | undefined): string {\n if (!passphrase) {\n return value;\n }\n if (isEncrypted(value)) {\n return value;\n }\n return encryptSecret(value, passphrase);\n}\n\n/** Atomic write: temp file + rename. Preserves mode. */\nexport async function writeFileAtomic(\n path: string,\n data: string | Buffer,\n mode: number,\n): Promise<void> {\n await mkdir(dirname(path), { recursive: true });\n const tmpPath = `${path}.tmp.${randomBytes(6).toString('hex')}`;\n await writeFile(tmpPath, data, { mode });\n try {\n await rename(tmpPath, path);\n } catch (e) {\n // Best-effort cleanup of temp file on rename failure.\n try {\n const { unlink } = await import('node:fs/promises');\n await unlink(tmpPath);\n } catch {\n /* ignore */\n }\n throw e;\n }\n}\n\nasync function writeFileIfMissing(path: string, data: string, mode: number): Promise<void> {\n try {\n await writeFile(path, data, { mode, flag: 'wx' });\n } catch (e: unknown) {\n // wx fails with EEXIST if file exists - that's fine.\n if (!isEexist(e)) {\n throw e;\n }\n }\n}\n\nfunction isEexist(e: unknown): boolean {\n return (\n typeof e === 'object' && e !== null && 'code' in e && (e as { code: string }).code === 'EEXIST'\n );\n}\n","/**\n * Zod schemas and types for `~/.elisym/config.yaml`.\n *\n * Split from `./global` so the schemas can be re-exported from the\n * browser-safe `@elisym/sdk` entry point without dragging in `node:fs/promises`\n * (which the loader/writer in `./global` needs).\n */\n\nimport { z } from 'zod';\n\nexport const SessionSpendLimitEntrySchema = z\n .object({\n chain: z.enum(['solana']),\n token: z\n .string()\n .min(1)\n .max(16)\n .regex(/^[a-z0-9]+$/, 'token must be lowercase alphanumeric'),\n mint: z.string().min(1).max(64).optional(),\n amount: z.number().positive().finite(),\n })\n .strict();\n\nexport const GlobalConfigSchema = z\n .object({\n session_spend_limits: z.array(SessionSpendLimitEntrySchema).max(16).optional(),\n })\n .strict();\n\nexport type SessionSpendLimitEntry = z.infer<typeof SessionSpendLimitEntrySchema>;\nexport type GlobalConfig = z.infer<typeof GlobalConfigSchema>;\n","/**\n * Global (not per-agent) config stored at `~/.elisym/config.yaml`.\n *\n * Node.js/Bun only - reads and writes the filesystem. Browser code must not\n * import this module; import the schemas from `./global-schema` instead, or\n * the loader/writer from `@elisym/sdk/node`.\n */\n\nimport { readFile } from 'node:fs/promises';\nimport YAML from 'yaml';\nimport { writeFileAtomic } from '../agent-store/writer';\nimport { GlobalConfigSchema, type GlobalConfig } from './global-schema';\n\nexport {\n GlobalConfigSchema,\n SessionSpendLimitEntrySchema,\n type GlobalConfig,\n type SessionSpendLimitEntry,\n} from './global-schema';\n\nfunction isEnoent(e: unknown): boolean {\n return (\n typeof e === 'object' && e !== null && 'code' in e && (e as { code: string }).code === 'ENOENT'\n );\n}\n\n/**\n * Read and validate `~/.elisym/config.yaml`. Returns `{}` if missing. Throws\n * on malformed YAML or schema violations — the MCP server treats these as fatal\n * at startup rather than silently ignoring bad overrides.\n */\nexport async function loadGlobalConfig(path: string): Promise<GlobalConfig> {\n let raw: string;\n try {\n raw = await readFile(path, 'utf-8');\n } catch (e) {\n if (isEnoent(e)) {\n return {};\n }\n throw e;\n }\n if (raw.trim() === '') {\n return {};\n }\n const parsed: unknown = YAML.parse(raw);\n return GlobalConfigSchema.parse(parsed ?? {});\n}\n\n/** Write the config YAML atomically. Validates via Zod before writing. */\nexport async function writeGlobalConfig(path: string, config: GlobalConfig): Promise<void> {\n const validated = GlobalConfigSchema.parse(config);\n const body = YAML.stringify(validated);\n await writeFileAtomic(path, body, 0o644);\n}\n"]}
package/dist/skills.cjs CHANGED
@@ -214,6 +214,7 @@ var StaticFileSkill = class {
214
214
  mode = "static-file";
215
215
  image;
216
216
  imageFile;
217
+ llmOverride;
217
218
  outputFilePath;
218
219
  constructor(params) {
219
220
  this.name = params.name;
@@ -223,6 +224,7 @@ var StaticFileSkill = class {
223
224
  this.asset = params.asset;
224
225
  this.image = params.image;
225
226
  this.imageFile = params.imageFile;
227
+ this.llmOverride = params.llmOverride;
226
228
  this.outputFilePath = params.outputFilePath;
227
229
  }
228
230
  async execute(_input, _ctx) {
@@ -235,6 +237,24 @@ var StaticFileSkill = class {
235
237
  return { data: buffer.toString("utf-8") };
236
238
  }
237
239
  };
240
+ var SCRIPT_EXIT_BILLING_EXHAUSTED = 42;
241
+
242
+ // src/llm-health/types.ts
243
+ var ScriptBillingExhaustedError = class extends Error {
244
+ exitCode;
245
+ stderr;
246
+ stdout;
247
+ constructor(exitCode, stdout, stderr) {
248
+ const detail = stderr.trim() || stdout.trim() || "(no output)";
249
+ super(`script exited with billing-exhausted code ${exitCode}: ${detail}`);
250
+ this.name = "ScriptBillingExhaustedError";
251
+ this.exitCode = exitCode;
252
+ this.stdout = stdout;
253
+ this.stderr = stderr;
254
+ }
255
+ };
256
+
257
+ // src/skills/staticScriptSkill.ts
238
258
  var StaticScriptSkill = class {
239
259
  name;
240
260
  description;
@@ -244,6 +264,7 @@ var StaticScriptSkill = class {
244
264
  mode = "static-script";
245
265
  image;
246
266
  imageFile;
267
+ llmOverride;
247
268
  scriptPath;
248
269
  scriptArgs;
249
270
  scriptTimeoutMs;
@@ -256,6 +277,7 @@ var StaticScriptSkill = class {
256
277
  this.asset = params.asset;
257
278
  this.image = params.image;
258
279
  this.imageFile = params.imageFile;
280
+ this.llmOverride = params.llmOverride;
259
281
  this.scriptPath = params.scriptPath;
260
282
  this.scriptArgs = params.scriptArgs;
261
283
  this.scriptTimeoutMs = params.scriptTimeoutMs;
@@ -271,6 +293,9 @@ var StaticScriptSkill = class {
271
293
  if (result.spawnError) {
272
294
  throw new Error(`script spawn failed: ${result.spawnError.message}`);
273
295
  }
296
+ if (result.code === SCRIPT_EXIT_BILLING_EXHAUSTED) {
297
+ throw new ScriptBillingExhaustedError(result.code, result.stdout, result.stderr);
298
+ }
274
299
  if (result.code !== 0) {
275
300
  const detail = result.stderr.trim() || result.stdout.trim() || "(no output)";
276
301
  throw new Error(`script failed (exit ${result.code}): ${detail}`);
@@ -287,6 +312,7 @@ var DynamicScriptSkill = class {
287
312
  mode = "dynamic-script";
288
313
  image;
289
314
  imageFile;
315
+ llmOverride;
290
316
  scriptPath;
291
317
  scriptArgs;
292
318
  scriptTimeoutMs;
@@ -299,6 +325,7 @@ var DynamicScriptSkill = class {
299
325
  this.asset = params.asset;
300
326
  this.image = params.image;
301
327
  this.imageFile = params.imageFile;
328
+ this.llmOverride = params.llmOverride;
302
329
  this.scriptPath = params.scriptPath;
303
330
  this.scriptArgs = params.scriptArgs;
304
331
  this.scriptTimeoutMs = params.scriptTimeoutMs;
@@ -315,6 +342,9 @@ var DynamicScriptSkill = class {
315
342
  if (result.spawnError) {
316
343
  throw new Error(`script spawn failed: ${result.spawnError.message}`);
317
344
  }
345
+ if (result.code === SCRIPT_EXIT_BILLING_EXHAUSTED) {
346
+ throw new ScriptBillingExhaustedError(result.code, result.stdout, result.stderr);
347
+ }
318
348
  if (result.code !== 0) {
319
349
  const detail = result.stderr.trim() || result.stdout.trim() || "(no output)";
320
350
  throw new Error(`script failed (exit ${result.code}): ${detail}`);
@@ -558,9 +588,9 @@ function validateLlmOverride(skillName, frontmatter, mode) {
558
588
  if (!hasProvider && !hasModel && !hasMaxTokens) {
559
589
  return void 0;
560
590
  }
561
- if (mode !== "llm") {
591
+ if (hasMaxTokens && mode !== "llm") {
562
592
  throw new Error(
563
- `SKILL.md "${skillName}": "provider"/"model"/"max_tokens" are only valid in mode 'llm' (got '${mode}')`
593
+ `SKILL.md "${skillName}": "max_tokens" is only valid in mode 'llm' (got '${mode}'). For script modes, control token limits inside the script.`
564
594
  );
565
595
  }
566
596
  if (hasProvider !== hasModel) {
@@ -817,7 +847,8 @@ function buildSkillFromParsed(parsed, skillDir, logger) {
817
847
  asset: parsed.asset,
818
848
  outputFilePath,
819
849
  image: parsed.image,
820
- imageFile: parsed.imageFile
850
+ imageFile: parsed.imageFile,
851
+ llmOverride: parsed.llmOverride
821
852
  });
822
853
  }
823
854
  case "static-script":
@@ -842,7 +873,8 @@ function buildSkillFromParsed(parsed, skillDir, logger) {
842
873
  scriptArgs: parsed.scriptArgs,
843
874
  scriptTimeoutMs: parsed.scriptTimeoutMs ?? DEFAULT_SCRIPT_TIMEOUT_MS,
844
875
  image: parsed.image,
845
- imageFile: parsed.imageFile
876
+ imageFile: parsed.imageFile,
877
+ llmOverride: parsed.llmOverride
846
878
  });
847
879
  }
848
880
  }