feat: D1 Durable Dana Conversation — restart-resumable ACP agent - #27
Open
ngoclam9415 wants to merge 64 commits into
Open
feat: D1 Durable Dana Conversation — restart-resumable ACP agent#27ngoclam9415 wants to merge 64 commits into
ngoclam9415 wants to merge 64 commits into
Conversation
Add DanaACPAgent implementing the ACP Agent protocol over stdio JSON-RPC, enabling dana-console to connect to Dana as a Custom ACP Agent. - dana/apps/acp/agent.py: DanaACPAgent translating ACP calls (initialize, session/new, session/load, session/resume, session/prompt, session/cancel) to AgentSession operations, streaming HostEvents back as session_update notifications - dana/apps/acp/translation.py: HostEvent → ACP update chunk translation (ACP types never enter STAR core) - dana/apps/acp/__main__.py: entry point with stderr-only logging - dana/__init__/init_environment.py: redirect structlog to stderr so stdout stays clean for JSON-RPC frames - pyproject.toml: dana-acp console script entry point - tests/integration/test_acp_agent.py: 14 in-process + subprocess tests covering load capability, replay-before-return, chunk streaming, burst ordering, busy, cancel, malformed content, stderr/stdout discipline
Untrack /sprint/, /v2/, CLAUDE.md (local-only working docs, consistent with existing AGENTS.md/.claude/.opencode ignores). CLAUDE.md removed from repo; local copy retained via gitignore.
Per-agent intercept-capable EventBus: first-wins aggregation, sync+async handlers, raise isolation, emit_sync via Misc.safe_asyncio_run. Lazy mount on BaseSTARAgent. Non-dict results warned+skipped. 17 tests.
.agents/, .codegraph/, .codex/, .superpowers/, memories/, tests/unit/core/guard/ — local tool artifacts (consistent with .claude/.opencode).
_build_native_tools_if_supported now returns early if schemas already built. Rebuilding every build_prompt re-ran inspect.signature on every resource method under the tracing chain, exhausting the recursion budget on long sessions (librarian console crash). Structural deps don't change per turn -> build once. Adds 2 tests.
Milestone M3 (longest pole of v2.0 extensibility backbone). Wires the S1
EventBus into both tool-execution paths and adds a deny-only PermissionPolicy.
- ext/operation.py: Operation + ToolIdentity (thin, read-only via MappingProxyType)
- ext/permission.py: PermissionPolicy deny-only (a tool_call subscriber)
- ext/guard.py: reference rm-rf + protected-path policy (S4 discovery target)
- tool_executor.py: emit tool_call/tool_result around dispatch in both single-
call paths; split out _dispatch_single_call[_async] (dispatch NOT merged)
- runtime/{protocols,__init__}.py: remove dead ToolHookProtocol/ApprovalProtocol
scaffold + constructor params hooks/approval (never wired)
Adversarial review fixes: non-dict tool_result modify no longer crashes the
batch (isinstance guard + warn); guard substring rules str()-coerce (defeats
list-arg bypass); strict-bool block (is True); Operation.arguments immutable.
Tests: 27 new (S3.1-S3.15 + 7 adversarial fix-regressions). Regression green:
tests/unit + tests/integration 1614 passed, 37 skipped, 1 xfailed.
Milestone M2. Emits see_end/think_end/act_end/reflect_end around the STAR phases in query()/aquery() so handlers can observe, modify, or block each phase. STAR contract (_see/_think/_act/_reflect) unchanged. - base_star_agent.py: _emit_phase[_async] helpers + per-phase block/modify wiring in _do_query/_do_aquery; reflect_end emit in the reflect wrappers. - Orchestrator-based wiring (not scatter-site): STARAgent._think/_act_async broadcast inline without super(), so base-site wiring would miss them. - Fix latent S1 bug: event_bus property used getattr(self,_event_bus,None) but STARAgent.__getattr__ returns a magic-method stub for any unknown attr, so the bus was never created on the real agent. Now reads self.__dict__. - Adversarial fixes: per-phase exit uses EXIT_FLAG is True (not _do_exit_star_loop, avoiding the empty-dict false-exit quirk); act_end block sets phase_blocked (skips reflect, prevents repeat); non-dict modify ignored + warned. Trade-off: broadcast fires before emit, so legacy broadcast observers see the pre-modify result (accepted for minimal blast radius; modify still changes the result for later phases). Tests: 10 new (T2.1-T2.8 + 2 adversarial). Regression: tests/unit + tests/integration 1624 passed, 37 skipped, 1 xfailed (the 9 done-flag-autonomy tests caught the event_bus bug pre-fix).
Milestone M4 — completes the v2.0 extensibility backbone (M1-M4 all shipped).
Drop-in Python extensions discovered from ~/.dana/extensions/ (global, always)
and .dana/extensions/ (project, trust-gated via DANA_TRUST_PROJECT_EXTENSIONS)
and bound to the agent's EventBus via a setup(agent) factory using agent.on().
- ext/extensions.py: ExtensionManager — discover/load/reload + LoadReport.
* Per-agent, lazy via agent.extensions (__dict__ storage, same __getattr__
lesson as S1/S2).
* Loader bypasses the pyc cache (read_text+compile+exec): SourceFileLoader
keys .pyc on (mtime,size) so a same-byte-size edit within 1s would exec
stale code — fatal for hot-reload correctness.
* Reload: unsub tracked handlers, pop stale sys.modules, re-exec, emit
SESSION_RELOAD. Must run at idle (S1 Finding A).
* Sub tracking via wrapping bus.subscribe during setup (try/finally).
- base_star_agent.py: agent.on alias + extensions property + load_extensions/
reload_extensions delegates. NOT auto-loaded at construction (host calls it;
zero regression risk to agent init).
- Trust gate: global = user's home (trusted); project = explicit flag.
Adversarial fixes: failing setup rolls back partial handler registrations
(transactional; was a reload leak); reload pops stale sys.modules entries.
Tests: 10 new (T4.1-T4.8 + 2 adversarial). Regression: tests/unit +
tests/integration 1634 passed, 37 skipped, 1 xfailed.
Concise showcase of the intercept-capable EventBus: drop-in extensions (~/.dana/extensions/*.py), the setup(agent)/agent.on contract, the block/modify handler shapes, and the 3 concrete examples (rm-rf guard, arg rewrite, STAR observer).
feat: v2.0 extensibility backbone (M1-M4)
…ce in _get_or_create_worker, remove dead _InFlight.wait()
- Add CancellationTree with cascade/detach/keep ownership semantics - Add CancellationNode with acknowledged/timeout/effect-unknown outcomes - Add kill escalation (force-kill all descendants regardless of ownership) - Extend FactType enum with D2 tool lifecycle facts (non-terminal + terminal) - Add comprehensive tests: cancellation matrix (6 contexts), outcome distinction, terminal fact enforcement, serialization, edge cases
- Add DurableJobManager with handoff request/confirm/fail lifecycle - Add DurableJobRecord with status tracking (handoff_requested, running, completed, failed, cancelled, handoff_failed) - Add cascade/detach integration: RUNNING jobs survive parent cancellation, HANDOFF_REQUESTED jobs fail on parent cancel - Add serialization round-trip for records and manager state - Add comprehensive tests: lifecycle, cascade/detach, edge cases, crash-before-handoff, serialization
…ion matching, add evaluator tests
- Add tool lifecycle HostEventTypes (TOOL_REQUESTED, TOOL_STARTED, TOOL_PROGRESS, TOOL_RESULT, TOOL_FAILURE, TOOL_ACKNOWLEDGED, TOOL_TIMED_OUT, TOOL_EFFECT_UNKNOWN, TOOL_CANCELLATION_REQUESTED, TOOL_AUTHORIZED_OR_DENIED, THOUGHT) - Extend host_events.py fact-to-event mapping for all D2 tool facts - Extend ACP translation with thought, tool-call, tool-update, result, and cancellation state mappings - Add AgentSession tool lifecycle wiring: emit_thought, journal_tool_*, execute_tool_call with full lifecycle - Add rollback flag (use_legacy_executor) for non-ACP host fallback - Add comprehensive unit tests for all five ACP states and tool lifecycle
Migration gate (spec §Dependencies and Gates): official-protocol parity achieved + cleanup tests pass. Removed: - dana/lib/resources/mcp/ (MCPClientResource, BrightQueryResource, GitHubMCPResource, SlackMCPResource) - dana/lib/resources/mcp_client.py (duplicate of mcp/mcp_client.py) - Updated dana/lib/resources/__init__.py to remove MCP re-exports All 678 unit tests pass (8 new MCP tests + 670 existing).
…otected state compatibility gating
- MCPCatalogAdapter: discover MCP tools, convert to namespaced ToolCatalogEntry, deterministic collision detection, invalidation, and re-discovery - MCPLease/MCPLeaseManager: session lease lifecycle (pending/active/failed/degraded/released), required-lease failure stops preflight, optional-lease failure degrades, restore from persisted state (session load) - MCPConfig/load_mcp_config: JSON config loading with env allowlist, rollback via mcp_enabled flag, filter_env_for_server - 57 new tests covering all ACs and edge cases - Updated mcp/__init__.py exports AC #1: Deterministic collisions AC #2: Dynamic catalog invalidation AC #3: Allowlisted environment enforced AC #4: Configuration loading + rollback
- ContentNormalizer normalizes text/image/embedded_resource/file_resource blocks - Validation module enforces MIME type, size limits, and path-safety checks - ArtifactStore provides hash-based dedup with OwnerScope isolation - ArtifactRetentionManager tracks independent retention policies - New FactTypes: ARTIFACT_REFERENCE, ARTIFACT_DELETED - NewJournalFact extended with artifact_refs field - SQLite and Postgres adapters pass through artifact_refs on append - Full test coverage: 63 new tests (validation, normalizer, store, retention)
…store - Empty payload, zero-byte file, unsupported MIME, symlink traversal - Concurrent duplicate uploads, large content - Missing artifact during sweep, retention expiry race
…e redundant traversal check
…ndler, and cleanup
…tent blocks, and capability advertisement
…ync, add SIGKILL fallback and waitpid fix(D6): return content_blocks_payload from _normalized_blocks_to_text_blocks, pass to session.prompt, fix dead D6 multimodal handlers in translation
…rt in session.prompt
Uncommitted D2/D4 implementation artifacts on the feat/acp-agent-session-kernel working tree. Committing to clean the tree before Sprint 3 (dana-code on AgentSession) begins. - dana/config.py: ModelTargetConfig + Config.models (D4 model catalog config) - dana/core/tool/tool_executor.py: tool_catalog param + ADR-004 catalog fast-path dispatch (D2) - dana/core/tool/identity.py: check_collision() convenience wrapper (D2) - dana/core/runtime/selector.py: RuntimeRegistry.build_switcher wiring for ModelSwitcher (D4) -- build_provider/apply_switch are placeholders - tests/integration/test_acp_agent.py +239 (D5/D6) - tests/unit/core/session/test_conversation_projection.py +171 (D6) 849 passed; 1 pre-existing unrelated failure (test_mcp_cleanup::test_reap_child_pids -- OS PID-reaping flake). Sprint 2 stories are done in the vault (2026-08-07).
…ridge (Wave 1) Wire dana-code CLI onto the host-neutral AgentSession STAR core in-process (Option B). Two execution paths selected by DANA_CODE_AGENTSESSION_ENABLED (default on): D7.1 — Async REPL + AgentSession construction (code_app.py): - AgentSession path: asyncio.run drives an async REPL; _initialize_session builds a real AgentSession (journal, OwnerScope, SESSION_CREATED fact); _converse_async does `async for event in session.prompt([TextBlock]): renderer.handle_host_event(event)`, catching SessionBusy. - Legacy path: DanaCodingAgent + renderer-as-Notifiable, unchanged (rollback). - ADR-001 honored: the AgentSession path has zero STAR core imports (DanaCodingAgent is lazy-imported only inside the legacy func). D7.2 — HostEvent -> RichCLIRenderer bridge (rich_cli_renderer.py + host_event_adapter.py): - New host_event_adapter.py: pure dispatcher (in-process analog of ACP host_event_to_acp_update), maps every HostEventType to a Rich component handler. - RichCLIRenderer.handle_host_event + 12 component handlers reusing existing spinner/stream-display/tool-card/result-panel/Live machinery + degradation paths. agent_session.py: agent_factory now optional + default_agent_factory() (lazy STARAgent import) so host adapters avoid importing STAR core. Backward-compatible (ACP passes its own factory -> no-op). Tests: 17 new (8 code_app + 9 adapter), no live LLM. Regression 858 passed (ACP integration included); 1 known pre-existing flake (test_mcp_cleanup). Code review: SHIP_WITH_CONDITIONS -> uat. Conditions folded into D7.3: TURN_INTERRUPTED handling, repo teardown, public session_id/version props, truthful Ctrl-C cancel-watcher, + the missing set_policy_evaluator/owner_scope accessors (pre-existing Sprint 2 D3 debt).
…ist import Addresses code-review findings (SHIP_WITH_CONDITIONS) on feaa043: - I2 (Important): _converse_async now catches (KeyboardInterrupt, asyncio.CancelledError) and explicitly `await gen.aclose()` so the AgentSession prompt-generator's `async with` lock releases promptly on mid-turn Ctrl-C. Under asyncio.run (Py 3.11+) SIGINT surfaces as CancelledError, not KeyboardInterrupt — both caught; re-raise for clean shutdown. Truthful TURN_CANCELLED + interrupt-and-continue UX deferred to the D7.3 cancel-watcher (ADR-005). - M2 (Minor): default_agent_factory defaults DANA_LLM_PROVIDER/DANA_MODEL to "openai"/"gpt-5" (was raw None), consistent with the CLI banner. - M3 (Minor): hoist render_host_event/cancellation_outcome imports to module level in rich_cli_renderer.py (was imported inside the render lock on every call). Remaining: I1 (runtime smoke of live dana-code REPL) + M1 (_current_version private reach-in) — next session. 208 passed (17 Wave-1 + 191 ACP/session regression); ruff clean.
_run_agentsession opened an aiosqlite journal connection in _initialize_session but never closed it. On asyncio.run shutdown the abandoned aiosqlite worker thread caused "Event loop is closed" errors and could hang teardown (the root cause behind the earlier smoke hang). Add _close_repo() (await self._repo.close()) called from a finally: around the REPL loop. SQLiteJournalRepository.close() exists (sqlite.py:458). tmux smoke verified: dana-code starts (banner + ❯ prompt), responds to /exit, and the tmux session ends cleanly — no shutdown hang. 8 Wave-1 tests pass; ruff clean.
…ixes empty turn default_agent_factory passed llm_provider/model explicitly, which bypassed STARAgent's config-manager resolution. The resulting LLM client was misconfigured (esp. azure, which needs AZURE_OPENAI_ENDPOINT/DEPLOYMENT resolved by config_manager) → aquery_text_stream yielded nothing → turns rendered no assistant content (silent empty completion). Fix: mirror dana.apps.acp.agent._default_agent_factory — pass NEITHER llm_provider NOR model; let STARAgent/config_manager resolve provider, model, api key, and azure endpoint/deployment from env/config. (Reverts the earlier M2 "default to openai/gpt-5" change, which treated a symptom, not the cause.) tmux smoke (azure · gpt-5.4) now renders a real LLM response end-to-end: ❯ say hi in one word How can I assist you today? Turn path verified. 208 passed (Wave-1 + ACP/session regression); ruff clean.
show_user_message echoed `❯ <text>` on USER_MESSAGE, but prompt_toolkit already renders the typed input → visible duplicate (`❯ say hi` twice). Add `echo_user_message` flag to RichCLIRenderer (default False — off, since the interactive prompt already shows the input). Enable for future replay/non-interactive consumers that need to surface the user turn. tmux smoke: `say hi` now appears once (was twice); response still renders. 17 Wave-1 tests pass; ruff clean.
_initialize_session manually set session._current_version to align with the persisted SESSION_CREATED fact, but _prepare_agent already sets _current_version from journal facts (max sequence) before the first append — so the manual line was redundant AND reached into private state. Removed; _prepare_agent handles version alignment lazily on the first turn. tmux smoke (azure, 25s): turn still renders. 8 Wave-1 tests pass; ruff clean.
…nfra) AgentSession was missing 5 public accessors (set_policy_evaluator, policy_evaluator, owner_scope, session_id, version) that both ACP (dana/apps/acp/agent.py:277,317,350-353) and the dana-code CLI (D7.3) depend on — the ACP permission path raised AttributeError at runtime (latent; not hit by current tests). Also fix a latent ImportError in ACP new_session: it imported SQLiteGrantStore from dana.core.policy.grants, but the class lives in dana.core.policy.store_sqlite (grants.py has 0 references). Same wrong import is the root cause of the D7.3 stash's 'BROKEN (ImportError)'.
… flags, cancel-watcher Wire the D2-D6 capability surface into dana-code (in-process AgentSession): - permissions.py: CLIPermissionAdapter (AC #2) — in-process analog of ACP session/request_permission; evaluates via PolicyEvaluator (ADR-006 precedence), prompts the terminal user, persists durable grants. - commands/__init__.py (AC #3, #6): extract /help /compact /status /reset; add /model (busy-reject via session._lock.locked(), atomic switch via ModelSwitcher) and /permissions; use public AgentSession accessors. /reset -> journal semantics (close repo + re-init session). - code_app.py: wire grant_store + evaluator + set_policy_evaluator in _initialize_session (gate DANA_CODE_PERMISSION_PREFLIGHT_ENABLED); split _handle_command into _async (AgentSession) / _legacy (sync); close the in-memory grant_db on exit (avoids shutdown hang). - Ctrl-C -> TURN_CANCELLED (ADR-005): cooperative cancel via session.cancel() + drain; mid-turn interrupt terminalizes truthfully (turn_cancelled fact) and RESUMES the REPL; between-turns Ctrl-C continues. Add TURN_INTERRUPTED to host_event_adapter TURN_TERMINAL_KIND. - code_capabilities.py (AC #1, #4, #5, #6): DANA_CODE_*_ENABLED rollback flags (permission preflight, model switch, tool catalog, MCP, multimodal). tmux smoke (azure): turn renders, /status shows live state, /model lists, /reset mints new session, mid-turn Ctrl-C -> turn_cancelled + resume, /exit CLEAN. 25 new tests green; 2660 total (minus known OS-PID flake).
… module Move ACP's _normalized_blocks_to_text_blocks (agent.py:656) to dana/core/content/blocks.py as normalized_blocks_to_text_blocks so both dana-acp and dana-code share one faithful multimodal block conversion (ADR-009). ACP now imports the shared helper; behaviour unchanged.
AC #5 (D6 multimodal): _build_prompt_blocks parses @/path attachments in a message into content blocks (image extensions → image block; other files → file_resource), validates provider capability (ADR-009), and passes content_blocks to session.prompt — mirroring ACP's _acp_prompt_to_normalized_blocks → normalized_blocks_to_text_blocks flow. Gated by DANA_CODE_MULTIMODAL_ENABLED. Convention documented in /help. M2 (ADR-002 durability): /model now journals a MODEL_CHANGED fact after a successful atomic switch (mirrors ACP session/set_session_model, agent.py:460-489) via session.append_fact. Stub builders match ACP (SimpleNamespace — real provider construction deferred). /model no-arg Current now env-falls-back like /status. Failed switches do not journal. Tests: +11 (MODEL_CHANGED journaled/failed-no-journal, env-fallback, 8 multimodal _build_prompt_blocks cases, shared helper). 2670 passed (1 known flake test_reap_child_pids). ruff clean. tmux smoke (azure): turn renders, /model Current=azure/gpt-5.4, /status live, /exit CLEAN.
…nt identity) — P0 part 1 Both default_agent_factory (agent_session.py) and ACP _default_agent_factory (agent.py) built a bare text-only STARAgent (enable_assistant=False, identity_override=None) with a 5659-char GENERIC STAR system prompt and no coding identity — so it could not answer coding questions (every prompt got a generic greeting; the d6b73d6 fix only corrected the empty stream, not the non-functional agent). Build DanaCodingAgent instead (coding IDENTITY system prompt) with provider/model read from env, mirroring the legacy _initialize_legacy_agent path that is verified to answer real prompts. DanaCodingAgent handles an explicit llm_provider/model correctly (legacy proves it), so this does NOT reintroduce the d6b73d6 misconfigured-azure-client empty-stream bug (smoke confirms non-empty stream). NECESSARY BUT INSUFFICIENT for turn-path parity: the AgentSession turn path (AgentSession.prompt -> aquery_text_stream) is text-only and bypasses the full STAR loop, so the model still does not engage with the user question the way the legacy query() STAR loop does (A/B confirmed: legacy answers 's[::-1]...'; AgentSession still gives 'What would you like me to do in this repo?'). Full parity requires AgentSession.prompt to drive the streaming STAR loop (aquery_stream + StreamEvent->HostEvent mapping) — a separate, deeper change. Tests: 2670 passed (1 known flake test_reap_child_pids). ruff clean.
…tream + StreamEvent->HostEvent) — fixes non-functional turns (P0 part 2) AgentSession.prompt() used aquery_text_stream — a stripped text-only path that bypasses the STAR loop — so the model never engaged with the user's prompt (every question got a canned greeting). Rewire it to drive the full streaming STAR loop (aquery_stream: see/think/act with tool-calling + reflection + JSON response parsing) and map each StreamEvent to a HostEvent + journal the corresponding fact, reusing the existing journal helpers: TEXT_DELTA -> ASSISTANT_CONTENT_CHUNK (chunk-buffer/flush preserved) THINKING -> THOUGHT (live-only, emit_thought) TOOL_CALL_START -> TOOL_REQUESTED + AUTHORIZED + STARTED TOOL_RESULT -> TOOL_RESULT terminal ERROR -> TURN_ERROR terminal DONE -> TURN_COMPLETED terminal (ASSISTANT_CONTENT_FINAL + batch) Preserved: TURN_STARTED/USER_CONTENT_FINAL durability (content_blocks for D6), asyncio.Lock serialization + SessionBusy, cooperative cancel() (TURN_CANCELLED — checked between events since aquery_stream doesn't honor cancel_event), journal version bookkeeping, last_terminal, the terminal batch append. Removed the redundant _add_user_message_to_timeline call (STARAgent._see adds the caller_message to the timeline itself). Shared fix: ACP uses AgentSession.prompt too, so ACP turns are fixed as well. Bonus: the STAR loop makes the agent's native tools reachable (AC #1) — 'Which tools do you have?' now lists Grep/bash__execute/Task/ todo__todo_write/Skill/etc. Test fakes (FakeAgent in test_agent_session + test_acp_agent) gained an aquery_stream stand-in yielding TEXT_DELTA + DONE. Verified: 2670 passed (1 known flake test_reap_child_pids). ruff clean. Live tmux smoke (azure, real questions): 'Which tools do you have?' -> real tool/skill listing; 'Write a Python one-liner to reverse a string' -> print('hello'[::-1]). /exit CLEAN. Legacy path A/B still correct.
…uivalence, mocked provider) Wave 3 parity harness: drives the same scripted turn through both the dana-code in-process AgentSession path and the dana-acp JSON-RPC path, asserting the logical event streams are equivalent (AC #1+#2). Both paths call AgentSession.prompt() (streaming STAR loop via aquery_stream, P0 part 2 f25fde3). The CLI path captures HostEvents directly; the ACP path translates each HostEvent to a session_update via host_event_to_acp_update. Parity assertion: ACP update-kind sequence == forward translation of the CLI HostEvent sequence. Coverage (AC #3): text turn, tool-call turn (tool lifecycle), thought, cancellation (TURN_CANCELLED / stop_reason=cancelled), model switch (MODEL_CHANGED journaled + busy-reject in both), permission (CLI adapter deny/allow; ACP parity xfailed with documented findings). AC #4: mocked provider (ScriptedAgent) — no live LLM, no network. Findings filed (not fixed — tests-only story): - FINDING 1: ACP PermissionOptionKind is typing.Literal, not enum → AttributeError on ALLOW_ONCE (agent.py:332+). - FINDING 2: ACP RequestPermissionResponse missing required 'outcome' field (agent.py:367). - FINDING 3: live-tool-call permission preflight not exercised — AgentSession.prompt() hardcodes authorized=True on TOOL_CALL_START, does not consult policy_evaluator. D7.5 scope. 13 passed, 2 xfailed. Full suite: 2683 passed, 3 xfailed, 1 known flake.
…on xfails Rewrite request_permission to the installed acp outcome schema: RequestPermissionResponse(outcome=AllowedOutcome(selected, option_id) | DeniedOutcome(cancelled)). DENY -> DeniedOutcome (denied_reason in response field_meta); ALLOW -> AllowedOutcome (allow_always if a durable grant matched, else allow_once); NEEDS_PROMPT -> DeniedOutcome fail-closed (ADR-006; the ACP host surfaces the reason + offers a grant/mode-change, then re-requests). No-evaluator -> pre-authorize a single use. Flip the 2 D7.4 parity xfails (test_permission_acp_deny/allow_parity) to plain passing asserts against the new outcome shape (real RequestPermissionRequest + ToolCallUpdate + PermissionOption list).
…wrapper (AC #4) D7.5 Piece C — MCP tools reachable from a live host turn (AC #4). Per D7.5 Decision 2 (STAR-loop native tools canonical; D2 ToolCatalog deferred), MCP tools are surfaced as ONE @named_tool('call_mcp_tool') async resource method. The model calls call_mcp_tool(tool_name, arguments); the wrapper dispatches to the per-server MCPExecutionAdapter. Avoids the per-tool native-wrapper schema impedance (native schemas from Python type hints vs MCP arbitrary JSON inputSchema) without touching deferred D2. Per-tool UX deferred to D7.6 D2 Catalog Migration. No policy gating (D7.6). - dana/core/mcp/dispatch_wrapper.py (new): MCPDispatchResource + build_mcp_dispatch_resource + MCPWiring - dana/core/mcp/config.py: load_mcp_config_from_env (DANA_MCP_SERVERS JSON) - dana/core/session/agent_session.py: _wire_mcp_tools (in _prepare_agent, gated) + dispose_mcp - dana/apps/code/code_app.py: dispose_mcp on REPL exit - tests/unit/core/mcp/test_mcp_dispatch_wrapper.py (24 tests) Verified: 2709 passed (1 known flake test_reap_child_pids), ruff clean, two-question tmux smoke green.
… permission preflight (AC #1, AC #2-live, hard-deny) D7.6 — D2 Catalog Migration (middle path): build a minimal native-tool ToolCatalog so the permission policy can classify tools, WITHOUT rerouting the STAR loop through the D2 engine (D7.5 Decision 2 — native tools stay canonical; the catalog feeds the policy classifier only). Pieces: - native_catalog.py: static effect-classification table for the 14 native tools (Read/Grep/Glob/read_tool_result/bash__get_task_output/bash__list_tasks/ TaskOutput/todo__todo_write/Edit/Write/Task/bash__execute/bash__kill_task/ Skill) + call_mcp_tool; build_native_tool_catalog. All known native tools are is_sensitive=False (flow to mode/grant/prompt; NEEDS_PROMPT -> proceed per the D7.5 adjusted ruling). Unknown/unlisted tools -> is_sensitive=True -> hard-denied (fail-cautious; a new tool MUST be classified explicitly). - ToolCatalog: add a per-turn pinned 'version' field (AC #1 stable identity + per-turn versioned; ADR-004). - AgentSession: build the catalog per turn (_build_tool_catalog, gated by DANA_CODE_TOOL_CATALOG_ENABLED) + register a TOOL_CALL EventBus hook (_register_policy_hook, gated by DANA_CODE_PERMISSION_PREFLIGHT_ENABLED). The hook (_on_tool_call) reconstructs a policy Operation from the bus Operation + the catalog, evaluates via policy_evaluator, and returns {'block': True} ONLY on PolicyDecision.DENY; ALLOW/NEEDS_PROMPT proceed. Pass-through when no catalog (preflight-on + catalog-off must NOT deny everything — P0 guard). Teardown unregisters the hook. - Feed the catalog to build_policy_operation in ACP request_permission + CLIPermissionAdapter (catalog_getter) so all surfaces classify consistently. Verification: 2725 tests pass (+16 new; 1 known flake test_reap_child_pids); ruff clean; two-question tmux smoke (azure) green — 'Which tools do you have?' -> real tool list; 'reverse a string' -> s[::-1]; /exit CLEAN. No P0 regression (normal native tools classify non-sensitive -> proceed). Unblocks: D7.3 AC #2-live (permission preflight in the turn), AC #1 (catalog-backed stable identity + per-turn version), hard-deny enforcement (unknown tools + destructive-on-protected-path). Filed findings (pre-existing, not fixed here): (1) the hard policy rm -rf rule keys off 'bash_tool' but the native tool is 'bash__execute' -> never fires; (2) the protected-path rule covers {DELETE, MODIFY} but not CREATE -> Write on .env is not hard-denied. Do NOT edit vault files; orchestrator handles status + Dev Agent Record.
…; CREATE added to protected-path destructive set
Two pre-existing hard-policy gaps filed during D7.6 (Decision Log):
1. bash rm -rf defense-in-depth rule keyed off 'bash_tool' (dead — the
actual native tool is 'bash__execute') -> the rule never fired. Now keys
off 'bash__execute'.
2. Protected-path destructive set was {DELETE, MODIFY} — Write (CREATE) on
.env/node_modules was NOT hard-denied. Added EffectKind.CREATE so
create/overwrite on protected paths is blocked.
Tests: updated test_default_policy_blocks_rm_rf / allows_safe_bash to the
real 'bash__execute' name; added test_default_policy_blocks_destructive_on_
protected_path covering DELETE/MODIFY/CREATE on .env + node_modules + a
non-protected-path allow case. 2726 passed (1 known flake test_reap_child_pids).
The live TOOL_CALL permission hook (D7.6) returned None (proceed) on PolicyDecision.NEEDS_PROMPT. Now the CLI prompts the user interactively: - AgentSession.set_permission_prompt_callback(callback): a host-provided async (operation) -> PermissionVerdict. The _on_tool_call hook, on NEEDS_PROMPT, calls it; allowed -> proceed, denied -> block (journal denied, do not execute). No callback (ACP) -> proceed (request_permission is the ACP resolution surface). - CLIPermissionAdapter.prompt_and_persist(op): public entry for the hook (prompts allow once/always/deny once/always + persists a durable grant on 'always'). The sync input() prompt runs off-thread (asyncio.to_thread) so the event loop is not blocked. - RichCLIRenderer.pause_live()/resume_live(): the CLI prompt callback pauses the transient Live display before prompting (so the prompt renders cleanly) and resumes after. - code_app _initialize_session wires the callback (pause Live -> adapter prompt_and_persist -> resume Live). CLI-only; gated by DANA_CODE_PERMISSION_PREFLIGHT_ENABLED. Tests: hook callback allow/deny (test_d76); prompt_and_persist allow-once (no grant) + allow-always (durable grant) (test_d73). 2730 passed (1 known flake test_reap_child_pids); ruff clean; two-question tmux smoke green (real tool list + s[::-1]; /exit CLEAN) — no P0 regression.
D7 follow-up 1+2: the model calls each MCP tool BY NAME (with its real inputSchema) instead of via the single-dispatch call_mcp_tool wrapper, and each MCP tool is classified individually for permission policy. Approach A2 (Decision 2 holds — NO D2 ToolExecutionEngine reroute; the STAR loop stays canonical; changes are additive): - dispatch_wrapper: build_mcp_dispatch_resource now also exposes per-MCP ToolCatalogEntry schemas (mcp_tool_to_catalog_entry — correct inputSchema, namespaced server:tool), the mcp_names set, and a dispatch_map (tool_name -> async callable -> formatted result). _format_mcp_result + _make_mcp_dispatcher extracted as module helpers. - native_catalog: build_native_tool_catalog accepts mcp_names; registered namespaced MCP tools classify as EXECUTE/non-sensitive (flow to mode/grant/prompt); unknown names stay fail-cautious (is_sensitive=True). - tool_executor: ToolExecutor gains an mcp_dispatch_getter (+ setter); _dispatch_single_call_async checks the MCP dispatch map BEFORE the @named_tool registry. Namespaced server:tool names do not collide with native names, so native dispatch is unchanged. - agent_session._build_tool_catalog: ensures _native_tools is built, appends the per-MCP schemas (idempotent — the cached _native_tools is not rebuilt, so they persist across turns), passes mcp_names to the catalog, and wires the mcp_dispatch_getter on the agent's ToolExecutor. The single-dispatch call_mcp_tool wrapper (b5f443d) is kept as a fallback (AC #4 stays green via both paths); per-tool is preferred (real schemas). Verification: 2736 tests pass (+6 new; 1 known flake test_reap_child_pids); ruff clean; two-question tmux smoke (no MCP config) green — Q1 real tool list, Q2 s[::-1], /exit CLEAN (no P0 regression; with no MCP config, no MCP tools are injected -> turn path unchanged). Per-tool dispatch end-to-end covered by unit tests (ToolExecutor dispatches fs:greet by name; AgentSession surfaces the schema + dispatches by name -> 'hello world').
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Dana now streams multi-turn text conversations through
dana-acpand automatically resumes the same session after the ACP process restarts. This is the first delivery (D1) of the ACP AgentSession kernel — a Session Journal backed by SQLite/PostgreSQL is the sole durable authority for every turn.What shipped
dana/core/session/{models,protected_state}.py—OwnerScope,JournalFact,FactType,ProtectedStateCodec(AES-GCM + HKDF + AAD)dana/core/session/journal/{sqlite,postgres,protocol,schema,models}.py— one contract, two real databases, optimistic concurrencydana/core/session/projections/{conversation,host_events}.py— ConversationView (committed-turn gating) + HostEvent streamdana/core/session/agent_session.py— serialized text turns, streaming seam, bounded flush, crash-safe journalingdana/core/session/legacy_timeline_migration.py— interrupted-turn detection + idempotent legacy Timeline importdana/apps/acp/—initialize,session/new,session/load,session/resume,session/prompt,session/canceldana/core/session/health.py— redacted operational reportKey invariants
TURN_STARTED+USER_CONTENT_FINALappended before the model callASSISTANT_CONTENT_FINAL+ terminal in one atomic batchbusyacp.*imports confined todana/apps/acp/; STAR core untouchedTest results
Dependencies added
agent-client-protocol>=0.10,<0.11aiosqlite>=0.20.0asyncpg>=0.30.0What's NOT changed
dana-code,adana,dana-replCLIs remain on legacy Timeline (incremental migration)STARAgent.query()/aquery()source-compatibleConsole companion PR
Branch
feat/acp-agent-session-restartin dana-console addssessionStoragesession-ID persistence +session/loadresume on reconnect.Design spec
Approved at
docs/superpowers/specs/2026-07-07-acp-star-adapter-design.md.