Skip to content

feat: Persisted Artifact Lifecycle for sandbox#305

Open
KyleZheng1284 wants to merge 8 commits into
NVIDIA-AI-Blueprints:developfrom
KyleZheng1284:codex/aiq-artifact-lifecycle
Open

feat: Persisted Artifact Lifecycle for sandbox#305
KyleZheng1284 wants to merge 8 commits into
NVIDIA-AI-Blueprints:developfrom
KyleZheng1284:codex/aiq-artifact-lifecycle

Conversation

@KyleZheng1284

@KyleZheng1284 KyleZheng1284 commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

Overview

Built on top of #298, wait for that to be merged in first. Associated JIRA

Adds a durable artifact runtime for deep research. Generated files (charts, CSVs,
ResearchNotes) are harvested off the sandbox and stored as BLOBs in the shared on-disk
job DB (jobs.db), surfaced in the UI, and served via /v1/jobs/.../artifacts/<id>/content.

Because the bytes live host-side, artifacts persist after the per-job sandbox container is
deleted and across a full backend restart. Also hardens teardown (sanitized failure logging,
no sandbox.cleanup events for non-sandbox jobs) and fixes local-macOS Landlock usability
(drop nonexistent /app from the policy; document best_effort).

For Testing
  1. Start the stack and run a deep research job with a chart prompt (e.g. "compare quarterly CapEx for two companies with a chart"), and wait for the report to finish.

  2. Open View Report in the UI and confirm the chart/figure and any CSV files show up in the report and also under the files tab under the general thinking tab

image

Note the artifact ID from the report or logs (looks like art_53af6e93...)

3.. The openshell container should be automatically deleted once the job is complete (docker ps will show none afterward), hard-refresh the UI, and confirm the chart still renders in the report and that the generated chart/table still exists under there.

  1. Stop and restart the backend startup script, reopen the same report, and confirm the chart is still there (it's served from jobs.db, not the container).
    Optional one-liner proof: run sqlite3 jobs.db "select artifact_id, filename, size_bytes, length(content) from artifacts;" and confirm length(content) equals size_bytes (full bytes stored on disk).

Validation

  • pytest for the affected suites: finalize/cleanup, OpenShell provider, artifacts, custom
    middleware, factory, and job runner.

  • Durability proof: after a job's OpenShell container was deleted, its artifact bytes remained
    in jobs.db (length(content) == size_bytes for every row) and the content endpoint still
    returned 200; survives backend restart since it reopens the same on-disk DB.

  • ruff check on changed Python; bash -n scripts/setup_openshell.sh.

  • I ran the relevant local checks or explained why they are not applicable.

  • I added or updated tests for behavior changes.

  • I updated documentation for user-facing or contributor-facing changes.

  • I confirmed this PR does not include secrets, credentials, or internal-only data.

  • I certify this contribution under the Developer Certificate of Origin (DCO) and signed my commits with git commit -s or an equivalent sign-off.

Where should reviewers start?

src/aiq_agent/agents/deep_researcher/sandbox/artifacts/manager.py and .../artifacts/store.py
(harvest → durable BLOB store), then deepagents_runtime.finalize (terminal harvest + cleanup).

Related Issues

Summary by CodeRabbit

  • New Features

    • Added durable file artifact updates with richer metadata and an “Open file” action for stored content.
    • Improved deep research job cancellation to show request status while cleanup finishes in the background.
    • Updated OpenShell setup and docs for per-job sandboxing and stricter isolation.
  • Bug Fixes

    • Made artifact delivery and terminal cleanup more reliable, including better handling of interrupted jobs.
    • Improved file display so size and MIME type are shown when available.

Signed-off-by: Kyle Zheng <kyzheng@nvidia.com>
Signed-off-by: Kyle Zheng <kyzheng@nvidia.com>
Signed-off-by: Kyle Zheng <kyzheng@nvidia.com>
The generated policy listed /app under filesystem_policy.read_only, but the
aiq-openshell-demo image has no /app directory. Under the default
landlock.compatibility: hard_requirement this fails closed at sandbox prepare
("Landlock path unavailable in hard_requirement mode: /app"), so the container
reaches Ready then immediately errors. Removing the path fixes prepare on all
hosts; production keeps hard_requirement, best_effort stays opt-in for local demos.

Signed-off-by: Kyle Zheng <kyzheng@nvidia.com>
Signed-off-by: Kyle Zheng <kyzheng@nvidia.com>
Signed-off-by: Kyle Zheng <kyzheng@nvidia.com>
Signed-off-by: Kyle Zheng <kyzheng@nvidia.com>
…esktop

Make the existing --landlock-compatibility flag discoverable and document the
two-knob requirement (policy landlock.compatibility vs AI-Q require_hard_landlock)
so local hosts without a Landlock LSM don't hit sandbox prepare failures.

Signed-off-by: Kyle Zheng <kyzheng@nvidia.com>
@copy-pr-bot

copy-pr-bot Bot commented Jul 2, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Jul 2, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

This PR converts OpenShell sandboxes to per-job, policy-bound, attested instances (replacing shared local sandbox attachment), reworks sandbox finalize/cleanup and artifact checkpoint harvesting, changes artifact.update SSE payload to durable content_url-based metadata consumed across the UI, and switches job cancellation to an event-driven request/interrupt flow.

Changes

OpenShell sandboxing, artifact harvesting, and cancellation flow

Layer / File(s) Summary
Policy and sandbox config contracts
configs/config_openshell.yml, configs/openshell/aiq-research-policy.yaml, src/aiq_agent/agents/deep_researcher/sandbox/config.py, src/aiq_agent/agents/deep_researcher/deepagents_runtime.py
Policy now enforces hard-requirement landlock and adjusted read-only paths; sandbox config gains openshell_image, network_allow/network_mode, existing_sandbox_name, allow_shared_sandbox, attest, expected_policy_version, and require_hard_landlock with validation rules.
Provider session lifecycle
src/aiq_agent/agents/deep_researcher/sandbox/providers/openshell.py, src/aiq_agent/agents/deep_researcher/sandbox/base.py
Adds policy parsing/spec building, attestation/policy-revision waiting, concurrency-safe SDK context management, set_event_emitter, try_operation_lease, and cleanup_succeeded.
Runtime finalize and artifact checkpoint harvesting
deepagents_runtime.py, agent.py, custom_middleware.py, factory.py, register.py, sandbox/artifacts/manager.py
finalize() becomes idempotent/lock-protected with sandbox.cleanup events; ArtifactHarvestMiddleware checkpoints artifacts after successful execute calls; cancellation paths ensure finalize(interrupted=...) runs exactly once for owned agents.
Durable artifact payload and UI consumption
sandbox/artifacts/models.py, frontends/ui/src/adapters/api/deep-research-client.ts, use-deep-research.ts, use-load-job-data.ts, store.ts, types.ts, FileCard.tsx
Artifact.to_sse_payload() emits a nested artifact.update payload with content_url; frontend introduces FileArtifactUpdate, updates onFileUpdate signature, and FileCard renders MIME/size metadata with an "Open file" link.
Event-driven job cancellation
frontends/aiq_api/src/aiq_api/jobs/runner.py, routes/jobs.py, deep-research-client.ts
Cancel endpoint now stores a job.cancellation_requested event and cancels the Dask task instead of setting status directly; runner reorders teardown/flush around terminal transitions.
Scripts, smoke test, and docs
scripts/setup_openshell.sh, scripts/smoke_openshell_isolation.py, scripts/README.md, docs/source/architecture/agents/sandbox.md, sandbox/README.md
Setup script defaults to per-job creation with configurable landlock compatibility; new deterministic isolation smoke test; docs updated to describe per-job attestation and safeguards.
Backend and frontend test coverage
tests/aiq_agent/agents/deep_researcher/..., tests/aiq_agent/jobs/test_runner.py, tests/aiq_agent/fastapi_extensions/test_deep_research.py, frontends/ui/src/**/*.spec.*
Extensive new/updated tests cover provider lifecycle, finalize/cleanup, artifact harvesting, middleware, cancellation event flow, and frontend artifact rendering.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant JobsRoute
  participant EventStore
  participant JobRunner
  participant DeepAgentsRuntime
  participant OpenShellSandboxProvider

  Client->>JobsRoute: POST cancel/job/{id}
  JobsRoute->>EventStore: store job.cancellation_requested
  JobsRoute->>JobRunner: cancel Dask task
  JobRunner->>DeepAgentsRuntime: finalize(interrupted=True)
  DeepAgentsRuntime->>DeepAgentsRuntime: try_operation_lease (harvest if idle)
  DeepAgentsRuntime->>OpenShellSandboxProvider: terminate()
  DeepAgentsRuntime->>EventStore: emit sandbox.cleanup succeeded/failed
  JobRunner->>EventStore: publish INTERRUPTED status
Loading
sequenceDiagram
  participant OpenShellSandboxProvider
  participant OpenShellSDK
  participant ArtifactManager
  participant EventStore
  participant UI

  OpenShellSandboxProvider->>OpenShellSDK: create sandbox with policy spec
  OpenShellSDK-->>OpenShellSandboxProvider: READY, loaded policy revision
  OpenShellSandboxProvider->>EventStore: emit sandbox.attestation succeeded
  ArtifactManager->>ArtifactManager: harvest_after_execute (checkpoint)
  ArtifactManager->>EventStore: store artifact.update (content_url)
  EventStore-->>UI: SSE artifact.update
  UI->>UI: render FileCard with mimeType/size, Open file
Loading

Possibly related PRs

  • NVIDIA-AI-Blueprints/aiq#271: Relies on the same artifact.update SSE payload and harvesting lifecycle to reconstruct report context and sources.
  • NVIDIA-AI-Blueprints/aiq#280: Introduces the provider-neutral OpenShell sandbox and artifact/SSE pipeline this PR builds on with per-job policy/lifecycle changes.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title follows Conventional Commits and accurately summarizes the main change: durable artifact lifecycle persistence for sandbox jobs.
Description check ✅ Passed The description includes all required template sections with concrete overview, validation, reviewer-start, and related-issue details.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Comment @coderabbitai help to get the list of available commands.

@KyleZheng1284 KyleZheng1284 changed the title A feat: Persisted Artifact Lifecycle for sandbox Jul 2, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
frontends/aiq_api/src/aiq_api/routes/jobs.py (1)

775-792: 🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

Restore a worker-visible cancellation signal The cancel endpoint now only records job.cancellation_requested and calls _cancel_dask_task(), but runner.py still only stops on JobStatus.INTERRUPTED. Since _cancel_dask_task() is just distributed.Client.cancel(..., force=True), an already-running task can keep going while the API returns cancellation_requested: True.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontends/aiq_api/src/aiq_api/routes/jobs.py` around lines 775 - 792, The
cancel flow only records job.cancellation_requested and calls
_cancel_dask_task(), but runner.py still only exits on JobStatus.INTERRUPTED, so
running work may continue after the API says cancellation was requested. Update
the cancellation path in jobs.py and the worker-side check in runner.py so a
cancellation_requested signal is visible to the worker loop, for example by
persisting a status/flag that the runner polls and treats as a stop condition
alongside JobStatus.INTERRUPTED. Ensure the symbols EventStore.store,
_cancel_dask_task, and the runner cancellation check are kept in sync.
tests/aiq_agent/agents/deep_researcher/sandbox/test_artifacts.py (1)

200-204: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider a concurrency test for harvest_after_execute vs final_harvest.

harvest_after_execute() and final_harvest() both fall through to the shared _lock-protected _harvest(), but only final_harvest()'s own memoization is exercised sequentially by existing tests. A test that invokes harvest_after_execute() from a background thread concurrently with final_harvest() would directly verify the dedup/quota counters (_seen, _count, _total_bytes) stay consistent under the new checkpoint entrypoint.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/aiq_agent/agents/deep_researcher/sandbox/test_artifacts.py` around
lines 200 - 204, Add a concurrency test around `ArtifactHarvester` to cover
`harvest_after_execute()` racing with `final_harvest()`, since both funnel
through the shared `_harvest()` path but only `final_harvest()` memoization is
currently tested. Use the existing `test_artifacts.py` setup with
`_ARTIFACT_DIR`, `_manifest_bytes`, and the harvester methods, then invoke
`harvest_after_execute()` from a background thread while calling
`final_harvest()` on the main thread. Assert the dedup/quota state (`_seen`,
`_count`, `_total_bytes`) remains consistent and that the checkpoint entrypoint
does not double-count artifacts.
frontends/ui/src/adapters/api/deep-research-client.ts (1)

166-181: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

ArtifactUpdateEvent type doesn't reflect the actual file payload shape.

The runtime parser (Lines 501-515) reads file_path, artifact_id, mime_type, size_bytes, sha256, inline, output_category off the raw payload, but the exported ArtifactUpdateEvent.data.data type (Lines 166-181) only declares content, url, content_url. Consumers importing this type directly (it's re-exported from index.ts) get an inaccurate contract for file-type artifacts.

♻️ Proposed fix
     data: {
       type: ArtifactType
       content?: string | TodoItem[]
       url?: string // For citation_source and citation_use types
       content_url?: string
+      file_path?: string
+      path?: string
+      artifact_id?: string
+      mime_type?: string
+      size_bytes?: number
+      sha256?: string
+      inline?: boolean
+      output_category?: string
     }

Also applies to: 499-515

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontends/ui/src/adapters/api/deep-research-client.ts` around lines 166 -
181, `ArtifactUpdateEvent` currently declares an incomplete `data.data` shape
for file artifacts, so update the type to match the raw payload parsed in
`parseArtifactUpdateEvent` and any related helpers. Add the file-specific fields
observed at runtime (`file_path`, `artifact_id`, `mime_type`, `size_bytes`,
`sha256`, `inline`, `output_category`) to the `ArtifactUpdateEvent` contract,
while preserving the existing `content`, `url`, and `content_url` cases for
other artifact types. Ensure the exported type from `deep-research-client.ts`
remains aligned with the parser so consumers re-exported from `index.ts` get the
accurate artifact payload shape.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@frontends/ui/src/adapters/api/deep-research-client.ts`:
- Around line 533-536: The `filePath` derivation in `deep-research-client.ts` is
relying on an unsafe `as string` assertion, which can let non-string
`file_path`/`path`/`url` values reach `split('/')` and fail in the SSE handler.
Update the artifact parsing logic around the `raw`/`filePath`/`fileName` block
to validate each candidate value is actually a string before using it, and fall
back safely to a default when none are strings. Keep the fix within the same
artifact-handling path so strict TypeScript behavior is preserved without
runtime casts.

In `@frontends/ui/src/features/chat/hooks/use-load-job-data.ts`:
- Line 375: The buffered file replay in useLoadJobData is replacing prior
FileArtifactUpdate entries by filename instead of merging them, so repeated
artifact.update events can lose earlier data. Update the handling around the
buffer.files Map in useLoadJobData to merge updates by file.filename the same
way addDeepResearchFile does, preserving existing content and metadata when a
later event arrives for the same file.

In `@src/aiq_agent/agents/deep_researcher/sandbox/config.py`:
- Around line 93-100: The deprecated sandbox_name alias in the sandbox config is
accepted silently, so add a deprecation notice when it is provided. Update the
validator logic in the config model around existing_sandbox_name and
sandbox_name to emit a warnings.warn with DeprecationWarning, or an equivalent
logger.warning, whenever sandbox_name is set and then map it through as the
fallback for existing_sandbox_name. Keep the warning tied to the config field
handling so users of the old alias get notified before it is removed.

In `@src/aiq_agent/agents/deep_researcher/sandbox/providers/openshell.py`:
- Around line 372-432: The initial argv validation in OpenShellProvider.setup
duplicates the non-empty shell check already enforced by
OpenShellProviderConfig._validate_lifecycle_mode, so trim the redundant
oscfg.shell guard from the setup path and rely on the config-level validator as
the single source of truth. Keep the rest of the sandbox initialization flow in
setup unchanged, including the shared_sandbox_name and policy handling branches.

---

Outside diff comments:
In `@frontends/aiq_api/src/aiq_api/routes/jobs.py`:
- Around line 775-792: The cancel flow only records job.cancellation_requested
and calls _cancel_dask_task(), but runner.py still only exits on
JobStatus.INTERRUPTED, so running work may continue after the API says
cancellation was requested. Update the cancellation path in jobs.py and the
worker-side check in runner.py so a cancellation_requested signal is visible to
the worker loop, for example by persisting a status/flag that the runner polls
and treats as a stop condition alongside JobStatus.INTERRUPTED. Ensure the
symbols EventStore.store, _cancel_dask_task, and the runner cancellation check
are kept in sync.

In `@frontends/ui/src/adapters/api/deep-research-client.ts`:
- Around line 166-181: `ArtifactUpdateEvent` currently declares an incomplete
`data.data` shape for file artifacts, so update the type to match the raw
payload parsed in `parseArtifactUpdateEvent` and any related helpers. Add the
file-specific fields observed at runtime (`file_path`, `artifact_id`,
`mime_type`, `size_bytes`, `sha256`, `inline`, `output_category`) to the
`ArtifactUpdateEvent` contract, while preserving the existing `content`, `url`,
and `content_url` cases for other artifact types. Ensure the exported type from
`deep-research-client.ts` remains aligned with the parser so consumers
re-exported from `index.ts` get the accurate artifact payload shape.

In `@tests/aiq_agent/agents/deep_researcher/sandbox/test_artifacts.py`:
- Around line 200-204: Add a concurrency test around `ArtifactHarvester` to
cover `harvest_after_execute()` racing with `final_harvest()`, since both funnel
through the shared `_harvest()` path but only `final_harvest()` memoization is
currently tested. Use the existing `test_artifacts.py` setup with
`_ARTIFACT_DIR`, `_manifest_bytes`, and the harvester methods, then invoke
`harvest_after_execute()` from a background thread while calling
`final_harvest()` on the main thread. Assert the dedup/quota state (`_seen`,
`_count`, `_total_bytes`) remains consistent and that the checkpoint entrypoint
does not double-count artifacts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: cb9a3b77-fad9-4a7e-9efe-9514bf9b7811

📥 Commits

Reviewing files that changed from the base of the PR and between db86f8f and 23dd4d5.

📒 Files selected for processing (38)
  • configs/config_openshell.yml
  • configs/openshell/aiq-research-policy.yaml
  • docs/source/architecture/agents/sandbox.md
  • frontends/aiq_api/README.md
  • frontends/aiq_api/src/aiq_api/jobs/runner.py
  • frontends/aiq_api/src/aiq_api/routes/jobs.py
  • frontends/ui/src/adapters/api/deep-research-client.spec.ts
  • frontends/ui/src/adapters/api/deep-research-client.ts
  • frontends/ui/src/adapters/api/index.ts
  • frontends/ui/src/features/chat/hooks/use-deep-research.spec.ts
  • frontends/ui/src/features/chat/hooks/use-deep-research.ts
  • frontends/ui/src/features/chat/hooks/use-load-job-data.ts
  • frontends/ui/src/features/chat/store.ts
  • frontends/ui/src/features/chat/types.ts
  • frontends/ui/src/features/layout/components/FileCard.spec.tsx
  • frontends/ui/src/features/layout/components/FileCard.tsx
  • scripts/README.md
  • scripts/setup_openshell.sh
  • scripts/smoke_openshell_isolation.py
  • src/aiq_agent/agents/deep_researcher/agent.py
  • src/aiq_agent/agents/deep_researcher/custom_middleware.py
  • src/aiq_agent/agents/deep_researcher/deepagents_runtime.py
  • src/aiq_agent/agents/deep_researcher/factory.py
  • src/aiq_agent/agents/deep_researcher/register.py
  • src/aiq_agent/agents/deep_researcher/sandbox/README.md
  • src/aiq_agent/agents/deep_researcher/sandbox/artifacts/manager.py
  • src/aiq_agent/agents/deep_researcher/sandbox/artifacts/models.py
  • src/aiq_agent/agents/deep_researcher/sandbox/base.py
  • src/aiq_agent/agents/deep_researcher/sandbox/config.py
  • src/aiq_agent/agents/deep_researcher/sandbox/providers/openshell.py
  • tests/aiq_agent/agents/deep_researcher/sandbox/test_artifacts.py
  • tests/aiq_agent/agents/deep_researcher/sandbox/test_openshell_provider.py
  • tests/aiq_agent/agents/deep_researcher/test_agent.py
  • tests/aiq_agent/agents/deep_researcher/test_custom_middleware.py
  • tests/aiq_agent/agents/deep_researcher/test_deepagents_runtime.py
  • tests/aiq_agent/agents/deep_researcher/test_factory.py
  • tests/aiq_agent/fastapi_extensions/test_deep_research.py
  • tests/aiq_agent/jobs/test_runner.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (13)
frontends/ui/**/*.{js,ts,jsx,tsx,vue}

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Run npm lint, type-check, and build validation for UI changes in frontends/ui

Files:

  • frontends/ui/src/features/layout/components/FileCard.spec.tsx
  • frontends/ui/src/adapters/api/index.ts
  • frontends/ui/src/features/chat/types.ts
  • frontends/ui/src/features/chat/store.ts
  • frontends/ui/src/adapters/api/deep-research-client.spec.ts
  • frontends/ui/src/features/chat/hooks/use-deep-research.spec.ts
  • frontends/ui/src/features/chat/hooks/use-load-job-data.ts
  • frontends/ui/src/features/layout/components/FileCard.tsx
  • frontends/ui/src/features/chat/hooks/use-deep-research.ts
  • frontends/ui/src/adapters/api/deep-research-client.ts
frontends/ui/**/*.{ts,tsx,jsx,js}

📄 CodeRabbit inference engine (AGENTS.md)

frontends/ui/**/*.{ts,tsx,jsx,js}: The UI is built with Next.js / React / TypeScript / Tailwind with KUI components; reuse existing KUI components and visual patterns rather than introducing new ones
Validate UI-affecting changes with npm run lint, npm run type-check, and npm run test:ci, and include a screenshot for visible changes

Files:

  • frontends/ui/src/features/layout/components/FileCard.spec.tsx
  • frontends/ui/src/adapters/api/index.ts
  • frontends/ui/src/features/chat/types.ts
  • frontends/ui/src/features/chat/store.ts
  • frontends/ui/src/adapters/api/deep-research-client.spec.ts
  • frontends/ui/src/features/chat/hooks/use-deep-research.spec.ts
  • frontends/ui/src/features/chat/hooks/use-load-job-data.ts
  • frontends/ui/src/features/layout/components/FileCard.tsx
  • frontends/ui/src/features/chat/hooks/use-deep-research.ts
  • frontends/ui/src/adapters/api/deep-research-client.ts
**

⚙️ CodeRabbit configuration file

**:

AI-Q Agent Guidance

Repository-global instructions for coding agents and for humans reviewing
agent-authored changes. These rules apply to every task in this repository.
Task-specific runbooks live in .agents/skills/ — load the
relevant skill before starting a workflow it covers.

Project overview

AI-Q is an NVIDIA AI Blueprint: an enterprise research agent built on the
NeMo Agent Toolkit (NAT). The deployed product is a research blueprint, not
a general skill runtime. New retrieval sources and tools are NAT functions;
agent behavior is driven by workflow YAML, Jinja2 prompts, and a data-source
registry — not by hard-coded logic.

Primary boundaries:

  • Backend Python package: src/aiq_agent/.
  • Data-source and tool packages: sources/ (each is its own package).
  • Frontends and tooling: frontends/ (web UI in frontends/ui/, eval harnesses
    in frontends/benchmarks/).
  • Configs, deployment, docs: configs/, deploy/, docs/.

Stay inside this repository. If your workspace also contains adjacent repos
(for example a sibling NeMo-Relay checkout), do not edit them as part of an AI-Q
change. Treat sources/* as independent packages: prefer the smallest change
scoped to the package you are touching.

Repository structure

Path Purpose
src/aiq_agent/ Backend agent, FastAPI extensions, auth, observability, knowledge
sources/ Data-source / tool packages (e.g. tavily_web_search, google_scholar_paper_search)
configs/ Workflow YAML configs (e.g. config_cli_default.yml)
frontends/ui/ Next.js / React / TypeScript / Tailwind / KUI web UI
frontends/benchmarks/ Eval harnesses: freshqa, deepsearch_qa, deepresearch_bench
deploy/ Docker Compose and Helm/Kubernetes assets; deploy/.env for secrets
docs/source/ ...

Files:

  • frontends/ui/src/features/layout/components/FileCard.spec.tsx
  • tests/aiq_agent/agents/deep_researcher/test_agent.py
  • tests/aiq_agent/fastapi_extensions/test_deep_research.py
  • frontends/aiq_api/README.md
  • frontends/ui/src/adapters/api/index.ts
  • tests/aiq_agent/agents/deep_researcher/test_factory.py
  • src/aiq_agent/agents/deep_researcher/sandbox/artifacts/models.py
  • src/aiq_agent/agents/deep_researcher/custom_middleware.py
  • frontends/ui/src/features/chat/types.ts
  • frontends/ui/src/features/chat/store.ts
  • configs/config_openshell.yml
  • frontends/ui/src/adapters/api/deep-research-client.spec.ts
  • src/aiq_agent/agents/deep_researcher/agent.py
  • src/aiq_agent/agents/deep_researcher/sandbox/base.py
  • scripts/README.md
  • frontends/ui/src/features/chat/hooks/use-deep-research.spec.ts
  • scripts/smoke_openshell_isolation.py
  • frontends/ui/src/features/chat/hooks/use-load-job-data.ts
  • tests/aiq_agent/agents/deep_researcher/test_custom_middleware.py
  • src/aiq_agent/agents/deep_researcher/sandbox/artifacts/manager.py
  • docs/source/architecture/agents/sandbox.md
  • configs/openshell/aiq-research-policy.yaml
  • src/aiq_agent/agents/deep_researcher/register.py
  • frontends/ui/src/features/layout/components/FileCard.tsx
  • frontends/ui/src/features/chat/hooks/use-deep-research.ts
  • src/aiq_agent/agents/deep_researcher/factory.py
  • tests/aiq_agent/agents/deep_researcher/sandbox/test_artifacts.py
  • src/aiq_agent/agents/deep_researcher/sandbox/config.py
  • frontends/aiq_api/src/aiq_api/routes/jobs.py
  • src/aiq_agent/agents/deep_researcher/sandbox/providers/openshell.py
  • tests/aiq_agent/agents/deep_researcher/test_deepagents_runtime.py
  • tests/aiq_agent/jobs/test_runner.py
  • src/aiq_agent/agents/deep_researcher/sandbox/README.md
  • scripts/setup_openshell.sh
  • frontends/aiq_api/src/aiq_api/jobs/runner.py
  • tests/aiq_agent/agents/deep_researcher/sandbox/test_openshell_provider.py
  • src/aiq_agent/agents/deep_researcher/deepagents_runtime.py
  • frontends/ui/src/adapters/api/deep-research-client.ts
frontends/ui/**/*

⚙️ CodeRabbit configuration file

frontends/ui/**/*: Review UI changes for strict TypeScript behavior, API contract alignment, auth/session handling, accessible controls,
resilient loading and error states, and report/chat state consistency. Prefer existing UI patterns and require tests
for changed user-visible workflows.

Files:

  • frontends/ui/src/features/layout/components/FileCard.spec.tsx
  • frontends/ui/src/adapters/api/index.ts
  • frontends/ui/src/features/chat/types.ts
  • frontends/ui/src/features/chat/store.ts
  • frontends/ui/src/adapters/api/deep-research-client.spec.ts
  • frontends/ui/src/features/chat/hooks/use-deep-research.spec.ts
  • frontends/ui/src/features/chat/hooks/use-load-job-data.ts
  • frontends/ui/src/features/layout/components/FileCard.tsx
  • frontends/ui/src/features/chat/hooks/use-deep-research.ts
  • frontends/ui/src/adapters/api/deep-research-client.ts
**/*.py

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Run ruff check and ruff format validation for Python code changes

**/*.py: Python code must be linted and formatted with Ruff using line length 120, target Python 3.11, rule sets E,F,W,I,PL,UP, and isort force-single-line configuration
Never commit secrets, tokens, or environment-specific hostnames; use environment variables and SecretStr instead, resolving API keys at runtime
Never print or log secret values, including in tool output or error messages
Missing-secret paths must degrade gracefully (stub/skip), not crash or leak
Do not hand-reformat unrelated code when making changes; match the existing import and formatting style

Files:

  • tests/aiq_agent/agents/deep_researcher/test_agent.py
  • tests/aiq_agent/fastapi_extensions/test_deep_research.py
  • tests/aiq_agent/agents/deep_researcher/test_factory.py
  • src/aiq_agent/agents/deep_researcher/sandbox/artifacts/models.py
  • src/aiq_agent/agents/deep_researcher/custom_middleware.py
  • src/aiq_agent/agents/deep_researcher/agent.py
  • src/aiq_agent/agents/deep_researcher/sandbox/base.py
  • scripts/smoke_openshell_isolation.py
  • tests/aiq_agent/agents/deep_researcher/test_custom_middleware.py
  • src/aiq_agent/agents/deep_researcher/sandbox/artifacts/manager.py
  • src/aiq_agent/agents/deep_researcher/register.py
  • src/aiq_agent/agents/deep_researcher/factory.py
  • tests/aiq_agent/agents/deep_researcher/sandbox/test_artifacts.py
  • src/aiq_agent/agents/deep_researcher/sandbox/config.py
  • frontends/aiq_api/src/aiq_api/routes/jobs.py
  • src/aiq_agent/agents/deep_researcher/sandbox/providers/openshell.py
  • tests/aiq_agent/agents/deep_researcher/test_deepagents_runtime.py
  • tests/aiq_agent/jobs/test_runner.py
  • frontends/aiq_api/src/aiq_api/jobs/runner.py
  • tests/aiq_agent/agents/deep_researcher/sandbox/test_openshell_provider.py
  • src/aiq_agent/agents/deep_researcher/deepagents_runtime.py
**/*test*.py

📄 CodeRabbit inference engine (CONTRIBUTING.md)

Run pytest for all behavior changes in Python code

Files:

  • tests/aiq_agent/agents/deep_researcher/test_agent.py
  • tests/aiq_agent/fastapi_extensions/test_deep_research.py
  • tests/aiq_agent/agents/deep_researcher/test_factory.py
  • tests/aiq_agent/agents/deep_researcher/test_custom_middleware.py
  • tests/aiq_agent/agents/deep_researcher/sandbox/test_artifacts.py
  • tests/aiq_agent/agents/deep_researcher/test_deepagents_runtime.py
  • tests/aiq_agent/jobs/test_runner.py
  • tests/aiq_agent/agents/deep_researcher/sandbox/test_openshell_provider.py
src/aiq_agent/**/*.py

📄 CodeRabbit inference engine (AGENTS.md)

src/aiq_agent/**/*.py: Respect authenticated data sources by honoring requires_auth, per-user token pass-through, and backend token validators; apply owner guardrails before loading protected report or artifact context into an agent
Do not weaken or bypass AuthMiddleware, validators, or auth gating without a prior design discussion

Files:

  • src/aiq_agent/agents/deep_researcher/sandbox/artifacts/models.py
  • src/aiq_agent/agents/deep_researcher/custom_middleware.py
  • src/aiq_agent/agents/deep_researcher/agent.py
  • src/aiq_agent/agents/deep_researcher/sandbox/base.py
  • src/aiq_agent/agents/deep_researcher/sandbox/artifacts/manager.py
  • src/aiq_agent/agents/deep_researcher/register.py
  • src/aiq_agent/agents/deep_researcher/factory.py
  • src/aiq_agent/agents/deep_researcher/sandbox/config.py
  • src/aiq_agent/agents/deep_researcher/sandbox/providers/openshell.py
  • src/aiq_agent/agents/deep_researcher/deepagents_runtime.py
src/aiq_agent/agents/**/*

⚙️ CodeRabbit configuration file

src/aiq_agent/agents/**/*: Review agent changes for research workflow correctness, graph state transitions, prompt/tool contracts,
HITL behavior, and failure handling. Flag changes that weaken source attribution, report generation,
async cancellation, checkpointing, or data-source selection without focused tests and docs.

Files:

  • src/aiq_agent/agents/deep_researcher/sandbox/artifacts/models.py
  • src/aiq_agent/agents/deep_researcher/custom_middleware.py
  • src/aiq_agent/agents/deep_researcher/agent.py
  • src/aiq_agent/agents/deep_researcher/sandbox/base.py
  • src/aiq_agent/agents/deep_researcher/sandbox/artifacts/manager.py
  • src/aiq_agent/agents/deep_researcher/register.py
  • src/aiq_agent/agents/deep_researcher/factory.py
  • src/aiq_agent/agents/deep_researcher/sandbox/config.py
  • src/aiq_agent/agents/deep_researcher/sandbox/providers/openshell.py
  • src/aiq_agent/agents/deep_researcher/sandbox/README.md
  • src/aiq_agent/agents/deep_researcher/deepagents_runtime.py
{deploy/**,configs/**}

⚙️ CodeRabbit configuration file

{deploy/**,configs/**}: Review deployment and config changes for secret separation, safe defaults, local-vs-production behavior, Helm and
Docker portability, and documentation parity. Flag committed credentials, environment-specific NVIDIA internals in
public defaults, and changes that make examples diverge from CI-tested paths.

Files:

  • configs/config_openshell.yml
  • configs/openshell/aiq-research-policy.yaml
docs/source/**/*

📄 CodeRabbit inference engine (AGENTS.md)

Update the docs under docs/source/ when behavior, configuration, or workflows change

Files:

  • docs/source/architecture/agents/sandbox.md
{docs/**,README.md,CONTRIBUTING.md,SECURITY.md,CODE-OF-CONDUCT.md}

⚙️ CodeRabbit configuration file

{docs/**,README.md,CONTRIBUTING.md,SECURITY.md,CODE-OF-CONDUCT.md}: Review documentation for command accuracy, branch-name consistency, current CI and copy-pr-bot behavior, public
vs internal boundary clarity, stale examples, and links that no longer match the repository layout.

Files:

  • docs/source/architecture/agents/sandbox.md
**/*config*.py

📄 CodeRabbit inference engine (AGENTS.md)

Config schemas must inherit from FunctionBaseConfig and YAML _type names must come from the registered config class

Files:

  • src/aiq_agent/agents/deep_researcher/sandbox/config.py
{src/aiq_agent/fastapi_extensions/**,frontends/aiq_api/src/aiq_api/**}

⚙️ CodeRabbit configuration file

{src/aiq_agent/fastapi_extensions/**,frontends/aiq_api/src/aiq_api/**}: Treat API, auth, and job-runner changes as externally visible contracts. Check authorization boundaries,
request tracing, async job lifecycle, websocket reconnect behavior, error responses, and cross-user data isolation.
Require tests for route behavior, access decisions, and job state transitions when those surfaces change.

Files:

  • frontends/aiq_api/src/aiq_api/routes/jobs.py
  • frontends/aiq_api/src/aiq_api/jobs/runner.py
🔇 Additional comments (50)
frontends/aiq_api/src/aiq_api/jobs/runner.py (1)

583-590: LGTM!

Also applies to: 606-609, 619-632, 644-654, 662-670, 681-697

frontends/aiq_api/src/aiq_api/routes/jobs.py (1)

761-774: LGTM!

frontends/ui/src/adapters/api/deep-research-client.spec.ts (1)

5-102: LGTM!

frontends/ui/src/features/chat/hooks/use-deep-research.spec.ts (1)

940-974: LGTM!

frontends/ui/src/features/layout/components/FileCard.spec.tsx (1)

65-88: LGTM!

tests/aiq_agent/fastapi_extensions/test_deep_research.py (1)

55-58: LGTM!

Also applies to: 253-308

tests/aiq_agent/jobs/test_runner.py (1)

612-642: LGTM!

Also applies to: 1942-1988, 2015-2025

src/aiq_agent/agents/deep_researcher/agent.py (2)

138-142: LGTM!

Also applies to: 156-159


327-346: 🚀 Performance & Scalability

No issue here: ArtifactManager.final_harvest() is memoized, so the cleanup path’s later call returns cached artifacts instead of repeating harvest I/O or re-emitting artifact.update events.

			> Likely an incorrect or invalid review comment.
src/aiq_agent/agents/deep_researcher/custom_middleware.py (1)

470-489: LGTM!

src/aiq_agent/agents/deep_researcher/factory.py (1)

44-44: LGTM!

Also applies to: 199-220, 233-248

src/aiq_agent/agents/deep_researcher/register.py (2)

18-18: LGTM!

Also applies to: 223-225, 234-248, 301-309


259-275: 🩺 Stability & Availability

No sandbox leak here
DeepAgentsRuntime only builds the provider object; the real OpenShell/Modal sandbox is created later in the provider’s _create_session(). If DeepResearcherAgent.__init__ fails, there isn’t a live sandbox yet to finalize.

			> Likely an incorrect or invalid review comment.
frontends/ui/src/features/layout/components/FileCard.tsx (3)

154-164: 🔒 Security & Privacy

Confirm contentUrl works with window.open under bearer-token auth.

Opening file.contentUrl via window.open(...) relies on the browser attaching credentials automatically, which only happens for cookies — not for an Authorization header. Elsewhere in this codebase (generate-pdf.ts), authenticated artifact byte-fetching explicitly forwards Authorization/idToken server-side specifically because outbound requests don't auto-attach headers. If /v1/jobs/.../artifacts/.../content requires a bearer token rather than a cookie, this "Open file" action will 401 for authenticated deployments.

As per path instructions, "Review UI changes for strict TypeScript behavior, API contract alignment, auth/session handling, accessible controls, resilient loading and error states, and report/chat state consistency."

Source: Path instructions


96-98: 🎯 Functional Correctness | ⚡ Quick win

Dangling aria-controls/aria-expanded when file has no content.

When file.content is absent, onClick is now a no-op and the chevron is hidden, but the Button still renders aria-expanded={isExpanded} and aria-controls={file-content-${file.id}} pointing at an element that can never render (isExpanded can never become true). This is a broken ARIA reference for assistive tech.

As per path instructions, "Review UI changes for ... accessible controls."

♿ Proposed fix
       <Button
         kind="tertiary"
         size="small"
         onClick={() => file.content && setIsExpanded(!isExpanded)}
-        aria-expanded={isExpanded}
-        aria-controls={`file-content-${file.id}`}
+        aria-expanded={file.content ? isExpanded : undefined}
+        aria-controls={file.content ? `file-content-${file.id}` : undefined}
         className="w-full justify-start text-left p-0"
       >

Also applies to: 140-152

Source: Path instructions


27-34: LGTM!

Also applies to: 68-73, 116-122

frontends/aiq_api/README.md (1)

75-75: LGTM!

Also applies to: 152-152

tests/aiq_agent/agents/deep_researcher/test_agent.py (1)

292-337: LGTM!

src/aiq_agent/agents/deep_researcher/sandbox/artifacts/manager.py (1)

181-204: LGTM!

src/aiq_agent/agents/deep_researcher/sandbox/artifacts/models.py (1)

87-112: LGTM!

tests/aiq_agent/agents/deep_researcher/sandbox/test_artifacts.py (1)

43-54: LGTM!

Also applies to: 125-130, 171-174, 176-185

tests/aiq_agent/agents/deep_researcher/sandbox/test_openshell_provider.py (2)

16-722: LGTM!

Also applies to: 845-887


71-86: 📐 Maintainability & Code Quality

No change needed in _provider() The helper seeds the same instance state as OpenShellSandboxProvider.__init__; it only skips the optional import check so these tests can run without the OpenShell SDK.

			> Likely an incorrect or invalid review comment.
tests/aiq_agent/agents/deep_researcher/test_custom_middleware.py (1)

28-28: LGTM!

Also applies to: 550-618

tests/aiq_agent/agents/deep_researcher/test_deepagents_runtime.py (1)

20-24: LGTM!

Also applies to: 40-44, 312-356, 358-475, 477-533, 534-546

tests/aiq_agent/agents/deep_researcher/test_factory.py (1)

26-26: LGTM!

Also applies to: 154-175

frontends/ui/src/adapters/api/deep-research-client.ts (3)

15-15: LGTM!

Also applies to: 153-164, 225-225, 787-792


537-553: LGTM!


793-812: LGTM!

configs/openshell/aiq-research-policy.yaml (1)

11-16: LGTM!

Also applies to: 23-28

configs/config_openshell.yml (1)

1-2: LGTM!

Per-job image/policy, allowlist network, and delete_on_exit: true + attest: true + require_hard_landlock: true line up correctly with the fail-closed contract enforced in openshell.py's _read_policy_data/_validate_policy_network and config.py's _validate_lifecycle_mode.

Also applies to: 142-165

src/aiq_agent/agents/deep_researcher/sandbox/config.py (2)

101-126: LGTM!

Fail-closed lifecycle validation (delete_on_exit/attest required for per-job mode, allow_shared_sandbox gate, expected_policy_version depends on attest) matches the provider's _create_session/_attest contract and the accompanying tests.

Also applies to: 148-152


127-146: 🎯 Functional Correctness

No issue herefrom __future__ import annotations is already present, so _validate_lifecycle_mode(self) -> OpenShellProviderConfig does not raise at import time.

			> Likely an incorrect or invalid review comment.
src/aiq_agent/agents/deep_researcher/sandbox/providers/openshell.py (5)

190-260: LGTM!

Strict ParseDict(..., ignore_unknown_fields=False), secret-free SandboxSpec construction (no environment/providers copied), and network-hosts-vs-config-allowlist validation all correctly implement the fail-closed contract.


263-360: LGTM!

supports_terminate=True matches the new terminate-aware context lifecycle, and routing upload/download through _active_os_context() instead of self._os_context closes a prior race where a concurrently-torn-down context could be used directly.


433-518: LGTM!

The double policy-version check (before vs. after _wait_for_loaded_policy) is redundant-looking but correct: the second check is the only one that fires when a version was already loaded and the wait branch was skipped entirely.


529-601: LGTM!

The enter/exit state machine (_os_context_entering/_os_context_exit_requested) correctly handles the terminate-during-creation race: deferred exit while entering, single detached cleanup, and _ensure_context_active checkpoints that abort publication if teardown won the race. Matches the accompanying test suite.


164-188: 🎯 Functional Correctness

No change needed The policy generator and checked-in OpenShell policy both use process.run_as_user/run_as_group: sandbox and network_policies[*].endpoints[*] with enforcement: enforce and access: read-only; this guard matches the repo’s contract.

			> Likely an incorrect or invalid review comment.
docs/source/architecture/agents/sandbox.md (1)

9-11: LGTM!

Accurately reflects the per-job attestation/policy-binding behavior and the two-knob Landlock guidance implemented in this cohort.

Also applies to: 24-30, 40-54, 66-66

src/aiq_agent/agents/deep_researcher/sandbox/README.md (1)

40-50: LGTM!

Documentation is consistent with the code changes: per-job creation/attestation semantics, the two independent Landlock knobs, the debug-only shared-attachment escape hatch, and the artifact.update event rename all match the implementation.

Also applies to: 65-65, 128-129, 146-151, 160-167, 213-219, 240-277, 300-301, 310-325

src/aiq_agent/agents/deep_researcher/deepagents_runtime.py (1)

90-170: LGTM!

Also applies to: 199-217, 272-273, 287-353, 392-401, 534-556

src/aiq_agent/agents/deep_researcher/sandbox/base.py (1)

36-36: LGTM!

Also applies to: 103-104, 146-168, 221-228

scripts/README.md (1)

54-65: LGTM!

Also applies to: 84-110, 217-217

scripts/setup_openshell.sh (1)

17-17: LGTM! The <<EOF switch is injection-safe: $LANDLOCK_COMPATIBILITY is validated to hard_requirement/best_effort before emission, and the heredoc header carries no other expandable tokens.

Also applies to: 51-51, 61-61, 80-110, 157-164, 880-886, 900-926, 1011-1011, 1063-1063, 1076-1076

scripts/smoke_openshell_isolation.py (1)

1-197: LGTM! Lazy session creation gives real concurrent isolation, the finally terminate is idempotent, and raise SystemExit(main()) correctly bypasses the except Exception FAIL handler on the success path.

frontends/ui/src/adapters/api/index.ts (1)

96-121: LGTM!

frontends/ui/src/features/chat/hooks/use-deep-research.ts (2)

22-22: LGTM!


216-216: 🎯 Functional Correctness

Merge buffered file updates before flush

buf.files.set(file.filename, file) keeps only the last event per filename, while the live path merges fields in addDeepResearchFile. If artifact.update can emit sparse updates for the same file, replayed sessions can drop earlier content or metadata.

frontends/ui/src/features/chat/store.ts (1)

2896-2907: LGTM!

frontends/ui/src/features/chat/types.ts (1)

363-372: 🗄️ Data Integrity & Integration | ⚡ Quick win

Verify all consumers handle now-optional content.

Making content optional is correct for durable artifacts fetched via contentUrl, but any renderer that previously assumed content: string (e.g. FileCard.tsx, not in this batch) must now branch on contentUrl/artifactId presence instead. Please confirm downstream rendering degrades gracefully (loading/fetch state) when content is absent.

As per path instructions, "Review UI changes for strict TypeScript behavior, API contract alignment, ... resilient loading and error states."

Source: Path instructions

Comment on lines +533 to 536
// Generated artifacts carry durable metadata; legacy text-file events carry content.
const raw = artifactData as Record<string, unknown>
const filePath = (raw.file_path || raw.path || artifactData.url || 'unknown') as string
const fileName = filePath.split('/').pop() || filePath

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Unsafe as string cast on filePath — a non-string file_path/path/url would throw at runtime.

(raw.file_path || raw.path || artifactData.url || 'unknown') as string is a compile-time-only assertion. If the backend ever sends a non-string value for these fields, filePath.split('/') on the next line throws inside the SSE handler.

As per path instructions, frontends/ui/**/* changes should be reviewed for "strict TypeScript behavior."

🛡️ Proposed fix
-            const raw = artifactData as Record<string, unknown>
-            const filePath = (raw.file_path || raw.path || artifactData.url || 'unknown') as string
+            const raw = artifactData as Record<string, unknown>
+            const rawPath = raw.file_path ?? raw.path ?? artifactData.url
+            const filePath = typeof rawPath === 'string' ? rawPath : 'unknown'
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Generated artifacts carry durable metadata; legacy text-file events carry content.
const raw = artifactData as Record<string, unknown>
const filePath = (raw.file_path || raw.path || artifactData.url || 'unknown') as string
const fileName = filePath.split('/').pop() || filePath
// Generated artifacts carry durable metadata; legacy text-file events carry content.
const raw = artifactData as Record<string, unknown>
const rawPath = raw.file_path ?? raw.path ?? artifactData.url
const filePath = typeof rawPath === 'string' ? rawPath : 'unknown'
const fileName = filePath.split('/').pop() || filePath
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontends/ui/src/adapters/api/deep-research-client.ts` around lines 533 -
536, The `filePath` derivation in `deep-research-client.ts` is relying on an
unsafe `as string` assertion, which can let non-string `file_path`/`path`/`url`
values reach `split('/')` and fail in the SSE handler. Update the artifact
parsing logic around the `raw`/`filePath`/`fileName` block to validate each
candidate value is actually a string before using it, and fall back safely to a
default when none are strings. Keep the fix within the same artifact-handling
path so strict TypeScript behavior is preserved without runtime casts.

Source: Path instructions

todos: null as TodoItem[] | null,
citations: [] as Array<{ url: string; content: string; isCited: boolean }>,
files: new Map<string, string>(), // filename -> latest content (deduped)
files: new Map<string, FileArtifactUpdate>(), // filename -> latest event (deduped)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the relevant hooks first, then inspect the narrowed slices.
ast-grep outline frontends/ui/src/features/chat/hooks/use-load-job-data.ts --view expanded || true
ast-grep outline frontends/ui/src/features/chat/hooks/use-deep-research.ts --view expanded || true

echo "---- use-load-job-data.ts relevant lines ----"
sed -n '340,620p' frontends/ui/src/features/chat/hooks/use-load-job-data.ts

echo "---- use-deep-research.ts relevant lines ----"
sed -n '180,540p' frontends/ui/src/features/chat/hooks/use-deep-research.ts

Repository: NVIDIA-AI-Blueprints/aiq

Length of output: 30183


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ast-grep outline frontends/ui/src/features/chat/hooks/use-load-job-data.ts --view expanded || true
ast-grep outline frontends/ui/src/features/chat/hooks/use-deep-research.ts --view expanded || true

echo "---- use-load-job-data.ts relevant lines ----"
sed -n '340,620p' frontends/ui/src/features/chat/hooks/use-load-job-data.ts

echo "---- use-deep-research.ts relevant lines ----"
sed -n '180,540p' frontends/ui/src/features/chat/hooks/use-deep-research.ts

Repository: NVIDIA-AI-Blueprints/aiq

Length of output: 30183


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "type FileArtifactUpdate|interface FileArtifactUpdate|FileArtifactUpdate" frontends/ui/src -g'*.ts' -g'*.tsx'
echo "---- use-load-job-data.ts ----"
sed -n '360,610p' frontends/ui/src/features/chat/hooks/use-load-job-data.ts
echo "---- use-deep-research.ts ----"
sed -n '200,520p' frontends/ui/src/features/chat/hooks/use-deep-research.ts

Repository: NVIDIA-AI-Blueprints/aiq

Length of output: 26161


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '130,250p' frontends/ui/src/adapters/api/deep-research-client.ts
echo "---- call sites ----"
rg -n "onFileUpdate\\(" frontends/ui/src -g'*.ts' -g'*.tsx'
echo "---- backend mentions of file artifact updates ----"
rg -n "FileArtifactUpdate|artifact update|onFileUpdate|filename" src frontends/ui/src -g'*.ts' -g'*.tsx'

Repository: NVIDIA-AI-Blueprints/aiq

Length of output: 4124


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '130,240p' frontends/ui/src/adapters/api/deep-research-client.ts
echo "---- deep research store actions ----"
rg -n "addDeepResearchFile|deepResearchFiles|setDeepResearchFile|updateDeepResearchFile" frontends/ui/src -g'*.ts' -g'*.tsx'

Repository: NVIDIA-AI-Blueprints/aiq

Length of output: 8172


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- FileArtifactUpdate definition ----"
sed -n '145,210p' frontends/ui/src/adapters/api/deep-research-client.ts
echo "---- store actions mentioning deepResearchFiles ----"
rg -n "deepResearchFiles|addDeepResearchFile|setDeepResearchFile|updateDeepResearchFile" frontends/ui/src -g'*.ts' -g'*.tsx' -A3 -B3

Repository: NVIDIA-AI-Blueprints/aiq

Length of output: 27776


🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n "file\\.filename|onFileUpdate|FileArtifactUpdate|artifact.*file|filename" src frontends/ui/src sources -g'*.py' -g'*.ts' -g'*.tsx'

Repository: NVIDIA-AI-Blueprints/aiq

Length of output: 25584


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '500,570p' frontends/ui/src/adapters/api/deep-research-client.ts
echo "---- file update tests ----"
sed -n '1,160p' frontends/ui/src/adapters/api/deep-research-client.spec.ts

Repository: NVIDIA-AI-Blueprints/aiq

Length of output: 6632


Merge buffered file updates by filename. buffer.files.set(file.filename, file) replaces the prior snapshot, so repeated artifact.update events for the same file can drop earlier content or metadata. Use the same merge-by-filename behavior as addDeepResearchFile in this replay path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontends/ui/src/features/chat/hooks/use-load-job-data.ts` at line 375, The
buffered file replay in useLoadJobData is replacing prior FileArtifactUpdate
entries by filename instead of merging them, so repeated artifact.update events
can lose earlier data. Update the handling around the buffer.files Map in
useLoadJobData to merge updates by file.filename the same way
addDeepResearchFile does, preserving existing content and metadata when a later
event arrives for the same file.

Comment on lines +93 to +100
existing_sandbox_name: str | None = Field(
default=None,
description="Debug-only existing OpenShell sandbox to attach to; requires allow_shared_sandbox=true.",
)
sandbox_name: str | None = Field(
default=None,
description="Existing named OpenShell sandbox to attach to (required when a policy file is used).",
description="Deprecated alias for existing_sandbox_name; requires allow_shared_sandbox=true.",
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Deprecated sandbox_name alias never warns.

sandbox_name is documented as deprecated in favor of existing_sandbox_name, but nothing emits a deprecation warning when it's used. Consider a warnings.warn(..., DeprecationWarning) (or a logger.warning) in the validator when sandbox_name is set, so operators still on the old field notice before it's removed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiq_agent/agents/deep_researcher/sandbox/config.py` around lines 93 -
100, The deprecated sandbox_name alias in the sandbox config is accepted
silently, so add a deprecation notice when it is provided. Update the validator
logic in the config model around existing_sandbox_name and sandbox_name to emit
a warnings.warn with DeprecationWarning, or an equivalent logger.warning,
whenever sandbox_name is set and then map it through as the fallback for
existing_sandbox_name. Keep the warning tied to the config field handling so
users of the old alias get notified before it is removed.

Comment on lines +372 to +432

if not oscfg.shell or any(not part.strip() for part in oscfg.shell):
raise ValueError("OpenShell shell must contain at least one non-empty argv element")

# Release any prior context (covers the recoverable-error reset path).
self._exit_context()

sandbox_kwargs: dict[str, object] = {
"cluster": oscfg.gateway,
"delete_on_exit": oscfg.delete_on_exit,
"ready_timeout_seconds": oscfg.ready_timeout_seconds,
}
if oscfg.sandbox_name:
sandbox_kwargs["sandbox"] = oscfg.sandbox_name
shared_name = oscfg.shared_sandbox_name
if shared_name is not None:
logger.warning(
"OpenShell shared-sandbox debug attachment enabled: sandbox=%s job=%s; physical job isolation is off",
shared_name,
self.job_id,
)
# Attachment does not transfer ownership: never delete a shared sandbox
# that this job did not create.
sandbox_kwargs.update(sandbox=shared_name, delete_on_exit=False)
else:
if not oscfg.policy:
raise ValueError("Per-job OpenShell creation requires a policy file")
policy_data = _read_policy_data(oscfg.policy, require_hard_landlock=oscfg.require_hard_landlock)
_validate_policy_network(
policy_data,
mode=cfg.network.mode,
allow=cfg.network.allow,
)
policy = _parse_policy_proto(policy_data, policy_path=oscfg.policy)
sandbox_kwargs.update(
spec=_build_sandbox_spec(policy=policy, image=oscfg.image, job_id=self.job_id),
delete_on_exit=oscfg.delete_on_exit,
)

os_sandbox = openshell.Sandbox(**sandbox_kwargs)
os_sandbox.__enter__()
self._os_context = os_sandbox
backend = OpenShellSandbox(sandbox=os_sandbox, timeout=cfg.timeout, shell=oscfg.shell)
logger.info(
"OpenShell sandbox READY: id=%s gateway=%s sandbox_name=%s policy=%s",
backend.id,
oscfg.gateway,
oscfg.sandbox_name,
oscfg.policy,
self._enter_context(os_sandbox)
backend: BaseSandbox | None = None
try:
self._ensure_context_active(os_sandbox)
self.physical_sandbox_name = getattr(os_sandbox.sandbox, "name", None)
phase, policy_version = self._attest(os_sandbox)
self._ensure_context_active(os_sandbox)
backend = OpenShellSandbox(sandbox=os_sandbox, timeout=cfg.timeout, shell=oscfg.shell)
self._ensure_context_active(os_sandbox)
sandbox_ref = os_sandbox.sandbox
logger.info(
"OpenShell sandbox attested: id=%s name=%s policy_version=%s shared=%s",
backend.id,
getattr(sandbox_ref, "name", None),
policy_version,
shared_name is not None,
)
self._emit_attestation(phase=phase, policy_version=policy_version, status="succeeded")
return backend
except BaseException:
self._safe_close(backend)
self._exit_context()
raise

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Shell argv check duplicates the config-level validator.

Lines 373-374 re-validate oscfg.shell even though OpenShellProviderConfig._validate_lifecycle_mode (config.py) already enforces non-empty argv at construction time. Harmless belt-and-suspenders, but if you want to trim it, the config validator is the single source of truth.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/aiq_agent/agents/deep_researcher/sandbox/providers/openshell.py` around
lines 372 - 432, The initial argv validation in OpenShellProvider.setup
duplicates the non-empty shell check already enforced by
OpenShellProviderConfig._validate_lifecycle_mode, so trim the redundant
oscfg.shell guard from the setup path and rely on the config-level validator as
the single source of truth. Keep the rest of the sandbox initialization flow in
setup unchanged, including the shared_sandbox_name and policy handling branches.

@AjayThorve AjayThorve added this to the v2.2 milestone Jul 7, 2026
@AjayThorve AjayThorve removed this from the v2.2 milestone Jul 8, 2026
AjayThorve added a commit that referenced this pull request Jul 8, 2026
#### Overview

Extract the provider-neutral artifact lifecycle and UI delivery work
from #305 onto current `develop`, so it can land independently of the
OpenShell policy-attestation work in #298.

This change:

- checkpoints artifact manifests after successful sandbox `execute`
calls and performs one idempotent terminal harvest before sandbox
teardown;
- avoids blocking cancellation on an in-flight provider operation while
preserving already checkpointed artifacts;
- emits canonical `artifact.update` metadata without embedding artifact
bytes or credentials;
- delivers live and replayed sandbox artifacts to the Files tab, using
the authenticated same-origin artifact endpoint for file access; and
- preserves the artifact store selected by current configuration,
including the existing SQL and S3-compatible implementations on
`develop`.

The existing #305 branch is unchanged. This PR contains no OpenShell
gateway, policy, attestation, Landlock, or cancellation-API changes.

#### Validation

```bash
PYTHONPATH=src:frontends/aiq_api/src uv run --no-sync pytest \
  tests/aiq_agent/agents/deep_researcher/sandbox/test_artifacts.py \
  tests/aiq_agent/agents/deep_researcher/test_custom_middleware.py \
  tests/aiq_agent/agents/deep_researcher/test_deepagents_runtime.py \
  tests/aiq_agent/agents/deep_researcher/test_factory.py \
  tests/aiq_agent/jobs/test_runner.py -q
# 225 passed

uv run --no-sync ruff check .
# All checks passed

uv run --no-sync ruff format --check .
# 307 files already formatted

cd frontends/ui
npm run type-check
# passed

npm run lint
# passed with 0 errors (existing warnings remain)

npm run test:ci
# 79 files passed; 1,324 tests passed; 1 skipped

cd ../../docs
make html
# succeeded; 3 existing cross-reference/version-switcher warnings
```

- [x] I ran the relevant local checks or explained why they are not
applicable.
- [x] I added or updated tests for behavior changes.
- [x] I updated documentation for user-facing or contributor-facing
changes.
- [x] I confirmed this PR does not include secrets, credentials, or
internal-only data.
- [x] I certify this contribution under the Developer Certificate of
Origin (DCO) and signed my commits with `git commit -s` or an equivalent
sign-off.

#### Where should reviewers start?

1. `src/aiq_agent/agents/deep_researcher/sandbox/artifacts/models.py`
for the canonical metadata-only SSE contract.
2. `src/aiq_agent/agents/deep_researcher/sandbox/artifacts/manager.py`,
`custom_middleware.py`, and
`frontends/aiq_api/src/aiq_api/jobs/runner.py` for checkpoint and
terminal-harvest ownership.
3. `frontends/ui/src/adapters/api/deep-research-client.ts` and
`frontends/ui/src/features/layout/components/FileCard.tsx` for
live/replayed Files-tab delivery.

#### Related Issues

- Relates to #305
- Independent of #298


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Deep research file artifacts now stream as structured
`artifact.update` events, enabling durable file links with richer
metadata (MIME, size, hashes, titles/captions, etc.).
* File cards can display MIME/size and show an **Open file** button when
a durable content URL is available.
* **Bug Fixes**
* Improved artifact harvesting across success, failure, and cancellation
to better preserve completed outputs.
* Updated streaming/replay behavior now merges incremental file metadata
updates without overwriting earlier content.
* **Documentation**
* Refreshed architecture and SSE documentation to reflect the updated
`artifact.update` payload shape.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Kyle Zheng <kyzheng@nvidia.com>
Co-authored-by: Ajay Thorve <AjayThorve@users.noreply.github.com>
tanleach pushed a commit to tanleach/aiq that referenced this pull request Jul 9, 2026
#### Overview

Extract the provider-neutral artifact lifecycle and UI delivery work
from NVIDIA-AI-Blueprints#305 onto current `develop`, so it can land independently of the
OpenShell policy-attestation work in NVIDIA-AI-Blueprints#298.

This change:

- checkpoints artifact manifests after successful sandbox `execute`
calls and performs one idempotent terminal harvest before sandbox
teardown;
- avoids blocking cancellation on an in-flight provider operation while
preserving already checkpointed artifacts;
- emits canonical `artifact.update` metadata without embedding artifact
bytes or credentials;
- delivers live and replayed sandbox artifacts to the Files tab, using
the authenticated same-origin artifact endpoint for file access; and
- preserves the artifact store selected by current configuration,
including the existing SQL and S3-compatible implementations on
`develop`.

The existing NVIDIA-AI-Blueprints#305 branch is unchanged. This PR contains no OpenShell
gateway, policy, attestation, Landlock, or cancellation-API changes.

#### Validation

```bash
PYTHONPATH=src:frontends/aiq_api/src uv run --no-sync pytest \
  tests/aiq_agent/agents/deep_researcher/sandbox/test_artifacts.py \
  tests/aiq_agent/agents/deep_researcher/test_custom_middleware.py \
  tests/aiq_agent/agents/deep_researcher/test_deepagents_runtime.py \
  tests/aiq_agent/agents/deep_researcher/test_factory.py \
  tests/aiq_agent/jobs/test_runner.py -q
# 225 passed

uv run --no-sync ruff check .
# All checks passed

uv run --no-sync ruff format --check .
# 307 files already formatted

cd frontends/ui
npm run type-check
# passed

npm run lint
# passed with 0 errors (existing warnings remain)

npm run test:ci
# 79 files passed; 1,324 tests passed; 1 skipped

cd ../../docs
make html
# succeeded; 3 existing cross-reference/version-switcher warnings
```

- [x] I ran the relevant local checks or explained why they are not
applicable.
- [x] I added or updated tests for behavior changes.
- [x] I updated documentation for user-facing or contributor-facing
changes.
- [x] I confirmed this PR does not include secrets, credentials, or
internal-only data.
- [x] I certify this contribution under the Developer Certificate of
Origin (DCO) and signed my commits with `git commit -s` or an equivalent
sign-off.

#### Where should reviewers start?

1. `src/aiq_agent/agents/deep_researcher/sandbox/artifacts/models.py`
for the canonical metadata-only SSE contract.
2. `src/aiq_agent/agents/deep_researcher/sandbox/artifacts/manager.py`,
`custom_middleware.py`, and
`frontends/aiq_api/src/aiq_api/jobs/runner.py` for checkpoint and
terminal-harvest ownership.
3. `frontends/ui/src/adapters/api/deep-research-client.ts` and
`frontends/ui/src/features/layout/components/FileCard.tsx` for
live/replayed Files-tab delivery.

#### Related Issues

- Relates to NVIDIA-AI-Blueprints#305
- Independent of NVIDIA-AI-Blueprints#298

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Deep research file artifacts now stream as structured
`artifact.update` events, enabling durable file links with richer
metadata (MIME, size, hashes, titles/captions, etc.).
* File cards can display MIME/size and show an **Open file** button when
a durable content URL is available.
* **Bug Fixes**
* Improved artifact harvesting across success, failure, and cancellation
to better preserve completed outputs.
* Updated streaming/replay behavior now merges incremental file metadata
updates without overwriting earlier content.
* **Documentation**
* Refreshed architecture and SSE documentation to reflect the updated
`artifact.update` payload shape.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Signed-off-by: Kyle Zheng <kyzheng@nvidia.com>
Co-authored-by: Ajay Thorve <AjayThorve@users.noreply.github.com>
Signed-off-by: Tanner Leach <tleach@nvidia.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants