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.
Files changed (168) hide show
  1. package/CHANGELOG.md +750 -0
  2. package/LICENSE +201 -0
  3. package/README.md +492 -0
  4. package/README.zh-CN.md +389 -0
  5. package/benchmarks/README.md +24 -0
  6. package/benchmarks/gateway.mjs +68 -0
  7. package/benchmarks/pae.mjs +90 -0
  8. package/benchmarks/replay.mjs +51 -0
  9. package/benchmarks/run-all.mjs +24 -0
  10. package/benchmarks/wal.mjs +50 -0
  11. package/bin/orbit.mjs +523 -0
  12. package/dist/.tsbuildinfo +1 -0
  13. package/dist/demo-deepseek.d.ts +2 -0
  14. package/dist/demo-deepseek.d.ts.map +1 -0
  15. package/dist/demo-deepseek.js +66 -0
  16. package/dist/demo-deepseek.js.map +1 -0
  17. package/dist/demo-host.d.ts +2 -0
  18. package/dist/demo-host.d.ts.map +1 -0
  19. package/dist/demo-host.js +98 -0
  20. package/dist/demo-host.js.map +1 -0
  21. package/dist/demo-replay.d.ts +2 -0
  22. package/dist/demo-replay.d.ts.map +1 -0
  23. package/dist/demo-replay.js +68 -0
  24. package/dist/demo-replay.js.map +1 -0
  25. package/dist/src/core/orbitRuntimeHost.d.ts +349 -0
  26. package/dist/src/core/orbitRuntimeHost.d.ts.map +1 -0
  27. package/dist/src/core/orbitRuntimeHost.js +706 -0
  28. package/dist/src/core/orbitRuntimeHost.js.map +1 -0
  29. package/dist/src/index.d.ts +92 -0
  30. package/dist/src/index.d.ts.map +1 -0
  31. package/dist/src/index.js +226 -0
  32. package/dist/src/index.js.map +1 -0
  33. package/dist/test/audit_chain.test.d.ts +2 -0
  34. package/dist/test/audit_chain.test.d.ts.map +1 -0
  35. package/dist/test/audit_chain.test.js +283 -0
  36. package/dist/test/audit_chain.test.js.map +1 -0
  37. package/dist/test/channel_hub.test.d.ts +2 -0
  38. package/dist/test/channel_hub.test.d.ts.map +1 -0
  39. package/dist/test/channel_hub.test.js +66 -0
  40. package/dist/test/channel_hub.test.js.map +1 -0
  41. package/dist/test/cordis_adapter.test.d.ts +2 -0
  42. package/dist/test/cordis_adapter.test.d.ts.map +1 -0
  43. package/dist/test/cordis_adapter.test.js +280 -0
  44. package/dist/test/cordis_adapter.test.js.map +1 -0
  45. package/dist/test/cost_routing.test.d.ts +2 -0
  46. package/dist/test/cost_routing.test.d.ts.map +1 -0
  47. package/dist/test/cost_routing.test.js +31 -0
  48. package/dist/test/cost_routing.test.js.map +1 -0
  49. package/dist/test/domain_transaction.test.d.ts +2 -0
  50. package/dist/test/domain_transaction.test.d.ts.map +1 -0
  51. package/dist/test/domain_transaction.test.js +351 -0
  52. package/dist/test/domain_transaction.test.js.map +1 -0
  53. package/dist/test/file_channel.test.d.ts +2 -0
  54. package/dist/test/file_channel.test.d.ts.map +1 -0
  55. package/dist/test/file_channel.test.js +179 -0
  56. package/dist/test/file_channel.test.js.map +1 -0
  57. package/dist/test/gateway.test.d.ts +2 -0
  58. package/dist/test/gateway.test.d.ts.map +1 -0
  59. package/dist/test/gateway.test.js +548 -0
  60. package/dist/test/gateway.test.js.map +1 -0
  61. package/dist/test/governance_profile.test.d.ts +2 -0
  62. package/dist/test/governance_profile.test.d.ts.map +1 -0
  63. package/dist/test/governance_profile.test.js +214 -0
  64. package/dist/test/governance_profile.test.js.map +1 -0
  65. package/dist/test/governance_schema.test.d.ts +2 -0
  66. package/dist/test/governance_schema.test.d.ts.map +1 -0
  67. package/dist/test/governance_schema.test.js +200 -0
  68. package/dist/test/governance_schema.test.js.map +1 -0
  69. package/dist/test/host_integration.test.d.ts +2 -0
  70. package/dist/test/host_integration.test.d.ts.map +1 -0
  71. package/dist/test/host_integration.test.js +104 -0
  72. package/dist/test/host_integration.test.js.map +1 -0
  73. package/dist/test/host_journal_persistence.test.d.ts +2 -0
  74. package/dist/test/host_journal_persistence.test.d.ts.map +1 -0
  75. package/dist/test/host_journal_persistence.test.js +367 -0
  76. package/dist/test/host_journal_persistence.test.js.map +1 -0
  77. package/dist/test/impact_domain.test.d.ts +2 -0
  78. package/dist/test/impact_domain.test.d.ts.map +1 -0
  79. package/dist/test/impact_domain.test.js +48 -0
  80. package/dist/test/impact_domain.test.js.map +1 -0
  81. package/dist/test/injected_clock.test.d.ts +13 -0
  82. package/dist/test/injected_clock.test.d.ts.map +1 -0
  83. package/dist/test/injected_clock.test.js +200 -0
  84. package/dist/test/injected_clock.test.js.map +1 -0
  85. package/dist/test/isolation_domain.test.d.ts +2 -0
  86. package/dist/test/isolation_domain.test.d.ts.map +1 -0
  87. package/dist/test/isolation_domain.test.js +302 -0
  88. package/dist/test/isolation_domain.test.js.map +1 -0
  89. package/dist/test/journal_wal.test.d.ts +2 -0
  90. package/dist/test/journal_wal.test.d.ts.map +1 -0
  91. package/dist/test/journal_wal.test.js +622 -0
  92. package/dist/test/journal_wal.test.js.map +1 -0
  93. package/dist/test/mcp_adapter.test.d.ts +2 -0
  94. package/dist/test/mcp_adapter.test.d.ts.map +1 -0
  95. package/dist/test/mcp_adapter.test.js +440 -0
  96. package/dist/test/mcp_adapter.test.js.map +1 -0
  97. package/dist/test/openai_compat_channel.test.d.ts +2 -0
  98. package/dist/test/openai_compat_channel.test.d.ts.map +1 -0
  99. package/dist/test/openai_compat_channel.test.js +325 -0
  100. package/dist/test/openai_compat_channel.test.js.map +1 -0
  101. package/dist/test/openapi_adapter.test.d.ts +2 -0
  102. package/dist/test/openapi_adapter.test.d.ts.map +1 -0
  103. package/dist/test/openapi_adapter.test.js +270 -0
  104. package/dist/test/openapi_adapter.test.js.map +1 -0
  105. package/dist/test/orbit_cli.test.d.ts +2 -0
  106. package/dist/test/orbit_cli.test.d.ts.map +1 -0
  107. package/dist/test/orbit_cli.test.js +137 -0
  108. package/dist/test/orbit_cli.test.js.map +1 -0
  109. package/dist/test/pact_verifier.test.d.ts +2 -0
  110. package/dist/test/pact_verifier.test.d.ts.map +1 -0
  111. package/dist/test/pact_verifier.test.js +41 -0
  112. package/dist/test/pact_verifier.test.js.map +1 -0
  113. package/dist/test/pae_adapter.test.d.ts +2 -0
  114. package/dist/test/pae_adapter.test.d.ts.map +1 -0
  115. package/dist/test/pae_adapter.test.js +469 -0
  116. package/dist/test/pae_adapter.test.js.map +1 -0
  117. package/dist/test/plugin_sandbox_guard.test.d.ts +2 -0
  118. package/dist/test/plugin_sandbox_guard.test.d.ts.map +1 -0
  119. package/dist/test/plugin_sandbox_guard.test.js +57 -0
  120. package/dist/test/plugin_sandbox_guard.test.js.map +1 -0
  121. package/dist/test/rate_limit_behavior.test.d.ts +2 -0
  122. package/dist/test/rate_limit_behavior.test.d.ts.map +1 -0
  123. package/dist/test/rate_limit_behavior.test.js +85 -0
  124. package/dist/test/rate_limit_behavior.test.js.map +1 -0
  125. package/dist/test/replay_compat.test.d.ts +2 -0
  126. package/dist/test/replay_compat.test.d.ts.map +1 -0
  127. package/dist/test/replay_compat.test.js +1122 -0
  128. package/dist/test/replay_compat.test.js.map +1 -0
  129. package/dist/test/replay_engine.test.d.ts +2 -0
  130. package/dist/test/replay_engine.test.d.ts.map +1 -0
  131. package/dist/test/replay_engine.test.js +85 -0
  132. package/dist/test/replay_engine.test.js.map +1 -0
  133. package/dist/test/shell_channel.test.d.ts +2 -0
  134. package/dist/test/shell_channel.test.d.ts.map +1 -0
  135. package/dist/test/shell_channel.test.js +173 -0
  136. package/dist/test/shell_channel.test.js.map +1 -0
  137. package/dist/test/teardown_isolation.test.d.ts +10 -0
  138. package/dist/test/teardown_isolation.test.d.ts.map +1 -0
  139. package/dist/test/teardown_isolation.test.js +279 -0
  140. package/dist/test/teardown_isolation.test.js.map +1 -0
  141. package/dist/test/token_budget.test.d.ts +2 -0
  142. package/dist/test/token_budget.test.d.ts.map +1 -0
  143. package/dist/test/token_budget.test.js +113 -0
  144. package/dist/test/token_budget.test.js.map +1 -0
  145. package/dist/test/trace_journal.test.d.ts +2 -0
  146. package/dist/test/trace_journal.test.d.ts.map +1 -0
  147. package/dist/test/trace_journal.test.js +43 -0
  148. package/dist/test/trace_journal.test.js.map +1 -0
  149. package/dist/test/trace_journal_persistence.test.d.ts +2 -0
  150. package/dist/test/trace_journal_persistence.test.d.ts.map +1 -0
  151. package/dist/test/trace_journal_persistence.test.js +175 -0
  152. package/dist/test/trace_journal_persistence.test.js.map +1 -0
  153. package/dist/test/trace_persistence.test.d.ts +2 -0
  154. package/dist/test/trace_persistence.test.d.ts.map +1 -0
  155. package/dist/test/trace_persistence.test.js +216 -0
  156. package/dist/test/trace_persistence.test.js.map +1 -0
  157. package/dist/test/trip_protector.test.d.ts +2 -0
  158. package/dist/test/trip_protector.test.d.ts.map +1 -0
  159. package/dist/test/trip_protector.test.js +42 -0
  160. package/dist/test/trip_protector.test.js.map +1 -0
  161. package/examples/README.md +34 -0
  162. package/examples/cli-record-replay.mjs +74 -0
  163. package/examples/custom-channel.mjs +102 -0
  164. package/examples/js-pae-plugin.mjs +128 -0
  165. package/examples/mcp-adapter.mjs +102 -0
  166. package/package.json +60 -0
  167. package/src/core/orbitRuntimeHost.ts +853 -0
  168. package/src/index.ts +256 -0
@@ -0,0 +1,706 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OrbitRuntimeHost = void 0;
4
+ const core_hub_1 = require("@orbit/core-hub");
5
+ const core_hub_2 = require("@orbit/core-hub");
6
+ const core_hub_3 = require("@orbit/core-hub");
7
+ const core_hub_4 = require("@orbit/core-hub");
8
+ const core_hub_5 = require("@orbit/core-hub");
9
+ const core_hub_6 = require("@orbit/core-hub");
10
+ const sandbox_runtime_1 = require("@orbit/sandbox-runtime");
11
+ const sandbox_runtime_2 = require("@orbit/sandbox-runtime");
12
+ const core_hub_7 = require("@orbit/core-hub");
13
+ const core_hub_8 = require("@orbit/core-hub");
14
+ const core_hub_9 = require("@orbit/core-hub");
15
+ const infra_common_1 = require("@orbit/infra-common");
16
+ const infra_common_2 = require("@orbit/infra-common");
17
+ const core_hub_10 = require("@orbit/core-hub");
18
+ const core_hub_11 = require("@orbit/core-hub");
19
+ const core_hub_12 = require("@orbit/core-hub");
20
+ const core_hub_13 = require("@orbit/core-hub");
21
+ const pae_engine_1 = require("@orbit/pae-engine");
22
+ const pae_engine_2 = require("@orbit/pae-engine");
23
+ const core_hub_14 = require("@orbit/core-hub");
24
+ const core_hub_15 = require("@orbit/core-hub");
25
+ const infra_common_3 = require("@orbit/infra-common");
26
+ const infra_common_4 = require("@orbit/infra-common");
27
+ const infra_common_5 = require("@orbit/infra-common");
28
+ // W20: the physical layer becomes host state.
29
+ const sandbox_runtime_3 = require("@orbit/sandbox-runtime");
30
+ const sandbox_runtime_4 = require("@orbit/sandbox-runtime");
31
+ const HOST_DEFAULT_TIMEOUT_MS = 10_000;
32
+ /**
33
+ * Durability options. When a path is supplied the corresponding journal is
34
+ * mirrored to an append-only write-ahead log on disk and recovered on the next
35
+ * boot, so a restart does not lose the audit trail or the recorded run. Omitting
36
+ * a path (the default) keeps the journal purely in-memory — the original
37
+ * behavior, and what every existing test relies on.
38
+ */
39
+ /** W31: PAE isolation rank for the trust-assumption cap (L0 < L1 < L2). */
40
+ function rankIsolation(level) {
41
+ return level === "L2" ? 2 : level === "L1" ? 1 : 0;
42
+ }
43
+ /**
44
+ * Top-level assembly: wires every component bottom-up and owns the host
45
+ * lifecycle. Components stay public for advanced scenarios (custom channels,
46
+ * audit), but day-to-day use should go through the facade methods below.
47
+ */
48
+ class OrbitRuntimeHost {
49
+ channelHub;
50
+ traceJournal;
51
+ pluginSandboxGuard;
52
+ pluginPactVerifier;
53
+ sandboxPool;
54
+ /** M3: dependency graph feeding isolation decisions. */
55
+ impactGraph;
56
+ /** M4: cost profiles and budget routing. */
57
+ costRouter;
58
+ /** W8: pure-function token budget + context compressor (single source of truth). */
59
+ tokenBudget;
60
+ /** W11: pure-function rate limiter (call-count budget, replay-safe). */
61
+ rateLimiter;
62
+ /** W11: behavior collector (record / live-proposal / replay-bypass). */
63
+ behaviorCollector;
64
+ /** W7: unified gateway entry — the determinism boundary (capabilityInvoke). */
65
+ gateway;
66
+ /** W15: registry of foreign-runtime adapters (the adaptation surface). */
67
+ paeRegistry;
68
+ /** W15: the channel that publishes the adaptation surface to the hub. */
69
+ paeChannel;
70
+ /** W8: kinds served by a PAE adapter; routing flips to "pae" when non-empty. */
71
+ paeAdapterKinds = new Set();
72
+ /**
73
+ * W20: the physical layer — isolation domains allocated from the impact
74
+ * graph. Created lazily: a host that never allocates domains keeps its
75
+ * previous hub surface and fingerprint byte for byte.
76
+ */
77
+ domainManager = null;
78
+ /** W20: the gateway surface over the domain manager's units. */
79
+ domainChannel = null;
80
+ /** W20: the graph changed since the last allocation (plan needs a re-sync). */
81
+ domainsStaleFlag = false;
82
+ /** W27: durable recording-window journal WAL path, if configured. */
83
+ recordJournalPath;
84
+ /** Injected clock, threaded to every time-dependent decision that is recorded. */
85
+ clock;
86
+ /** W27: the recording window currently attached to the gateway (for flush). */
87
+ activeRecordJournal = null;
88
+ /**
89
+ * W27: the same instance as {@link traceJournal}, held at its durable type.
90
+ * `traceJournal` stays declared as the base class on purpose — call sites must
91
+ * not depend on durability — so retention/compaction needs its own handle.
92
+ */
93
+ durableTraceJournal;
94
+ /** W27: audit retention bound; undefined means unbounded. */
95
+ auditRetention;
96
+ /** W29: resolved governance tier; drives limiter / trip / compression / PAE / durability. */
97
+ governanceProfile;
98
+ /** W30: HMAC key for the audit hash chain; undefined = unsigned journal. */
99
+ auditSigningKey;
100
+ constructor(opts) {
101
+ this.recordJournalPath = opts?.recordJournalPath;
102
+ this.clock = opts?.clock;
103
+ this.governanceProfile = (0, infra_common_5.resolveGovernanceProfile)(opts?.governanceProfile);
104
+ // A compliance tier with an ephemeral audit trail is a contradiction in
105
+ // terms: fail at construction, not at the first reboot.
106
+ if (this.governanceProfile.traceDurability === "required" &&
107
+ !opts?.traceJournalPath) {
108
+ throw new Error("governance profile 'strict' requires traceJournalPath — a compliance tier must have a durable audit trail");
109
+ }
110
+ // W30: strict also requires the ability to PROVE the trail. The signing
111
+ // key is what turns an append-only file into a tamper-evident one.
112
+ if (this.governanceProfile.name === "strict" && !opts?.auditSigningKey) {
113
+ throw new Error("governance profile 'strict' requires auditSigningKey — a compliance tier must sign its audit chain");
114
+ }
115
+ this.auditSigningKey = opts?.auditSigningKey;
116
+ if (opts?.auditRetention !== undefined) {
117
+ if (!Number.isInteger(opts.auditRetention) || opts.auditRetention < 0) {
118
+ throw new RangeError(`auditRetention expects a non-negative integer, received ${String(opts.auditRetention)}`);
119
+ }
120
+ this.auditRetention = opts.auditRetention;
121
+ }
122
+ this.costRouter = new core_hub_7.CostRouter();
123
+ this.tokenBudget = new core_hub_11.TokenBudgetEngine((0, core_hub_14.tokenBudgetConfigForProfile)(this.governanceProfile));
124
+ this.rateLimiter = new core_hub_12.RateLimiter({
125
+ maxCallsPerWindow: this.governanceProfile.limiter.maxCallsPerWindow,
126
+ windowSizeCalls: this.governanceProfile.limiter.windowSizeCalls
127
+ });
128
+ this.behaviorCollector = new core_hub_13.BehaviorCollector();
129
+ this.paeRegistry = new pae_engine_1.PaeAdapterRegistry();
130
+ this.paeChannel = new pae_engine_2.PaeChannel(this.paeRegistry);
131
+ this.channelHub = new core_hub_1.ChannelHub();
132
+ this.durableTraceJournal = new core_hub_4.PersistedTraceJournal(opts?.traceJournalPath, this.auditSigningKey);
133
+ this.traceJournal = this.durableTraceJournal;
134
+ this.impactGraph = new sandbox_runtime_2.ImpactDomainGraph();
135
+ this.pluginPactVerifier = new core_hub_6.PluginPactVerifier();
136
+ this.pluginSandboxGuard = new core_hub_5.PluginSandboxGuard(this.traceJournal,
137
+ // W29: the trip threshold comes from the profile, softened by the
138
+ // plugin's dependency out-degree (see tripThresholdForProfile).
139
+ (pluginId) => (0, core_hub_14.tripThresholdForProfile)(this.governanceProfile, this.impactGraph.outDegree(pluginId)), opts?.clock);
140
+ this.sandboxPool = new sandbox_runtime_1.SandboxPool(this.channelHub, this.traceJournal, this.costRouter);
141
+ // Close the capability loop: plugin-originated channel calls must pass the
142
+ // declared-capability check. Injected as a function so the channel layer
143
+ // never depends on the pact layer above it.
144
+ this.channelHub.attachCapabilityGate((pluginUnitId, kind, funcName) => this.pluginPactVerifier.hasCapability(pluginUnitId, this.requiredCapabilityFor(kind, funcName)));
145
+ // W7: wire the gateway. Checkers are injected (not concrete components) so
146
+ // the gateway stays above the pact/safeguard layers. tripAllowed routes
147
+ // through the gateway's own per-plugin trip map. W8: budget / compression /
148
+ // route / tokenConfigHash are now derived from the real TokenBudgetEngine
149
+ // and channel registry instead of literal stubs.
150
+ const checkers = {
151
+ tripAllowed: (pluginId) => this.tripPreCheckFor(pluginId),
152
+ pactPass: (pluginId, kind, funcName) => this.pluginPactVerifier.hasCapability(pluginId, this.requiredCapabilityFor(kind, funcName)),
153
+ budgetDecision: (pluginId) => this.tokenBudget.budgetPolicy(pluginId),
154
+ rateLimited: (pluginId) => this.rateLimiter.isLimited(pluginId),
155
+ // W15: a call on the adaptation channel is by definition PAE-routed; the
156
+ // legacy "any adapter kind declared" rule is kept for callers that only
157
+ // announce a routing intent without registering a real adapter.
158
+ route: (_pluginId, kind) => kind === infra_common_4.ChannelKind.PAE_TOOL || this.paeAdapterKinds.size > 0 ? "pae" : "native",
159
+ compression: (output) => this.tokenBudget.decideCompression(output),
160
+ fingerprint: () => this.runFingerprint(),
161
+ accountTokens: (pluginId, output) => {
162
+ if (typeof output === "string") {
163
+ this.tokenBudget.account(pluginId, this.tokenBudget.estimateTokens(output));
164
+ }
165
+ },
166
+ estimateTokens: (output) => (typeof output === "string" ? this.tokenBudget.estimateTokens(output) : 0),
167
+ consumeRateLimit: (pluginId) => {
168
+ this.rateLimiter.acquire(pluginId);
169
+ }
170
+ };
171
+ this.gateway = new core_hub_10.CapabilityGateway(this.channelHub, checkers, opts?.clock);
172
+ // W11: collectors/hooks live inside the gateway; attach the host-owned one.
173
+ this.gateway.attachCollector(this.behaviorCollector);
174
+ this.channelHub.registerBuiltInChannel(infra_common_4.ChannelKind.MEM_KV_STORE, new core_hub_2.MemoryKvChannel(this.clock));
175
+ this.channelHub.registerBuiltInChannel(infra_common_4.ChannelKind.LLM_ACCESS, new core_hub_3.LlmMockChannel());
176
+ // W15: the adaptation surface is a built-in channel with no tools until an
177
+ // adapter is registered — foreign runtimes get no private path in.
178
+ this.channelHub.registerBuiltInChannel(infra_common_4.ChannelKind.PAE_TOOL, this.paeChannel);
179
+ this.costRouter.register(infra_common_4.ChannelKind.MEM_KV_STORE, { costPerCall: 0, latencyMs: 1, quality: 1 });
180
+ this.costRouter.register(infra_common_4.ChannelKind.LLM_ACCESS, { costPerCall: 1, latencyMs: 320, quality: 1 });
181
+ // Adaptation costs more than a native call (an extra hop, possibly a
182
+ // process boundary) and is priced accordingly for budget routing.
183
+ this.costRouter.register(infra_common_4.ChannelKind.PAE_TOOL, { costPerCall: 2, latencyMs: 40, quality: 1 });
184
+ }
185
+ /**
186
+ * Capability required by a channel method. Native channels map statically;
187
+ * PAE tools are resolved through the registry so a foreign tool is governed
188
+ * at the same granularity as a native method (read vs write), never wholesale.
189
+ * An unknown tool is treated as a write — the conservative default — so a
190
+ * mis-registered surface fails closed.
191
+ */
192
+ requiredCapabilityFor(kind, funcName) {
193
+ if (kind === infra_common_4.ChannelKind.PAE_TOOL) {
194
+ return this.paeRegistry.capabilityOf(funcName) ?? "channel:write";
195
+ }
196
+ if (kind === infra_common_4.ChannelKind.DOMAIN_TOOL) {
197
+ // W20: a domain hop is a cross-process call into another unit's process.
198
+ // The capability is not yet declared per unit tool, so the conservative
199
+ // default governs — the same fail-closed rule the adapter surface uses.
200
+ return "channel:write";
201
+ }
202
+ return requiredCapability(kind, funcName);
203
+ }
204
+ /** Host-private trip pre-check delegating to the gateway's per-plugin map. */
205
+ tripPreCheckFor(pluginId) {
206
+ return this.gateway.tripPreCheck(pluginId);
207
+ }
208
+ /**
209
+ * The run-version fingerprint a recorded trace carries (W7, extended W15/W20).
210
+ *
211
+ * Both optional fields are **omitted rather than empty**: a host that never
212
+ * registers an adapter or allocates a domain produces exactly the fingerprint
213
+ * it produced before those layers existed, so a trace recorded on an older
214
+ * kernel is reported as configuration drift — never as a mystery digest
215
+ * mismatch.
216
+ */
217
+ runFingerprint() {
218
+ return {
219
+ kernelVersion: infra_common_2.KERNEL_VERSION,
220
+ pactVersions: {},
221
+ tokenConfigHash: this.tokenBudget.configHash(),
222
+ paeEnabled: this.paeAdapterKinds.size > 0,
223
+ ...(this.paeRegistry.isEmpty() ? {} : { paeAdaptersHash: this.paeRegistry.configHash() }),
224
+ ...(this.domainManager?.planOf() ? { domainPlanHash: this.domainManager.planHash() } : {}),
225
+ // W29: only a NON-default tier is a fingerprint surface. `standard` is
226
+ // omitted so a default host keeps its pre-W29 fingerprint byte for byte
227
+ // (backward-compat rule: new hash fields are omitted, never empty).
228
+ ...(this.governanceProfile.name === "standard"
229
+ ? {}
230
+ : { governanceProfileHash: (0, infra_common_5.governanceProfileHash)(this.governanceProfile) })
231
+ };
232
+ }
233
+ /**
234
+ * W29: the resolved governance tier and its concrete numbers. Read-only —
235
+ * the tier is a construction-time decision; switching it means constructing
236
+ * a host with `governanceProfile`.
237
+ */
238
+ get currentGovernanceProfile() {
239
+ return this.governanceProfile;
240
+ }
241
+ async bootHost() {
242
+ // W27: recover durable journals *before* anything can append. A missing file
243
+ // is normal (first boot) and leaves the journal empty; an existing file
244
+ // replays its entries so the audit trail survives the restart. Recovery
245
+ // rebuilds the whole chain, so it must precede channel setup — otherwise
246
+ // setup-time audit entries would be overwritten by the recovered snapshot.
247
+ await this.traceJournal.load();
248
+ // W30: a compliance tier starts only against a PROVABLE audit trail. A
249
+ // chain that fails verification after recovery means the log was edited
250
+ // while the host was down — a strict tier refuses to run on it.
251
+ if (this.governanceProfile.name === "strict" && this.auditSigningKey) {
252
+ const report = this.verifyAuditChain();
253
+ if (!report.consistent) {
254
+ throw new Error(`governance profile 'strict' refuses to boot: audit chain broken at entry #${report.brokenAt} ` +
255
+ `(${report.brokenReason})`);
256
+ }
257
+ }
258
+ // Apply the retention bound to whatever the previous run left behind, before
259
+ // this run starts appending. Compaction also physically removes a tail that
260
+ // a crash truncated (recovery merely ignores it), so the log is well-formed
261
+ // again from the first append of the new run.
262
+ await this.pruneAuditLog();
263
+ await this.channelHub.setupAllBuiltInChannels(this.newHostCtx());
264
+ if (this.recordJournalPath) {
265
+ await this.resumeRecording();
266
+ }
267
+ }
268
+ /**
269
+ * W30: prove the audit trail has not been tampered with. Recomputes the
270
+ * hash chain from the genesis seed and reports the first broken entry.
271
+ * An unsigned journal (no key configured) reports `signed: false` and is
272
+ * vacuously consistent.
273
+ */
274
+ verifyAuditChain() {
275
+ if (!this.auditSigningKey) {
276
+ return { consistent: true, total: this.traceJournal.entries().length, signed: false };
277
+ }
278
+ return (0, core_hub_15.verifyAuditChain)(this.traceJournal.entries(), this.auditSigningKey);
279
+ }
280
+ /**
281
+ * W27: enforce the configured audit-retention bound and compact the WAL.
282
+ *
283
+ * Called automatically at boot and shutdown; exposed so an operator can prune
284
+ * a long-running host on demand without a restart.
285
+ *
286
+ * @returns the number of audit entries retained.
287
+ */
288
+ async pruneAuditLog() {
289
+ if (this.auditRetention === undefined) {
290
+ return this.traceJournal.entries().length;
291
+ }
292
+ return this.durableTraceJournal.retainLast(this.auditRetention);
293
+ }
294
+ /** Reverse-order teardown: pool -> pact -> guards -> channels -> journal -> graph. */
295
+ async shutdownHost() {
296
+ // W27: drain any pending WAL writes before tearing components down, so the
297
+ // last recorded calls/audit entries are not lost on a clean shutdown.
298
+ //
299
+ // A drain that loses a write now reports it (see PersistedRecordJournal),
300
+ // but the teardown below must still run to completion: aborting here would
301
+ // leak every child process and foreign adapter the host owns. So the flush
302
+ // failure is carried and re-raised only after the release is done — the
303
+ // caller gets the signal *and* a clean process.
304
+ let drainError = null;
305
+ try {
306
+ await this.activeRecordJournal?.flush();
307
+ }
308
+ catch (err) {
309
+ drainError = err;
310
+ }
311
+ try {
312
+ await this.traceJournal.flush();
313
+ }
314
+ catch (err) {
315
+ drainError ??= err;
316
+ }
317
+ // Bound the log *at rest*: flush first so nothing pending is lost, then
318
+ // apply retention. Order matters — pruning before the flush would let the
319
+ // drained writes push the file back over the bound.
320
+ await this.pruneAuditLog();
321
+ this.sandboxPool.clear();
322
+ this.pluginPactVerifier.clear();
323
+ this.pluginSandboxGuard.releaseAllGuard();
324
+ // W20: release the physical layer before the hubs — every domain host is a
325
+ // real child process and must not outlive the kernel that spawned it.
326
+ await this.releaseIsolationDomains();
327
+ await this.channelHub.teardown();
328
+ // Channel teardown already released every adapter (PaeChannel.teardown);
329
+ // dropping the registry afterwards leaves no dangling foreign surface.
330
+ this.paeRegistry.clear();
331
+ this.traceJournal.clear();
332
+ this.impactGraph.clear();
333
+ if (drainError !== null)
334
+ throw drainError;
335
+ }
336
+ // Facade -----------------------------------------------------------
337
+ /** Register a plugin; its declared channel deps feed the impact graph (M3). */
338
+ registerPlugin(pact) {
339
+ // W31: the strict tier demands a declared parameter contract for every
340
+ // plugin — a compliance tier must state what it accepts.
341
+ if (this.governanceProfile.schemaMode === "required" && !pact.schema) {
342
+ throw new Error(`governance profile 'strict' requires a schema on plugin '${pact.id}' — a compliance tier must declare its parameter contract`);
343
+ }
344
+ this.pluginPactVerifier.registerPluginUnit(pact, (0, infra_common_1.makeUniqueMark)());
345
+ this.impactGraph.addNode(pact.id);
346
+ for (const dep of pact.declareChannelDeps ?? []) {
347
+ this.impactGraph.addEdge(pact.id, dep);
348
+ }
349
+ // W20: the graph feeds domain allocation — a change invalidates the plan.
350
+ this.domainsStaleFlag = true;
351
+ }
352
+ /** Spawn an agent sandbox; its channel deps feed the impact graph (M3). */
353
+ spawnAgentBox(cfg) {
354
+ const box = this.sandboxPool.spawnSandbox(cfg);
355
+ this.impactGraph.addNode(cfg.agentBoxId);
356
+ for (const dep of cfg.channelDeps ?? []) {
357
+ this.impactGraph.addEdge(cfg.agentBoxId, dep);
358
+ }
359
+ // W20: same invalidation rule as `registerPlugin`.
360
+ this.domainsStaleFlag = true;
361
+ return box;
362
+ }
363
+ /**
364
+ * W8: declare a PAE adapter for a channel kind. Once any adapter is
365
+ * registered the gateway's `route` decision becomes "pae" for governed
366
+ * calls, and the run fingerprint's `paeEnabled` flips — so a trace recorded
367
+ * before PAE existed is detected as config drift, never as a digest mismatch.
368
+ */
369
+ registerPaeAdapter(kind) {
370
+ this.paeAdapterKinds.add(kind);
371
+ }
372
+ /**
373
+ * W15: attach a real foreign-runtime adapter.
374
+ *
375
+ * The adapter is validated, indexed and published as tools on the PAE
376
+ * channel, and a **dynamic pact** is derived from its declared surface and
377
+ * registered like any hand-written plugin manifest. That is what keeps the
378
+ * promise of "MCP-grade ecosystem reach without a governance downgrade": the
379
+ * foreign tool now passes the same capability check, trip protection, budget
380
+ * accounting and recording as a native channel method, because it *is* one.
381
+ *
382
+ * @returns the derived pact (already registered unless `registerPact: false`).
383
+ */
384
+ registerPaeToolAdapter(adapter, opts = {}) {
385
+ // W29: the governance tier admits adapter kinds explicitly. `strict`
386
+ // admits none — a compliance tier has no foreign-runtime surface at all.
387
+ this.assertPaeAdmitted(adapter.meta.kind, adapter.meta.isolation);
388
+ this.paeRegistry.register(adapter, (0, infra_common_1.makeUniqueMark)());
389
+ this.paeChannel.syncTools();
390
+ this.paeAdapterKinds.add(infra_common_4.ChannelKind.PAE_TOOL);
391
+ const pact = this.paeRegistry.derivePact(adapter.meta.adapterId, {
392
+ requireHostMinEdition: opts.requireHostMinEdition
393
+ });
394
+ if (opts.registerPact !== false) {
395
+ this.registerPlugin(pact);
396
+ }
397
+ return pact;
398
+ }
399
+ /**
400
+ * W29+W31: the governance gate for foreign adapters — kind admission
401
+ * (`paeAdmission`) and isolation cap (`maxIsolationLevel`, the trust
402
+ * assumption). `strict` admits no kind at all and caps isolation at L1; a
403
+ * denied adapter is rejected outright rather than half-connected.
404
+ */
405
+ assertPaeAdmitted(kind, isolation) {
406
+ const admission = this.governanceProfile.paeAdmission;
407
+ if (admission !== "all" && !admission.includes(kind)) {
408
+ throw new Error(`governance profile '${this.governanceProfile.name}' does not admit PAE adapter kind '${kind}' ` +
409
+ `(admitted: ${admission.length === 0 ? "none" : admission.join(", ")})`);
410
+ }
411
+ const cap = this.governanceProfile.maxIsolationLevel;
412
+ if (rankIsolation(isolation) > rankIsolation(cap)) {
413
+ throw new Error(`governance profile '${this.governanceProfile.name}' caps PAE isolation at ${cap}; ` +
414
+ `adapter '${kind}' claims ${isolation} (trust assumption)`);
415
+ }
416
+ }
417
+ /**
418
+ * Register an adapter whose surface is only knowable *after* a handshake.
419
+ *
420
+ * `registerPaeToolAdapter` resolves the capability surface at registration
421
+ * time, which presupposes that the surface already exists. An MCP server
422
+ * announces its tools over the wire, so there is nothing to resolve until the
423
+ * connection is up. This variant connects first, lets the adapter discover
424
+ * what the peer actually exposes, and then registers exactly that.
425
+ *
426
+ * The handshake is not a governance bypass: registration still runs the same
427
+ * static validation and still derives a dynamic pact from the *discovered*
428
+ * surface, so a server that later announces a new tool is configuration drift
429
+ * on the next replay rather than a silently expanded capability.
430
+ */
431
+ async connectPaeToolAdapter(adapter, opts = {}) {
432
+ // W29+W31: gate BEFORE the handshake — a denied kind or isolation must
433
+ // not spawn a child process just to be rejected (register re-checks).
434
+ this.assertPaeAdmitted(adapter.meta.kind, adapter.meta.isolation);
435
+ if (adapter.setup) {
436
+ await adapter.setup({
437
+ traceMarkId: (0, infra_common_1.makeUniqueMark)(),
438
+ maxWaitMs: opts.maxWaitMs ?? HOST_DEFAULT_TIMEOUT_MS
439
+ });
440
+ }
441
+ return this.registerPaeToolAdapter(adapter, {
442
+ registerPact: opts.registerPact,
443
+ requireHostMinEdition: opts.requireHostMinEdition
444
+ });
445
+ }
446
+ /**
447
+ * Detach an adapter: its tools stop being dispatchable, its derived pact is
448
+ * revoked, and the routing flag clears once the surface is empty. A trace
449
+ * recorded while it was present then replays as configuration drift, not as
450
+ * a mysterious digest mismatch.
451
+ *
452
+ * Resources held by the adapter are released in the background; use
453
+ * `releasePaeToolAdapter` when you need to wait for that to complete (for
454
+ * example before the process exits, or in a test that asserts a subprocess
455
+ * is gone).
456
+ */
457
+ unregisterPaeToolAdapter(adapterId) {
458
+ this.paeRegistry.unregister(adapterId);
459
+ this.paeChannel.syncTools();
460
+ this.pluginPactVerifier.unregisterPluginUnit(adapterId);
461
+ this.impactGraph.removeNode(adapterId);
462
+ // W20: removing a node can shrink closures, so the plan must be recomputed.
463
+ this.domainsStaleFlag = true;
464
+ if (this.paeRegistry.isEmpty()) {
465
+ this.paeAdapterKinds.delete(infra_common_4.ChannelKind.PAE_TOOL);
466
+ }
467
+ }
468
+ /**
469
+ * Detach an adapter and await its release.
470
+ *
471
+ * Same as `unregisterPaeToolAdapter`, plus a wait for `teardown` to finish —
472
+ * the difference matters for adapters that own an OS process (MCP), where
473
+ * "unregistered" and "peer actually shut down" are separated by real time.
474
+ */
475
+ async releasePaeToolAdapter(adapterId) {
476
+ this.unregisterPaeToolAdapter(adapterId);
477
+ await this.paeRegistry.drainReleases();
478
+ }
479
+ /**
480
+ * Connect adapters registered after `bootHost`. Idempotent — adapters that
481
+ * are already connected are skipped, so it is safe to call after every batch
482
+ * of registrations.
483
+ */
484
+ async bootPaeAdapters() {
485
+ await this.paeRegistry.setupAll({
486
+ traceMarkId: (0, infra_common_1.makeUniqueMark)(),
487
+ maxWaitMs: HOST_DEFAULT_TIMEOUT_MS
488
+ });
489
+ this.paeChannel.syncTools();
490
+ }
491
+ /**
492
+ * Capability negotiation for a foreign tool: returns the descriptor when the
493
+ * mapping is at least as faithful as required, otherwise throws. Callers make
494
+ * an informed choice instead of silently receiving a degraded result.
495
+ */
496
+ negotiatePaeTool(toolName, minFidelity = "full") {
497
+ return this.paeRegistry.negotiate(toolName, minFidelity, (0, infra_common_1.makeUniqueMark)());
498
+ }
499
+ /**
500
+ * M2: open a recording window; sandboxes running in "record" mode fill it.
501
+ *
502
+ * W27: when the host was constructed with `recordJournalPath`, the window is
503
+ * durable — every recorded call is mirrored to the WAL. Opening a *new* window
504
+ * truncates that WAL first (a fresh window must not inherit the previous
505
+ * run's calls); use {@link resumeRecording} to continue a prior window instead.
506
+ */
507
+ beginRecording() {
508
+ const journal = this.recordJournalPath
509
+ ? new core_hub_8.PersistedRecordJournal(this.recordJournalPath, { truncate: true })
510
+ : new core_hub_8.RecordJournal();
511
+ this.activeRecordJournal = journal;
512
+ this.channelHub.attachRecordJournal(journal);
513
+ this.gateway.attachJournal(journal);
514
+ return journal;
515
+ }
516
+ /**
517
+ * W27: reopen the durable recording window persisted by a previous process.
518
+ *
519
+ * Recovers the WAL (crash-safe: a truncated trailing line is dropped) and
520
+ * re-attaches it, so `orderIndex` continues from the recovered length and the
521
+ * combined journal replays as one uninterrupted run. Without a configured
522
+ * `recordJournalPath` this degrades to a plain in-memory window.
523
+ */
524
+ async resumeRecording() {
525
+ if (!this.recordJournalPath)
526
+ return this.beginRecording();
527
+ const journal = await core_hub_8.PersistedRecordJournal.recover(this.recordJournalPath);
528
+ // Heal the file before the resumed window appends to it: a crash-truncated
529
+ // tail would otherwise sit in the *interior* once the next line lands, and an
530
+ // invalid interior line is a hard fault. No-op when the log is healthy.
531
+ await journal.healIfNeeded();
532
+ this.activeRecordJournal = journal;
533
+ this.channelHub.attachRecordJournal(journal);
534
+ this.gateway.attachJournal(journal);
535
+ return journal;
536
+ }
537
+ /**
538
+ * W27: the recording window currently attached to the gateway, or `null` if
539
+ * none was ever opened. After a boot with `recordJournalPath` configured this
540
+ * is the journal recovered from the WAL, so callers can inspect or replay a
541
+ * window persisted by a previous process without reopening it.
542
+ */
543
+ currentRecordJournal() {
544
+ return this.activeRecordJournal;
545
+ }
546
+ /** M2: attach a replay engine over a previously recorded journal. */
547
+ attachReplayEngine(journal) {
548
+ const engine = new core_hub_9.ReplayEngine(journal);
549
+ this.channelHub.attachReplayEngine(engine);
550
+ this.gateway.attachReplayEngine(engine);
551
+ return engine;
552
+ }
553
+ /** W7: the unified gateway entry — deterministic boundary for governed calls. */
554
+ async capabilityInvoke(params) {
555
+ // W31: progressive contractification — when the tier checks schemas and
556
+ // the target tool declares one, reject non-conforming arguments BEFORE
557
+ // the call executes. Replay bypasses the check: arguments were already
558
+ // validated at record time, and injection must stay a pure replay. The
559
+ // function is async so the rejection is a rejected promise, never a
560
+ // synchronous throw (callers use await / assert.rejects uniformly).
561
+ if (params.mode !== "replay" && this.governanceProfile.schemaMode !== "optional") {
562
+ this.assertArgsAgainstSchema(params.funcName, params.args);
563
+ }
564
+ return this.gateway.capabilityInvoke(params);
565
+ }
566
+ /**
567
+ * W31: gateway-side parameter validation for PAE tools that declare a
568
+ * schema. The error is raised as a plain Error (user-classified 400 by the
569
+ * console bridge); the recorded call never happens, so nothing to replay.
570
+ */
571
+ assertArgsAgainstSchema(funcName, args) {
572
+ const binding = this.paeRegistry.lookup(funcName);
573
+ const schema = binding?.tool.schema;
574
+ if (!schema)
575
+ return; // no contract declared -> nothing to check
576
+ const result = (0, infra_common_3.validateArgsAgainstSchema)(schema, args);
577
+ if (!result.ok) {
578
+ throw new Error(`parameter contract violated for tool '${funcName}' at ${result.path}: ${result.error}`);
579
+ }
580
+ }
581
+ /** M3: nodes that would be affected if the given node fails (reachability closure). */
582
+ isolationDomain(nodeId) {
583
+ return [...this.impactGraph.closure(nodeId)];
584
+ }
585
+ /** M3: isolation theorem query — two nodes are provably independent. */
586
+ areIsolated(a, b) {
587
+ return this.impactGraph.areIndependent(a, b);
588
+ }
589
+ /** M4: choose the cheapest channel that fits the budget and latency target. */
590
+ routeChannel(kinds, budget, maxLatencyMs) {
591
+ return this.costRouter.choose(kinds, budget, maxLatencyMs);
592
+ }
593
+ /* ------------------------------------------------------------------ */
594
+ /* W20 · 图驱动域分配与域间事务化调用 */
595
+ /* ------------------------------------------------------------------ */
596
+ /** Whether the impact graph changed since the last domain allocation. */
597
+ domainsStale() {
598
+ return this.domainsStaleFlag;
599
+ }
600
+ /**
601
+ * Allocate isolation domains from the current impact graph and publish their
602
+ * unit surface on the gateway (W19 channel, W20 host state).
603
+ *
604
+ * Idempotent for a given graph: the sync is a diff, so re-running after a
605
+ * no-op graph change neither restarts child processes nor perturbs the plan
606
+ * hash. Registering the channel happens here and only here — a host that
607
+ * never allocates domains keeps its previous hub surface and fingerprint.
608
+ */
609
+ async allocateIsolationDomains(opts = {}) {
610
+ let manager = this.domainManager;
611
+ if (!manager) {
612
+ manager = new sandbox_runtime_3.IsolationDomainManager({
613
+ transportFactory: opts.transportFactory,
614
+ clock: opts.clock,
615
+ defaultTimeoutMs: opts.defaultTimeoutMs,
616
+ maxImpactClosure: opts.maxImpactClosure,
617
+ maxDomainSize: opts.maxDomainSize
618
+ });
619
+ const channel = new sandbox_runtime_4.DomainChannel(manager);
620
+ this.domainManager = manager;
621
+ this.domainChannel = channel;
622
+ this.channelHub.registerBuiltInChannel(infra_common_4.ChannelKind.DOMAIN_TOOL, channel);
623
+ // A domain hop is a cross-process call: priced and timed like the
624
+ // adaptation surface, so budget routing sees the real cost.
625
+ this.costRouter.register(infra_common_4.ChannelKind.DOMAIN_TOOL, { costPerCall: 2, latencyMs: 40, quality: 1 });
626
+ }
627
+ const plan = await manager.syncDomains(this.impactGraph, this.domainCtx(), opts);
628
+ this.domainChannel?.syncTools();
629
+ this.domainsStaleFlag = false;
630
+ return plan;
631
+ }
632
+ /** The current domain plan, or `null` before the first allocation. */
633
+ domainPlan() {
634
+ return this.domainManager?.planOf() ?? null;
635
+ }
636
+ /** Running domains: id, units and isolation level, sorted. */
637
+ domains() {
638
+ return this.domainManager?.domainsOf() ?? [];
639
+ }
640
+ /**
641
+ * Invoke a unit in its domain through the gateway — the cross-domain hop.
642
+ * Recorded and replayed like any other governed call; settled in the domain
643
+ * transaction ledger either way.
644
+ */
645
+ invokeDomainUnit(unitId, tool, args = [], opts = {}) {
646
+ return this.gateway.capabilityInvoke({
647
+ kind: infra_common_4.ChannelKind.DOMAIN_TOOL,
648
+ pluginId: opts.pluginUnitId,
649
+ funcName: `${unitId}:${tool}`,
650
+ args,
651
+ mode: opts.mode ?? "live",
652
+ ctx: opts.ctx
653
+ });
654
+ }
655
+ /** The cross-domain transaction ledger (decision → execution → result). */
656
+ domainLedger() {
657
+ return this.domainManager?.txnLedger() ?? [];
658
+ }
659
+ /** Reconcile the ledger — cross-domain events must balance (VISION 2.2). */
660
+ reconcileDomainTransactions() {
661
+ return this.domainManager
662
+ ? this.domainManager.reconcile()
663
+ : {
664
+ balanced: true,
665
+ pairs: [],
666
+ orphans: [],
667
+ rejected: [],
668
+ totals: { transactions: 0, settled: 0, failed: 0, rejected: 0 }
669
+ };
670
+ }
671
+ /** Tear down every domain and release its host process. */
672
+ async releaseIsolationDomains() {
673
+ if (!this.domainManager)
674
+ return;
675
+ const manager = this.domainManager;
676
+ this.domainManager = null;
677
+ this.domainChannel = null;
678
+ this.domainsStaleFlag = false;
679
+ await manager.teardownAll();
680
+ }
681
+ /** Context for domain synchronization calls. */
682
+ domainCtx() {
683
+ const ctx = this.newHostCtx();
684
+ return { traceMarkId: ctx.traceMarkId, maxWaitMs: ctx.maxWaitMs };
685
+ }
686
+ newHostCtx() {
687
+ return { traceMarkId: (0, infra_common_1.makeUniqueMark)(), maxWaitMs: HOST_DEFAULT_TIMEOUT_MS };
688
+ }
689
+ }
690
+ exports.OrbitRuntimeHost = OrbitRuntimeHost;
691
+ /** Minimal mapping from a channel method to its required capability. */
692
+ function requiredCapability(kind, funcName) {
693
+ if (kind === infra_common_4.ChannelKind.MEM_KV_STORE) {
694
+ return funcName === "writeEntry" || funcName === "removeEntry" ? "channel:write" : "channel:read";
695
+ }
696
+ if (kind === infra_common_4.ChannelKind.FILE_SYSTEM) {
697
+ const writes = new Set(["writeTextFile", "appendTextFile", "removePath", "makeDir"]);
698
+ return writes.has(funcName) ? "channel:write" : "channel:read";
699
+ }
700
+ if (kind === infra_common_4.ChannelKind.SHELL_EXEC) {
701
+ // Executing commands is treated as a mutating capability by default.
702
+ return "channel:write";
703
+ }
704
+ return "channel:read";
705
+ }
706
+ //# sourceMappingURL=orbitRuntimeHost.js.map