Publish portable agent state market research
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
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));
|
||||
Reference in New Issue
Block a user