orbit-agent-runtime 0.8.0 → 0.11.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/CHANGELOG.md +125 -0
- package/README.md +6 -2
- package/README.zh-CN.md +5 -1
- package/bin/orbit.mjs +561 -523
- package/dist/.tsbuildinfo +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -3
- package/dist/src/index.js.map +1 -1
- package/dist/test/gateway.test.js +1 -1
- package/dist/test/gateway.test.js.map +1 -1
- package/dist/test/report_signing.test.d.ts +2 -0
- package/dist/test/report_signing.test.d.ts.map +1 -0
- package/dist/test/report_signing.test.js +87 -0
- package/dist/test/report_signing.test.js.map +1 -0
- package/examples/README.md +38 -34
- package/examples/langgraph-orchestration.mjs +245 -0
- package/package.json +4 -1
- package/src/index.ts +5 -1
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
/**
|
|
7
|
+
* W35 — compliance report signing (ED25519, audit-grade).
|
|
8
|
+
*
|
|
9
|
+
* A signed report is verifiable by any third party holding the public key —
|
|
10
|
+
* no shared secret, no access to the original system. That is what makes a
|
|
11
|
+
* compliance report acceptable to an external auditor.
|
|
12
|
+
*/
|
|
13
|
+
const node_test_1 = require("node:test");
|
|
14
|
+
const strict_1 = __importDefault(require("node:assert/strict"));
|
|
15
|
+
const index_1 = require("../src/index");
|
|
16
|
+
const SEED = "a1b2c3d4e5f60718293a4b5c6d7e8f901a2b3c4d5e6f708192a3b4c5d6e7f8091";
|
|
17
|
+
const OTHER_SEED = "00000000000000000000000000000000000000000000000000000000000000ff";
|
|
18
|
+
function sampleReport() {
|
|
19
|
+
return {
|
|
20
|
+
meta: { product: "Orbit Agent Runtime", version: "0.11.0", generatedAt: "2026-09-02T00:00:00.000Z" },
|
|
21
|
+
governance: {
|
|
22
|
+
tier: "Strict(合规)", profile: "strict", compression: "aggressive",
|
|
23
|
+
limiter: "60/60", trip: "3 次 / 5000ms", paeAdmission: "关闭",
|
|
24
|
+
traceDurability: "required", maxIsolationLevel: "L1", schemaMode: "required"
|
|
25
|
+
},
|
|
26
|
+
audit: { entries: 7, signed: true, consistent: true, status: "PASS", text: "链一致" },
|
|
27
|
+
interventions: { 限流: 1 },
|
|
28
|
+
determinism: { calls: 4, flagged: 1 },
|
|
29
|
+
summary: "审计链完整且已签名"
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
(0, node_test_1.test)("report signing: seed deterministically derives a stable key pair", () => {
|
|
33
|
+
const a = (0, index_1.deriveReportKeyPair)(SEED);
|
|
34
|
+
const b = (0, index_1.deriveReportKeyPair)(SEED);
|
|
35
|
+
strict_1.default.equal(a.privateKeyPem, b.privateKeyPem, "same seed -> same private key");
|
|
36
|
+
strict_1.default.equal(a.publicKeyPem, b.publicKeyPem, "same seed -> same public key");
|
|
37
|
+
const c = (0, index_1.deriveReportKeyPair)(OTHER_SEED);
|
|
38
|
+
strict_1.default.notEqual(a.publicKeyPem, c.publicKeyPem, "different seed -> different key");
|
|
39
|
+
strict_1.default.equal((0, index_1.publicKeyFingerprint)(a.publicKeyPem), (0, index_1.publicKeyFingerprint)(b.publicKeyPem));
|
|
40
|
+
});
|
|
41
|
+
(0, node_test_1.test)("report signing: a valid seed must be 32 bytes of hex", () => {
|
|
42
|
+
strict_1.default.throws(() => (0, index_1.deriveReportKeyPair)("short"), /32 bytes/);
|
|
43
|
+
strict_1.default.throws(() => (0, index_1.deriveReportKeyPair)("zz".repeat(32)), /32 bytes/);
|
|
44
|
+
});
|
|
45
|
+
(0, node_test_1.test)("report signing: sign then verify round-trips", () => {
|
|
46
|
+
const report = sampleReport();
|
|
47
|
+
const signed = (0, index_1.signComplianceReport)(report, SEED);
|
|
48
|
+
strict_1.default.ok(signed.sig, "report carries a signature");
|
|
49
|
+
strict_1.default.equal(signed.sig.algorithm, "ed25519");
|
|
50
|
+
strict_1.default.equal(signed.sig.publicKeyFingerprint, (0, index_1.publicKeyFingerprint)((0, index_1.deriveReportKeyPair)(SEED).publicKeyPem));
|
|
51
|
+
const verified = (0, index_1.verifyComplianceReport)(signed, (0, index_1.deriveReportKeyPair)(SEED).publicKeyPem);
|
|
52
|
+
strict_1.default.equal(verified.ok, true, "a genuine signed report verifies");
|
|
53
|
+
});
|
|
54
|
+
(0, node_test_1.test)("report signing: signing is deterministic", () => {
|
|
55
|
+
const s1 = (0, index_1.signComplianceReport)(sampleReport(), SEED);
|
|
56
|
+
const s2 = (0, index_1.signComplianceReport)(sampleReport(), SEED);
|
|
57
|
+
strict_1.default.equal(s1.sig.signature, s2.sig.signature, "same report + seed -> same signature");
|
|
58
|
+
});
|
|
59
|
+
(0, node_test_1.test)("report signing: tampering with the report body breaks verification", () => {
|
|
60
|
+
const signed = (0, index_1.signComplianceReport)(sampleReport(), SEED);
|
|
61
|
+
const tampered = {
|
|
62
|
+
...signed,
|
|
63
|
+
meta: { ...signed.meta, version: "0.99.0-evil" }
|
|
64
|
+
};
|
|
65
|
+
const verified = (0, index_1.verifyComplianceReport)(tampered, (0, index_1.deriveReportKeyPair)(SEED).publicKeyPem);
|
|
66
|
+
strict_1.default.equal(verified.ok, false);
|
|
67
|
+
strict_1.default.match(verified.reason ?? "", /digest/);
|
|
68
|
+
});
|
|
69
|
+
(0, node_test_1.test)("report signing: the wrong key (or an unsigned report) fails", () => {
|
|
70
|
+
const signed = (0, index_1.signComplianceReport)(sampleReport(), SEED);
|
|
71
|
+
const wrongKey = (0, index_1.verifyComplianceReport)(signed, (0, index_1.deriveReportKeyPair)(OTHER_SEED).publicKeyPem);
|
|
72
|
+
strict_1.default.equal(wrongKey.ok, false);
|
|
73
|
+
strict_1.default.match(wrongKey.reason ?? "", /signer mismatch/);
|
|
74
|
+
const unsigned = (0, index_1.verifyComplianceReport)(sampleReport(), (0, index_1.deriveReportKeyPair)(SEED).publicKeyPem);
|
|
75
|
+
strict_1.default.equal(unsigned.ok, false);
|
|
76
|
+
strict_1.default.match(unsigned.reason ?? "", /no ed25519 signature/);
|
|
77
|
+
});
|
|
78
|
+
(0, node_test_1.test)("report signing: a mutated signature fails", () => {
|
|
79
|
+
const signed = (0, index_1.signComplianceReport)(sampleReport(), SEED);
|
|
80
|
+
const broken = {
|
|
81
|
+
...signed,
|
|
82
|
+
sig: { ...signed.sig, signature: signed.sig.signature.replace(/^./, "A") }
|
|
83
|
+
};
|
|
84
|
+
const verified = (0, index_1.verifyComplianceReport)(broken, (0, index_1.deriveReportKeyPair)(SEED).publicKeyPem);
|
|
85
|
+
strict_1.default.equal(verified.ok, false);
|
|
86
|
+
});
|
|
87
|
+
//# sourceMappingURL=report_signing.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report_signing.test.js","sourceRoot":"","sources":["../../test/report_signing.test.ts"],"names":[],"mappings":"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"}
|
package/examples/README.md
CHANGED
|
@@ -1,34 +1,38 @@
|
|
|
1
|
-
# Examples
|
|
2
|
-
|
|
3
|
-
Run each example with a plain `node` — no build step, no extra dependencies:
|
|
4
|
-
|
|
5
|
-
```bash
|
|
6
|
-
# 1. a custom deterministic channel: wire it in, record, replay byte-identically
|
|
7
|
-
node examples/custom-channel.mjs
|
|
8
|
-
|
|
9
|
-
# 2. a JS plugin adapter (PAE, L0): foreign in-process tools through the gateway
|
|
10
|
-
node examples/js-pae-plugin.mjs
|
|
11
|
-
|
|
12
|
-
# 3. an MCP adapter (PAE, L2): a real stdio child process, handshake-discovered
|
|
13
|
-
node examples/mcp-adapter.mjs
|
|
14
|
-
|
|
15
|
-
# 4. the orbit CLI record → replay → diff loop, driven exactly as a user would
|
|
16
|
-
node examples/cli-record-replay.mjs
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
|
23
|
-
|
|
24
|
-
| `
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
Run each example with a plain `node` — no build step, no extra dependencies:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
# 1. a custom deterministic channel: wire it in, record, replay byte-identically
|
|
7
|
+
node examples/custom-channel.mjs
|
|
8
|
+
|
|
9
|
+
# 2. a JS plugin adapter (PAE, L0): foreign in-process tools through the gateway
|
|
10
|
+
node examples/js-pae-plugin.mjs
|
|
11
|
+
|
|
12
|
+
# 3. an MCP adapter (PAE, L2): a real stdio child process, handshake-discovered
|
|
13
|
+
node examples/mcp-adapter.mjs
|
|
14
|
+
|
|
15
|
+
# 4. the orbit CLI record → replay → diff loop, driven exactly as a user would
|
|
16
|
+
node examples/cli-record-replay.mjs
|
|
17
|
+
|
|
18
|
+
# 5. LangGraph-style orchestration on a provable runtime (P1.2)
|
|
19
|
+
node examples/langgraph-orchestration.mjs
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
| Example | Shows |
|
|
23
|
+
|---|---|
|
|
24
|
+
| `custom-channel.mjs` | Implementing `IChannelProvider`, registering it, and proving the record → replay loop is byte-identical — plus drift detection when a call is tampered with |
|
|
25
|
+
| `js-pae-plugin.mjs` | Mapping plain JS functions onto the capability contract via `JsPaeAdapter`; the adapter surface becomes a governed channel whose output replays verbatim |
|
|
26
|
+
| `mcp-adapter.mjs` | Spawning a real MCP server child, `initialize` → `tools/list` handshake, recorded call, then replay **after the peer is dead** (the frozen output is injected, the child is never re-entered) |
|
|
27
|
+
| `cli-record-replay.mjs` | The three-command reproducibility story: `orbit record` → `orbit replay` (zero channel calls) → `orbit diff` (digest-chain reconciliation) |
|
|
28
|
+
| `langgraph-orchestration.mjs` | P1.2 — a minimal LangGraph-style StateGraph whose agent nodes call tools through the Orbit gateway: the SAME graph runs twice, record (real tool side effects) then replay (frozen outputs, zero re-execution), and the final answer is byte-identical while the signed audit chain verifies PASS |
|
|
29
|
+
|
|
30
|
+
The imports use relative paths so the examples run inside the repository;
|
|
31
|
+
after `npm i orbit-agent-runtime` they become plain package imports:
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
import { OrbitRuntimeHost, ChannelKind } from "orbit-agent-runtime";
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Every example exits non-zero on any failed assertion, so they can be used as
|
|
38
|
+
smoke checks in CI (`node examples/custom-channel.mjs && ...`).
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example 5 — LangGraph-style orchestration on a provable runtime.
|
|
3
|
+
*
|
|
4
|
+
* PRODUCT_PLAN P1.2 (ecosystem embedding) first deliverable. LangGraph is the
|
|
5
|
+
* orchestrator's home turf — a StateGraph of nodes and edges that passes a
|
|
6
|
+
* state object around. What LangGraph does NOT do is make the *tool calls*
|
|
7
|
+
* inside its nodes reproducible, auditable or provable. That is the runtime
|
|
8
|
+
* layer beneath the orchestrator, and that is what Orbit provides.
|
|
9
|
+
*
|
|
10
|
+
* This example runs a minimal, self-contained LangGraph-style engine
|
|
11
|
+
* (`MiniStateGraph`: nodes, edges, conditional routing, state reduction —
|
|
12
|
+
* the semantics a LangGraph user would recognise) whose agent nodes call
|
|
13
|
+
* tools through the Orbit gateway. It then proves the product promise at the
|
|
14
|
+
* GRAPH level, not just the single-call level:
|
|
15
|
+
*
|
|
16
|
+
* 1. record — run the graph once with real tool execution (KV writes);
|
|
17
|
+
* 2. replay — run the SAME graph again with a replay engine attached: every
|
|
18
|
+
* tool call is served frozen output, ZERO real side effects occur, and
|
|
19
|
+
* the final answer is byte-identical to the recorded run;
|
|
20
|
+
* 3. audit — the signed audit chain verifies PASS, so the graph run is
|
|
21
|
+
* tamper-evident and reportable.
|
|
22
|
+
*
|
|
23
|
+
* In a real deployment the MiniStateGraph below is replaced by LangGraph (or
|
|
24
|
+
* any orchestrator); nothing else changes — tools stay behind the gateway.
|
|
25
|
+
*
|
|
26
|
+
* Run: node examples/langgraph-orchestration.mjs
|
|
27
|
+
*
|
|
28
|
+
* For npm consumers the imports below become
|
|
29
|
+
* import { OrbitRuntimeHost, ChannelKind, ... } from "orbit-agent-runtime";
|
|
30
|
+
* The relative paths are only for running inside this repository.
|
|
31
|
+
*/
|
|
32
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
33
|
+
import { tmpdir } from "node:os";
|
|
34
|
+
import { join } from "node:path";
|
|
35
|
+
|
|
36
|
+
import { OrbitRuntimeHost, ChannelKind } from "../dist/src/index.js";
|
|
37
|
+
import { SeededRng, PersistedRecordJournal, ReplayEngine, makeUniqueMark } from "../dist/src/index.js";
|
|
38
|
+
|
|
39
|
+
/* ---------------------------------------------------------------------------
|
|
40
|
+
* MiniStateGraph — a minimal LangGraph-style engine.
|
|
41
|
+
*
|
|
42
|
+
* Node: (state) => partial state (reduced into the running state); may be
|
|
43
|
+
* async. Edge: unconditional (from -> to) or conditional
|
|
44
|
+
* (router(state) -> next). invoke() walks from START until END,
|
|
45
|
+
* awaiting each node and reducing state at every step.
|
|
46
|
+
* The engine itself is deterministic; tool side effects live behind the
|
|
47
|
+
* Orbit gateway and are what record/replay governs.
|
|
48
|
+
* ------------------------------------------------------------------------- */
|
|
49
|
+
|
|
50
|
+
const START = Symbol("START");
|
|
51
|
+
const END = Symbol("END");
|
|
52
|
+
|
|
53
|
+
class MiniStateGraph {
|
|
54
|
+
constructor() {
|
|
55
|
+
this.nodes = new Map();
|
|
56
|
+
this.edges = new Map(); // node -> next | router(state) -> next
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
addNode(name, fn) {
|
|
60
|
+
this.nodes.set(name, fn);
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
addEdge(from, to) {
|
|
65
|
+
this.edges.set(from, to);
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
addConditionalEdges(from, router) {
|
|
70
|
+
this.edges.set(from, router);
|
|
71
|
+
return this;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Run the graph from START until END; returns the final state. */
|
|
75
|
+
async invoke(initialState, onCall = () => {}) {
|
|
76
|
+
let state = { ...initialState };
|
|
77
|
+
let node = START;
|
|
78
|
+
const visits = new Map();
|
|
79
|
+
while (node !== END) {
|
|
80
|
+
if (node === START) {
|
|
81
|
+
node = this.edges.get(START);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
visits.set(node, (visits.get(node) ?? 0) + 1);
|
|
85
|
+
if (visits.get(node) > 50) throw new Error(`graph loop detected at node '${node}'`);
|
|
86
|
+
const fn = this.nodes.get(node);
|
|
87
|
+
if (!fn) throw new Error(`node '${node}' not registered`);
|
|
88
|
+
const patch = (await fn(state, onCall)) ?? {};
|
|
89
|
+
state = { ...state, ...patch };
|
|
90
|
+
const edge = this.edges.get(node);
|
|
91
|
+
node = typeof edge === "function" ? edge(state) : edge;
|
|
92
|
+
}
|
|
93
|
+
return state;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/* ---------------------------------------------------------------------------
|
|
98
|
+
* The agent graph — research: plan (model) -> tools -> answer.
|
|
99
|
+
* Tools: mem.writeEntry (side effect), mem.readEntry (read).
|
|
100
|
+
* ------------------------------------------------------------------------- */
|
|
101
|
+
|
|
102
|
+
/** A fake model: turns the question into a deterministic tool plan. */
|
|
103
|
+
function planTools(rng, question) {
|
|
104
|
+
// The "model" consults the question and a seeded rng, so the SAME graph run
|
|
105
|
+
// is reproducible without calling a real model — the record/replay contrast
|
|
106
|
+
// stays about tool-call determinism, not model reproducibility.
|
|
107
|
+
const roll = rng.next();
|
|
108
|
+
const plan = [];
|
|
109
|
+
if (roll < 0.6) {
|
|
110
|
+
plan.push({ tool: "writeEntry", args: ["notes", `observed: ${question}`, 0] });
|
|
111
|
+
}
|
|
112
|
+
plan.push({ tool: "readEntry", args: ["notes"] });
|
|
113
|
+
plan.push({ tool: "readEntry", args: ["facts"] });
|
|
114
|
+
return plan;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function buildResearchGraph(host, pluginId, seed) {
|
|
118
|
+
const rng = new SeededRng(seed);
|
|
119
|
+
const ctxFor = () => ({ traceMarkId: makeUniqueMark(), maxWaitMs: 5000, pluginUnitId: pluginId });
|
|
120
|
+
const graph = new MiniStateGraph();
|
|
121
|
+
|
|
122
|
+
graph.addEdge(START, "planner");
|
|
123
|
+
graph.addNode("planner", (state) => {
|
|
124
|
+
const plan = planTools(rng, state.question);
|
|
125
|
+
return { plan, model: "simulated", seed };
|
|
126
|
+
});
|
|
127
|
+
graph.addConditionalEdges("planner", (state) => (state.plan.length > 0 ? "tools" : END));
|
|
128
|
+
|
|
129
|
+
graph.addNode("tools", async (state) => {
|
|
130
|
+
const results = [];
|
|
131
|
+
for (const item of state.plan) {
|
|
132
|
+
const out = await host.capabilityInvoke({
|
|
133
|
+
kind: ChannelKind.MEM_KV_STORE,
|
|
134
|
+
pluginId,
|
|
135
|
+
funcName: item.tool,
|
|
136
|
+
args: item.args,
|
|
137
|
+
mode: state.mode,
|
|
138
|
+
ctx: ctxFor()
|
|
139
|
+
});
|
|
140
|
+
results.push({ tool: item.tool, key: item.args[0], out });
|
|
141
|
+
}
|
|
142
|
+
return { results };
|
|
143
|
+
});
|
|
144
|
+
graph.addEdge("tools", "answer");
|
|
145
|
+
|
|
146
|
+
graph.addNode("answer", (state) => {
|
|
147
|
+
const note = state.results?.find((r) => r.tool === "readEntry" && r.key === "notes")?.out;
|
|
148
|
+
const fact = state.results?.find((r) => r.tool === "readEntry" && r.key === "facts")?.out;
|
|
149
|
+
return { answer: `research[${state.question}]: note=${note ?? "none"} fact=${fact ?? "none"}` };
|
|
150
|
+
});
|
|
151
|
+
graph.addEdge("answer", END);
|
|
152
|
+
return graph;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* ---------------------------------------------------------------------------
|
|
156
|
+
* Assertions & run
|
|
157
|
+
* ------------------------------------------------------------------------- */
|
|
158
|
+
|
|
159
|
+
function assert(cond, msg) {
|
|
160
|
+
if (!cond) {
|
|
161
|
+
console.error(`✗ ${msg}`);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
console.log(`✓ ${msg}`);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function main() {
|
|
168
|
+
const root = await mkdtemp(join(tmpdir(), "orbit-langgraph-"));
|
|
169
|
+
const walFile = join(root, "record.wal.jsonl");
|
|
170
|
+
const tracePath = join(root, "trace.wal.jsonl");
|
|
171
|
+
const seed = 20260902;
|
|
172
|
+
const pluginId = "research";
|
|
173
|
+
const question = "what is the capital of france?";
|
|
174
|
+
|
|
175
|
+
/* ---- Phase 1 · record: real tool execution ---- */
|
|
176
|
+
const recordHost = new OrbitRuntimeHost({
|
|
177
|
+
auditSigningKey: "example-key",
|
|
178
|
+
recordJournalPath: walFile,
|
|
179
|
+
traceJournalPath: tracePath
|
|
180
|
+
});
|
|
181
|
+
await recordHost.bootHost();
|
|
182
|
+
recordHost.registerPlugin({
|
|
183
|
+
id: pluginId,
|
|
184
|
+
displayName: "Research",
|
|
185
|
+
edition: "1.0.0",
|
|
186
|
+
requireHostMinEdition: "1.0.0",
|
|
187
|
+
allowCapabilities: ["channel:read", "channel:write"]
|
|
188
|
+
});
|
|
189
|
+
const recordJournal = recordHost.beginRecording();
|
|
190
|
+
const recordGraph = buildResearchGraph(recordHost, pluginId, seed);
|
|
191
|
+
const recorded = await recordGraph.invoke({ question, pluginId, mode: "record" });
|
|
192
|
+
// A completed graph run is itself an audit event (signed into the chain).
|
|
193
|
+
recordHost.traceJournal.append({
|
|
194
|
+
entryClass: "GRAPH_RUN",
|
|
195
|
+
traceMarkId: "graph-record",
|
|
196
|
+
factPayload: { graph: "research", calls: recordJournal.size(), answer: recorded.answer }
|
|
197
|
+
});
|
|
198
|
+
console.log(`record : ${recordJournal.size()} tool calls | answer=${recorded.answer}`);
|
|
199
|
+
await recordHost.shutdownHost();
|
|
200
|
+
|
|
201
|
+
/* ---- Phase 2 · replay: the SAME graph, zero real side effects ---- */
|
|
202
|
+
const replayHost = new OrbitRuntimeHost({
|
|
203
|
+
auditSigningKey: "example-key",
|
|
204
|
+
recordJournalPath: walFile,
|
|
205
|
+
traceJournalPath: tracePath
|
|
206
|
+
});
|
|
207
|
+
await replayHost.bootHost();
|
|
208
|
+
replayHost.registerPlugin({
|
|
209
|
+
id: pluginId,
|
|
210
|
+
displayName: "Research",
|
|
211
|
+
edition: "1.0.0",
|
|
212
|
+
requireHostMinEdition: "1.0.0",
|
|
213
|
+
allowCapabilities: ["channel:read", "channel:write"]
|
|
214
|
+
});
|
|
215
|
+
const recovered = await PersistedRecordJournal.recover(walFile);
|
|
216
|
+
const replayJournal = replayHost.beginRecording();
|
|
217
|
+
replayJournal.restoreSnapshot(recovered.snapshot());
|
|
218
|
+
replayHost.attachReplayEngine(replayJournal);
|
|
219
|
+
// The KV store on this host is EMPTY and writes never reach it: replay
|
|
220
|
+
// serves frozen outputs from the recorded window. If replay really
|
|
221
|
+
// re-executed, readEntry("notes") would return null and the answer would
|
|
222
|
+
// differ — the byte-identical assertion below is therefore meaningful.
|
|
223
|
+
const replayGraph = buildResearchGraph(replayHost, pluginId, seed);
|
|
224
|
+
const replayed = await replayGraph.invoke({ question, pluginId, mode: "replay" });
|
|
225
|
+
|
|
226
|
+
/* ---- Phase 3 · assertions ---- */
|
|
227
|
+
console.log(`replay : ${recovered.size()} calls injected | answer=${replayed.answer}`);
|
|
228
|
+
assert(recorded.answer === replayed.answer, "graph-level answer is byte-identical across record and replay");
|
|
229
|
+
assert(
|
|
230
|
+
JSON.stringify(recorded.results) === JSON.stringify(replayed.results),
|
|
231
|
+
"every tool result is byte-identical (frozen outputs, zero re-execution)"
|
|
232
|
+
);
|
|
233
|
+
assert(recovered.size() === recordJournal.size(), `all ${recordJournal.size()} tool calls were recorded`);
|
|
234
|
+
const chain = replayHost.verifyAuditChain();
|
|
235
|
+
assert(chain.consistent === true && chain.signed === true, `audit chain PASS (${chain.total} entries)`);
|
|
236
|
+
console.log("\nLangGraph-style orchestration on a provable runtime: record → replay → audit, all green.");
|
|
237
|
+
|
|
238
|
+
await replayHost.shutdownHost();
|
|
239
|
+
await rm(root, { recursive: true, force: true });
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
main().catch((err) => {
|
|
243
|
+
console.error(`✗ ${err.message}`);
|
|
244
|
+
process.exit(1);
|
|
245
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "orbit-agent-runtime",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Deterministic · Provable · Governable — a plugin-based agent runtime kernel with hot plugin registration, provable fault isolation, full-chain traceability and sandboxed execution.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agent",
|
|
@@ -38,6 +38,8 @@
|
|
|
38
38
|
"clean": "node -e \"fs.rmSync('dist',{recursive:true,force:true})\" && node -e \"for (const p of ['infra-common','core-hub','sandbox-runtime','pae-engine']) fs.rmSync('packages/'+p+'/dist',{recursive:true,force:true})\"",
|
|
39
39
|
"test": "npm run build && node --test \"dist/test/*.test.js\"",
|
|
40
40
|
"test:console": "node --test web/test/*.test.mjs",
|
|
41
|
+
"test:docsite": "node --test tools/render-md.test.mjs",
|
|
42
|
+
"docsite": "node tools/build-site.mjs",
|
|
41
43
|
"start:web": "node web/bridge-server.mjs",
|
|
42
44
|
"test:web": "node --test web/test/*.test.mjs",
|
|
43
45
|
"demo": "npm run build && node dist/demo-host.js",
|
|
@@ -47,6 +49,7 @@
|
|
|
47
49
|
"example:pae": "npm run build && node examples/js-pae-plugin.mjs",
|
|
48
50
|
"example:mcp": "npm run build && node examples/mcp-adapter.mjs",
|
|
49
51
|
"example:cli": "npm run build && node examples/cli-record-replay.mjs",
|
|
52
|
+
"example:langgraph": "npm run build && node examples/langgraph-orchestration.mjs",
|
|
50
53
|
"benchmark": "npm run build && node benchmarks/run-all.mjs",
|
|
51
54
|
"prepublishOnly": "npm run clean && npm run build && npm test && npm run test:console"
|
|
52
55
|
},
|
package/src/index.ts
CHANGED
|
@@ -49,7 +49,11 @@ export {
|
|
|
49
49
|
auditChainHash,
|
|
50
50
|
chainTailOf,
|
|
51
51
|
stableJson,
|
|
52
|
-
AUDIT_GENESIS_HASH
|
|
52
|
+
AUDIT_GENESIS_HASH,
|
|
53
|
+
deriveReportKeyPair,
|
|
54
|
+
signComplianceReport,
|
|
55
|
+
verifyComplianceReport,
|
|
56
|
+
publicKeyFingerprint
|
|
53
57
|
} from "@orbit/core-hub";
|
|
54
58
|
export type { AuditChainReport } from "@orbit/core-hub";
|
|
55
59
|
|