orbit-agent-runtime 0.8.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 +750 -0
- package/LICENSE +201 -0
- package/README.md +492 -0
- package/README.zh-CN.md +389 -0
- package/benchmarks/README.md +24 -0
- package/benchmarks/gateway.mjs +68 -0
- package/benchmarks/pae.mjs +90 -0
- package/benchmarks/replay.mjs +51 -0
- package/benchmarks/run-all.mjs +24 -0
- package/benchmarks/wal.mjs +50 -0
- package/bin/orbit.mjs +523 -0
- package/dist/.tsbuildinfo +1 -0
- package/dist/demo-deepseek.d.ts +2 -0
- package/dist/demo-deepseek.d.ts.map +1 -0
- package/dist/demo-deepseek.js +66 -0
- package/dist/demo-deepseek.js.map +1 -0
- package/dist/demo-host.d.ts +2 -0
- package/dist/demo-host.d.ts.map +1 -0
- package/dist/demo-host.js +98 -0
- package/dist/demo-host.js.map +1 -0
- package/dist/demo-replay.d.ts +2 -0
- package/dist/demo-replay.d.ts.map +1 -0
- package/dist/demo-replay.js +68 -0
- package/dist/demo-replay.js.map +1 -0
- package/dist/src/core/orbitRuntimeHost.d.ts +349 -0
- package/dist/src/core/orbitRuntimeHost.d.ts.map +1 -0
- package/dist/src/core/orbitRuntimeHost.js +706 -0
- package/dist/src/core/orbitRuntimeHost.js.map +1 -0
- package/dist/src/index.d.ts +92 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +226 -0
- package/dist/src/index.js.map +1 -0
- package/dist/test/audit_chain.test.d.ts +2 -0
- package/dist/test/audit_chain.test.d.ts.map +1 -0
- package/dist/test/audit_chain.test.js +283 -0
- package/dist/test/audit_chain.test.js.map +1 -0
- package/dist/test/channel_hub.test.d.ts +2 -0
- package/dist/test/channel_hub.test.d.ts.map +1 -0
- package/dist/test/channel_hub.test.js +66 -0
- package/dist/test/channel_hub.test.js.map +1 -0
- package/dist/test/cordis_adapter.test.d.ts +2 -0
- package/dist/test/cordis_adapter.test.d.ts.map +1 -0
- package/dist/test/cordis_adapter.test.js +280 -0
- package/dist/test/cordis_adapter.test.js.map +1 -0
- package/dist/test/cost_routing.test.d.ts +2 -0
- package/dist/test/cost_routing.test.d.ts.map +1 -0
- package/dist/test/cost_routing.test.js +31 -0
- package/dist/test/cost_routing.test.js.map +1 -0
- package/dist/test/domain_transaction.test.d.ts +2 -0
- package/dist/test/domain_transaction.test.d.ts.map +1 -0
- package/dist/test/domain_transaction.test.js +351 -0
- package/dist/test/domain_transaction.test.js.map +1 -0
- package/dist/test/file_channel.test.d.ts +2 -0
- package/dist/test/file_channel.test.d.ts.map +1 -0
- package/dist/test/file_channel.test.js +179 -0
- package/dist/test/file_channel.test.js.map +1 -0
- package/dist/test/gateway.test.d.ts +2 -0
- package/dist/test/gateway.test.d.ts.map +1 -0
- package/dist/test/gateway.test.js +548 -0
- package/dist/test/gateway.test.js.map +1 -0
- package/dist/test/governance_profile.test.d.ts +2 -0
- package/dist/test/governance_profile.test.d.ts.map +1 -0
- package/dist/test/governance_profile.test.js +214 -0
- package/dist/test/governance_profile.test.js.map +1 -0
- package/dist/test/governance_schema.test.d.ts +2 -0
- package/dist/test/governance_schema.test.d.ts.map +1 -0
- package/dist/test/governance_schema.test.js +200 -0
- package/dist/test/governance_schema.test.js.map +1 -0
- package/dist/test/host_integration.test.d.ts +2 -0
- package/dist/test/host_integration.test.d.ts.map +1 -0
- package/dist/test/host_integration.test.js +104 -0
- package/dist/test/host_integration.test.js.map +1 -0
- package/dist/test/host_journal_persistence.test.d.ts +2 -0
- package/dist/test/host_journal_persistence.test.d.ts.map +1 -0
- package/dist/test/host_journal_persistence.test.js +367 -0
- package/dist/test/host_journal_persistence.test.js.map +1 -0
- package/dist/test/impact_domain.test.d.ts +2 -0
- package/dist/test/impact_domain.test.d.ts.map +1 -0
- package/dist/test/impact_domain.test.js +48 -0
- package/dist/test/impact_domain.test.js.map +1 -0
- package/dist/test/injected_clock.test.d.ts +13 -0
- package/dist/test/injected_clock.test.d.ts.map +1 -0
- package/dist/test/injected_clock.test.js +200 -0
- package/dist/test/injected_clock.test.js.map +1 -0
- package/dist/test/isolation_domain.test.d.ts +2 -0
- package/dist/test/isolation_domain.test.d.ts.map +1 -0
- package/dist/test/isolation_domain.test.js +302 -0
- package/dist/test/isolation_domain.test.js.map +1 -0
- package/dist/test/journal_wal.test.d.ts +2 -0
- package/dist/test/journal_wal.test.d.ts.map +1 -0
- package/dist/test/journal_wal.test.js +622 -0
- package/dist/test/journal_wal.test.js.map +1 -0
- package/dist/test/mcp_adapter.test.d.ts +2 -0
- package/dist/test/mcp_adapter.test.d.ts.map +1 -0
- package/dist/test/mcp_adapter.test.js +440 -0
- package/dist/test/mcp_adapter.test.js.map +1 -0
- package/dist/test/openai_compat_channel.test.d.ts +2 -0
- package/dist/test/openai_compat_channel.test.d.ts.map +1 -0
- package/dist/test/openai_compat_channel.test.js +325 -0
- package/dist/test/openai_compat_channel.test.js.map +1 -0
- package/dist/test/openapi_adapter.test.d.ts +2 -0
- package/dist/test/openapi_adapter.test.d.ts.map +1 -0
- package/dist/test/openapi_adapter.test.js +270 -0
- package/dist/test/openapi_adapter.test.js.map +1 -0
- package/dist/test/orbit_cli.test.d.ts +2 -0
- package/dist/test/orbit_cli.test.d.ts.map +1 -0
- package/dist/test/orbit_cli.test.js +137 -0
- package/dist/test/orbit_cli.test.js.map +1 -0
- package/dist/test/pact_verifier.test.d.ts +2 -0
- package/dist/test/pact_verifier.test.d.ts.map +1 -0
- package/dist/test/pact_verifier.test.js +41 -0
- package/dist/test/pact_verifier.test.js.map +1 -0
- package/dist/test/pae_adapter.test.d.ts +2 -0
- package/dist/test/pae_adapter.test.d.ts.map +1 -0
- package/dist/test/pae_adapter.test.js +469 -0
- package/dist/test/pae_adapter.test.js.map +1 -0
- package/dist/test/plugin_sandbox_guard.test.d.ts +2 -0
- package/dist/test/plugin_sandbox_guard.test.d.ts.map +1 -0
- package/dist/test/plugin_sandbox_guard.test.js +57 -0
- package/dist/test/plugin_sandbox_guard.test.js.map +1 -0
- package/dist/test/rate_limit_behavior.test.d.ts +2 -0
- package/dist/test/rate_limit_behavior.test.d.ts.map +1 -0
- package/dist/test/rate_limit_behavior.test.js +85 -0
- package/dist/test/rate_limit_behavior.test.js.map +1 -0
- package/dist/test/replay_compat.test.d.ts +2 -0
- package/dist/test/replay_compat.test.d.ts.map +1 -0
- package/dist/test/replay_compat.test.js +1122 -0
- package/dist/test/replay_compat.test.js.map +1 -0
- package/dist/test/replay_engine.test.d.ts +2 -0
- package/dist/test/replay_engine.test.d.ts.map +1 -0
- package/dist/test/replay_engine.test.js +85 -0
- package/dist/test/replay_engine.test.js.map +1 -0
- package/dist/test/shell_channel.test.d.ts +2 -0
- package/dist/test/shell_channel.test.d.ts.map +1 -0
- package/dist/test/shell_channel.test.js +173 -0
- package/dist/test/shell_channel.test.js.map +1 -0
- package/dist/test/teardown_isolation.test.d.ts +10 -0
- package/dist/test/teardown_isolation.test.d.ts.map +1 -0
- package/dist/test/teardown_isolation.test.js +279 -0
- package/dist/test/teardown_isolation.test.js.map +1 -0
- package/dist/test/token_budget.test.d.ts +2 -0
- package/dist/test/token_budget.test.d.ts.map +1 -0
- package/dist/test/token_budget.test.js +113 -0
- package/dist/test/token_budget.test.js.map +1 -0
- package/dist/test/trace_journal.test.d.ts +2 -0
- package/dist/test/trace_journal.test.d.ts.map +1 -0
- package/dist/test/trace_journal.test.js +43 -0
- package/dist/test/trace_journal.test.js.map +1 -0
- package/dist/test/trace_journal_persistence.test.d.ts +2 -0
- package/dist/test/trace_journal_persistence.test.d.ts.map +1 -0
- package/dist/test/trace_journal_persistence.test.js +175 -0
- package/dist/test/trace_journal_persistence.test.js.map +1 -0
- package/dist/test/trace_persistence.test.d.ts +2 -0
- package/dist/test/trace_persistence.test.d.ts.map +1 -0
- package/dist/test/trace_persistence.test.js +216 -0
- package/dist/test/trace_persistence.test.js.map +1 -0
- package/dist/test/trip_protector.test.d.ts +2 -0
- package/dist/test/trip_protector.test.d.ts.map +1 -0
- package/dist/test/trip_protector.test.js +42 -0
- package/dist/test/trip_protector.test.js.map +1 -0
- package/examples/README.md +34 -0
- package/examples/cli-record-replay.mjs +74 -0
- package/examples/custom-channel.mjs +102 -0
- package/examples/js-pae-plugin.mjs +128 -0
- package/examples/mcp-adapter.mjs +102 -0
- package/package.json +60 -0
- package/src/core/orbitRuntimeHost.ts +853 -0
- package/src/index.ts +256 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Benchmark — durable journal (WAL) write throughput.
|
|
3
|
+
*
|
|
4
|
+
* Measures append → flush for a persisted record journal: the in-memory append
|
|
5
|
+
* plus the fire-and-forget mirror through the serialised write chain. This is
|
|
6
|
+
* the sustained audit-write budget a host can rely on before WAL pressure
|
|
7
|
+
* becomes the bottleneck.
|
|
8
|
+
*
|
|
9
|
+
* Run: node benchmarks/wal.mjs
|
|
10
|
+
*/
|
|
11
|
+
import * as os from "node:os";
|
|
12
|
+
import * as path from "node:path";
|
|
13
|
+
import { promises as fs } from "node:fs";
|
|
14
|
+
import { PersistedRecordJournal, ChannelKind, digestInputs } from "../dist/src/index.js";
|
|
15
|
+
|
|
16
|
+
const N = Number(process.env.N || 50_000);
|
|
17
|
+
|
|
18
|
+
async function main() {
|
|
19
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), "orbit-bench-wal-"));
|
|
20
|
+
const file = path.join(dir, "bench.wal.jsonl");
|
|
21
|
+
|
|
22
|
+
const journal = new PersistedRecordJournal(file);
|
|
23
|
+
const t0 = process.hrtime.bigint();
|
|
24
|
+
for (let i = 0; i < N; i += 1) {
|
|
25
|
+
journal.append({
|
|
26
|
+
channelKind: ChannelKind.MEM_KV_STORE,
|
|
27
|
+
funcName: "readEntry",
|
|
28
|
+
inputDigest: digestInputs(`k${i}`),
|
|
29
|
+
outputSnapshot: `v${i}`,
|
|
30
|
+
durationMs: 0
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
await journal.flush();
|
|
34
|
+
const t1 = process.hrtime.bigint();
|
|
35
|
+
const ns = Number(t1 - t0);
|
|
36
|
+
|
|
37
|
+
const bytes = (await fs.stat(file)).size;
|
|
38
|
+
console.log("=== durable journal (WAL) append + flush ===");
|
|
39
|
+
console.log(`appends : ${N}`);
|
|
40
|
+
console.log(`on disk : ${(bytes / 1024).toFixed(0)} KiB`);
|
|
41
|
+
console.log(`throughput : ${Math.round((N / ns) * 1e9)} appends/s`);
|
|
42
|
+
console.log(`per append : ${(ns / N / 1000).toFixed(1)} µs`);
|
|
43
|
+
|
|
44
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
main().catch((err) => {
|
|
48
|
+
console.error(err);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
});
|
package/bin/orbit.mjs
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* orbit — command line interface for Orbit Agent Runtime.
|
|
4
|
+
*
|
|
5
|
+
* Three commands form the deterministic-replay loop:
|
|
6
|
+
* orbit record <script> Run a script against a live kernel and
|
|
7
|
+
* capture every channel call into a trace.
|
|
8
|
+
* orbit replay <trace> Re-run the recorded script with ZERO
|
|
9
|
+
* real channel calls, then reconcile the
|
|
10
|
+
* replayed chain against the original.
|
|
11
|
+
* orbit diff <a> <trace-b> Compare two traces and locate the first
|
|
12
|
+
* digest-chain breakpoint.
|
|
13
|
+
*
|
|
14
|
+
* Zero third-party dependencies: only Node built-ins. The kernel is loaded
|
|
15
|
+
* from the compiled CommonJS bundle via createRequire so the CLI works both
|
|
16
|
+
* from the repo (node bin/orbit.mjs) and after `npm i -g`.
|
|
17
|
+
*
|
|
18
|
+
* Usage:
|
|
19
|
+
* orbit record <script.js> [--out trace.jsonl] [--config orbit.config.json]
|
|
20
|
+
* orbit replay <trace.jsonl> [--via script.js] [--config orbit.config.json]
|
|
21
|
+
* orbit diff <a.jsonl> <b.jsonl>
|
|
22
|
+
* orbit --version
|
|
23
|
+
* orbit help
|
|
24
|
+
*
|
|
25
|
+
* Every command accepts --json for machine-readable output.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { createRequire } from "node:module";
|
|
29
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
30
|
+
import * as fs from "node:fs";
|
|
31
|
+
import * as path from "node:path";
|
|
32
|
+
|
|
33
|
+
const require = createRequire(import.meta.url);
|
|
34
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
35
|
+
const __dirname = path.dirname(__filename);
|
|
36
|
+
|
|
37
|
+
const PKG = JSON.parse(fs.readFileSync(path.join(__dirname, "..", "package.json"), "utf8"));
|
|
38
|
+
|
|
39
|
+
// Load the compiled kernel (CommonJS). Named exports are reachable through the
|
|
40
|
+
// default-import interop object.
|
|
41
|
+
const orbit = require("../dist/src/index.js");
|
|
42
|
+
const {
|
|
43
|
+
OrbitRuntimeHost,
|
|
44
|
+
ChannelKind,
|
|
45
|
+
ReplayEngine,
|
|
46
|
+
saveRecordJournal,
|
|
47
|
+
loadRecordJournal,
|
|
48
|
+
TraceFileInvalidError,
|
|
49
|
+
OpenAICompatChannel,
|
|
50
|
+
FileChannel,
|
|
51
|
+
ShellChannel,
|
|
52
|
+
digestInputs
|
|
53
|
+
} = orbit;
|
|
54
|
+
|
|
55
|
+
// ----------------------------------------------------------------- helpers
|
|
56
|
+
|
|
57
|
+
function out(obj) {
|
|
58
|
+
process.stdout.write(`${JSON.stringify(obj, null, 2)}\n`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function fail(msg, code) {
|
|
62
|
+
process.stderr.write(`orbit: ${msg}\n`);
|
|
63
|
+
process.exitCode = code;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isJsonFlag(v) {
|
|
67
|
+
return v === true || v === "true";
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Minimal argv parser: positionals + `--key value` / `--key=value` / `--flag`. */
|
|
71
|
+
function parseArgs(argv) {
|
|
72
|
+
const positionals = [];
|
|
73
|
+
const flags = {};
|
|
74
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
75
|
+
const a = argv[i];
|
|
76
|
+
if (a.startsWith("--")) {
|
|
77
|
+
const eq = a.indexOf("=");
|
|
78
|
+
if (eq !== -1) {
|
|
79
|
+
flags[a.slice(2, eq)] = a.slice(eq + 1);
|
|
80
|
+
} else {
|
|
81
|
+
const key = a.slice(2);
|
|
82
|
+
const next = argv[i + 1];
|
|
83
|
+
if (next !== undefined && !next.startsWith("--")) {
|
|
84
|
+
flags[key] = next;
|
|
85
|
+
i += 1;
|
|
86
|
+
} else {
|
|
87
|
+
flags[key] = true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
} else {
|
|
91
|
+
positionals.push(a);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return { positionals, flags };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function mergeDeep(target, src) {
|
|
98
|
+
if (typeof src !== "object" || src === null) return target;
|
|
99
|
+
for (const k of Object.keys(src)) {
|
|
100
|
+
const v = src[k];
|
|
101
|
+
if (
|
|
102
|
+
typeof v === "object" &&
|
|
103
|
+
v !== null &&
|
|
104
|
+
!Array.isArray(v) &&
|
|
105
|
+
typeof target[k] === "object" &&
|
|
106
|
+
target[k] !== null
|
|
107
|
+
) {
|
|
108
|
+
mergeDeep(target[k], v);
|
|
109
|
+
} else {
|
|
110
|
+
target[k] = v;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return target;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Drop secrets before persisting the config into a trace's meta file. */
|
|
117
|
+
function sanitizeConfig(cfg) {
|
|
118
|
+
const clean = JSON.parse(JSON.stringify(cfg));
|
|
119
|
+
if (clean.llm) delete clean.llm.apiKey;
|
|
120
|
+
if (clean.shell) delete clean.shell.apiKey;
|
|
121
|
+
return clean;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Resolve the runtime configuration.
|
|
126
|
+
* Priority (low -> high): built-in defaults < orbit.config.json < env vars.
|
|
127
|
+
*/
|
|
128
|
+
function loadConfig(configPath) {
|
|
129
|
+
const base = {
|
|
130
|
+
llm: { kind: "mock" },
|
|
131
|
+
file: { enabled: false },
|
|
132
|
+
shell: { enabled: false }
|
|
133
|
+
};
|
|
134
|
+
const file = configPath
|
|
135
|
+
? path.resolve(process.cwd(), configPath)
|
|
136
|
+
: path.join(process.cwd(), "orbit.config.json");
|
|
137
|
+
if (fs.existsSync(file)) {
|
|
138
|
+
let userCfg;
|
|
139
|
+
try {
|
|
140
|
+
userCfg = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
141
|
+
} catch (err) {
|
|
142
|
+
throw new Error(`invalid JSON in ${file}: ${err.message}`);
|
|
143
|
+
}
|
|
144
|
+
mergeDeep(base, userCfg || {});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (process.env.ORBIT_LLM_BASE_URL) {
|
|
148
|
+
base.llm = {
|
|
149
|
+
kind: "openai-compat",
|
|
150
|
+
baseUrl: process.env.ORBIT_LLM_BASE_URL,
|
|
151
|
+
apiKey: process.env.ORBIT_LLM_API_KEY ?? "",
|
|
152
|
+
model: process.env.ORBIT_LLM_MODEL ?? "deepseek-chat"
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
if (process.env.ORBIT_FILE_ROOT) {
|
|
156
|
+
base.file = { enabled: true, rootDir: process.env.ORBIT_FILE_ROOT };
|
|
157
|
+
}
|
|
158
|
+
if (process.env.ORBIT_SHELL_ALLOW) {
|
|
159
|
+
base.shell = {
|
|
160
|
+
enabled: true,
|
|
161
|
+
allowedCommands: process.env.ORBIT_SHELL_ALLOW.split(",").map((s) => s.trim()).filter(Boolean),
|
|
162
|
+
envAllowlist: process.env.ORBIT_SHELL_ENV
|
|
163
|
+
? process.env.ORBIT_SHELL_ENV.split(",").map((s) => s.trim()).filter(Boolean)
|
|
164
|
+
: []
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
return base;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Assemble a kernel host from a config: register LLM / File / Shell channels
|
|
172
|
+
* as requested, set them up, then boot the host (which wires built-ins).
|
|
173
|
+
*/
|
|
174
|
+
async function buildHost(config) {
|
|
175
|
+
const host = new OrbitRuntimeHost();
|
|
176
|
+
const hostCtx = { traceMarkId: `orbit-cli-${Date.now()}`, maxWaitMs: 30_000 };
|
|
177
|
+
|
|
178
|
+
if (config.llm && config.llm.kind === "openai-compat") {
|
|
179
|
+
const ch = new OpenAICompatChannel({
|
|
180
|
+
apiKey: config.llm.apiKey ?? "",
|
|
181
|
+
baseUrl: config.llm.baseUrl,
|
|
182
|
+
model: config.llm.model
|
|
183
|
+
});
|
|
184
|
+
host.channelHub.registerPluginExtChannel(ChannelKind.LLM_ACCESS, ch);
|
|
185
|
+
await ch.setup(hostCtx);
|
|
186
|
+
}
|
|
187
|
+
if (config.file && config.file.enabled) {
|
|
188
|
+
const ch = new FileChannel({ rootDir: config.file.rootDir });
|
|
189
|
+
host.channelHub.registerPluginExtChannel(ChannelKind.FILE_SYSTEM, ch);
|
|
190
|
+
await ch.setup(hostCtx);
|
|
191
|
+
}
|
|
192
|
+
if (config.shell && config.shell.enabled) {
|
|
193
|
+
const ch = new ShellChannel({
|
|
194
|
+
allowedCommands: config.shell.allowedCommands,
|
|
195
|
+
envAllowlist: config.shell.envAllowlist,
|
|
196
|
+
workDir: config.shell.workDir,
|
|
197
|
+
timeoutMs: config.shell.timeoutMs
|
|
198
|
+
});
|
|
199
|
+
host.channelHub.registerPluginExtChannel(ChannelKind.SHELL_EXEC, ch);
|
|
200
|
+
await ch.setup(hostCtx);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
await host.bootHost();
|
|
204
|
+
return host;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The context handed to a user script. `call` fires a channel method under the
|
|
209
|
+
* current mode (record or replay); no pluginUnitId is set, so the capability
|
|
210
|
+
* gate is skipped — CLI scripts are trusted host-level code. `llm.chat` is
|
|
211
|
+
* sugar over the LLM channel.
|
|
212
|
+
*/
|
|
213
|
+
function makeScriptContext(host, replayMode) {
|
|
214
|
+
let seq = 0;
|
|
215
|
+
const call = (kind, funcName, ...args) => {
|
|
216
|
+
const ctx = { traceMarkId: `orbit-cli-call-${seq++}`, maxWaitMs: 60_000, replayMode };
|
|
217
|
+
return host.channelHub.fireChannelCall(kind, ctx, funcName, ...args);
|
|
218
|
+
};
|
|
219
|
+
return {
|
|
220
|
+
host,
|
|
221
|
+
hub: host.channelHub,
|
|
222
|
+
ChannelKind,
|
|
223
|
+
call,
|
|
224
|
+
llm: {
|
|
225
|
+
chat: (prompt, opts) => {
|
|
226
|
+
const ctx = { traceMarkId: `orbit-cli-llm-${seq++}`, maxWaitMs: 60_000, replayMode };
|
|
227
|
+
return host.channelHub.fireChannelCall(ChannelKind.LLM_ACCESS, ctx, "chatRound", prompt, opts);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** Import a user script and invoke its default-exported async function(ctx). */
|
|
234
|
+
async function runScript(scriptPath, ctx) {
|
|
235
|
+
const abs = path.resolve(process.cwd(), scriptPath);
|
|
236
|
+
let mod;
|
|
237
|
+
try {
|
|
238
|
+
mod = await import(pathToFileURL(abs).href);
|
|
239
|
+
} catch (err) {
|
|
240
|
+
throw new Error(
|
|
241
|
+
`failed to load script ${scriptPath}: ${err.message}` +
|
|
242
|
+
"\n(orbit CLI runs JavaScript modules only — compile TypeScript first, e.g. with tsx/tsc)"
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
const fn = mod.default ?? mod;
|
|
246
|
+
if (typeof fn !== "function") {
|
|
247
|
+
throw new Error(`script ${scriptPath} must default-export an async function: export default async (ctx) => { ... }`);
|
|
248
|
+
}
|
|
249
|
+
return fn(ctx);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// ----------------------------------------------------------------- commands
|
|
253
|
+
|
|
254
|
+
async function cmdRecord(scriptPath, opts) {
|
|
255
|
+
if (!scriptPath) {
|
|
256
|
+
fail("record requires <script>", 2);
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const config = loadConfig(opts.config);
|
|
260
|
+
const host = await buildHost(config);
|
|
261
|
+
const journal = host.beginRecording();
|
|
262
|
+
const ctx = makeScriptContext(host, "record");
|
|
263
|
+
const startedAt = Date.now();
|
|
264
|
+
try {
|
|
265
|
+
await runScript(scriptPath, ctx);
|
|
266
|
+
const outPath = opts.out
|
|
267
|
+
? path.resolve(process.cwd(), opts.out)
|
|
268
|
+
: path.resolve(process.cwd(), "orbit-trace.jsonl");
|
|
269
|
+
const count = await saveRecordJournal(journal, outPath);
|
|
270
|
+
const meta = {
|
|
271
|
+
script: scriptPath,
|
|
272
|
+
orbitVersion: PKG.version,
|
|
273
|
+
nodeVersion: process.version,
|
|
274
|
+
createdAt: new Date().toISOString(),
|
|
275
|
+
recordCount: count,
|
|
276
|
+
config: sanitizeConfig(config)
|
|
277
|
+
};
|
|
278
|
+
const metaPath = `${outPath}.meta.json`;
|
|
279
|
+
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2));
|
|
280
|
+
const ms = Date.now() - startedAt;
|
|
281
|
+
if (isJsonFlag(opts.json)) {
|
|
282
|
+
out({ ok: true, trace: outPath, meta: metaPath, calls: count, elapsedMs: ms });
|
|
283
|
+
} else {
|
|
284
|
+
process.stdout.write(
|
|
285
|
+
`✓ recorded ${count} channel calls from ${scriptPath}\n` +
|
|
286
|
+
` trace : ${outPath}\n` +
|
|
287
|
+
` meta : ${metaPath}\n` +
|
|
288
|
+
` took : ${ms}ms\n`
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
} finally {
|
|
292
|
+
await host.shutdownHost();
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async function cmdReplay(tracePath, opts) {
|
|
297
|
+
if (!tracePath) {
|
|
298
|
+
fail("replay requires <trace>", 2);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const abs = path.resolve(process.cwd(), tracePath);
|
|
302
|
+
let journal;
|
|
303
|
+
try {
|
|
304
|
+
journal = await loadRecordJournal(abs);
|
|
305
|
+
} catch (err) {
|
|
306
|
+
if (err instanceof TraceFileInvalidError) {
|
|
307
|
+
fail(err.message, 2);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
throw err;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const metaPath = `${abs}.meta.json`;
|
|
314
|
+
let script = opts.via;
|
|
315
|
+
let replayConfig = null;
|
|
316
|
+
if (fs.existsSync(metaPath)) {
|
|
317
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, "utf8"));
|
|
318
|
+
if (!script) script = meta.script;
|
|
319
|
+
replayConfig = meta.config || null;
|
|
320
|
+
}
|
|
321
|
+
if (!script) {
|
|
322
|
+
fail(`trace ${tracePath} has no driving script; re-run record or pass --via <script>`, 2);
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const config = replayConfig || loadConfig(opts.config);
|
|
327
|
+
const host = await buildHost(config);
|
|
328
|
+
const replayJournal = host.beginRecording();
|
|
329
|
+
host.attachReplayEngine(journal);
|
|
330
|
+
const ctx = makeScriptContext(host, "replay");
|
|
331
|
+
try {
|
|
332
|
+
await runScript(script, ctx);
|
|
333
|
+
} finally {
|
|
334
|
+
await host.shutdownHost();
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const report = new ReplayEngine(journal).reconcile(journal.snapshot(), replayJournal.snapshot());
|
|
338
|
+
if (isJsonFlag(opts.json)) {
|
|
339
|
+
out({ ok: report.digestChainConsistent, ...report });
|
|
340
|
+
} else {
|
|
341
|
+
process.stdout.write(
|
|
342
|
+
`replay of ${tracePath}\n` +
|
|
343
|
+
` original calls : ${report.originalCount}\n` +
|
|
344
|
+
` replayed calls : ${report.replayedCount}\n`
|
|
345
|
+
);
|
|
346
|
+
if (report.digestChainConsistent) {
|
|
347
|
+
process.stdout.write(" result : ✓ VERIFIED — digest chain consistent\n");
|
|
348
|
+
} else {
|
|
349
|
+
process.stdout.write(` result : ✗ DRIFT at call #${report.driftAtOrderIndex}\n`);
|
|
350
|
+
process.exitCode = 1;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
async function cmdDiff(aPath, bPath, opts) {
|
|
356
|
+
if (!aPath || !bPath) {
|
|
357
|
+
fail("diff requires <a> <b>", 2);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
const A = await loadRecordJournal(path.resolve(process.cwd(), aPath));
|
|
361
|
+
const B = await loadRecordJournal(path.resolve(process.cwd(), bPath));
|
|
362
|
+
const a = A.snapshot();
|
|
363
|
+
const b = B.snapshot();
|
|
364
|
+
const n = Math.min(a.length, b.length);
|
|
365
|
+
|
|
366
|
+
let firstDrift = -1;
|
|
367
|
+
let driftField = null;
|
|
368
|
+
let driftA = null;
|
|
369
|
+
let driftB = null;
|
|
370
|
+
for (let i = 0; i < n; i += 1) {
|
|
371
|
+
const x = a[i];
|
|
372
|
+
const y = b[i];
|
|
373
|
+
if (x.channelKind !== y.channelKind) {
|
|
374
|
+
firstDrift = i; driftField = "channelKind"; driftA = x.channelKind; driftB = y.channelKind; break;
|
|
375
|
+
}
|
|
376
|
+
if (x.funcName !== y.funcName) {
|
|
377
|
+
firstDrift = i; driftField = "funcName"; driftA = x.funcName; driftB = y.funcName; break;
|
|
378
|
+
}
|
|
379
|
+
if (x.inputDigest !== y.inputDigest) {
|
|
380
|
+
firstDrift = i; driftField = "inputDigest"; driftA = x.inputDigest; driftB = y.inputDigest; break;
|
|
381
|
+
}
|
|
382
|
+
if (digestInputs(x.outputSnapshot) !== digestInputs(y.outputSnapshot)) {
|
|
383
|
+
firstDrift = i; driftField = "outputSnapshot"; driftA = "(digest differs)"; driftB = "(digest differs)"; break;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const consistent = firstDrift === -1;
|
|
388
|
+
const report = {
|
|
389
|
+
a: aPath,
|
|
390
|
+
b: bPath,
|
|
391
|
+
aCount: a.length,
|
|
392
|
+
bCount: b.length,
|
|
393
|
+
lengthMatch: a.length === b.length,
|
|
394
|
+
firstDriftIndex: firstDrift,
|
|
395
|
+
driftField,
|
|
396
|
+
consistent
|
|
397
|
+
};
|
|
398
|
+
if (isJsonFlag(opts.json)) {
|
|
399
|
+
out(report);
|
|
400
|
+
} else {
|
|
401
|
+
process.stdout.write(`diff ${aPath} vs ${bPath}\n calls: a=${a.length} b=${b.length}\n`);
|
|
402
|
+
if (consistent) {
|
|
403
|
+
process.stdout.write(" result: ✓ identical call chains\n");
|
|
404
|
+
} else {
|
|
405
|
+
process.stdout.write(` result: ✗ divergence at call #${firstDrift} (${driftField})\n`);
|
|
406
|
+
process.stdout.write(` a: ${driftA}\n`);
|
|
407
|
+
process.stdout.write(` b: ${driftB}\n`);
|
|
408
|
+
process.exitCode = 1;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
async function cmdAudit(tracePath, opts) {
|
|
414
|
+
if (!tracePath) {
|
|
415
|
+
fail("audit requires <trace.wal.jsonl>", 2);
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const file = path.resolve(process.cwd(), tracePath);
|
|
419
|
+
const journal = new orbit.PersistedTraceJournal(file);
|
|
420
|
+
await journal.load();
|
|
421
|
+
const entries = journal.snapshot();
|
|
422
|
+
if (!opts.key) {
|
|
423
|
+
const summary = {
|
|
424
|
+
file,
|
|
425
|
+
entries: entries.length,
|
|
426
|
+
signed: false,
|
|
427
|
+
note: "pass --key <hmac-key> to verify the chain signature"
|
|
428
|
+
};
|
|
429
|
+
if (isJsonFlag(opts.json)) {
|
|
430
|
+
out(summary);
|
|
431
|
+
} else {
|
|
432
|
+
process.stdout.write(`audit ${file}\n entries : ${entries.length}\n signed : no key given — pass --key to verify the chain\n`);
|
|
433
|
+
}
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const report = orbit.verifyAuditChain(entries, opts.key);
|
|
437
|
+
if (isJsonFlag(opts.json)) {
|
|
438
|
+
out({ file, ...report });
|
|
439
|
+
} else {
|
|
440
|
+
process.stdout.write(`audit ${file}\n entries : ${report.total}\n signed : ${report.signed}\n`);
|
|
441
|
+
if (report.consistent) {
|
|
442
|
+
process.stdout.write(" result : ✓ audit chain consistent (no tampering detected)\n");
|
|
443
|
+
} else {
|
|
444
|
+
process.stdout.write(` result : ✗ chain broken at entry #${report.brokenAt} — ${report.brokenReason}\n`);
|
|
445
|
+
process.exitCode = 1;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function printUsage() {
|
|
451
|
+
process.stdout.write(
|
|
452
|
+
`orbit ${PKG.version} — deterministic-replay CLI for Orbit Agent Runtime
|
|
453
|
+
|
|
454
|
+
Usage:
|
|
455
|
+
orbit record <script.js> [--out trace.jsonl] [--config orbit.config.json]
|
|
456
|
+
orbit replay <trace.jsonl> [--via script.js] [--config orbit.config.json]
|
|
457
|
+
orbit diff <a.jsonl> <b.jsonl>
|
|
458
|
+
orbit audit <trace.wal.jsonl> [--key <hmac-key>] [--json]
|
|
459
|
+
orbit --version
|
|
460
|
+
orbit help
|
|
461
|
+
|
|
462
|
+
Options:
|
|
463
|
+
--out <path> Output trace path for record (default: ./orbit-trace.jsonl)
|
|
464
|
+
--via <script> Driving script for replay (default: read from trace meta)
|
|
465
|
+
--config <path> Config file (default: ./orbit.config.json)
|
|
466
|
+
--json Machine-readable output
|
|
467
|
+
|
|
468
|
+
Script contract:
|
|
469
|
+
export default async function (ctx) {
|
|
470
|
+
const reply = await ctx.llm.chat("hello");
|
|
471
|
+
const prev = await ctx.call(ctx.ChannelKind.MEM_KV_STORE, "readEntry", "k");
|
|
472
|
+
return { reply, prev };
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
Config (orbit.config.json) — all keys optional:
|
|
476
|
+
{ "llm": { "kind": "mock" | "openai-compat", "baseUrl": "...", "model": "..." },
|
|
477
|
+
"file": { "enabled": true, "rootDir": "./sandbox-fs" },
|
|
478
|
+
"shell": { "enabled": true, "allowedCommands": ["node","echo"], "envAllowlist": ["PATH"] } }
|
|
479
|
+
Env overrides: ORBIT_LLM_BASE_URL / ORBIT_LLM_API_KEY / ORBIT_LLM_MODEL,
|
|
480
|
+
ORBIT_FILE_ROOT, ORBIT_SHELL_ALLOW (csv) / ORBIT_SHELL_ENV (csv).
|
|
481
|
+
`
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// --------------------------------------------------------------------- entry
|
|
486
|
+
|
|
487
|
+
async function main() {
|
|
488
|
+
const { positionals, flags } = parseArgs(process.argv.slice(2));
|
|
489
|
+
const cmd = positionals.shift();
|
|
490
|
+
const json = isJsonFlag(flags.json);
|
|
491
|
+
|
|
492
|
+
try {
|
|
493
|
+
switch (cmd) {
|
|
494
|
+
case "record":
|
|
495
|
+
await cmdRecord(positionals[0], { out: flags.out, config: flags.config, json });
|
|
496
|
+
break;
|
|
497
|
+
case "replay":
|
|
498
|
+
await cmdReplay(positionals[0], { via: flags.via, config: flags.config, json });
|
|
499
|
+
break;
|
|
500
|
+
case "diff":
|
|
501
|
+
await cmdDiff(positionals[0], positionals[1], { json });
|
|
502
|
+
break;
|
|
503
|
+
case "audit":
|
|
504
|
+
await cmdAudit(positionals[0], { key: flags.key, json });
|
|
505
|
+
break;
|
|
506
|
+
case "help":
|
|
507
|
+
case undefined:
|
|
508
|
+
printUsage();
|
|
509
|
+
break;
|
|
510
|
+
case "--version":
|
|
511
|
+
case "version":
|
|
512
|
+
process.stdout.write(`${PKG.version}\n`);
|
|
513
|
+
break;
|
|
514
|
+
default:
|
|
515
|
+
fail(`unknown command: ${cmd}`, 2);
|
|
516
|
+
printUsage();
|
|
517
|
+
}
|
|
518
|
+
} catch (err) {
|
|
519
|
+
fail(err instanceof Error ? err.message : String(err), 1);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
main();
|