194 lines
9.8 KiB
JavaScript
194 lines
9.8 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
|
|
function canonical(value) {
|
|
if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`;
|
|
if (value && typeof value === "object") {
|
|
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`;
|
|
}
|
|
return JSON.stringify(value);
|
|
}
|
|
|
|
function digest(value) {
|
|
return crypto.createHash("sha256").update(canonical(value)).digest("hex");
|
|
}
|
|
|
|
function device(name) {
|
|
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
|
|
const publicPem = publicKey.export({ type: "spki", format: "pem" });
|
|
return { name, did: `did:key:${digest(publicPem).slice(0, 32)}`, publicKey, privateKey };
|
|
}
|
|
|
|
function createEvent(signer, body) {
|
|
const id = digest(body);
|
|
const signature = crypto.sign(null, Buffer.from(canonical(body)), signer.privateKey).toString("base64");
|
|
return { id, body, signature };
|
|
}
|
|
|
|
function verifyEvent(event, keys) {
|
|
if (digest(event.body) !== event.id) return false;
|
|
const key = keys.get(event.body.writer);
|
|
return Boolean(key && crypto.verify(null, Buffer.from(canonical(event.body)), key, Buffer.from(event.signature, "base64")));
|
|
}
|
|
|
|
function eventOrder(a, b) {
|
|
return a.body.clock - b.body.clock || a.id.localeCompare(b.id);
|
|
}
|
|
|
|
function reduce(events, keys, ownerDid) {
|
|
const unique = [...new Map(events.map((event) => [event.id, event])).values()].sort(eventOrder);
|
|
const rejected = [];
|
|
const grants = new Map();
|
|
const revocations = new Map();
|
|
|
|
for (const event of unique) {
|
|
if (!verifyEvent(event, keys)) {
|
|
rejected.push({ id: event.id, reason: "bad-signature-or-id" });
|
|
continue;
|
|
}
|
|
const { kind, writer, payload, clock } = event.body;
|
|
if (kind === "grant.issue") {
|
|
if (writer !== ownerDid) rejected.push({ id: event.id, reason: "non-owner-grant" });
|
|
else grants.set(payload.grant_id, payload);
|
|
}
|
|
if (kind === "grant.revoke") {
|
|
if (writer !== ownerDid) rejected.push({ id: event.id, reason: "non-owner-revocation" });
|
|
else revocations.set(payload.grant_id, clock);
|
|
}
|
|
}
|
|
|
|
const registers = new Map();
|
|
for (const event of unique) {
|
|
if (!verifyEvent(event, keys) || event.body.kind !== "state.put") continue;
|
|
const { writer, clock, payload, grant_id: grantId } = event.body;
|
|
const grant = grants.get(grantId);
|
|
const revokedAt = revocations.get(grantId);
|
|
let reason = null;
|
|
if (!grant) reason = "missing-grant";
|
|
else if (grant.grantee !== writer) reason = "wrong-grantee";
|
|
else if (!grant.actions.includes("state.put")) reason = "action-not-allowed";
|
|
else if (clock > grant.expires_at) reason = "expired-grant";
|
|
else if (revokedAt !== undefined && clock >= revokedAt) reason = "revoked-grant";
|
|
if (reason) {
|
|
rejected.push({ id: event.id, reason });
|
|
continue;
|
|
}
|
|
const current = registers.get(payload.path);
|
|
if (!current || eventOrder(current.event, event) < 0) registers.set(payload.path, { value: payload.value, event });
|
|
}
|
|
|
|
const state = Object.fromEntries([...registers.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([path, item]) => [path, item.value]));
|
|
return { state, state_hash: digest(state), accepted_events: unique.length - rejected.length, rejected };
|
|
}
|
|
|
|
function exportHarnessA(internal, signer, ownerDid, grantId, startClock) {
|
|
const events = [];
|
|
let clock = startClock;
|
|
const put = (path, value) => events.push(createEvent(signer, {
|
|
version: "probe-0.1", owner: ownerDid, writer: signer.did, clock: clock++, kind: "state.put", grant_id: grantId, payload: { path, value }
|
|
}));
|
|
for (const [key, value] of Object.entries(internal.user.preferences)) put(`profile.preferences.${key}`, value);
|
|
for (const memory of internal.memories) put(`memory.${memory.id}`, { text: memory.text, provenance: memory.source });
|
|
put("task.current", internal.task);
|
|
return {
|
|
events,
|
|
loss: ["tool_rules: target-independent execution semantics undefined"],
|
|
omitted: ["private_material: never serialized"]
|
|
};
|
|
}
|
|
|
|
function importHarnessB(state) {
|
|
const profile = { preferences: {} };
|
|
const recollections = {};
|
|
let work = null;
|
|
for (const [path, value] of Object.entries(state)) {
|
|
if (path.startsWith("profile.preferences.")) profile.preferences[path.split(".").at(-1)] = value;
|
|
else if (path.startsWith("memory.")) recollections[path.split(".").at(-1)] = value;
|
|
else if (path === "task.current") work = value;
|
|
}
|
|
return { profile, recollections, work, vault: "local-only" };
|
|
}
|
|
|
|
function auditAgentFile(path) {
|
|
if (!path) return null;
|
|
const file = JSON.parse(fs.readFileSync(path, "utf8"));
|
|
const agents = file.agents ?? [];
|
|
return {
|
|
format: "Letta Agent File",
|
|
agents: agents.length,
|
|
blocks: (file.blocks ?? []).length,
|
|
tools: (file.tools ?? []).length,
|
|
messages: agents.reduce((sum, agent) => sum + (agent.messages ?? []).length, 0),
|
|
contains_secret_values: agents.some((agent) => Object.values(agent.secrets ?? {}).some((value) => value !== null)),
|
|
known_portability_gaps: ["archival passages not represented", "framework-specific blocks/tool rules require adapters", "schema migration is roadmap work"]
|
|
};
|
|
}
|
|
|
|
const owner = device("owner");
|
|
const harnessA = device("harness-a");
|
|
const harnessB = device("harness-b");
|
|
const keys = new Map([[owner.did, owner.publicKey], [harnessA.did, harnessA.publicKey], [harnessB.did, harnessB.publicKey]]);
|
|
const grantA = createEvent(owner, { version: "probe-0.1", owner: owner.did, writer: owner.did, clock: 1, kind: "grant.issue", payload: { grant_id: "grant-a", grantee: harnessA.did, actions: ["state.put"], expires_at: 100 } });
|
|
const grantB = createEvent(owner, { version: "probe-0.1", owner: owner.did, writer: owner.did, clock: 2, kind: "grant.issue", payload: { grant_id: "grant-b", grantee: harnessB.did, actions: ["state.put"], expires_at: 100 } });
|
|
|
|
const internalA = {
|
|
user: { preferences: { language: "zh-CN", answer_style: "direct" } },
|
|
memories: [{ id: "m1", text: "User works in Beijing", source: "user-stated" }],
|
|
task: { id: "research-1", status: "active", title: "portable state research" },
|
|
tool_rules: [{ tool: "shell", allow: "workspace-only" }],
|
|
private_material: { demo_credential: "redacted-local-only" }
|
|
};
|
|
const exportedA = exportHarnessA(internalA, harnessA, owner.did, "grant-a", 10);
|
|
const base = reduce([grantA, grantB, ...exportedA.events], keys, owner.did);
|
|
const importedB = importHarnessB(base.state);
|
|
|
|
const concurrentA = createEvent(harnessA, { version: "probe-0.1", owner: owner.did, writer: harnessA.did, clock: 20, kind: "state.put", grant_id: "grant-a", payload: { path: "profile.preferences.theme", value: "dark" } });
|
|
const concurrentB = createEvent(harnessB, { version: "probe-0.1", owner: owner.did, writer: harnessB.did, clock: 20, kind: "state.put", grant_id: "grant-b", payload: { path: "profile.preferences.theme", value: "light" } });
|
|
const mergedAB = reduce([grantA, grantB, ...exportedA.events, concurrentA, concurrentB], keys, owner.did);
|
|
const mergedBA = reduce([concurrentB, ...exportedA.events, grantB, concurrentA, grantA], keys, owner.did);
|
|
|
|
const tampered = structuredClone(exportedA.events[0]);
|
|
tampered.body.payload.value = "tampered";
|
|
const tamperResult = reduce([grantA, tampered], keys, owner.did);
|
|
|
|
const revokeB = createEvent(owner, { version: "probe-0.1", owner: owner.did, writer: owner.did, clock: 30, kind: "grant.revoke", payload: { grant_id: "grant-b" } });
|
|
const postRevoke = createEvent(harnessB, { version: "probe-0.1", owner: owner.did, writer: harnessB.did, clock: 31, kind: "state.put", grant_id: "grant-b", payload: { path: "profile.preferences.language", value: "en-US" } });
|
|
const revokeResult = reduce([grantA, grantB, ...exportedA.events, revokeB, postRevoke], keys, owner.did);
|
|
|
|
const tests = [];
|
|
const test = (name, fn) => { fn(); tests.push({ name, status: "pass" }); };
|
|
test("cross-harness preference import", () => assert.equal(importedB.profile.preferences.language, "zh-CN"));
|
|
test("cross-harness memory import", () => assert.equal(importedB.recollections.m1.text, "User works in Beijing"));
|
|
test("cross-harness task import", () => assert.equal(importedB.work.status, "active"));
|
|
test("merge converges independent of delivery order", () => assert.equal(mergedAB.state_hash, mergedBA.state_hash));
|
|
test("concurrent conflict resolves deterministically", () => assert.equal(mergedAB.state["profile.preferences.theme"], mergedBA.state["profile.preferences.theme"]));
|
|
test("tampering is rejected", () => assert.equal(tamperResult.rejected[0].reason, "bad-signature-or-id"));
|
|
test("revoked grant blocks later write", () => assert.ok(revokeResult.rejected.some((item) => item.reason === "revoked-grant")));
|
|
test("pre-revocation state remains valid", () => assert.equal(revokeResult.state["profile.preferences.language"], "zh-CN"));
|
|
test("secrets are omitted", () => assert.ok(!canonical(exportedA).includes("redacted-local-only")));
|
|
test("semantic loss is explicit", () => assert.ok(exportedA.loss[0].includes("tool_rules")));
|
|
|
|
const result = {
|
|
experiment: "portable-state-probe",
|
|
runtime: process.version,
|
|
tests,
|
|
summary: {
|
|
passed: tests.length,
|
|
failed: 0,
|
|
canonical_state_fields: Object.keys(base.state).length,
|
|
converged_state_hash: mergedAB.state_hash,
|
|
tamper_rejections: tamperResult.rejected.length,
|
|
revocation_rejections: revokeResult.rejected.filter((item) => item.reason === "revoked-grant").length,
|
|
explicit_loss_items: exportedA.loss.length,
|
|
omitted_secret_classes: exportedA.omitted.length
|
|
},
|
|
interpretation: {
|
|
demonstrated: ["signed event portability", "deterministic merge", "capability revocation", "secret omission", "loss reporting"],
|
|
not_demonstrated: ["behavioral equivalence across real model runtimes", "large-scale sync performance", "key recovery", "multi-user policy composition"]
|
|
},
|
|
agent_file_audit: auditAgentFile(process.argv[2])
|
|
};
|
|
|
|
console.log(JSON.stringify(result, null, 2));
|