initJsExtensions method
Loads installed JS extensions in the background (fire-and-forget from
the constructor, like MCP boot — never blocks the REPL): build the
store + host, wire the sinks, loadAll, register the tool surface +
prompt section, and attach the hooks.
Attach ORDER matters: the constructor already wrapped approval +
redaction, so JsExtensionHost.attachHooks lands OUTERMOST — the JS
hooks run LAST (after redaction) and a Dart-side deny always wins.
Implementation
Future<void> initJsExtensions() async {
final factory = config.extRuntimeFactory;
if (!config.jsExtensionsEnabled || factory == null) return;
final host = JsExtensionHost(
env: _env,
store: _extStore(),
runtimeFactory: factory,
bootstrapJs: config.extBootstrapJs,
config: ExtHostConfig(
redact: config.redactionPipeline == null
? null
: (text) => config.redactionPipeline!.redact(text),
),
);
_ext.host = host;
host.onLog = (line) => io.writeln('[ext] $line');
host.onAppendNote = _appendExtNote;
host.onFollowUp = (text) => _agent.followUp(UserMessage.text(text));
host.onIoWrite = io.write;
host.onIoWriteln = io.writeln;
host.setToolSyncCallbacks(
syncTools: _syncExtTools,
rebuildPrompt: _extRefreshSurface,
);
final report = await host.loadAll(
platform: ExtPlatformTag.cli,
trustPrompt: io.isInteractive ? _extTrustPrompt : null,
reservedNames: _extReservedNames(),
);
_reportExtLoad(report);
if (host.tools.isNotEmpty) {
_toolRegistry.registerAll(host.tools);
_agent.state.tools = _toolRegistry.tools;
}
_extRefreshSurface();
final resolution = _toolGate.resolution;
if (resolution != null) {
// Re-apply the availability decision to the fresh ext surface (same
// path as /tools reload — mirrors _onMcpChanged).
AgentCliTools(this).refilterMcpTools(resolution);
}
// pi mode (issue #679): the benchmark profile carries no JS
// extension hook side effects — `beforeToolCall`/`afterToolCall`
// hooks (logging, notes, follow-ups) stay off the agent. The tool
// surface is already swept; this is the other half of the pin.
if (!_piActive) {
host.attachHooks(_agent);
}
// v1: fired once after the load completes; extension side effects land
// through the session bridges (notes append once a session exists).
await host.sessionStart();
}