Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
158 commits
Select commit Hold shift + click to select a range
154c249
feat(jobs): long-running job substrate (Tier A milestone 1)
shoom1 Jun 14, 2026
5724e57
feat(jobs): shrink LLM-facing surface — job_status returns result; JO…
shoom1 Jun 14, 2026
10eda8e
Merge pull request #83 from shoom1/feature/job-control
shoom1 Jun 14, 2026
46a831f
feat(jobs): harness Jobs UI monitor (Tier A milestone 2)
shoom1 Jun 15, 2026
1eff22f
fix(jobs_demo): read API keys from ~/.research_demo/.env; drop field-…
shoom1 Jun 15, 2026
42df7a6
refactor(jobs): move run_shell_job out of the framework into the demo
shoom1 Jun 15, 2026
1c23140
Merge pull request #84 from shoom1/feature/job-ui-monitor
shoom1 Jun 15, 2026
b5244a5
feat(jobs): resume association layer (push/resume milestone 1)
shoom1 Jun 18, 2026
eb5aef5
Merge pull request #85 from shoom1/feature/job-resume-association
shoom1 Jun 18, 2026
e173ef8
feat(jobs): ADK idiomatic push/resume execution (milestone 2)
shoom1 Jun 18, 2026
308dcf5
test(jobs): live end-to-end validation of ADK push/resume
shoom1 Jun 18, 2026
4f01887
Merge pull request #86 from shoom1/feature/job-resume-adk-execution
shoom1 Jun 18, 2026
31cbfb2
feat(jobs): push/resume coordinator + /resume (milestone 3)
shoom1 Jun 18, 2026
697fa63
Merge pull request #87 from shoom1/feature/job-resume-coordinator
shoom1 Jun 18, 2026
1241089
feat(jobs): graceful restart handling for push/resume (milestone 4 / A)
shoom1 Jun 18, 2026
57b3936
Merge pull request #88 from shoom1/feature/job-resume-restart-handling
shoom1 Jun 18, 2026
c3b6866
feat(sessions): durable ADK sessions by default via DatabaseSessionSe…
shoom1 Jun 18, 2026
6059d7b
feat(sessions): native durable sessions both backends; drop JSON layer
shoom1 Jun 18, 2026
18b29d6
chore(sessions): remove dead code superseded by session_store
shoom1 Jun 18, 2026
d484a88
Merge pull request #89 from shoom1/feature/resumable-sessions
shoom1 Jun 18, 2026
35def4b
fix(sessions): LangGraph durable sessions need async aget_state
shoom1 Jun 19, 2026
d0c29e6
Merge pull request #90 from shoom1/fix/langgraph-async-session-state
shoom1 Jun 19, 2026
8f25d3f
fix(langgraph): gemini-2.5 needs thinking_budget, not thinking_level
shoom1 Jun 19, 2026
72921f2
Merge pull request #91 from shoom1/fix/langgraph-gemini-thinking-budget
shoom1 Jun 19, 2026
6f6320b
feat(config): add per-agent ModelSettings (ADK)
shoom1 Jun 22, 2026
bcc0c39
feat(config): resolve tools by registry name or dotted path
shoom1 Jun 22, 2026
31398e7
feat(config): add unified YAML agent loader
shoom1 Jun 22, 2026
7803a2e
feat(config): MCP server support for the ADK backend
shoom1 Jun 22, 2026
0bc9a79
feat(config): skills support for the ADK backend (scripts gated off)
shoom1 Jun 22, 2026
16267f3
feat(config): reuse an existing native ADK config (passthrough + tran…
shoom1 Jun 22, 2026
6a44951
Merge remote-tracking branch 'origin/develop' into feature/agent-config
shoom1 Jun 22, 2026
7a08001
feat(config): warn when ADK-only fields are set on the LangGraph backend
shoom1 Jun 22, 2026
138be87
Merge pull request #92 from shoom1/feature/agent-config
shoom1 Jun 22, 2026
509e400
feat(adk): native Claude support — orchestrator routing + DirectAnthr…
shoom1 Jun 25, 2026
3083efc
feat(adk): Claude adaptive thinking + effort for >=4.6; bump google-a…
shoom1 Jun 25, 2026
0bb3f70
fix(security): harden code execution boundary (P0-1, P0-4)
shoom1 Jul 3, 2026
90bd420
fix(security): untrust project settings + protect app config (P0-2, P…
shoom1 Jul 3, 2026
cccf14a
fix(security): stop persisting/logging secrets in the clear (P0-5, P0-6)
shoom1 Jul 3, 2026
88c1d4b
test: enable sandbox_execute flag in direct-execution test (P0-4 foll…
shoom1 Jul 3, 2026
06c69af
Merge pull request #93 from shoom1/fix/p0-security
shoom1 Jul 4, 2026
f4113e4
fix(adk): Claude request robustness — version parse, non-streaming ti…
shoom1 Jul 4, 2026
604f81c
Merge feature/adk-claude-support into develop
shoom1 Jul 4, 2026
6184223
fix(cli): adopt session id every run so unnamed runs don't collapse (…
shoom1 Jul 4, 2026
b773707
fix(cli): route orchestrator swap on backend_type, not a LangGraph im…
shoom1 Jul 4, 2026
138659e
Merge pull request #94 from shoom1/fix/p1-remediation
shoom1 Jul 4, 2026
56f58e3
feat(exec): OS-sandbox execute_python by default, availability-aware …
shoom1 Jul 4, 2026
a57cdc9
Merge pull request #95 from shoom1/fix/os-sandbox-default
shoom1 Jul 4, 2026
233cbfa
fix(security): give sandbox_execute its own capability (python.exec.s…
shoom1 Jul 4, 2026
239a620
Merge pull request #96 from shoom1/fix/sandbox-execute-capability
shoom1 Jul 4, 2026
4e34269
feat(sandbox): SessionStatus model + uniform session_status() across …
shoom1 Jul 5, 2026
f057e1f
feat(sandbox): detect docker/podman availability (lru_cache, fail-clo…
shoom1 Jul 5, 2026
a0c9a84
feat(sandbox): ContainerRuntime seam + docker run argv with isolation…
shoom1 Jul 5, 2026
5e93185
feat(sandbox): docker backend settings (image, limits, network, mount…
shoom1 Jul 5, 2026
d7a38f1
refactor(sandbox): extract shared kernel_exec (iopub loop + validate)…
shoom1 Jul 5, 2026
1c4d110
feat(sandbox): in-container kernel driver (NDJSON stdio protocol + SI…
shoom1 Jul 5, 2026
0ee1238
feat(sandbox): ContainerSession — reader thread, timeout->interrupt->…
shoom1 Jul 5, 2026
ff521b8
feat(sandbox): JupyterDockerBackend — settings->spec, session pool, f…
shoom1 Jul 5, 2026
0a0a815
fix(sandbox): kill container if wait_ready fails; strengthen isolatio…
shoom1 Jul 5, 2026
3ae02eb
feat(sandbox): register jupyter_docker backend + backend-aware tool d…
shoom1 Jul 5, 2026
9a38927
test(sandbox): offline end-to-end driver+session integration via loca…
shoom1 Jul 5, 2026
92d7ed9
test(sandbox): opt-in live docker isolation smoke test + docs
shoom1 Jul 5, 2026
fe3feb3
fix(sandbox): drain container stderr; execute() returns errors instea…
shoom1 Jul 5, 2026
888a48f
Merge pull request #97 from shoom1/feature/docker-sandbox
shoom1 Jul 5, 2026
9b7bac0
ci(sandbox): GitHub Actions + docker marker/guard + expanded isolatio…
shoom1 Jul 5, 2026
aaaf023
fix(deps): declare anthropic as a core dependency; install langgraph …
shoom1 Jul 5, 2026
cc891e2
fix(sandbox): make /workspace writable for non-root container; test/a…
shoom1 Jul 5, 2026
27784c4
Merge pull request #98 from shoom1/feature/docker-sandbox-ci
shoom1 Jul 5, 2026
f3d8981
fix(sandbox): make host the sole timeout authority for docker interrupt
shoom1 Jul 6, 2026
f6fd93c
harden(sandbox): enforce network=none, run container as host uid, cap…
shoom1 Jul 6, 2026
cdf05f5
harden(sandbox): address remaining review findings
shoom1 Jul 6, 2026
be385f5
harden(sandbox): address multi-round security review
shoom1 Jul 8, 2026
efa4138
feat(sandbox): unify config into stateful_executor_backend
shoom1 Jul 8, 2026
55b5ed0
feat(sandbox): stage inputs into inputs/<name> for sandbox_execute
shoom1 Jul 8, 2026
c364b18
feat(permissions): gate each sandbox_execute inputs path by filesyste…
shoom1 Jul 8, 2026
60719f0
feat(sandbox): shared outputs/ mount for final deliverables
shoom1 Jul 8, 2026
72693b3
feat(research_demo): add data_analyst agent + sample dataset
shoom1 Jul 8, 2026
8f5bae7
test(sandbox): docker-live end-to-end data_analyst flow
shoom1 Jul 8, 2026
8e18015
fix(sandbox): pre-create host-owned /workspace/outputs mount point
shoom1 Jul 8, 2026
157a182
fix(sandbox): copy outputs/ to shared dir instead of nesting a bind m…
shoom1 Jul 8, 2026
b3eb719
fix(sandbox): local outputs/ pre-create, data-mount point pre-create,…
shoom1 Jul 8, 2026
e89098a
Merge pull request #99 from shoom1/feature/stateful-executor-toolset
shoom1 Jul 8, 2026
3545913
fix(sandbox): mount data at /data instead of nested /workspace/data
shoom1 Jul 9, 2026
2d1f2db
feat(cli): render stateful-executor runs (code in, output out)
shoom1 Jul 9, 2026
2d2311b
feat(tools): compile_document — guarded LaTeX-to-PDF tool
shoom1 Jul 10, 2026
a4a4cc5
fix(tools): compile_document never raises on run/copy failures; valid…
shoom1 Jul 10, 2026
6334023
feat(tools): export compile_document; add gated real-LaTeX test + lat…
shoom1 Jul 10, 2026
fe73053
feat(research_demo): report_writer skill (knowledge-only LaTeX report…
shoom1 Jul 10, 2026
e3c42c9
fix(research_demo): rename skill to kebab-case report-writer (ADK def…
shoom1 Jul 10, 2026
145d05b
fix(research_demo): complete kebab rename — SKILL.md name/heading/res…
shoom1 Jul 10, 2026
0e32a1c
feat(research_demo): wire report_writer sub-agent (compile_document +…
shoom1 Jul 10, 2026
5f66ee4
fix(tools,research_demo): process-group timeout kill; scope shell-esc…
shoom1 Jul 10, 2026
54b7199
Merge pull request #100 from shoom1/feature/report-writer-skill
shoom1 Jul 10, 2026
767c89f
fix(deps): declare html2text in pyproject (P2-1)
shoom1 Jul 10, 2026
8be9f70
fix(tools): contain glob/grep patterns to the authorized root (P0-4)
shoom1 Jul 10, 2026
a1a45bb
fix(document): harden compile_document host execution (P0-3)
shoom1 Jul 10, 2026
3a4434a
fix(permissions): fail closed when the engine is missing (P1-7)
shoom1 Jul 10, 2026
3cfdc7e
refine(document): keep TeX config vars in the scrubbed env (P0-3)
shoom1 Jul 10, 2026
6e33620
fix(tools): apply grep containment on the ripgrep path too (P0-4 review)
shoom1 Jul 10, 2026
75ac08f
fix(review): address minor findings from the hardening review
shoom1 Jul 10, 2026
d15b9db
fix(review2): close residuals from the branch re-review (P0-3, P0-4)
shoom1 Jul 11, 2026
c33ff74
fix(document): anchor option-like source filename (P0-3 critical)
shoom1 Jul 11, 2026
bdcb544
fix(document): build in a private temp dir, isolate intermediates (P0-3)
shoom1 Jul 11, 2026
4e7d1f3
fix(document): tail-read the compiler .log instead of whole file (P0-3)
shoom1 Jul 11, 2026
62c6c9d
test(document_compile): add OSError coverage and tighten log-tail bound
shoom1 Jul 11, 2026
9831277
fix(document): cap captured subprocess output in _run (P0-3)
shoom1 Jul 11, 2026
72e35ed
fix(document.compile): assert SIGKILL on timeout, rename capture-limi…
shoom1 Jul 11, 2026
e4ad61a
fix(document): exact TEXMF env allowlist instead of prefix (P0-3)
shoom1 Jul 11, 2026
a5d6fd3
fix(tools): pre-limit scan/size ceilings for glob and grep (P0-4)
shoom1 Jul 11, 2026
59a45b8
chore: stop tracking AGENTS.md
shoom1 Jul 11, 2026
51da5e5
fix(document.compile): absolute TEXINPUTS roots, ignore_cleanup_error…
shoom1 Jul 11, 2026
92ade46
test(document): fix gated LaTeX test for the temp-build isolation con…
shoom1 Jul 11, 2026
2dd8b15
fix(grep): don't let directories consume the file budget; report scan…
shoom1 Jul 11, 2026
2ea7573
fix(document): cap TeX child file size and CPU (RLIMIT_FSIZE/CPU) (P0-3)
shoom1 Jul 11, 2026
22aa503
fix(grep): bound ripgrep output to a temp file + capped read (P0-4)
shoom1 Jul 11, 2026
4c0d481
Merge pull request #101 from shoom1/fix/review-hardening-2026-07
shoom1 Jul 11, 2026
c6a276c
feat(file_utils): add copy_regular_file_no_follow (P0-2)
shoom1 Jul 11, 2026
1856398
fix(sandbox): symlink-safe input staging (P0-2)
shoom1 Jul 11, 2026
885b6a1
fix(sandbox): symlink-safe output collection + 0700 outputs dir (P0-2)
shoom1 Jul 11, 2026
7eabbb2
fix(sandbox): guard symlinked inputs-dir (I1) + best-effort collect_o…
shoom1 Jul 12, 2026
c85fa90
Merge pull request #102 from shoom1/fix/p02-symlink-safe-sandbox-tran…
shoom1 Jul 12, 2026
85b61a0
feat(config): deny-by-default allowlist for project settings.json (P0-1)
shoom1 Jul 12, 2026
a51fe4c
feat(config): filter cwd-relative .env like project settings (P0-1)
shoom1 Jul 12, 2026
64d442d
test(config): pin list/tuple env_file trusted path (Task 2 review)
shoom1 Jul 12, 2026
7c95c0f
feat(permissions): write Allow-always grants to user project_grants.j…
shoom1 Jul 12, 2026
e524188
feat(permissions): load grants from user project_grants.json; drop re…
shoom1 Jul 12, 2026
751f306
fix(config): expanduser + list-aware dotenv trust guard; soften brick…
shoom1 Jul 12, 2026
1440b93
Merge pull request #103 from shoom1/fix/p01-config-trust-model
shoom1 Jul 12, 2026
3d819ac
feat(webfetch): resolve-all + is_global SSRF validator core (P0-5)
shoom1 Jul 12, 2026
ddc7404
feat(webfetch): PinnedTransport (resolve-validate-pin) (P0-5)
shoom1 Jul 12, 2026
02d29cf
feat(webfetch): pinned transport + streamed body cap in fetcher; drop…
shoom1 Jul 12, 2026
ca6b3fb
test(webfetch): restore same-host-redirect robots coverage (Task 3 re…
shoom1 Jul 12, 2026
f14926d
feat(webfetch): fetch robots.txt through the pinned transport, capped…
shoom1 Jul 12, 2026
96ba271
feat(webfetch): get_or_create_fetcher builds + shares the pinned tran…
shoom1 Jul 12, 2026
767bfe8
fix(webfetch): shared PinnedTransport survives per-call client close;…
shoom1 Jul 12, 2026
302e8ee
Merge pull request #104 from shoom1/fix/p05-webfetch-ssrf
shoom1 Jul 12, 2026
0661f7c
fix(settings): trust-split save so /settings changes survive the P0-1…
shoom1 Jul 15, 2026
ba86781
Merge pull request #105 from shoom1/fix/settings-save-trust-split
shoom1 Jul 16, 2026
5df127b
fix(settings): guard /settings dialog to project-scoped keys
shoom1 Jul 16, 2026
151d31e
Merge pull request #106 from shoom1/fix/settings-ui-project-scope-guard
shoom1 Jul 16, 2026
06a4546
fix(settings): API key export overwrites env; drop multi-tenant docs …
shoom1 Jul 18, 2026
8545854
refactor(tools)!: registry-owned tool identity and declared variants
shoom1 Aug 3, 2026
0d2fc14
feat(tools): tools declare the services they require
shoom1 Aug 3, 2026
9c624e1
feat(workflow)!: validate agent graphs before allocating anything
shoom1 Aug 3, 2026
03d2ec6
fix(config): validate every runtime-effective model; per-provider dis…
shoom1 Aug 3, 2026
81ac3ac
fix(config): credential constructor arguments actually bind
shoom1 Aug 3, 2026
68f8724
feat(workflow)!: SessionRef and user-scoped session APIs
shoom1 Aug 3, 2026
2c89da4
fix(workflow): serialize turns, make init transactional, own resources
shoom1 Aug 3, 2026
61b7950
fix(cli): make the turn boundary safe; no harness-level replay
shoom1 Aug 3, 2026
a152c1b
fix(jobs): make the whole persisted job record coherent across processes
shoom1 Aug 3, 2026
a0f66db
feat(skills)!: expose run_skill_script only when a code executor is s…
shoom1 Aug 3, 2026
5a73940
docs: record the review-pass contracts and publish the import surface
shoom1 Aug 3, 2026
f696b3f
Merge pull request #109 from shoom1/fix/api-key-export
shoom1 Aug 3, 2026
eb8ec3a
Merge pull request #110 from shoom1/refactor/framework-review-2026-08
shoom1 Aug 3, 2026
0e4662a
fix(demo): harden startup and add release acceptance coverage
shoom1 Aug 4, 2026
d3d5500
fix(adk): restore safe multi-agent delegation
shoom1 Aug 4, 2026
5b510e9
Merge pull request #111 from shoom1/test/pre-release-acceptance-0.6.0
shoom1 Aug 4, 2026
173db8b
fix(permissions): clarify persistent project grants
shoom1 Aug 4, 2026
f6d5ff5
chore(release): 0.6.0
shoom1 Aug 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 98 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
name: CI

on:
pull_request:
push:
branches: [develop, main]

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
offline:
name: Offline tests (no Docker)
runs-on: ubuntu-latest
defaults:
run:
shell: bash -el {0}
steps:
- uses: actions/checkout@v4

- name: Set up conda env (agenticcli)
uses: conda-incubator/setup-miniconda@v3
with:
miniforge-version: latest
environment-file: environment.yml
activate-environment: agenticcli

- name: Install langgraph extra (langgraph-backend tests)
run: conda run -n agenticcli pip install -e '.[langgraph]'

- name: Run offline test suite
run: conda run -n agenticcli python -m pytest -m 'not llm and not docker' -q

wheel-acceptance:
name: Built-wheel acceptance (console smoke)
runs-on: ubuntu-latest
defaults:
run:
shell: bash -el {0}
steps:
- uses: actions/checkout@v4

- name: Set up conda env (agenticcli)
uses: conda-incubator/setup-miniconda@v3
with:
miniforge-version: latest
environment-file: environment.yml
activate-environment: agenticcli

# pexpect drives the console over a pty. Installed explicitly via the dev
# extra rather than relied on transitively.
- name: Install dev extra (pexpect)
run: conda run -n agenticcli pip install -e '.[dev]'

# Its own step rather than part of the offline suite: it builds a wheel
# and installs that wheel's dependencies into a throwaway virtualenv, so
# it is slow and needs the network. The opt-in env var is what the tests
# gate on, so the offline selector stays fast and offline.
#
# Targets the file rather than `-m wheel` alone: a bare marker selection
# still *collects* the whole suite, which would need the langgraph extra
# installed only to collect tests this job does not run.
- name: Run built-wheel acceptance
env:
AGENTIC_WHEEL_ACCEPTANCE: "1"
run: >
conda run -n agenticcli python -m pytest
tests/examples/test_research_demo_wheel.py -m wheel -v

docker-isolation:
name: Docker isolation tests
runs-on: ubuntu-latest
defaults:
run:
shell: bash -el {0}
steps:
- uses: actions/checkout@v4

- name: Set up conda env (agenticcli)
uses: conda-incubator/setup-miniconda@v3
with:
miniforge-version: latest
environment-file: environment.yml
activate-environment: agenticcli

- name: Install langgraph extra (for clean full test collection)
run: conda run -n agenticcli pip install -e '.[langgraph]'

# Keep this tag in sync with settings.sandbox_image's default. Pre-pulling
# avoids first-run pull latency exceeding sandbox_start_timeout.
- name: Pre-pull sandbox image
run: docker pull quay.io/jupyter/scipy-notebook:python-3.12

- name: Run docker isolation tests (fail if runtime missing)
env:
SANDBOX_REQUIRE_DOCKER: "1"
run: conda run -n agenticcli python -m pytest -m docker -v
66 changes: 66 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

38 changes: 35 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ agentic-cli/
│ ├── __init__.py # Package exports, lazy imports
│ ├── config.py # BaseSettings (pydantic-settings)
│ ├── settings_mixins.py # Composable settings field groups
│ ├── settings_persistence.py # save_settings() (excludes SECRET_FIELDS)
│ ├── settings_persistence.py # Trust-split save (PROJECT_SETTABLE_KEYS → project, rest → user config; excludes SECRET_FIELDS)
│ ├── constants.py # Shared constants, truncate()
│ ├── file_utils.py # atomic_write_json / atomic_write_text
│ ├── logging.py
Expand Down Expand Up @@ -157,9 +157,18 @@ Workflow:
### Key Design Patterns
- **Tool error handling**: All tools return `{"success": bool, ...}` dicts. Never raise `ToolError`.
- **Tool registration**: Use `@register_tool(category=..., capabilities=..., description=...)` decorator. `capabilities=` is required — pass `EXEMPT` for tools that need no permission check or a list of `Capability(name, target_arg=...)` tuples the engine matches against rules. Tools are auto-discovered via the global `ToolRegistry`.
- **Permissions**: `workflow/permissions/` holds a framework-independent engine that evaluates declared capabilities against rules from four sources (builtin, user `~/.{app_name}/settings.json`, project `./.{app_name}/settings.json`, in-memory session). ADK + LangGraph gate tool calls via `workflow/adk/permission_plugin.py::PermissionPlugin` and `workflow/langgraph/permission_wrap.py::wrap_tool_for_permission`.
- **ADK transfer-tool description**: ADK builds `transfer_to_agent`'s model-visible description from its docstring, which through 1.37.0 tells the model to "use TransferToAgentTool instead of this function directly" — advice meant for Python callers that made Gemini emit the class name, which ADK then rejects. `workflow/adk/transfer_tool_description.py` is a `before_model_callback` plugin that rewrites that description on the prepared `LlmRequest`: only when the tool is the **exact** native `TransferToAgentTool` (an application tool sharing the name is untouched) and only while the misleading text is present, so it is idempotent and becomes a **no-op** once the installed ADK ships a corrected docstring (upstream fixed it in 2.x). Only `description` is written — declaration name, parameter schema, required fields and the agent-name enum are preserved, and the upstream function's `__doc__` is never mutated.
- **Permissions**: `workflow/permissions/` holds a framework-independent engine that evaluates declared capabilities against rules from four sources (builtin, user `~/.{app_name}/settings.json`, project `./.{app_name}/settings.json`, in-memory session). Interactive "Allow always" grants persist to `~/.{app_name}/project_grants.json` keyed by resolved project path — user-owned, never into the project settings file a repo could ship. ADK + LangGraph gate tool calls via `workflow/adk/permission_plugin.py::PermissionPlugin` and `workflow/langgraph/permission_wrap.py::wrap_tool_for_permission`.
- **Service registry**: Tools access services and shared state via `get_service(key)` from `workflow.service_registry`. A single ContextVar holds a `dict[str, Any]` set by the workflow manager during processing. Complex services (KBManager, SandboxManager, MemoryStore) are lazily created; simple state (plan string, task list) lives directly in the registry dict.
- **Manager detection**: `BaseWorkflowManager._detect_required_managers()` scans each agent's tool names against the `_TOOL_SERVICE_MAP` (name → service key, in `base_manager.py`); `_ensure_managers_initialized()` then lazily instantiates only the services actually needed (KBManager, SandboxManager, MemoryStore, …). Adding a new service-backed tool means adding its name → service entry to `_TOOL_SERVICE_MAP`. (There is no `@requires` decorator.)
- **Manager detection**: tools declare their own service needs — `@register_tool(..., requires="kb_manager")` (or a tuple for several) — and the key is validated against `service_registry.KNOWN_SERVICE_KEYS` at registration (only *constructible* services are declarable; `user_kb_manager` is created together with `kb_manager`). `BaseWorkflowManager._detect_required_managers()` reads that metadata off the registry for each agent's tools (by registry identity, so `register(func, name=...)`'s original callable still declares its services); `_build_services()` then lazily constructs only the services actually needed, into a local dict that is released in full if a later constructor raises (nothing is published, so nothing else could close it). A downstream tool may request any framework-provided service without editing the framework; there is no mechanism for registering new service *types*, and no central name→service map.
- **Canonical tool names + permission identity**: `ToolDefinition.name` is the single identity. `register_tool(name="public_name")` wraps the callable so `func.__name__` is the registered name (backends derive the model-visible name from the callable). Permission gating **and tool assembly** resolve through a **registry-owned identity binding**: each `ToolRegistry` owns a `id(obj) → (weakref, definition)` map (`registry.bind_identity()`/`identify()`; the module-level `tools.registry.bind_tool_identity()`/`identify_tool()` answer for `get_registry()`). Every hit is confirmed with `is` against the weak reference, so neither a name, a class name, nor a forged `__eq__`/`__hash__` can stand in for it, and a recycled address inherits nothing. Identity is **per registry** — a tool registered into an application's own `ToolRegistry` is not one of the framework's, so it stays untouched during assembly and is denied at permission time — which is also what lets a short-lived registry, its definitions and their closures be garbage collected. Everything the framework issues is bound at its construction site: registered callables, factory service-bound variants, renamed wrappers, and the native ADK tool objects the framework builds (skill tools, in `tools/skills/toolset.py`). `workflow/adk/permission_plugin.py` unwraps `.func` only for the exact ADK types whose contract is to call it (`FunctionTool`, `LongRunningFunctionTool`, and ADK's own `TransferToAgentTool`), and gates genuine `McpTool` instances (`isinstance`, since ADK creates them on connect) under a synthetic `mcp` capability. `TransferToAgentTool` is a `FunctionTool` **subclass** ADK auto-injects into any agent with `sub_agents`; matching only exact `FunctionTool` denied the built-in routing tool as unregistered, so delegation could not work at all. It is trusted on exactly the same terms as the others — ADK constructs it as `super().__init__(func=transfer_to_agent)` and overrides only `_get_declaration()` (to add the agent-name enum), never `run_async`, so what it invokes is still exactly `self.func`. Listing the **exact class** keeps every other subclass, forged object and same-named tool denied; no name authority is restored. Anything unbound is denied — and, in assembly (service detection, service-tool substitution, canonicalization, long-running wrapping), left exactly as the application supplied it: a plain callable named `kb_search` is not the framework's tool and must not be given its services, its service-bound variant, or its long-running contract. Substitution additionally requires the service variant to *be* that same definition (`identify_tool(variant) is definition`); factories bind each closure to the exact module-level tool it re-binds, so a tool an application has taken over keeps its own implementation.
- **One name, one tool**: `ToolRegistry.register()` raises on any name that is already registered — matching capabilities are *not* grounds for sharing one, since they say nothing about the docstring or the model-visible schema. Sharing is declared, never inferred: `declare_tool(name, ...)` (exported from `agentic_cli.tools`) declares a tool that has **no backend-neutral implementation** (`ToolDefinition.func is None`), and each backend registers its own with `register_tool(..., variant_of=name)` — same identity and permission contract, its own signature and docstring. Re-declaring the same contract is idempotent; changing its description or capabilities raises. `definition.variants` is ordered by defining module, so it never depends on import order, and assembly substitutes a variant's canonical callable via `registry.canonical_for()` (never `None`). A `replace=True` retires the previous definition's callables, and retired backend variants are excluded from `include_state_tools` injection, so the model never sees two tools with one name. That is how the ADK and LangGraph `save_plan`/`get_plan`/`save_tasks`/`get_tasks` coexist (declared in `tools/_core/state_tools.py`); previously they contested the name and import order decided the winner. A bare-name reference to a declared-only tool raises "ambiguous" rather than guessing a backend. `replace=True` takes a name over deliberately and **retires the old definition's identities**, so its callables resolve to nothing (denied, and left alone by assembly) rather than inheriting the replacement's capabilities.
- **Turn/lifecycle concurrency**: a manager runs one turn at a time — `process()`/`resume_with_job_result()` enter through `_turn_admission()` (which holds `_turn_lock`), and `initialize_services`/`reinitialize`/`cleanup` hold `_lifecycle_lock` **and** `_turn_lock`. Lock order is lifecycle → turn; a turn initializes *before* taking the turn lock, which is what keeps the two from deadlocking — and because of that a cleanup can land in between, so admission re-checks `_backend_ready()` while holding the turn lock and reinitializes once (or fails cleanly) rather than running against released resources. Initialization is transactional: services are built on a worker thread into a *local* dict and published only while the attempt still owns init (a cancelled attempt releases what the thread went on to build), and a failed attempt rolls back. A failed in-place reinitialization leaves the manager uninitialized; the controller reports `FAILED` and refuses to hand it out, but **keeps** it so a retry can revive it with its preserved (possibly in-memory) sessions intact. `WorkflowController` serializes init/reinitialize/swap/close on its own lifecycle lock, and a background init that finishes after `close()` releases its manager instead of publishing it.
- **HITL callback is context-local**: `set_input_callback()` stores into a per-manager `ContextVar`, so a second consumer installing its callback cannot capture a running turn's prompt, and one consumer's `clear_input_callback()` cannot unregister another's. `MessageProcessor` cancels and awaits its consumer task **before** clearing the callback, so no tool is left asking a question nobody owns.
- **No harness-level turn replay**: ADK persists a turn's input during invocation setup, so the CLI never re-invokes an event source. Retries belong to the provider client (`HttpRetryOptions`). `MessageProcessor` returns a typed `TurnResult`.
- **Session identity**: durable sessions are addressed by `SessionRef(app_name, user_id, session_id)` (`workflow/sessions.py`). Every session hook (`session_exists`/`list_sessions`/`delete_session`/`recent_messages`/`load_session`) takes an optional `user_id`, defaulting to `settings.default_user` only when the caller omits it. Backends without a session store leave `supports_sessions` False, and the base hooks raise `NotImplementedError` rather than answering with a misleading `False`/`[]`.
- **Active turn**: the in-flight `(user, session)` is a `ContextVar` (`workflow/sessions.py::get_active_turn`), set with a token by `_workflow_context()` — concurrent turns on one manager stay isolated and nesting restores the outer turn.
- **Resource ownership**: `cleanup()` is idempotent and awaits an async `close()` on owned resources (`BaseWorkflowManager._aclose_owned`); `WorkflowController.close()` is the single shutdown path (cancel init → shut executor → clean manager).
- **Atomic writes**: Use `atomic_write_json`/`atomic_write_text` from `file_utils.py` for file persistence.

### Console Output
Expand All @@ -181,3 +190,26 @@ Available session methods:
- **MockVectorStore** and **MockEmbeddingService**: In `knowledge_base/_mocks.py` for testing without ML dependencies
- **FAISS tests**: Guard with `pytest.importorskip("faiss")` since FAISS is not installed in dev env
- **Integration tests**: `tests/integration/` covers ADK and LangGraph pipeline tests

### Live LLM tests (real API calls)

Tests that hit real provider APIs use the existing framework — **don't invent new gating or key handling.**

- **Marker**: `@pytest.mark.llm`; modules set `pytestmark = [pytest.mark.llm, pytest.mark.skipif(<no key>, ...)]`.
- **Key loading is handled by the live-test framework** (`tests/integration/conftest.py`) — keys are not
plain shell env vars, so go through pytest rather than re-deriving it.
- **Run**: `-m llm` (live; needs network — disable the Bash sandbox) or `-m 'not llm'` (offline).
A bare `pytest` run makes real API calls when keys are available.
- **Example**: `tests/integration/test_adk_claude_live.py`.

### Live docker sandbox tests (real container runtime)

The `jupyter_docker` backend's isolation boundary is verified against a real daemon.

- **Marker**: `@pytest.mark.docker`; skipped unless docker/podman is available. The bulk of the
backend is tested offline via the faked `ContainerRuntime` seam (incl. an end-to-end run of the
real `driver.py` as a subprocess) — these live tests only cover what needs a real container.
- **Run**: `-m docker` (needs a container runtime) or `-m 'not llm and not docker'` (offline CI).
- **Fail-loud in CI**: set `SANDBOX_REQUIRE_DOCKER=1` so a missing/broken runtime FAILS instead of
silently skipping (a skip reads as green). CI: `.github/workflows/ci.yml` (offline + docker jobs).
- **Example**: `tests/tools/test_sandbox_docker_live.py`.
Loading
Loading