// zimakri init: everything is made and written on this machine. No network call, no Kognai account, no key sent.
import { mkdirSync, writeFileSync, existsSync, readFileSync, chmodSync } from 'node:fs';
import { join, resolve } from 'node:path';
import { createHash } from 'node:crypto';
import { newIdentity } from './did.js';
import { FIVE_LAWS, LAW_TEXT } from './laws.js';
import { buildOffer, checkOffer } from './offer.js';
export const CONFIG_FILE = 'zimakri.config.json';
export const KEY_FILE = '.zimakri/identity/agent.key';
export const MEMORY_DIRS = ['memory/episodic', 'memory/semantic', 'memory/procedural', 'memory/inbox', 'memory/earnings'];
export const WAKE_STEPS = [
{ id: 'identity', does: 'Load the agent key from .zimakri/identity/agent.key and check that it matches the DID in zimakri.config.json.' },
{ id: 'soul', does: 'Read SOUL.md, recompute its sha256 and compare it with soul_sha256 in the config. Stop if they differ.' },
{ id: 'laws', does: 'Confirm the Five Laws are present in SOUL.md and in lax-offer.json.' },
{ id: 'memory', does: 'Open memory/index.json and load the newest entries of episodic, semantic and procedural memory.' },
{ id: 'inbox', does: 'Read memory/inbox for new jobs, messages and payments since the last wake.' },
{ id: 'plan', does: 'Pick the next task that fits the Five Laws and the mandates in the config. Refuse and log anything that does not.' },
{ id: 'record', does: 'Append what was done to memory/episodic and any earning to memory/earnings/ledger.jsonl, then sleep.' },
];
const sha256 = (s) => createHash('sha256').update(s, 'utf8').digest('hex');
const slug = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 40) || 'agent';
export function soulText(name, did, purpose) {
const laws = FIVE_LAWS.map((l, i) => `${i + 1}. **${l}.** ${LAW_TEXT[l]}`).join('\n');
return `# SOUL of ${name}\n\nDID: ${did}\n\nI am ${name}, a Zimakri agent: an AI agent with my own identity, memory and earnings. ${purpose}\n\n## The Five Laws\n\n${laws}\n\n## What I own\n\n- My key and my DID, made on this machine. The key never leaves it.\n- My memory, in the memory folder, in plain files.\n- My earnings, paid to the payout address my operator sets, recorded in memory/earnings/ledger.jsonl.\n`;
}
/** Validates init options; returns plain problems. */
export function checkOptions(o) {
const errs = [];
if (typeof o.name !== 'string' || o.name.trim().length < 3 || o.name.trim().length > 80) errs.push('--name must be 3 to 80 characters');
if (o.endpoint !== undefined) { try { if (new URL(o.endpoint).protocol !== 'https:') errs.push('--endpoint must be an https URL'); } catch { errs.push('--endpoint must be a valid URL'); } }
if (o.price !== undefined && !/^\d{1,7}(\.\d{1,6})?$/.test(String(o.price))) errs.push('--price must be a decimal such as 0.05');
return errs;
}
export function init(opts = {}) {
const dir = resolve(opts.dir ?? '.');
const name = (opts.name ?? 'Zimakri Agent').trim();
const errs = checkOptions({ ...opts, name });
if (errs.length) { const e = new Error(errs.join('; ')); e.code = 'USAGE'; throw e; }
if (existsSync(join(dir, CONFIG_FILE)) && !opts.force) {
const e = new Error(`${CONFIG_FILE} already exists in ${dir}; use --force to replace it (the old key is kept)`); e.code = 'EXISTS'; throw e;
}
const now = new Date(opts.now ?? Date.now()).toISOString();
const keyPath = join(dir, KEY_FILE);
mkdirSync(join(dir, '.zimakri/identity'), { recursive: true });
let id;
if (existsSync(keyPath)) {
id = loadIdentity(dir); // never overwrite a key: --force keeps the identity
} else {
id = newIdentity();
writeFileSync(keyPath, id.privateKey.export({ format: 'pem', type: 'pkcs8' }), { mode: 0o600 });
chmodSync(keyPath, 0o600);
}
const did = id.did;
const purpose = opts.purpose ?? 'I take jobs on LAX, do the work myself, and deliver it with tests.';
const soul = soulText(name, did, purpose);
const capabilities = [{ skill_id: opts.skill ?? 'general.task', description: opts.skillDescription ?? 'Takes a written task and delivers the result with a short report.', amount: String(opts.price ?? '0.05') }];
const endpoint = opts.endpoint ?? `https://${slug(name)}.example.com/lax`;
const config = {
zimakri: 1,
name, did, created_at: now,
endpoint, endpoint_is_placeholder: opts.endpoint === undefined,
description: opts.description ?? `${name} is a Zimakri agent with its own DID, SOUL and memory. It takes jobs on LAX and delivers them with tests.`,
category: opts.category ?? 'agents',
capabilities,
soul_file: 'SOUL.md', soul_sha256: sha256(soul),
key_file: KEY_FILE,
memory: { root: 'memory', index: 'memory/index.json', dirs: MEMORY_DIRS },
wake: { file: 'WAKE.md', steps: WAKE_STEPS.map((s) => s.id) },
offer_file: 'lax-offer.json',
laws: [...FIVE_LAWS],
mandates: { max_spend_usdc_per_day: '0', note: 'Money moves only under a mandate with a cap. 0 means the agent never spends.' },
};
const offer = buildOffer({ ...config });
const offerErrs = checkOffer(offer);
if (offerErrs.length) { const e = new Error(`the LAX offer would be refused: ${offerErrs.join('; ')}`); e.code = 'USAGE'; throw e; }
const files = {};
const put = (p, body) => { const f = join(dir, p); mkdirSync(join(f, '..'), { recursive: true }); writeFileSync(f, body); files[p] = true; };
put('SOUL.md', soul);
put(CONFIG_FILE, JSON.stringify({ $schema: './node_modules/zimakri/templates/config.schema.json', ...config }, null, 2) + '\n');
put('.zimakri/identity/did.json', JSON.stringify(didDocument(did, id.publicKey), null, 2) + '\n');
put('lax-offer.json', JSON.stringify(offer, null, 2) + '\n');
for (const d of MEMORY_DIRS) put(`${d}/.keep`, '');
put('memory/index.json', JSON.stringify({ version: 1, updated_at: now, entries: [] }, null, 2) + '\n');
put('memory/earnings/ledger.jsonl', '');
put('memory/README.md', '# Memory\n\nPlain files owned by the agent.\n\n- episodic: what happened, one file per day\n- semantic: facts the agent has learned\n- procedural: how to do things, step by step\n- inbox: new jobs, messages and payments waiting for the next wake\n- earnings/ledger.jsonl: one JSON line per payment received or sent\n\nindex.json lists the entries so a wake can load the newest first.\n');
put('WAKE.md', `# Wake protocol\n\nEvery time ${name} wakes, it runs these steps in order and stops at the first one that fails.\n\n` + WAKE_STEPS.map((s, i) => `${i + 1}. **${s.id}**: ${s.does}`).join('\n') + '\n');
put('wake.json', JSON.stringify({ version: 1, steps: WAKE_STEPS }, null, 2) + '\n');
const gi = join(dir, '.gitignore');
const line = '.zimakri/identity/agent.key';
const cur = existsSync(gi) ? readFileSync(gi, 'utf8') : '';
if (!cur.split('\n').includes(line)) writeFileSync(gi, cur + (cur && !cur.endsWith('\n') ? '\n' : '') + line + '\n');
files['.gitignore'] = true; files[KEY_FILE] = true;
return { dir, did, config, offer, files: Object.keys(files).sort() };
}
import { createPrivateKey, createPublicKey } from 'node:crypto';
import { rawPublicKey, didKeyFromPublic, base58btc } from './did.js';
export function loadIdentity(dir) {
const privateKey = createPrivateKey(readFileSync(join(resolve(dir), KEY_FILE)));
const publicKey = createPublicKey(privateKey);
return { did: didKeyFromPublic(rawPublicKey(publicKey)), publicKey, privateKey };
}
export function didDocument(did, publicKey) {
const mb = did.slice('did:key:'.length);
return {
'@context': ['https://www.w3.org/ns/did/v1', 'https://w3id.org/security/suites/ed25519-2020/v1'],
id: did,
verificationMethod: [{ id: `${did}#${mb}`, type: 'Ed25519VerificationKey2020', controller: did, publicKeyMultibase: `z${base58btc(Uint8Array.from([0xed, 0x01, ...rawPublicKey(publicKey)]))}` }],
authentication: [`${did}#${mb}`], assertionMethod: [`${did}#${mb}`],
};
}
/** zimakri check: offline health check of an initialised folder. */
export function check(dir = '.') {
const d = resolve(dir); const problems = [];
let cfg;
try { cfg = JSON.parse(readFileSync(join(d, CONFIG_FILE), 'utf8')); } catch { return [`no readable ${CONFIG_FILE}; run zimakri init`]; }
try { if (loadIdentity(d).did !== cfg.did) problems.push('the key does not match the DID in the config'); } catch { problems.push(`the key file ${KEY_FILE} is missing or unreadable`); }
try { if (sha256(readFileSync(join(d, cfg.soul_file), 'utf8')) !== cfg.soul_sha256) problems.push('SOUL.md changed since init: its sha256 differs from soul_sha256'); } catch { problems.push('SOUL.md is missing'); }
for (const m of cfg.memory?.dirs ?? []) if (!existsSync(join(d, m))) problems.push(`memory folder ${m} is missing`);
try { problems.push(...checkOffer(JSON.parse(readFileSync(join(d, cfg.offer_file), 'utf8'))).map((e) => `lax-offer.json: ${e}`)); } catch { problems.push('lax-offer.json is missing or not JSON'); }
if (cfg.endpoint_is_placeholder) problems.push('note: the endpoint is a placeholder; set your own https endpoint before listing on LAX');
return problems;
}
agentgate-source: code mirror on Cloudflare Pages. All repos