From ec341bca2a03a2bfd5b34e2d3d231bc287170d90 Mon Sep 17 00:00:00 2001 From: CCDevelopForFun Date: Tue, 22 Sep 2026 12:25:57 -0400 Subject: [PATCH] fix(pi): allow declared built-ins with MCP --- ROADMAP.md | 2 +- cli/cmd/agentctl/main.go | 25 ++++----- cli/cmd/agentctl/workspace_mcp.go | 19 +++++-- cli/cmd/agentctl/workspace_mcp_test.go | 17 +++++- docs/architecture/harness-matrix.md | 8 +-- docs/architecture/overview.md | 2 +- docs/features.md | 2 +- runtime/src/adapter.test.ts | 72 +++++++++++++++++++++++++- runtime/src/adapter.ts | 22 ++++++-- 9 files changed, 136 insertions(+), 33 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 6803080..6c50d5e 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -138,7 +138,7 @@ Today every `agentctl run` is a one-shot subprocess. v0.6 ships **interactive ch ## v0.7.0 — Make `agentctl run` a first-class scheduler task (Option-B pivot) ✅ SHIPPED 2026-06-25 -Delivered across slices 7.1–7.5 (PRs #30–#34): `--input k=v` + `${inputs.foo}` interpolation, `--input KEY=@` / `--input-file`, `--output-file` + `spec.outputSchema`, `--skip-if-output-exists`, Maestro/Airflow/Temporal examples, and `--workspace` durable agent memory via a harness-agnostic MCP server (works on Pi and opencode). Coordinated bumps: `agentctl` v0.7.0, `@agent-controller/runtime` + `@agent-controller/runtime-opencode` 0.7.0; ADL schema unchanged (additive `outputSchema`). See [CHANGELOG.md](CHANGELOG.md) for the full per-slice entry. Deferred (tracked follow-ups): the K8s `outputSchema` round-trip + a v0.7-bundled runtime image, and two Pi-adapter improvements to make `--workspace` first-class on Pi (built-in-tool coexistence; per-run MCP config isolation). +Delivered across slices 7.1–7.5 (PRs #30–#34): `--input k=v` + `${inputs.foo}` interpolation, `--input KEY=@` / `--input-file`, `--output-file` + `spec.outputSchema`, `--skip-if-output-exists`, Maestro/Airflow/Temporal examples, and `--workspace` durable agent memory via a harness-agnostic MCP server (works on Pi and opencode). Coordinated bumps: `agentctl` v0.7.0, `@agent-controller/runtime` + `@agent-controller/runtime-opencode` 0.7.0; ADL schema unchanged (additive `outputSchema`). See [CHANGELOG.md](CHANGELOG.md) for the full per-slice entry. Deferred (tracked follow-ups): the K8s `outputSchema` round-trip + a v0.7-bundled runtime image, and per-run Pi MCP config isolation. Pi built-in tools now coexist with MCP when all four are explicitly declared. **Direction change recorded 2026-06-15.** The pre-v0.7 plan described agentctl shipping its own in-process workflow engine (`agentctl workflow run`) with declarative `AgentWorkflow` YAML, parallel/conditional/foreach step shapes, etc. After thinking through how this would compose with Maestro / Airflow / Temporal — the orchestrators that actually run production multi-step workloads — we picked **Option B**: agentctl is the AGENT RUNTIME, the external scheduler is the ORCHESTRATOR. diff --git a/cli/cmd/agentctl/main.go b/cli/cmd/agentctl/main.go index 379af17..e0a4705 100644 --- a/cli/cmd/agentctl/main.go +++ b/cli/cmd/agentctl/main.go @@ -359,27 +359,20 @@ func newRunCmd() *cobra.Command { "Use a shared volume + the file-handoff flags (--output-file / " + "--input KEY=@) for cross-step state under Kubernetes") } - // Pi-adapter caveat (codex pass 1 of slice 7.5): the Pi - // adapter switches to `noTools: "builtin"` whenever ANY MCP - // server is present, which suppresses the built-in - // read/bash/edit/write tools — Pi's API can't keep declared - // built-ins AND allow unknown MCP tool names at the same - // time. So injecting the workspace server would silently - // drop a Pi agent's declared built-in tools. Warn rather - // than fail (the agent may not need those built-ins). The - // opencode and claude adapters grant each declared tool - // independently (claude via the SDK's `allowedTools`) and - // are unaffected. + // Pi can preserve its default built-ins alongside dynamically + // registered MCP tools only when all four are declared together. + // A partial built-in list is suppressed, so warn before injecting + // the workspace MCP server. Opencode and Claude can grant each + // declared tool independently and are unaffected. isPiAdapter := spec.Runtime.Type != "local-opencode" && spec.Runtime.Type != "local-codex" && spec.Runtime.Type != "local-claude" if isPiAdapter { builtins := declaredBuiltinTools(&spec) - if len(builtins) > 0 { + if len(builtins) > 0 && !allPiBuiltinToolsDeclared(builtins) { fmt.Fprintf(cmd.ErrOrStderr(), "[warning] --workspace adds an MCP server, and the Pi adapter "+ - "disables built-in tools when any MCP server is present, so this "+ - "run will lose its declared built-in tool(s): %s. Use "+ - "runtime.type: local-opencode (unaffected), or drop --workspace "+ - "and hand off via files (--output-file / --input KEY=@).\n", + "requires all four Pi built-ins together. This run will lose its "+ + "partial built-in selection: %s. Declare read, bash, edit, and write "+ + "together, use runtime.type: local-opencode, or drop --workspace.\n", strings.Join(builtins, ", ")) } } diff --git a/cli/cmd/agentctl/workspace_mcp.go b/cli/cmd/agentctl/workspace_mcp.go index 0414fcb..5572663 100644 --- a/cli/cmd/agentctl/workspace_mcp.go +++ b/cli/cmd/agentctl/workspace_mcp.go @@ -30,9 +30,9 @@ import ( const workspaceMCPServerName = "agentctl-workspace" // declaredBuiltinTools returns the names of Pi-builtin tools (bash, read, -// edit, write) the spec declares. Used to warn that --workspace's injected -// MCP server will cause the Pi adapter to suppress these (see the caveat -// in newRunCmd). Returns nil when none are declared. +// edit, write) the spec declares. Used to warn when --workspace's injected +// MCP server meets a partial built-in selection. Returns nil when none are +// declared. func declaredBuiltinTools(spec *adl.CompiledSpec) []string { var builtins []string for _, tr := range spec.Tools { @@ -43,6 +43,19 @@ func declaredBuiltinTools(spec *adl.CompiledSpec) []string { return builtins } +func allPiBuiltinToolsDeclared(tools []string) bool { + declared := make(map[string]bool, len(tools)) + for _, name := range tools { + declared[name] = true + } + for _, name := range []string{"read", "bash", "edit", "write"} { + if !declared[name] { + return false + } + } + return true +} + // injectWorkspaceMCPServer adds the workspace memory MCP server to // spec.MCPServers (slice 7.5). The injected entry runs agentctl itself // (` __workspace-mcp --workspace `) over stdio, so the four diff --git a/cli/cmd/agentctl/workspace_mcp_test.go b/cli/cmd/agentctl/workspace_mcp_test.go index 31da147..622b5d4 100644 --- a/cli/cmd/agentctl/workspace_mcp_test.go +++ b/cli/cmd/agentctl/workspace_mcp_test.go @@ -278,6 +278,21 @@ func TestDeclaredBuiltinTools(t *testing.T) { } } +func TestAllPiBuiltinToolsDeclared(t *testing.T) { + if allPiBuiltinToolsDeclared(nil) != false { + t.Fatal("nil input must not enable MCP coexistence") + } + if allPiBuiltinToolsDeclared([]string{}) != false { + t.Fatal("empty input must not enable MCP coexistence") + } + if allPiBuiltinToolsDeclared([]string{"read", "bash", "edit", "write"}) != true { + t.Fatal("all four Pi built-ins should enable MCP coexistence") + } + if allPiBuiltinToolsDeclared([]string{"read", "bash", "edit"}) != false { + t.Fatal("a partial Pi built-in list must retain the MCP warning") + } +} + // minimalClaudeAgentSpecWithBash declares a builtin tool under // runtime.type: local-claude, so declaredBuiltinTools(&spec) is non-empty // and the Pi-only "--workspace will drop your built-ins" warning would fire @@ -326,7 +341,7 @@ func TestRunWorkspaceNoPiWarningForClaudeAdapter(t *testing.T) { if err == nil || !strings.Contains(err.Error(), "claude runtime not found") { t.Fatalf("expected a deterministic 'claude runtime not found' error, got: %v", err) } - if strings.Contains(stderr.String(), "will lose its declared built-in tool") { + if strings.Contains(stderr.String(), "[warning] --workspace adds an MCP server, and the Pi adapter") { t.Errorf("claude adapter must not get the Pi-only built-ins warning; stderr: %q", stderr.String()) } } diff --git a/docs/architecture/harness-matrix.md b/docs/architecture/harness-matrix.md index 7585f6f..3ae8bbd 100644 --- a/docs/architecture/harness-matrix.md +++ b/docs/architecture/harness-matrix.md @@ -64,10 +64,10 @@ This document is the artifact that justifies the claim "ADL is harness-agnostic. | Feature | Pi | opencode | codex | claude | hermes-agent | |---|---|---|---|---|---| -| Pi built-in `bash` | ⚠️ — ✅ on its own (since v0.1.9); **deactivated when `spec.mcpServers[]` is non-empty** because the adapter switches to `noTools: "builtin"` to suppress all built-in tools and let MCP tools enter the registry. Combining built-ins with MCP servers in the same spec is currently unsupported; v0.2+ work. | ✅ — maps to opencode permission `bash: allow`. Coexists with `spec.mcpServers[]` (no exclusivity restriction on the opencode side) | ✅ — the codex sandbox (`-s workspace-write`) provides shell/read/write access natively; declaring `bash` in `spec.tools` is accepted without error (the ADL built-in flag is recognized). The codex sandbox controls what the model can do regardless of the ADL tools list. | ✅ — maps to the SDK's `Bash` tool via `Options.allowedTools`. Coexists with `spec.mcpServers[]` (no exclusivity restriction, unlike the Pi adapter) | 🚧 | -| Pi built-in `read` | ⚠️ — same MCP-incompatibility caveat as `bash` | ✅ — maps to opencode permission `read: allow` | ✅ — same as `bash`: accepted without error; the codex sandbox provides the capability directly. | ✅ — maps to the SDK's `Read` tool via `Options.allowedTools` | 🚧 | -| Pi built-in `edit` | ⚠️ — same MCP-incompatibility caveat as `bash` | ✅ — maps to opencode permission `edit: allow` | ✅ — same as `bash`. | ✅ — maps to the SDK's `Edit` tool via `Options.allowedTools` | 🚧 | -| Pi built-in `write` | ⚠️ — same MCP-incompatibility caveat as `bash` | ⚠️ — opencode has no separate `write` permission; `write` collapses onto `edit: allow` (which already gates file creation/overwrite). Declaring both `edit` and `write` is idempotent | ✅ — same as `bash`. | ✅ — maps to the SDK's `Write` tool via `Options.allowedTools` | 🚧 | +| Pi built-in `bash` | ⚠️ — ✅ on its own (since v0.1.9). With `spec.mcpServers[]`, all four Pi built-ins (`read`, `bash`, `edit`, `write`) must be explicitly declared together; the adapter then leaves Pi's default built-ins enabled while MCP tools register dynamically. Declaring none keeps all built-ins disabled; partial built-in selection with MCP remains unsupported. | ✅ — maps to opencode permission `bash: allow`. Coexists with `spec.mcpServers[]` (no exclusivity restriction on the opencode side) | ✅ — the codex sandbox (`-s workspace-write`) provides shell/read/write access natively; declaring `bash` in `spec.tools` is accepted without error (the ADL built-in flag is recognized). The codex sandbox controls what the model can do regardless of the ADL tools list. | ✅ — maps to the SDK's `Bash` tool via `Options.allowedTools`. Coexists with `spec.mcpServers[]` | 🚧 | +| Pi built-in `read` | ⚠️ — same all-four-or-none MCP rule as `bash` | ✅ — maps to opencode permission `read: allow` | ✅ — same as `bash`: accepted without error; the codex sandbox provides the capability directly. | ✅ — maps to the SDK's `Read` tool via `Options.allowedTools` | 🚧 | +| Pi built-in `edit` | ⚠️ — same all-four-or-none MCP rule as `bash` | ✅ — maps to opencode permission `edit: allow` | ✅ — same as `bash`. | ✅ — maps to the SDK's `Edit` tool via `Options.allowedTools` | 🚧 | +| Pi built-in `write` | ⚠️ — same all-four-or-none MCP rule as `bash` | ⚠️ — opencode has no separate `write` permission; `write` collapses onto `edit: allow` (which already gates file creation/overwrite). Declaring both `edit` and `write` is idempotent | ✅ — same as `bash`. | ✅ — maps to the SDK's `Write` tool via `Options.allowedTools` | 🚧 | | Custom tool (registry manifest with Pi-extension entrypoint) | ✅ | ❌ — rejected at **compile time** in `cli/internal/adl/compiler.go::checkOpencodeIncompatibilities` as of v0.3.4 (was adapter-startup in `buildOpencodeConfig`; the runtime check stays as defense-in-depth). Error names every offending tool | ❌ — **rejected at adapter startup** by `assertCodexCompatible`. Error names every offending tool. | ❌ — rejected at **compile time** in `cli/internal/adl/compiler.go::checkClaudeIncompatibilities`. Adapter startup's `assertClaudeCompatible` retains the same check as defense-in-depth. Error names every offending tool. | 🚧 | | `config` on built-in tools (e.g. bash allowlist) | ❌ — rejected at compile time as of v0.1.11. Pi built-ins (`bash`/`read`/`edit`/`write`) have no per-tool config plumbing. Use [`@gotgenes/pi-permission-system`](https://www.npmjs.com/package/@gotgenes/pi-permission-system) as a `spec.extensions[].source: npm:` entry instead — see [`examples/bash-allowlist.yaml`](../../examples/bash-allowlist.yaml). The rejection lives in `cli/internal/adl/compiler.go::Compile`, so `agentctl compile` catches it. **NOTE:** `agentctl validate` (JSON Schema only) does NOT catch it today — CI gates that stop at `validate` will still pass the silently-broken spec. Moving the check into Schema or into a post-validate hook is a v0.3 follow-up. | ❌ — rejected at compile time (same canonical check as Pi). The opencode adapter retains a runtime defense-in-depth check that fires if a hand-crafted CompiledSpec bypasses the compiler. | ❌ — rejected at compile time (same canonical check). Adapter startup retains a defense-in-depth check. | ❌ — rejected at compile time (same canonical, runtime-agnostic check as Pi/opencode/codex — this check runs before the runtime-specific dispatch table). | 🚧 | diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 4786315..39b81ea 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -117,7 +117,7 @@ flowchart LR | Pi-format extensions (audit-log, custom tools) | Pi adapter (`local` / `local-pi`) | Pi extension modules don't run inside opencode | | Session resume (`--resume`) | Pi adapter | opencode resume not yet wired | | `bash` allowlist via tool config | Pi adapter (v0.1.11+) | opencode rejects per-tool config | -| MCP + built-in tools coexisting in the same spec | opencode adapter | Pi adapter currently deactivates built-ins when MCP is non-empty | +| MCP + independently selectable built-in tools | opencode adapter | Pi supports MCP plus built-ins only when all four (`read`, `bash`, `edit`, `write`) are explicitly declared together | | Native opencode MCP / subagent support without Pi indirection | opencode adapter | Native cfg.mcp + cfg.agent[subagent] | | Cancellation event (`reason: cancelled`) on SIGINT | opencode adapter | Pi adapter currently surfaces SIGINT as `error` | diff --git a/docs/features.md b/docs/features.md index bb809cb..7f2f5f9 100644 --- a/docs/features.md +++ b/docs/features.md @@ -827,7 +827,7 @@ ls /tmp/ws-demo # .agentctl-workspace.db (+ notes.md if the agent jou sqlite3 /tmp/ws-demo/.agentctl-workspace.db 'SELECT key, value FROM kv;' ``` -The memory tools ride on `spec.mcpServers`, so the same workspace works on Pi and opencode. Hermetic negative checks: `--workspace` with a `kubernetes` binding errors (host-local DB); `--workspace ""` errors; on Pi, combining `--workspace` with declared built-in tools prints a warning (Pi suppresses built-ins when any MCP server is present). +The memory tools ride on `spec.mcpServers`, so the same workspace works on Pi and opencode. Hermetic negative checks: `--workspace` with a `kubernetes` binding errors (host-local DB); `--workspace ""` errors. On Pi, built-ins remain disabled with MCP unless all four (`read`, `bash`, `edit`, `write`) are explicitly declared together. ### 10.6 Scheduler examples diff --git a/runtime/src/adapter.test.ts b/runtime/src/adapter.test.ts index 42ecc87..13bf1c1 100644 --- a/runtime/src/adapter.test.ts +++ b/runtime/src/adapter.test.ts @@ -189,6 +189,8 @@ const captured = { modelReturnUndefined: false, // Records calls to SessionManager static factories. sessionManagerCalls: [] as Array<{ method: string; args: any[] }>, + settingsManagerCreateArgs: [] as unknown[], + settingsOverrides: [] as Array>, }; vi.mock("@earendil-works/pi-ai/providers/all", () => ({ @@ -244,11 +246,22 @@ vi.mock("@earendil-works/pi-coding-agent", () => { } } + class SettingsManager { + static create(...args: unknown[]) { + captured.settingsManagerCreateArgs = args; + return { + applyOverrides: (overrides: Record) => { + captured.settingsOverrides.push(overrides); + }, + }; + } + } + function getAgentDir() { return "/mock/agent/dir"; } - return { createAgentSession, DefaultResourceLoader, SessionManager, getAgentDir }; + return { createAgentSession, DefaultResourceLoader, SessionManager, SettingsManager, getAgentDir }; }); import { runSession } from "./adapter.js"; @@ -264,6 +277,8 @@ beforeEach(() => { loaderErrors.errors = []; captured.promptResolvers = []; captured.sessionManagerCalls = []; + captured.settingsManagerCreateArgs = []; + captured.settingsOverrides = []; // Reset fake filesystem state between tests. fakeFs.files.clear(); fakeFs.dirs.clear(); @@ -802,6 +817,21 @@ describe("adapter", () => { expect(captured.createAgentArgs.noTools).toBeUndefined(); }); + it("does not create MCP coexistence settings without MCP servers", async () => { + const spec = fixture(); + spec.tools = ["read", "bash", "edit", "write"].map((name) => ({ + name, + builtin: true, + })); + const ended = runSession(spec, () => {}); + await flushAndShutdown(); + await ended; + + expect(captured.createAgentArgs.tools).toEqual(["read", "bash", "edit", "write"]); + expect(captured.createAgentArgs.settingsManager).toBeUndefined(); + expect(captured.settingsOverrides).toEqual([]); + }); + // MCP tool allowlist fix: when mcpServers is non-empty, passing tools: [] // sets allowedToolNames to an empty Set in Pi, which blocks ALL tools from // _toolRegistry — including MCP tools registered post-session-start by @@ -834,6 +864,46 @@ describe("adapter", () => { expect(captured.createAgentArgs.noTools).toBe("builtin"); }); + it("keeps Pi built-ins enabled when an MCP spec explicitly declares all four", async () => { + const spec = mcpFixture(); + spec.tools = ["read", "bash", "edit", "write"].map((name) => ({ + name, + builtin: true, + })); + const ended = runSession(spec, () => {}); + await flushAndShutdown(); + await ended; + + expect(captured.createAgentArgs.tools).toBeUndefined(); + expect(captured.createAgentArgs.noTools).toBeUndefined(); + expect(captured.resourceLoaderArgs.settingsManager).toBeUndefined(); + expect(captured.createAgentArgs.settingsManager).toBeDefined(); + expect(captured.settingsManagerCreateArgs).toEqual([process.cwd(), "/mock/agent/dir"]); + expect(captured.settingsOverrides).toEqual([ + { defaultTools: ["read", "bash", "edit", "write"] }, + ]); + }); + + it("keeps custom tools active with all Pi built-ins and MCP", async () => { + const spec = mcpFixture(); + spec.tools = [ + ...["read", "bash", "edit", "write"].map((name) => ({ name, builtin: true })), + { name: "get_time", entrypoint: "/abs/tools/get_time/entrypoint.ts" }, + ]; + const ended = runSession(spec, () => {}); + await flushAndShutdown(); + await ended; + + expect(captured.createAgentArgs.tools).toBeUndefined(); + expect(captured.createAgentArgs.noTools).toBeUndefined(); + expect(captured.resourceLoaderArgs.additionalExtensionPaths).toContain( + "/abs/tools/get_time/entrypoint.ts", + ); + expect(captured.settingsOverrides).toEqual([ + { defaultTools: ["read", "bash", "edit", "write"] }, + ]); + }); + // Finding 2: unknown model error it("throws a clear error when getBuiltinModel returns undefined", async () => { captured.modelReturnUndefined = true; diff --git a/runtime/src/adapter.ts b/runtime/src/adapter.ts index 213dd4d..df83e6d 100644 --- a/runtime/src/adapter.ts +++ b/runtime/src/adapter.ts @@ -1,4 +1,4 @@ -import { createAgentSession, DefaultResourceLoader, SessionManager, getAgentDir } from "@earendil-works/pi-coding-agent"; +import { createAgentSession, DefaultResourceLoader, SessionManager, SettingsManager, getAgentDir } from "@earendil-works/pi-coding-agent"; import { getBuiltinModel } from "@earendil-works/pi-ai/providers/all"; import { join, basename, resolve, dirname } from "node:path"; import { mkdirSync, writeFileSync, existsSync, readFileSync, copyFileSync, realpathSync, readdirSync, unlinkSync } from "node:fs"; @@ -782,6 +782,14 @@ export async function runSession( // and /.pi/skills/. Only ADL-declared skills (via additionalSkillPaths) // are loaded, keeping the environment hermetic and consistent with the ADL // allowlist principle used for extensions. + const hasMcpServers = spec.mcpServers && spec.mcpServers.length > 0; + const piBuiltinToolNames = ["read", "bash", "edit", "write"]; + const enablesAllPiBuiltins = piBuiltinToolNames.every((name) => + spec.tools.some((tool) => tool.builtin && tool.name === name), + ); + const sessionSettingsManager = hasMcpServers && enablesAllPiBuiltins + ? SettingsManager.create(process.cwd(), getAgentDir()) + : undefined; // eslint-disable-next-line @typescript-eslint/no-explicit-any const resourceLoader = new DefaultResourceLoader({ cwd: process.cwd(), @@ -806,6 +814,9 @@ export async function runSession( // it assumes the caller has already loaded it. Without this call no extensions // (get_time, audit-log) are active. await (resourceLoader as any).reload(); + // reload() re-reads ambient settings, so apply the ADL-declared tool override + // afterwards to make the active built-ins deterministic for this session. + sessionSettingsManager?.applyOverrides({ defaultTools: piBuiltinToolNames }); // Fail fast if any ADL-declared entrypoint failed to load. Pi records load // failures in extensionsResult.errors as { path, error } pairs but continues @@ -950,7 +961,6 @@ export async function runSession( // (registered by the vendored subagent extension) in the parent's allowlist. // Without this, the parent model would not be able to call the subagent tool // even though the extension is loaded. - const hasMcpServers = spec.mcpServers && spec.mcpServers.length > 0; const toolAllowlist = [ ...spec.tools.map((t) => t.name), ...(spec.subagents && spec.subagents.length > 0 ? ["subagent"] : []), @@ -968,13 +978,15 @@ export async function runSession( ...(fakeModelRuntime ? { modelRuntime: fakeModelRuntime } : {}), resourceLoader, // When MCP servers are declared: omit `tools` so allowedToolNames stays - // undefined (MCP tools can enter the registry and auto-activate), and use - // noTools: "builtin" to suppress Pi's built-in read/bash/edit/write tools. + // undefined and MCP tools can enter the registry and auto-activate. Suppress + // Pi's built-ins unless the spec explicitly declares all four; Pi cannot + // combine a partial built-in allowlist with dynamically registered MCP tools. // When no MCP servers: pass tools: toolAllowlist as before (explicit // allowlist that blocks builtins and activates only declared tool names). ...(hasMcpServers - ? { noTools: "builtin" as const } + ? (enablesAllPiBuiltins ? {} : { noTools: "builtin" as const }) : { tools: toolAllowlist }), + ...(sessionSettingsManager ? { settingsManager: sessionSettingsManager } : {}), sessionManager, });