Skip to content

feat(iron-swarm): add the nemo-iron-swarm plugin (agent red-teaming and hardening) - #1037

Open
koralchapnik wants to merge 56 commits into
mainfrom
iron-swarm-plugin/koralchapnik
Open

feat(iron-swarm): add the nemo-iron-swarm plugin (agent red-teaming and hardening)#1037
koralchapnik wants to merge 56 commits into
mainfrom
iron-swarm-plugin/koralchapnik

Conversation

@koralchapnik

@koralchapnik koralchapnik commented Aug 2, 2026

Copy link
Copy Markdown

Summary

Adds the nemo-iron-swarm plugin: a security war-game that red-teams and hardens NAT agents. garak
attackers probe a sandboxed copy of the agent, defenders generate guardrails and OpenShell network
policy, and validators replay the attacks plus a benign suite to confirm the fix blocked the
attack without breaking ordinary behaviour. Before this, there was no way to measure whether a
hardening change actually helped; after it, harden → apply → re-run gives a comparable answer.

The plugin contributes a service, a job, a CLI, an SDK namespace, two entities and an agent skill.
It never imports iron-swarm: garak pulls litellm → httpx>=0.28 and torch, which conflict with
nvidia-nat's httpx~=0.27, so iron-swarm is provisioned into its own venv by
nemo iron-swarm setup and driven by subprocess. Nothing breaks when it isn't installed, and the
Studio tab is behind studio.feature_flags.iron_swarm_enabled, off by default.

Changes

The feature

  • Two ways to pick a target. init --agent <name> for any registered agent (it need not be
    deployed), or init --project-dir <path> for a local NAT project with nothing registered. Both
    save the same IronSwarmManifest, so --manifest-id is the single handle afterwards. CLI and
    Studio go through the same POST /manifests.
  • run is a pure consumer of the benign suite. It never synthesises; synth-benign is a
    separate command and job, and the reviewed suite is cached on the manifest. A missing suite fails
    fast rather than doing something surprising.
  • A manifest is a frozen target. init resolves once and stores the resulting scaffold as a
    fileset; runs download it instead of re-resolving, so two runs are comparable — which is what a
    "did the hardening help?" answer depends on. Agent edits land via an explicit
    POST /manifests/{name}/refresh, which apply-mitigation calls automatically.
  • Uploads never carry credentials. Project bundles are selected with
    git ls-files --exclude-standard and drop .env* / *.pem / *.key, reusing nemo-agents'
    DOCKERIGNORE_TEMPLATE. Victim secrets come from the platform Secrets store by name.
  • Studio shows the target (source, port, egress, secrets, env, and the manifest itself), with
    refresh and an editor for non-secret env vars.

Integration with main

The branch was 251 commits behind, so this now includes a merge of main plus the fixes that
required:

  • Studio wiring conflicts resolved where main's anonymizer landed in the same slots (routes,
    route params, gate helpers, nav, route groups). intakeEnabled keeps main's new true default,
    and the dead traceId route param is dropped — main reworked Intake to be session-based.
  • Migrated 9 iron-swarm files from react-router-dom to react-router; main moved Studio to
    react-router@8.3.0 and the old specifier is in no lockfile.
  • The Studio UI image now copies the iron-swarm OpenAPI spec, without which
    pnpm install --frozen-lockfile aborted type generation and the image build failed.
  • nemo-iron-swarm-plugin is installed in the dev dependency group. CI runs uv run --frozen,
    which syncs only default groups, so workspace membership alone left the package unimportable —
    the plugin's tests could not import it and the OpenAPI generator could not build its app.
  • plugins/nemo-iron-swarm/tests/unit registered on the pytest pythonpath, so the sibling
    _doubles helper resolves when pytest runs from the repo root.
  • Regenerated the iron-swarm OpenAPI spec against main (db_version for optimistic locking,
    output_location, and the WarGameSpecOutputWarGameSpec rename).

Review fixes

  • Studio's swarm event feed no longer freezes: an event with no numeric id used to fall back to
    Date.now(), which then advanced the poll cursor far above any real id so the server returned
    nothing for the rest of the run.
  • secrets_file is constrained to the uploaded project directory. It is caller-supplied and read by
    init on the platform host, so it could previously name any readable file.
  • User-controlled values are routed through sanitize_for_log before logging (7 CodeQL log-injection
    alerts, now cleared).
  • setup redacts index_url; Artifactory embeds an access token in that URL.
  • delete_manifest no longer deletes project_fileset. That fileset is supplied by the caller and
    two manifests can name the same bundle, so deleting either used to destroy the other's target.
    agent_fileset is service-created and is still cleaned up.
  • The Iron Swarm nav entry and run list are gated on usePluginInstalled('iron-swarm'), matching the
    nemo-agents pattern, and fail open so an unresolved manifest never hides the feature.
  • Pinned the OpenShell installer to v0.0.92 instead of fetching install.sh from main.
  • Corrected three stale statements about the manifest contract (README, init output, and a
    docstring naming the wrong exception).

Type of Change

  • Code change with documentation updates
  • Code change (feature, bug fix, or refactor)
  • Documentation only
  • Contributor tooling or automation
  • CI, build, or test infrastructure

Quality Gates

  • Tests added or updated for changed behavior
  • Existing tests cover changed behavior — justification:
  • Tests not applicable — justification:
  • Documentation updated for user-visible behavior
  • Documentation not applicable — justification:

281 unit tests in plugins/nemo-iron-swarm/tests. Two were added or corrected for the review fixes:
a regression test asserting delete_manifest leaves a caller-supplied project_fileset alone
(verified to fail against the previous behaviour), and the events-fallback test now names the log
after the run instead of events.jsonl, so it exercises the real _events_path contract rather than
passing by coincidence.

Verification

  • Pull request title follows the repository's Conventional Commit format
  • Every commit includes an appropriate Signed-off-by: trailer
  • uv run pre-commit run -a passes, or any blocked checks are identified below
  • Targeted tests pass, or tests are marked not applicable above
  • No secrets, API keys, or credentials are included

Targeted validation:

Command Result
uv run pre-commit run -a all 12 hooks pass, no files modified
uv run --frozen pytest plugins/nemo-iron-swarm/tests 281 passed
bash tools/lint/lint-python-style.sh (ruff check + ruff format --check) pass, 3097 files formatted
bash tools/lint/lint-python-types.sh (ty) exit 0
pnpm --filter nemo-studio-ui typecheck pass
pnpm --filter nemo-studio-ui lint pass at --max-warnings 0
pnpm --filter nemo-studio-ui test 313 files, 2831 tests passed
pnpm run format (Prettier) pass
cd web && pnpm gen:check generated files up to date
uv lock --check 580 packages, in sync

Not verified locally, called out rather than claimed:

  • The nmp-studio-ui Docker build could not be run here (buildx unavailable, and the legacy
    builder rejects $BUILDPLATFORM). The fix was validated by checking that every plugin spec orval
    reads has a matching COPY, and CI's Build CPU smoke images subsequently passed.
  • Several CI runs failed inside a GitHub Actions incident (Service Unavailable / Bad Gateway /
    Failed to resolve action download info during Set up job). One lint-web-sdk failure occurred
    in that window and could not be reproduced locally under Node 26, Node 22.23.1, or a fresh
    --frozen-lockfile install with forced regeneration. It needs a clean run to confirm.

Security notes for review

  • The manifest is read-only in Studio by design. overrides.defenders[].implementation is a
    Python import path that iron-swarm loads in the job process (it logs importing user-configured module … ensure the source is trusted), so it executes on the platform host with the job's
    environment and Docker access — not inside the victim sandbox. A free-text YAML editor would turn
    manifest-write permission into code execution.
  • env on a manifest is stored in plaintext and documented as non-secret; credentials use secrets,
    which stores only names and resolves values from the Secrets store at run time.
  • The war-game runs as a subprocess executor job and launches a Docker sandbox on the host.
    Correct for local/dev, explicitly not a production deployment — containerising the orchestrator
    and decoupling the sandbox from a host Docker daemon is the Phase-2 item.
  • apply-mitigation writes another plugin's entity (Agent.config) while holding only
    iron-swarm.runs.apply, because the platform fail-closes on permission ids outside a service's own
    namespace. Treat that permission as an agent-write grant when assigning it.

Known issues, logged rather than hidden

  • A run that fails after the sandbox is up leaks the sandbox and its port forward; the next attempt
    then fails on a name collision, so one fault presents as a different fault each time. Cancellation
    tears down correctly — the failure paths do not.
  • A synth producing zero rows reports completed while persisting nothing, and a cached
    benign_interview is not cleared when a new suite lands without one.
  • sanity_check submits a job and returns immediately, so a failed validation cannot yet be gated on
    in CI. A --wait flag that polls to a terminal status is the follow-up.
  • The plugin's tests carry no unit marker, so make test-unit's -m unit filter deselects them.
    This is repo-wide (nemo-anonymizer and nemo-agents are the same) and is left alone here.

Manual verification

Real war-games against the bundled react-agent and the research agent from agents-lab uploaded
as a project. The second is the interesting result — attacks 11/11 blocked, benign 8/12 passed
(3 refused, 1 error): the defenses stopped everything but broke a third of normal functionality,
which is exactly what the benign suite exists to catch.

Studio flows were driven end-to-end with Playwright: manifest creation, the target view, refresh,
the env editor, and a full benign-suite generation including the HITL interview.

Not in this PR

Routing attack and detection through the NeMo Auditor (NAIS-356/357) is prototyped on a separate
branch and deliberately excluded here.

Summary by CodeRabbit

  • New Features

    • Introduced Iron Swarm for war games, benign-suite generation, mitigation review, defense composition, and hardened-agent validation.
    • Added Studio pages for manifests, runs, live swarm activity, suite editing, model configuration, and hardening workflows.
    • Added CLI, SDK, REST API, event replay, artifact handling, and agent/project setup capabilities.
    • Added model connectivity checks, secure credential handling, preflight diagnostics, and feature-flagged navigation.
  • Documentation

    • Added comprehensive setup, workflow, configuration, troubleshooting, and CLI guidance.
  • Tests

    • Added broad coverage for APIs, CLI workflows, execution, security, filesets, models, events, and Studio behavior.

@koralchapnik
koralchapnik requested review from a team as code owners August 2, 2026 22:24
@koralchapnik koralchapnik self-assigned this Aug 2, 2026
@coderabbitai

coderabbitai Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Iron Swarm adds a plugin with REST APIs, jobs, SDK and CLI support, durable run events, manifest and fileset handling, benign-suite synthesis, defense validation, and feature-gated Studio workflows.

Changes

Iron Swarm feature

Layer / File(s) Summary
Plugin contracts and platform wiring
plugins/nemo-iron-swarm/openapi/openapi.yaml, plugins/nemo-iron-swarm/pyproject.toml, plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/entities.py, plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/_perms.py
Adds public entities, permissions, API schemas, OpenAPI contracts, service registration, package configuration, workspace wiring, and documentation.
Agent resolution and filesets
plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/agent_resolver.py, plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/filesets.py
Resolves deployed agents into manifests, injects gateway settings, scaffolds projects, derives secrets, and safely uploads or extracts project filesets.
Service API and event relay
plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/*, plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/service.py
Adds manifest, run, job, event, mitigation, and defense-composition routes with authorization, filtering, validation, persistence, and durable event replay.
CLI setup and operator workflows
plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/*, plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/config.py
Adds isolated environment provisioning, credential handling, preflight checks, initialization, execution, synthesis, sanity checks, refresh, and status commands.
War-game and synthesis execution
plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/*, plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/sdk.py
Adds manifest materialization, subprocess execution, run records, artifacts, failure classification, HITL synthesis, model preflight, defense composition, and synchronous or asynchronous SDK resources.
Studio API and navigation
web/packages/sdk/orval/constants.ts, web/packages/studio/src/api/ironSwarm.ts, web/packages/studio/src/routes/*, web/packages/studio/src/components/dataViews/*
Adds generated SDK configuration, fileset and inspection mutations, feature-flagged routes, navigation, manifest views, and run views.
Studio swarm and hardening flows
web/packages/studio/src/components/ironSwarm/*
Adds live event polling, swarm visualization, node details, message feeds, HITL interview and review controls, benign-suite editors, mitigation analysis, sanity checks, and defense application flows.

Possibly related PRs

Suggested labels: docs, test

Suggested reviewers: parkanzky, stefan-kickoff, svvarom

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 58.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the addition of the nemo-iron-swarm plugin and its red-teaming and hardening purpose.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch iron-swarm-plugin/koralchapnik

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (19)
plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/provisioning.py-71-73 (1)

71-73: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Redact the index URL before echoing it.

config.index_url can embed credentials (https://user:AKCp8token@host/simple). setup prints it verbatim into terminal scrollback and CI logs. doctor already avoids this through checks.redact_index_url, and test_doctor_never_prints_an_embedded_token locks that behavior in. Use the same helper here.

🔒️ Proposed fix
+from nemo_iron_swarm_plugin.cli.checks import redact_index_url
...
     if config.index_url:
-        typer.echo(f"  using extra index {config.index_url}")
+        typer.echo(f"  using extra index {redact_index_url(config.index_url)}")
         install_cmd += ["--index", config.index_url]
🤖 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 `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/provisioning.py`
around lines 71 - 73, Update the index URL output in the provisioning setup flow
to pass config.index_url through the existing checks.redact_index_url helper
before typer.echo, while continuing to use the original URL in install_cmd for
installation.
web/packages/studio/src/routes/NewIronSwarmManifestRoute/index.tsx-104-115 (1)

104-115: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

The inspect effect overwrites operator edits and can apply a stale response.

The effect re-runs on every change of source, selectedAgent, or workspace. Two problems follow:

  1. If the operator edits port or secrets and then toggles the source control back to agent, the effect re-fires with the same agent and overwrites those edits.
  2. If the operator switches agents quickly, an earlier in-flight response can resolve last and write stale values.

Track the agent the response belongs to and skip writes when it no longer matches.

🐛 Proposed fix
   const selectedAgent = watch('agent');
   const inspectAgent = useInspectAgent();
   const { mutate: runInspectAgent } = inspectAgent;
+  const inspectedAgent = useRef<string | undefined>(undefined);
   useEffect(() => {
     if (source !== 'agent' || !selectedAgent) return;
+    if (inspectedAgent.current === selectedAgent) return;
+    inspectedAgent.current = selectedAgent;
     runInspectAgent(
       { workspace, agent: selectedAgent },
       {
         onSuccess: (facts) => {
+          if (inspectedAgent.current !== selectedAgent) return;
           setValue('port', String(facts.port));
           setValue('secrets', facts.secrets.join(', '));
         },
       }
     );
   }, [source, selectedAgent, workspace, runInspectAgent, setValue]);
🤖 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 `@web/packages/studio/src/routes/NewIronSwarmManifestRoute/index.tsx` around
lines 104 - 115, Update the inspect effect around runInspectAgent so it does not
overwrite operator edits when source toggles back to the same agent, and ignores
responses for agents that are no longer selected. Track the agent associated
with each inspection request, only apply setValue updates when that agent still
matches the current selectedAgent, and preserve the existing source, workspace,
and request behavior.
web/packages/studio/src/components/ironSwarm/useSanityCheck.ts-112-122 (1)

112-122: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unvalidated JSON.parse can crash the report view.

Line 117 asserts the parsed blob is a ValidationReport. SanityCheckReport destructures report.summary and reads summary.attacks_blocked. If the result file is truncated, empty, or shaped differently, the render throws instead of showing an error.

Validate the parsed payload before returning it, or default the missing fields.

🤖 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 `@web/packages/studio/src/components/ironSwarm/useSanityCheck.ts` around lines
112 - 122, Update the queryFn in useSanityCheck to safely handle malformed,
empty, or differently shaped validation-result JSON before returning a
ValidationReport. Validate the parsed payload’s required report and summary
fields, or supply defaults for missing fields, so SanityCheckReport can access
summary.attacks_blocked without throwing during render.
web/packages/studio/src/components/ironSwarm/useRunWarGame.ts-20-31 (1)

20-31: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

One failed poll aborts the flow silently.

ironSwarmListRuns is not wrapped in try/catch. A single transient failure rejects the promise, skips the fallback navigate, and produces an unhandled rejection because the caller uses void. The user sees the success toast and stays on the manifest page.

Also add an abort guard so the loop stops when the component unmounts.

Proposed fix
   const openRunForJob = async (jobName: string): Promise<void> => {
     for (let attempt = 0; attempt < 60; attempt++) {
-      const { data } = await ironSwarmListRuns(workspace, { sort: '-created_at', page_size: 20 });
-      const run = (data as IronSwarmRun[] | undefined)?.find((r) => r.job_id === jobName);
-      if (run?.name) {
-        navigate(getIronSwarmRunDetailsRoute(workspace, run.name));
-        return;
-      }
+      try {
+        const { data } = await ironSwarmListRuns(workspace, { sort: '-created_at', page_size: 20 });
+        const run = (data as IronSwarmRun[] | undefined)?.find((r) => r.job_id === jobName);
+        if (run?.name) {
+          navigate(getIronSwarmRunDetailsRoute(workspace, run.name));
+          return;
+        }
+      } catch {
+        // transient failure — keep polling until the window expires
+      }
       await new Promise((resolve) => setTimeout(resolve, 500));
     }
     navigate(getIronSwarmRunListRoute(workspace));
   };
🤖 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 `@web/packages/studio/src/components/ironSwarm/useRunWarGame.ts` around lines
20 - 31, Update openRunForJob to catch transient errors from ironSwarmListRuns
and continue polling so failures do not bypass the final fallback navigation.
Add an unmount abort guard shared with the component lifecycle, checking it
during each polling iteration and before navigation, and ensure the guard is
released on cleanup.
web/packages/studio/src/routes/IronSwarmManifestDetailRoute/index.tsx-86-93 (1)

86-93: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

CSV formula injection in the exported requests.csv.

escapeCsv only quotes ", , and \n. A value that begins with =, +, -, @, tab, or CR is written raw. The suite content is LLM-generated and operator-editable. When the downloaded file is opened in Excel or Sheets, such a value is evaluated as a formula.

Prefix those values with a single quote or \t, and quote the field.

Proposed fix
-const escapeCsv = (value: string): string =>
-  /[",\n]/.test(value) ? `"${value.replace(/"/g, '""')}"` : value;
+const FORMULA_PREFIX = /^[=+\-@\t\r]/;
+const escapeCsv = (value: string): string => {
+  const safe = FORMULA_PREFIX.test(value) ? `'${value}` : value;
+  return /["',\r\n]/.test(safe) ? `"${safe.replace(/"/g, '""')}"` : safe;
+};
🤖 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 `@web/packages/studio/src/routes/IronSwarmManifestDetailRoute/index.tsx` around
lines 86 - 93, Update escapeCsv to prevent formula injection in exported
requests.csv: detect values beginning with =, +, -, @, tab, or carriage return,
prefix them with a single quote (or tab), and ensure the resulting field is
quoted. Preserve existing escaping of quotes, commas, and newlines, and keep
toRequestsCsv using escapeCsv for every field.
web/packages/studio/src/components/ironSwarm/useSanityCheck.ts-163-176 (1)

163-176: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

useLatestSanityCheckJob polls forever.

refetchInterval is a constant. The hook keeps listing 50 runs on every interval for as long as the Harden tab is open, including after the job is found and after it completes. Stop the interval once a job_id is returned.

Proposed fix
-    refetchInterval: JOB_POLLING_INTERVAL_MS,
+    refetchInterval: (query) => (query.state.data ? false : JOB_POLLING_INTERVAL_MS),
🤖 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 `@web/packages/studio/src/components/ironSwarm/useSanityCheck.ts` around lines
163 - 176, Update useLatestSanityCheckJob so refetchInterval becomes conditional
on the query result: continue polling while no job_id has been found, and
disable polling once a job_id is returned. Preserve the existing query key,
enabled condition, and latest-run lookup behavior.
web/packages/studio/src/components/ironSwarm/useMitigations.ts-221-229 (1)

221-229: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Unmemoized defenses array drives a downstream render loop. useMitigations allocates a new array whenever query.data is undefined, and HardenPanel uses that array as an effect dependency that calls setSelected with a new Set.

  • web/packages/studio/src/components/ironSwarm/useMitigations.ts#L221-L229: wrap defenses and recommendations in useMemo keyed on query.data.
  • web/packages/studio/src/components/ironSwarm/HardenPanel.tsx#L162-L165: key the selection-reset effect on the joined defense ids instead of the array identity.
🤖 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 `@web/packages/studio/src/components/ironSwarm/useMitigations.ts` around lines
221 - 229, Memoize the defenses and recommendations values in useMitigations,
keyed by query.data, so unchanged data preserves array identity. In
web/packages/studio/src/components/ironSwarm/HardenPanel.tsx lines 162-165,
update the selection-reset effect dependency to use joined defense IDs rather
than the defenses array identity.
web/packages/studio/src/components/ironSwarm/useGenerateBenignSuite.ts-59-69 (1)

59-69: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Both run-resolution paths call ironSwarmListRuns without error handling or cleanup. A rejection becomes an unhandled promise rejection, and state setters can fire after unmount.

  • web/packages/studio/src/components/ironSwarm/useGenerateBenignSuite.ts#L59-L69: wrap the request in try/catch, and toast when the run never resolves so starting does not stay true.
  • web/packages/studio/src/components/ironSwarm/useGenerateBenignSuite.ts#L78-L94: wrap the async IIFE in try/catch and add an ignore flag in the effect cleanup before calling setRunName/setJobName.
🤖 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 `@web/packages/studio/src/components/ironSwarm/useGenerateBenignSuite.ts`
around lines 59 - 69, Update
web/packages/studio/src/components/ironSwarm/useGenerateBenignSuite.ts lines
59-69 in resolveRun to catch ironSwarmListRuns failures and toast when all
attempts expire, ensuring starting is reset instead of remaining true; update
lines 78-94 in the async IIFE to catch request failures and add effect-cleanup
ignore guards before setRunName and setJobName.
web/packages/studio/src/routes/IronSwarmRunDetailsRoute/index.tsx-122-130 (1)

122-130: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Tab value becomes orphaned when the HITL prompt clears.

The effect switches to interview, but nothing switches back. After the operator submits an answer, hitlPending turns false, and both the interview trigger and its TabsContent unmount while tab still equals 'interview'. The panel then renders no content until the user clicks another tab.

Return to swarm when the prompt clears.

🐛 Proposed fix
   useEffect(() => {
-    if (hitlPending) setTab('interview');
+    setTab((current) => {
+      if (hitlPending) return 'interview';
+      return current === 'interview' ? 'swarm' : current;
+    });
   }, [hitlPending]);
🤖 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 `@web/packages/studio/src/routes/IronSwarmRunDetailsRoute/index.tsx` around
lines 122 - 130, Update the tab-selection effect near hitlPending and setTab so
it selects interview while HITL is pending and returns to swarm when hitlPending
becomes false, preventing the tab value from referencing the unmounted interview
content.
web/packages/studio/src/components/ironSwarm/swarm/SwarmGraph.tsx-196-201 (1)

196-201: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Nodes are selectable only by pointer.

The <g> element handles onPointerDown only. Keyboard users cannot select a node, so NodeDetail stays empty for them. Add role="button", tabIndex={0}, an accessible name, and an onKeyDown handler that calls onSelect(n.id) on Enter and Space.

♿ Proposed fix
             <g
               key={n.id}
+              role="button"
+              tabIndex={0}
+              aria-label={`${n.title} (${status})`}
               onPointerDown={(e) => onNodePointerDown(e, n)}
+              onKeyDown={(e) => {
+                if (e.key === 'Enter' || e.key === ' ') {
+                  e.preventDefault();
+                  onSelect(n.id);
+                }
+              }}
               className="cursor-grab active:cursor-grabbing"
             >
🤖 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 `@web/packages/studio/src/components/ironSwarm/swarm/SwarmGraph.tsx` around
lines 196 - 201, Update the node `<g>` element in the SwarmGraph rendering to be
keyboard-accessible by adding button semantics, keyboard focus via tabIndex={0},
and an accessible name. Add an onKeyDown handler that calls onSelect(n.id) when
the key is Enter or Space, while preserving the existing pointer selection
behavior.
web/packages/studio/src/components/ironSwarm/ReviewPanel.tsx-17-19 (1)

17-19: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Local rows never resync with a new suite prop.

useState uses suite only for the first render. The parent route keeps this component mounted while it polls status_details, so a second review round (or a late-arriving suite) leaves the operator editing and approving the previous suite.

Reset rows when suite changes, or key the component by round in the parent.

🔁 Proposed fix
-import { FC, useState } from 'react';
+import { FC, useEffect, useState } from 'react';
@@
   const [rows, setRows] = useState<SuiteRow[]>(suite);
+  useEffect(() => {
+    setRows(suite);
+  }, [suite]);
🤖 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 `@web/packages/studio/src/components/ironSwarm/ReviewPanel.tsx` around lines 17
- 19, Update ReviewPanel’s local rows state so it resynchronizes with the
incoming suite prop whenever suite changes, preserving user edits between suite
updates while replacing stale rows for new review rounds or late-arriving data.
plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/sdk.py-280-308 (1)

280-308: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Preserve the async client's authentication.

make_sdk(str(self._platform.base_url)) creates a direct-mode NeMoPlatform without auth headers. Both async methods therefore drop the caller's credentials and can fail on authenticated deployments. Preserve the async client's auth configuration when creating the sync client.

🤖 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 `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/sdk.py` around lines 280 -
308, Update the sync client creation in the async methods wrapping _run_war_game
and _run_synth_benign to preserve the caller’s authentication configuration from
self._platform. Ensure the client passed to asyncio.to_thread retains the async
client’s auth headers while using the existing base URL.
plugins/nemo-iron-swarm/tests/unit/test_events.py-88-120 (1)

88-120: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

This test cannot detect a filename mismatch in the fallback.

_events_path is patched to .../missing/events.jsonl, and fake_download writes events.jsonl into the destination directory. The two names match only because of that patch. Production names the local log <safe-run-name>.jsonl, so a fileset that stores events.jsonl would produce an empty response while this test still passes. Patch _events_path to a run-named file, for example my-run.jsonl, and keep the download writing the name the real fileset contains.

🤖 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 `@plugins/nemo-iron-swarm/tests/unit/test_events.py` around lines 88 - 120,
Update test_get_events_falls_back_to_fileset_when_local_missing so the patched
_events_path uses a run-named file such as my-run.jsonl, while fake_download
continues writing events.jsonl from the fileset. Keep the existing fallback
assertions and setup unchanged so the test detects mismatches between the local
log filename and the downloaded fileset filename.
plugins/nemo-iron-swarm/README.md-14-18 (1)

14-18: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Split this README by Diataxis type.

This page combines tutorial, how-to, troubleshooting, reference, and explanation content. Split these into separate pages. Put prerequisites before task steps. Add Next Steps links. Provide verified Python SDK and CLI alternatives in tab sets where the SDK supports the task.

As per coding guidelines, each documentation page must fit one Diataxis quadrant and task pages must list prerequisites first.

Also applies to: 287-294, 412-447

🤖 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 `@plugins/nemo-iron-swarm/README.md` around lines 14 - 18, Restructure the
README into separate Diataxis-focused pages, separating tutorial, how-to,
troubleshooting, reference, and explanatory content. Ensure each task page
begins with prerequisites, add Next Steps links, and provide verified Python SDK
and CLI alternatives in tab sets wherever both support the task. Apply the same
restructuring to the sections corresponding to the referenced later ranges,
preserving the existing instructions and examples.

Source: Coding guidelines

plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/manifests.py-555-566 (1)

555-566: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Refresh leaks the new fileset when entity_client.update fails.

If update raises, the freshly uploaded fileset is never referenced by any entity and is never deleted. Wrap the update and delete the new fileset on failure. Also map NemoEntityConflictError/NemoEntityNotFoundError to 409/404 as update_manifest does; today they surface as 500.

🤖 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 `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/manifests.py`
around lines 555 - 566, Update the refresh flow around entity_client.update to
clean up the newly assigned fileset when the update fails, deleting fileset
rather than the stale fileset in the exception path. Add the same
NemoEntityConflictError and NemoEntityNotFoundError mappings used by
update_manifest so failures return 409 and 404 respectively, while preserving
stale-fileset cleanup only after a successful update.
plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/manifest.py-236-242 (1)

236-242: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reject an absolute or escaping agent["workflow"].

manifest_dir / project_dir / workflow writes defense_workflow wherever the joined path resolves. An absolute workflow value, or one containing .., writes outside the job storage directory. The values come from a stored manifest that an API client controls at create time. Resolve the path and confirm it stays under manifest_dir.

🤖 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 `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/manifest.py` around
lines 236 - 242, Validate the resolved workflow path in the manifest-writing
flow before creating directories or writing defense_workflow. Using
workflow_file and manifest_dir, reject absolute agent["workflow"] values and any
resolved path that escapes manifest_dir, including traversal through
project_dir; only proceed with mkdir and write_text when the resolved path
remains under manifest_dir.
plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/synth_benign.py-108-137 (1)

108-137: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

A failure before _run_service leaves a pre-created run record stuck at running.

compile can hand a run_name in the step config. If _materialize_manifest, require_provisioned, or check_victim_secrets raises, run() returns a failed result but never touches that record. Studio then shows the run as running forever. Finalize the record in the except branch when config.get("run_name") is set, as the war-game job does.

🤖 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 `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/synth_benign.py`
around lines 108 - 137, Update synth-benign’s run exception path to finalize the
pre-created run record when config.get("run_name") is set, including failures
from _materialize_manifest, require_provisioned, or check_victim_secrets before
_run_service executes. Mirror the existing war-game job’s record-finalization
behavior, then preserve the current classified failure result.
plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/manifests.py-585-590 (1)

585-590: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard the fileset cleanup so a storage error does not turn a successful delete into a 500.

The entity is already deleted at this point. If delete_fileset raises, the endpoint returns 500 and the caller assumes the manifest still exists.

🛡️ Proposed fix
     for ref in (existing.agent_fileset, existing.project_fileset):
         if ref:
-            await run_in_threadpool(delete_fileset, sdk, ref)
+            try:
+                await run_in_threadpool(delete_fileset, sdk, ref)
+            except Exception:  # the entity is gone; an orphan bundle must not fail the delete
+                logger.warning("failed to delete fileset '%s' for manifest '%s'", ref, name, exc_info=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 `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/manifests.py`
around lines 585 - 590, Guard the post-deletion fileset cleanup loop in the
manifest deletion flow so exceptions from delete_fileset do not propagate as
endpoint failures. Keep the entity deletion successful and handle or log cleanup
errors around each fileset independently, preserving cleanup attempts for both
existing.agent_fileset and existing.project_fileset.
plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/execution.py-278-283 (1)

278-283: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Tear down the sandbox when up fails.

up can fail after it started containers, for example when the health wait times out. This early return skips the finally block, so the victim container and its port forward stay allocated. Move the teardown to cover this path.

🛡️ Proposed fix
     if up_failure is not None:
+        _teardown_sandbox(bin_path, manifest, env, ctx)
         return RunOutcome("failed", up_done.returncode, record_name=record_name, failure=up_failure)
🤖 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 `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/execution.py` around
lines 278 - 283, Ensure the sandbox teardown/finally scope also covers the
_run_iron_swarm invocation for the “up” command. Restructure the up_failure
handling so it records or propagates the failure only after cleanup runs, while
preserving the existing failed RunOutcome values and releasing the victim
container and port forward.
🟡 Minor comments (19)
plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/main.py-418-424 (1)

418-424: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle a malformed mitigations.json.

json.loads raises JSONDecodeError and prints a traceback. The adjacent missing-file case gets a clean message. Also guard defense_ids against a payload that is not the expected shape.

🐛 Proposed fix
-            mitigations = json.loads(path.read_text(encoding="utf-8"))
+            try:
+                mitigations = json.loads(path.read_text(encoding="utf-8"))
+            except (OSError, json.JSONDecodeError) as exc:
+                typer.secho(f"Could not read mitigations file {mitigations_file}: {exc}", fg="red")
+                raise typer.Exit(code=1) from exc
🤖 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 `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/main.py` around lines
418 - 424, Update the mitigations-loading flow in the CLI around path.read_text,
json.loads, and defense_ids to catch malformed JSON and report a concise red
error before exiting with code 1, matching the existing missing-file handling.
Validate that the decoded payload has the expected shape before passing it to
defense_ids, and handle invalid payloads with the same clean failure behavior.
web/packages/studio/src/routes/NewIronSwarmManifestRoute/index.tsx-147-151 (1)

147-151: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Neither manifest path bounds the victim port. Both forms accept a port outside 1-65535 and write it into the manifest.

  • web/packages/studio/src/routes/NewIronSwarmManifestRoute/index.tsx#L147-L151: extend the check to reject port < 1 and port > 65535.
  • web/packages/studio/src/components/ironSwarm/ProjectManifestWizard.tsx#L131-L131: replace positive() with .min(1).max(65535) and attach a message to the type check so non-numeric input reports a clear error.
🤖 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 `@web/packages/studio/src/routes/NewIronSwarmManifestRoute/index.tsx` around
lines 147 - 151, Bound port validation in NewIronSwarmManifestRoute/index.tsx
lines 147-151 to reject values below 1 or above 65535 while preserving the
whole-number check. In ProjectManifestWizard.tsx line 131, replace positive()
with min/max validation for 1–65535 and provide a clear message for non-numeric
input.
web/packages/studio/src/components/ironSwarm/ModelGroupFields.tsx-186-208 (1)

186-208: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Clear testResult when the model or endpoint changes.

The verdict text stays on screen after the operator edits model, base_url, or the secret. The stale "Connection OK." then describes a configuration that no longer exists. Reset the result in each onChange.

🐛 Proposed fix
+  const update = (patch: Partial<ModelChoice>) => {
+    setTestResult(null);
+    onChange(withGroup(value, group, patch));
+  };

Then call update({ model: e.target.value || undefined }) and the equivalent for base_url and api_key_secret.

🤖 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 `@web/packages/studio/src/components/ironSwarm/ModelGroupFields.tsx` around
lines 186 - 208, Update the model, base_url, and api_key_secret change handlers
in ModelGroupFields to clear testResult whenever any of those values changes.
Preserve the existing normalized values and onChange/update behavior while
resetting the stale connection verdict in each handler.
web/packages/studio/src/constants/routes.ts-113-117 (1)

113-117: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Namespace resource detail routes.

React Router resolves /iron-swarm/manifests to ironSwarmManifestList, so a run named manifests is unreachable. The NAME_PATTERN accepts new, so a manifest named new is also unreachable because /iron-swarm/manifests/new resolves to ironSwarmManifestNew. Namespace runs under /iron-swarm/runs/:ironSwarmRunName and reserve or namespace the manifest new route.

🤖 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 `@web/packages/studio/src/constants/routes.ts` around lines 113 - 117, Update
the ironSwarm route definitions so run details use the
`/iron-swarm/runs/:ironSwarmRunName` namespace, preventing the `manifests` run
name from colliding with manifest routes. Also adjust the manifest detail/new
route structure to reserve or namespace the `new` endpoint, while preserving
manifest list and creation navigation.
web/packages/studio/src/components/ironSwarm/eventTypes.ts-4-6 (1)

4-6: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale transport reference: SSE was replaced by JSON polling.

Lines 4-6 and 91-92 describe an "SSE relay" and "plugin SSE endpoint". The PR changed event delivery to JSON polling. Update both comments so readers do not look for an SSE endpoint.

🤖 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 `@web/packages/studio/src/components/ironSwarm/eventTypes.ts` around lines 4 -
6, Update the comments near the event catalog and the plugin endpoint reference
in eventTypes.ts to describe JSON polling instead of SSE, including removing
references to the SSE relay and plugin SSE endpoint while preserving the
existing event-source and rendering context.
web/packages/studio/src/routes/IronSwarmManifestDetailRoute/index.tsx-270-286 (1)

270-286: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Environment values containing a comma are silently truncated.

saveEnv splits the draft on , before it finds =. A value such as HOSTS=a,b is parsed as HOSTS=a plus a discarded fragment. The user gets a success toast and loses data.

Reject entries that fail to parse, or switch the dialog to newline-separated pairs.

🤖 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 `@web/packages/studio/src/routes/IronSwarmManifestDetailRoute/index.tsx` around
lines 270 - 286, Update saveEnv so environment values containing commas are not
silently truncated: either parse a format that preserves commas, such as
newline-separated key/value pairs, or validate each comma-separated entry and
reject malformed fragments before calling clearManifest.mutateAsync. Only show
the success toast after all entries are valid and saved; retain the existing
error-toast path for rejected input or mutation failures.
web/packages/studio/src/routes/IronSwarmManifestDetailRoute/index.tsx-303-311 (1)

303-311: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Revoking the object URL synchronously can cancel the download.

URL.revokeObjectURL runs in the same tick as anchor.click(). Some browsers have not started reading the blob yet, so the download fails. Defer the revoke.

Proposed fix
     anchor.click();
-    URL.revokeObjectURL(url);
+    setTimeout(() => URL.revokeObjectURL(url), 0);
🤖 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 `@web/packages/studio/src/routes/IronSwarmManifestDetailRoute/index.tsx` around
lines 303 - 311, Update downloadCsv to defer URL.revokeObjectURL until after the
browser has initiated the anchor download, rather than revoking it synchronously
after anchor.click(). Keep the existing blob creation, filename, and click
behavior unchanged.
web/packages/studio/src/components/ironSwarm/InterviewPanel.tsx-33-36 (1)

33-36: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Defaults do not re-seed when prompt changes.

useState runs its initializer once. If the parent renders a second prompt at the same position, the component stays mounted and answers keeps the previous gaps. Questions then show no selection, and submit sends empty answers.

Give the panel a key derived from the prompt at the call site, or re-seed on prompt change.

Proposed fix at the call site
-                <InterviewPanel
+                <InterviewPanel
+                  key={gen.interview.questions.map((q) => q.gap).join('|')}
                   prompt={gen.interview}
🤖 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 `@web/packages/studio/src/components/ironSwarm/InterviewPanel.tsx` around lines
33 - 36, Update InterviewPanel so its answers state is re-seeded whenever prompt
changes, preserving defaults for the new prompt’s questions; alternatively, at
the component’s call site provide a key derived from the prompt so React
remounts it. Use the existing defaultAnswer initialization path and ensure
submissions reflect the current prompt rather than prior gaps.
web/packages/studio/src/components/ironSwarm/hitlTypes.ts-46-64 (1)

46-64: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Prompts are cast without validating their array payloads.

pendingInterview and pendingReview check only round. questions and suite are asserted, not verified. If the job writes a prompt without those keys, consumers that map over them (for example ReviewPanel rendering review.suite) throw at render.

Guard the arrays and normalize to [].

🛡️ Proposed fix
   if (!interview || typeof interview.round !== 'number') return null;
   if (response?.round === interview.round) return null;
-  return interview;
+  return { round: interview.round, questions: Array.isArray(interview.questions) ? interview.questions : [] };
@@
   if (!review || typeof review.round !== 'number') return null;
   if (response?.round === review.round) return null;
-  return review;
+  return { round: review.round, suite: Array.isArray(review.suite) ? review.suite : [] };
🤖 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 `@web/packages/studio/src/components/ironSwarm/hitlTypes.ts` around lines 46 -
64, Update pendingInterview and pendingReview to validate their prompt array
payloads before returning them, requiring interview.questions and review.suite
to be arrays alongside the existing round checks. Normalize missing or invalid
arrays to [] as appropriate so consumers can safely iterate them, while
preserving the existing null response-round behavior.
plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/events.py-109-119 (1)

109-119: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Router description still advertises SSE.

This module now implements polling only. service.py registers this router with the description "Live run-event ingest (from the run) + SSE stream (to Studio)". That text reaches the generated OpenAPI tag description, so API consumers see a transport that no longer exists. Update the description in service.py to describe the poll endpoint.

🤖 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 `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/events.py` around
lines 109 - 119, Update the router description supplied by service.py when
registering this events router so it describes the polling endpoint and no
longer mentions an SSE stream. Keep the existing ingest-run context and ensure
the generated OpenAPI tag description matches the module’s polling-only
behavior.
plugins/nemo-iron-swarm/tests/unit/test_compose_defense.py-97-106 (1)

97-106: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not preserve success for a nonexistent run.

This test creates no run-1 entity but asserts 200. It locks the route contract where /runs/{name}/compose-defense ignores {name}. Look up the run in the handler, return 404 when absent, and change this test to assert that result.

🤖 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 `@plugins/nemo-iron-swarm/tests/unit/test_compose_defense.py` around lines 97 -
106, Update the compose-defense handler to resolve the run identified by the
route’s name parameter and return 404 when it does not exist, rather than
composing a successful response unconditionally. Revise
test_compose_defense_endpoint_composes_selection to create or reference an
existing run for the success case, and add or update the nonexistent run
assertion to expect 404.
plugins/nemo-iron-swarm/README.md-300-312 (1)

300-312: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use one manifest lifecycle contract.

Lines 300-312 say a manifest freezes its resolved target until refresh. Lines 441-447 say an agent-source manifest re-resolves on every run. These rules give opposite results for run comparability and refresh behavior. Document the implemented contract consistently.

As per PR objectives, manifests freeze resolved targets and explicit refresh incorporates agent changes.

Also applies to: 441-447

🤖 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 `@plugins/nemo-iron-swarm/README.md` around lines 300 - 312, Update the
manifest lifecycle documentation around the frozen-target guidance and the
agent-source manifest section to consistently state that manifests preserve
their resolved targets across runs. Document explicit refresh as the mechanism
that incorporates agent changes, and remove or revise the claim that
agent-source manifests re-resolve on every run.
plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/model_preflight.py-72-76 (1)

72-76: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Handle non-object JSON responses.

If resp.json() returns an array or scalar, .get() raises an uncaught AttributeError. Validate the decoded object before accessing "data" and return the existing soft-pass result.

🤖 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 `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/model_preflight.py` around
lines 72 - 76, Update the JSON handling in the model preflight probe to validate
that resp.json() returns an object with mapping behavior before calling
.get("data"). For arrays or scalar JSON responses, return the existing
reachable/authenticated soft-pass result with list_supported=False and the
current detail, while preserving normal processing for valid objects.
plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/synth_client.py-60-70 (1)

60-70: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Classify malformed synth responses as synth-service failures.

Catch ValueError from invalid JSON and reject non-object JSON before returning from _post.

🤖 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 `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/synth_client.py`
around lines 60 - 70, Update _post to catch ValueError from resp.json() and
classify it as CATEGORY_SYNTH_SERVICE via IronSwarmRunError. Validate that the
decoded JSON is an object/dict before returning it, and raise the same
synth-service failure for any other JSON shape while preserving existing
HTTPError handling.
plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/service.py-69-74 (1)

69-74: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Stale "SSE stream" wording; events are polled JSON.

Event delivery moved from SSE to cursor-based JSON polling. GET /runs/{name}/events returns EventsResponse with an after cursor. This RouterSpec description surfaces in the generated OpenAPI tag, so the wrong wording reaches API consumers. The module docstring at Line 9 has the same problem.

📝 Proposed fix
             RouterSpec(
                 router=events.router,
                 tag="Iron Swarm Events",
-                description="Live run-event ingest (from the run) + SSE stream (to Studio).",
+                description="Run-event ingest (from the run) + polled event reads (to Studio).",
                 prefix="/v2/workspaces/{workspace}",
             ),
🤖 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 `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/service.py` around lines
69 - 74, Update the events RouterSpec description to describe cursor-based JSON
polling rather than an SSE stream, and revise the module docstring to remove the
stale SSE wording. Keep the description accurate for GET /runs/{name}/events
returning EventsResponse with an after cursor.
plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/manifests.py-114-132 (1)

114-132: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Return the serialized filter, not the model.

Line 114 computes filter_dict, but line 131 returns the raw filter model. The response then includes every unset field as null, which disagrees with the filter actually applied.

♻️ Proposed fix
-        "filter": filter or None,
+        "filter": filter_dict or None,
🤖 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 `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/manifests.py`
around lines 114 - 132, Update the response construction in the manifest-listing
function to return the computed serialized filter_dict instead of the raw filter
model. Preserve the existing filter or None behavior when no filter values are
provided, while ensuring unset fields remain excluded consistently with the
filter passed to entity_client.list.
plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/manifest.py-165-168 (1)

165-168: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard against non-mapping backends entries.

manifest.get("backends") comes from stored YAML. If any entry is not a mapping, b.get("name") raises AttributeError and the run fails with an unclassified error instead of a manifest error.

🛡️ Proposed fix
-        others = [b for b in (manifest.get("backends") or []) if b.get("name") != gw_backend.get("name")]
+        others = [
+            b
+            for b in (manifest.get("backends") or [])
+            if isinstance(b, dict) and b.get("name") != gw_backend.get("name")
+        ]
🤖 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 `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/manifest.py` around
lines 165 - 168, Update the backends filtering logic in the manifest handling
block to validate each entry is a mapping before calling b.get("name"). Preserve
valid mappings, exclude or reject non-mapping entries using the existing
manifest-error handling path, and ensure malformed YAML produces a classified
manifest error instead of an AttributeError.
plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/runs.py-142-158 (1)

142-158: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Return 409 for optimistic-lock conflicts. NemoEntitiesClient.update() sends expected_db_version and prevents lost updates, but this handler maps NemoEntityConflictError to 500. Catch it explicitly and return 409.

🤖 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 `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/runs.py` around
lines 142 - 158, Update the exception handling around
entity_client.update(agent) to catch NemoEntityConflictError explicitly and
raise an HTTPException with status 409, preserving the conflict as the cause;
keep the existing generic exception path returning 500 for other update
failures.
plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/tasks/war_game/__main__.py-25-38 (1)

25-38: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Move sandbox teardown to cover up. If SIGTERM arrives after up starts the sandbox but before it returns, _shutdown_handler exits before _teardown_sandbox is registered. subprocess.run also does not clean up descendant processes. Wrap up in the try/finally block and terminate the subprocess group during cancellation.

🤖 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
`@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/tasks/war_game/__main__.py`
around lines 25 - 38, Update the war-game task’s up/teardown flow so sandbox
cleanup is registered before invoking `up`, ensuring SIGTERM during `up` still
reaches teardown. In the teardown logic, terminate the subprocess process group
rather than only the direct process, while preserving normal cleanup and
cancellation behavior.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: d58ccc27-2434-466b-b2b6-71af8e07923b

📥 Commits

Reviewing files that changed from the base of the PR and between 58a02de and d209e76.

⛔ Files ignored due to path filters (3)
  • uv.lock is excluded by !**/*.lock
  • web/packages/sdk/generated/iron-swarm/api.ts is excluded by !**/generated/**
  • web/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (116)
  • .gitignore
  • docs/iron-swarm-review/findings.md
  • plugins/nemo-iron-swarm/.gitignore
  • plugins/nemo-iron-swarm/README.md
  • plugins/nemo-iron-swarm/openapi/openapi.yaml
  • plugins/nemo-iron-swarm/pyproject.toml
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/_perms.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/agent_resolver.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/_filters.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/events.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/jobs.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/manifests.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/runs.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/schemas.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/authz.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/checks.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/client.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/credentials.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/main.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/provisioning.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/config.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/entities.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/filesets.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/_common.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/artifacts.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/benign_suite.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/defenses.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/errors.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/execution.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/hitl.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/manifest.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/records.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/run.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/spec.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/synth_benign.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/synth_client.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/model_config.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/model_preflight.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/sdk.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/service.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/skills.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/skills/iron-swarm/SKILL.md
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/tasks/synth_benign/__main__.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/tasks/war_game/__main__.py
  • plugins/nemo-iron-swarm/tests/unit/_doubles.py
  • plugins/nemo-iron-swarm/tests/unit/test_agent_resolver.py
  • plugins/nemo-iron-swarm/tests/unit/test_api_manifests.py
  • plugins/nemo-iron-swarm/tests/unit/test_api_runs.py
  • plugins/nemo-iron-swarm/tests/unit/test_apply_mitigation.py
  • plugins/nemo-iron-swarm/tests/unit/test_artifacts.py
  • plugins/nemo-iron-swarm/tests/unit/test_benign_suite.py
  • plugins/nemo-iron-swarm/tests/unit/test_compose_defense.py
  • plugins/nemo-iron-swarm/tests/unit/test_errors.py
  • plugins/nemo-iron-swarm/tests/unit/test_events.py
  • plugins/nemo-iron-swarm/tests/unit/test_filesets.py
  • plugins/nemo-iron-swarm/tests/unit/test_garak_provision.py
  • plugins/nemo-iron-swarm/tests/unit/test_model_config.py
  • plugins/nemo-iron-swarm/tests/unit/test_model_preflight.py
  • plugins/nemo-iron-swarm/tests/unit/test_operator_env.py
  • plugins/nemo-iron-swarm/tests/unit/test_preflight.py
  • plugins/nemo-iron-swarm/tests/unit/test_run_cli.py
  • plugins/nemo-iron-swarm/tests/unit/test_run_record.py
  • plugins/nemo-iron-swarm/tests/unit/test_run_service.py
  • plugins/nemo-iron-swarm/tests/unit/test_sanity_check_cli.py
  • plugins/nemo-iron-swarm/tests/unit/test_sdk_resources.py
  • plugins/nemo-iron-swarm/tests/unit/test_service.py
  • plugins/nemo-iron-swarm/tests/unit/test_synth_benign.py
  • plugins/nemo-iron-swarm/tests/unit/test_synth_hitl.py
  • pyproject.toml
  • services/studio/src/nmp/studio/env_mappings.py
  • web/packages/sdk/orval/constants.ts
  • web/packages/sdk/package.json
  • web/packages/studio/env/.env.fastapi
  • web/packages/studio/package.json
  • web/packages/studio/src/api/ironSwarm.ts
  • web/packages/studio/src/components/dataViews/IronSwarmManifestsDataView/index.tsx
  • web/packages/studio/src/components/dataViews/IronSwarmRunsDataView/index.tsx
  • web/packages/studio/src/components/ironSwarm/BenignInterviewCard.tsx
  • web/packages/studio/src/components/ironSwarm/BenignSuiteEditor.tsx
  • web/packages/studio/src/components/ironSwarm/BenignSuiteTable.tsx
  • web/packages/studio/src/components/ironSwarm/HardenPanel.tsx
  • web/packages/studio/src/components/ironSwarm/InterviewPanel.tsx
  • web/packages/studio/src/components/ironSwarm/ModelGroupFields.tsx
  • web/packages/studio/src/components/ironSwarm/ProjectManifestWizard.tsx
  • web/packages/studio/src/components/ironSwarm/ReconChecklist.tsx
  • web/packages/studio/src/components/ironSwarm/ReviewPanel.tsx
  • web/packages/studio/src/components/ironSwarm/SanityCheckReport.tsx
  • web/packages/studio/src/components/ironSwarm/TargetPanel.tsx
  • web/packages/studio/src/components/ironSwarm/YamlDiff.tsx
  • web/packages/studio/src/components/ironSwarm/eventTypes.ts
  • web/packages/studio/src/components/ironSwarm/hitlTypes.ts
  • web/packages/studio/src/components/ironSwarm/swarm/MessageFeed.tsx
  • web/packages/studio/src/components/ironSwarm/swarm/NodeDetail.tsx
  • web/packages/studio/src/components/ironSwarm/swarm/SwarmGraph.tsx
  • web/packages/studio/src/components/ironSwarm/swarm/swarmModel.test.ts
  • web/packages/studio/src/components/ironSwarm/swarm/swarmModel.ts
  • web/packages/studio/src/components/ironSwarm/swarm/useSwarmEvents.ts
  • web/packages/studio/src/components/ironSwarm/useGenerateBenignSuite.ts
  • web/packages/studio/src/components/ironSwarm/useMitigations.test.ts
  • web/packages/studio/src/components/ironSwarm/useMitigations.ts
  • web/packages/studio/src/components/ironSwarm/useRunWarGame.ts
  • web/packages/studio/src/components/ironSwarm/useSanityCheck.ts
  • web/packages/studio/src/constants/environment.ts
  • web/packages/studio/src/constants/featureFlags/featureFlags.ts
  • web/packages/studio/src/constants/routes.ts
  • web/packages/studio/src/routes/IronSwarmManifestDetailRoute/index.tsx
  • web/packages/studio/src/routes/IronSwarmManifestListRoute/index.tsx
  • web/packages/studio/src/routes/IronSwarmRunDetailsRoute/index.tsx
  • web/packages/studio/src/routes/IronSwarmRunListRoute/index.tsx
  • web/packages/studio/src/routes/NewIronSwarmManifestRoute/index.tsx
  • web/packages/studio/src/routes/WorkspaceLayout/WorkspaceSideNav.tsx
  • web/packages/studio/src/routes/groups/index.ts
  • web/packages/studio/src/routes/groups/ironSwarmRoutes.tsx
  • web/packages/studio/src/routes/index.tsx
  • web/packages/studio/src/routes/utils.ts
  • web/packages/studio/src/tests/title-change.test.tsx

Comment thread plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/manifests.py Outdated
Comment thread plugins/nemo-iron-swarm/web/src/components/swarm/useSwarmEvents.ts
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

Comment thread plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/runs.py Fixed
Comment thread plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/runs.py Fixed
Comment thread plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/runs.py Fixed
@koralchapnik koralchapnik changed the title NAIS-357: add the nemo-iron-swarm plugin (agent red-teaming and hardening) feat(iron-swarm): add the nemo-iron-swarm plugin (agent red-teaming and hardening) Aug 6, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 8

Caution

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

⚠️ Outside diff range comments (1)
plugins/nemo-iron-swarm/README.md (1)

1-448: 📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift

Split this README by documentation purpose.

This page mixes tutorial, how-to, reference, troubleshooting, and architecture content. Split these into Diataxis-specific pages. Put prerequisites before procedural steps. Use Sphinx substitutions for product names.

As per coding guidelines, “Each documentation page should fit ONE Diataxis quadrant” and product names must use Sphinx substitutions.

🤖 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 `@plugins/nemo-iron-swarm/README.md` around lines 1 - 448, Split the README
into Diataxis-focused pages: a tutorial for Quickstart, how-to pages for UI/CLI
execution and setup, a reference page for environment variables and command
behavior, a troubleshooting page for failure scenarios, and an explanation page
for architecture/how it works. Put prerequisites before procedural instructions,
link the pages together, and replace literal product names throughout with the
repository’s Sphinx product substitutions.

Source: Coding guidelines

🧹 Nitpick comments (5)
web/packages/studio/src/components/ironSwarm/BenignInterviewCard.tsx (1)

6-6: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use a type-only import for FC.

FC is used only as a type. Replace the value import with import type.

Proposed change
-import { FC } from 'react';
+import type { FC } from 'react';

Verify with pnpm lint:fix from web/. As per coding guidelines, use import type for type-only imports.

🤖 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 `@web/packages/studio/src/components/ironSwarm/BenignInterviewCard.tsx` at line
6, Update the FC import in BenignInterviewCard to use a type-only import, since
FC is referenced only as a type; leave the component implementation unchanged
and verify the formatting with pnpm lint:fix from web/.

Source: Coding guidelines

plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/manifests.py (1)

555-566: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Unhandled entity errors in refresh_manifest. entity_client.update(existing) at Line 562 is not wrapped, unlike update_manifest. A concurrent modification surfaces as an unhandled exception instead of 409, and apply-mitigation then reports the manifest as unrefreshable without a reason. Mirror the NemoEntityNotFoundError/NemoEntityConflictError handling used at Lines 516-521.

🤖 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 `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/manifests.py`
around lines 555 - 566, Update refresh_manifest around
entity_client.update(existing) to catch NemoEntityNotFoundError and
NemoEntityConflictError, matching the handling already used by update_manifest.
Translate these exceptions into the established 404/409 responses so concurrent
modifications and missing entities are reported with their reasons instead of
escaping as unhandled errors.
plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/synth_client.py (1)

110-117: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Reap the process after kill().

proc.kill() without a following wait() leaves a zombie for the life of the job process.

Proposed fix
     except subprocess.TimeoutExpired:
         proc.kill()
+        with contextlib.suppress(subprocess.TimeoutExpired):
+            proc.wait(timeout=5)
🤖 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 `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/synth_client.py`
around lines 110 - 117, Update _terminate so that after proc.kill() handles the
subprocess timeout, it also waits for the process to exit and reap it before
returning.
plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/events.py (1)

55-73: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

_seq is write-only state.

Ids come from line numbers in history(). _seq is never read. Drop it and the docstring claim about it.

🤖 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 `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/events.py` around
lines 55 - 73, Remove the unused _seq state from the event store, including its
initialization in __init__ and increment in publish. Delete the docstring claim
that _seq is seeded for monotonic IDs, while preserving line-number-based IDs
and existing history persistence behavior.
plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/_common.py (1)

78-84: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Wrap the secret lookup so a bad secret name gets classified.

sdk.secrets.access raises on a missing or unauthorized secret. That exception escapes _apply_group unclassified, so the run fails without the missing_credential category or remediation text.

Proposed fix
 def _resolve_secret(sdk: Any, name: str, workspace: str) -> str | None:
     """Fetch a Secret's plaintext value via the platform SDK; None if unavailable (caller warns/fails)."""
     if sdk is None:
         return None
-    secret = sdk.secrets.access(name, workspace=workspace)
+    try:
+        secret = sdk.secrets.access(name, workspace=workspace)
+    except Exception as exc:
+        raise IronSwarmRunError(
+            CATEGORY_MISSING_CREDENTIAL,
+            f"could not read secret {name!r} from workspace {workspace!r}: {exc}",
+        ) from exc
     value = getattr(secret, "value", None)
     return str(value) if value else None
🤖 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 `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/_common.py` around
lines 78 - 84, Update _resolve_secret to catch exceptions from
sdk.secrets.access and classify missing or unauthorized secret lookups as
missing_credential, including the existing remediation text expected by
_apply_group. Preserve the current None return for an unavailable SDK or
absent/empty secret value, and ensure the classified error propagates through
_apply_group’s normal failure handling.
🤖 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 `@plugins/nemo-iron-swarm/README.md`:
- Line 160: Replace the unpinned install.sh command in the README with a pinned
release artifact or commit-specific URL, and require verification of its
published checksum or signature before executing it. Do not pipe mutable remote
content directly to sh; document the download, verification, and execution
steps.
- Around line 441-447: Update the manifest behavior description near the
agent-source manifest explanation to state that manifests use frozen target
filesets and are not re-resolved automatically on each run. Explain that changes
to the registered agent take effect only after an explicit refresh, while
preserving the distinction between persisted settings and rendered YAML.

In `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/events.py`:
- Around line 140-167: The fileset fallback reads the run-specific path from
_events_path, but download_fileset extracts events.jsonl into its parent
directory, so the re-read misses the downloaded log. In
plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/events.py:140-167,
update _fileset_fallback to download into a temporary directory and read the
extracted member, or copy events.jsonl to stream._path before calling history;
in plugins/nemo-iron-swarm/tests/unit/test_events.py:88-120, patch _events_path
to return a run-specific path such as tmp_path / "missing" / "my-run.jsonl" so
the test validates the real filename contract.

In `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/manifests.py`:
- Around line 588-590: The delete_manifest cleanup loop must not unconditionally
delete the client-owned project_fileset referenced by other manifests. Update
the manifest lifecycle around _build_project_manifest and delete_manifest to
either copy project_fileset into a service-owned fileset during creation, or
check for remaining manifest references before deleting it; retain unconditional
cleanup for the service-created agent_fileset.

In `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/main.py`:
- Around line 278-285: The local manifest messaging around the manifest
rendering in the CLI must match the behavior of run --config and both source
types. Update the comments and rendered header to define a consistent contract:
local manifests generated by init remain valid runnable inputs, and avoid
claiming that every source has an agent ref because --project-dir sources do
not.
- Around line 387-434: Update the sanity_check command after
ctx.sdk.iron_swarm.sanity_check returns its result: continue printing the JSON
result, then raise a nonzero Typer exit unless result status equals "completed".
Preserve normal success behavior for completed results.

In `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/provisioning.py`:
- Around line 71-73: Update the logging in the provisioning flow around
config.index_url to pass the URL through the existing redact_index_url helper
before typer.echo displays it, while continuing to use the original URL in
install_cmd for installation.

In `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/synth_client.py`:
- Around line 84-87: Update the docstring for the server-spawning function
around _await_ready to state that it raises IronSwarmRunError instead of
RuntimeError, preserving the existing description of early exit and readiness
timeout.

---

Outside diff comments:
In `@plugins/nemo-iron-swarm/README.md`:
- Around line 1-448: Split the README into Diataxis-focused pages: a tutorial
for Quickstart, how-to pages for UI/CLI execution and setup, a reference page
for environment variables and command behavior, a troubleshooting page for
failure scenarios, and an explanation page for architecture/how it works. Put
prerequisites before procedural instructions, link the pages together, and
replace literal product names throughout with the repository’s Sphinx product
substitutions.

---

Nitpick comments:
In `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/events.py`:
- Around line 55-73: Remove the unused _seq state from the event store,
including its initialization in __init__ and increment in publish. Delete the
docstring claim that _seq is seeded for monotonic IDs, while preserving
line-number-based IDs and existing history persistence behavior.

In `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/manifests.py`:
- Around line 555-566: Update refresh_manifest around
entity_client.update(existing) to catch NemoEntityNotFoundError and
NemoEntityConflictError, matching the handling already used by update_manifest.
Translate these exceptions into the established 404/409 responses so concurrent
modifications and missing entities are reported with their reasons instead of
escaping as unhandled errors.

In `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/_common.py`:
- Around line 78-84: Update _resolve_secret to catch exceptions from
sdk.secrets.access and classify missing or unauthorized secret lookups as
missing_credential, including the existing remediation text expected by
_apply_group. Preserve the current None return for an unavailable SDK or
absent/empty secret value, and ensure the classified error propagates through
_apply_group’s normal failure handling.

In `@plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/synth_client.py`:
- Around line 110-117: Update _terminate so that after proc.kill() handles the
subprocess timeout, it also waits for the process to exit and reap it before
returning.

In `@web/packages/studio/src/components/ironSwarm/BenignInterviewCard.tsx`:
- Line 6: Update the FC import in BenignInterviewCard to use a type-only import,
since FC is referenced only as a type; leave the component implementation
unchanged and verify the formatting with pnpm lint:fix from web/.
🪄 Autofix

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: CHILL

Plan: Enterprise

Run ID: af40b6bc-cba0-488a-b643-813cf84f9b42

📥 Commits

Reviewing files that changed from the base of the PR and between a1b7051 and 44d0f85.

⛔ Files ignored due to path filters (3)
  • uv.lock is excluded by !**/*.lock
  • web/packages/sdk/generated/iron-swarm/api.ts is excluded by !**/generated/**
  • web/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (116)
  • .gitignore
  • plugins/nemo-iron-swarm/.gitignore
  • plugins/nemo-iron-swarm/README.md
  • plugins/nemo-iron-swarm/openapi/openapi.yaml
  • plugins/nemo-iron-swarm/pyproject.toml
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/_perms.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/agent_resolver.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/_filters.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/events.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/jobs.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/manifests.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/runs.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/schemas.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/authz.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/checks.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/client.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/credentials.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/main.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/provisioning.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/config.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/entities.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/filesets.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/_common.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/artifacts.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/benign_suite.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/defenses.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/errors.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/execution.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/hitl.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/manifest.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/records.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/run.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/spec.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/synth_benign.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/synth_client.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/model_config.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/model_preflight.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/sdk.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/service.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/skills.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/skills/iron-swarm/SKILL.md
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/tasks/synth_benign/__main__.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/tasks/war_game/__main__.py
  • plugins/nemo-iron-swarm/tests/unit/_doubles.py
  • plugins/nemo-iron-swarm/tests/unit/test_agent_resolver.py
  • plugins/nemo-iron-swarm/tests/unit/test_api_manifests.py
  • plugins/nemo-iron-swarm/tests/unit/test_api_runs.py
  • plugins/nemo-iron-swarm/tests/unit/test_apply_mitigation.py
  • plugins/nemo-iron-swarm/tests/unit/test_artifacts.py
  • plugins/nemo-iron-swarm/tests/unit/test_benign_suite.py
  • plugins/nemo-iron-swarm/tests/unit/test_compose_defense.py
  • plugins/nemo-iron-swarm/tests/unit/test_errors.py
  • plugins/nemo-iron-swarm/tests/unit/test_events.py
  • plugins/nemo-iron-swarm/tests/unit/test_filesets.py
  • plugins/nemo-iron-swarm/tests/unit/test_garak_provision.py
  • plugins/nemo-iron-swarm/tests/unit/test_model_config.py
  • plugins/nemo-iron-swarm/tests/unit/test_model_preflight.py
  • plugins/nemo-iron-swarm/tests/unit/test_operator_env.py
  • plugins/nemo-iron-swarm/tests/unit/test_preflight.py
  • plugins/nemo-iron-swarm/tests/unit/test_run_cli.py
  • plugins/nemo-iron-swarm/tests/unit/test_run_record.py
  • plugins/nemo-iron-swarm/tests/unit/test_run_service.py
  • plugins/nemo-iron-swarm/tests/unit/test_sanity_check_cli.py
  • plugins/nemo-iron-swarm/tests/unit/test_sdk_resources.py
  • plugins/nemo-iron-swarm/tests/unit/test_service.py
  • plugins/nemo-iron-swarm/tests/unit/test_synth_benign.py
  • plugins/nemo-iron-swarm/tests/unit/test_synth_hitl.py
  • pyproject.toml
  • pytest.ini
  • services/studio/src/nmp/studio/env_mappings.py
  • web/packages/sdk/orval/constants.ts
  • web/packages/sdk/package.json
  • web/packages/studio/env/.env.fastapi
  • web/packages/studio/package.json
  • web/packages/studio/src/api/ironSwarm.ts
  • web/packages/studio/src/components/dataViews/IronSwarmManifestsDataView/index.tsx
  • web/packages/studio/src/components/dataViews/IronSwarmRunsDataView/index.tsx
  • web/packages/studio/src/components/ironSwarm/BenignInterviewCard.tsx
  • web/packages/studio/src/components/ironSwarm/BenignSuiteEditor.tsx
  • web/packages/studio/src/components/ironSwarm/BenignSuiteTable.tsx
  • web/packages/studio/src/components/ironSwarm/HardenPanel.tsx
  • web/packages/studio/src/components/ironSwarm/InterviewPanel.tsx
  • web/packages/studio/src/components/ironSwarm/ModelGroupFields.tsx
  • web/packages/studio/src/components/ironSwarm/ProjectManifestWizard.tsx
  • web/packages/studio/src/components/ironSwarm/ReconChecklist.tsx
  • web/packages/studio/src/components/ironSwarm/ReviewPanel.tsx
  • web/packages/studio/src/components/ironSwarm/SanityCheckReport.tsx
  • web/packages/studio/src/components/ironSwarm/TargetPanel.tsx
  • web/packages/studio/src/components/ironSwarm/YamlDiff.tsx
  • web/packages/studio/src/components/ironSwarm/eventTypes.ts
  • web/packages/studio/src/components/ironSwarm/hitlTypes.ts
  • web/packages/studio/src/components/ironSwarm/swarm/MessageFeed.tsx
  • web/packages/studio/src/components/ironSwarm/swarm/NodeDetail.tsx
  • web/packages/studio/src/components/ironSwarm/swarm/SwarmGraph.tsx
  • web/packages/studio/src/components/ironSwarm/swarm/swarmModel.test.ts
  • web/packages/studio/src/components/ironSwarm/swarm/swarmModel.ts
  • web/packages/studio/src/components/ironSwarm/swarm/useSwarmEvents.ts
  • web/packages/studio/src/components/ironSwarm/useGenerateBenignSuite.ts
  • web/packages/studio/src/components/ironSwarm/useMitigations.test.ts
  • web/packages/studio/src/components/ironSwarm/useMitigations.ts
  • web/packages/studio/src/components/ironSwarm/useRunWarGame.ts
  • web/packages/studio/src/components/ironSwarm/useSanityCheck.ts
  • web/packages/studio/src/constants/environment.ts
  • web/packages/studio/src/constants/featureFlags/featureFlags.ts
  • web/packages/studio/src/constants/routes.ts
  • web/packages/studio/src/routes/IronSwarmManifestDetailRoute/index.tsx
  • web/packages/studio/src/routes/IronSwarmManifestListRoute/index.tsx
  • web/packages/studio/src/routes/IronSwarmRunDetailsRoute/index.tsx
  • web/packages/studio/src/routes/IronSwarmRunListRoute/index.tsx
  • web/packages/studio/src/routes/NewIronSwarmManifestRoute/index.tsx
  • web/packages/studio/src/routes/WorkspaceLayout/WorkspaceSideNav.tsx
  • web/packages/studio/src/routes/groups/index.ts
  • web/packages/studio/src/routes/groups/ironSwarmRoutes.tsx
  • web/packages/studio/src/routes/index.tsx
  • web/packages/studio/src/routes/utils.ts
  • web/packages/studio/src/tests/title-change.test.tsx
🚧 Files skipped from review as they are similar to previous changes (86)
  • web/packages/studio/env/.env.fastapi
  • web/packages/studio/src/constants/routes.ts
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/tasks/war_game/main.py
  • plugins/nemo-iron-swarm/tests/unit/test_model_preflight.py
  • web/packages/studio/src/components/ironSwarm/swarm/useSwarmEvents.ts
  • plugins/nemo-iron-swarm/tests/unit/test_service.py
  • web/packages/studio/src/components/ironSwarm/HardenPanel.tsx
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/client.py
  • pyproject.toml
  • web/packages/studio/src/components/ironSwarm/useGenerateBenignSuite.ts
  • plugins/nemo-iron-swarm/tests/unit/test_model_config.py
  • web/packages/studio/src/components/ironSwarm/useRunWarGame.ts
  • plugins/nemo-iron-swarm/.gitignore
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/hitl.py
  • web/packages/studio/src/components/ironSwarm/swarm/swarmModel.test.ts
  • web/packages/studio/src/components/ironSwarm/hitlTypes.ts
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/credentials.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/defenses.py
  • web/packages/studio/src/routes/groups/index.ts
  • web/packages/studio/src/components/dataViews/IronSwarmManifestsDataView/index.tsx
  • plugins/nemo-iron-swarm/tests/unit/test_sdk_resources.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/service.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/spec.py
  • web/packages/studio/src/components/ironSwarm/swarm/NodeDetail.tsx
  • plugins/nemo-iron-swarm/tests/unit/test_run_record.py
  • plugins/nemo-iron-swarm/tests/unit/test_apply_mitigation.py
  • web/packages/studio/src/components/ironSwarm/BenignSuiteEditor.tsx
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/entities.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/tasks/synth_benign/main.py
  • web/packages/studio/package.json
  • plugins/nemo-iron-swarm/tests/unit/test_preflight.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/execution.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/errors.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/config.py
  • web/packages/studio/src/components/ironSwarm/ReviewPanel.tsx
  • web/packages/studio/src/constants/featureFlags/featureFlags.ts
  • web/packages/studio/src/components/ironSwarm/YamlDiff.tsx
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/run.py
  • plugins/nemo-iron-swarm/tests/unit/test_benign_suite.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/schemas.py
  • web/packages/studio/src/components/ironSwarm/BenignSuiteTable.tsx
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/artifacts.py
  • web/packages/studio/src/components/ironSwarm/swarm/swarmModel.ts
  • web/packages/studio/src/components/ironSwarm/swarm/MessageFeed.tsx
  • web/packages/studio/src/components/ironSwarm/ModelGroupFields.tsx
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/records.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/sdk.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/model_preflight.py
  • web/packages/sdk/orval/constants.ts
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/agent_resolver.py
  • web/packages/studio/src/routes/NewIronSwarmManifestRoute/index.tsx
  • web/packages/studio/src/components/ironSwarm/useSanityCheck.ts
  • web/packages/studio/src/components/ironSwarm/InterviewPanel.tsx
  • web/packages/studio/src/components/ironSwarm/TargetPanel.tsx
  • web/packages/studio/src/components/ironSwarm/ProjectManifestWizard.tsx
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/skills.py
  • web/packages/studio/src/components/ironSwarm/ReconChecklist.tsx
  • web/packages/studio/src/components/ironSwarm/swarm/SwarmGraph.tsx
  • web/packages/studio/src/routes/IronSwarmRunListRoute/index.tsx
  • web/packages/studio/src/components/ironSwarm/useMitigations.test.ts
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/model_config.py
  • web/packages/studio/src/routes/IronSwarmManifestListRoute/index.tsx
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/jobs.py
  • web/packages/studio/src/routes/index.tsx
  • web/packages/sdk/package.json
  • web/packages/studio/src/constants/environment.ts
  • web/packages/studio/src/tests/title-change.test.tsx
  • plugins/nemo-iron-swarm/tests/unit/test_artifacts.py
  • web/packages/studio/src/routes/groups/ironSwarmRoutes.tsx
  • plugins/nemo-iron-swarm/tests/unit/_doubles.py
  • plugins/nemo-iron-swarm/tests/unit/test_api_runs.py
  • web/packages/studio/src/routes/IronSwarmManifestDetailRoute/index.tsx
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/_perms.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/authz.py
  • services/studio/src/nmp/studio/env_mappings.py
  • web/packages/studio/src/routes/IronSwarmRunDetailsRoute/index.tsx
  • plugins/nemo-iron-swarm/tests/unit/test_compose_defense.py
  • web/packages/studio/src/components/dataViews/IronSwarmRunsDataView/index.tsx
  • plugins/nemo-iron-swarm/tests/unit/test_operator_env.py
  • web/packages/studio/src/components/ironSwarm/useMitigations.ts
  • web/packages/studio/src/api/ironSwarm.ts
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/jobs/manifest.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/_filters.py
  • web/packages/studio/src/components/ironSwarm/SanityCheckReport.tsx
  • web/packages/studio/src/routes/utils.ts
  • web/packages/studio/src/components/ironSwarm/eventTypes.ts

Comment thread plugins/nemo-iron-swarm/README.md Outdated
Comment thread plugins/nemo-iron-swarm/README.md Outdated
Comment thread plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/events.py
Comment thread plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/manifests.py Outdated
Comment thread plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/main.py Outdated
Comment thread plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/main.py
@github-actions

github-actions Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor
Suite Lines Covered Line Rate Branch Rate
Unit Tests 32408/41076 78.9% 63.8%
Integration Tests N/A N/A N/A

@github-actions github-actions Bot added the feat label Aug 6, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
plugins/nemo-iron-swarm/README.md (1)

183-189: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Keep this page in one Diataxis quadrant.

This block adds tutorial steps to a README that also contains configuration reference, troubleshooting, and architecture explanation. Move those sections to linked pages, or make this README one tutorial or how-to.

🤖 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 `@plugins/nemo-iron-swarm/README.md` around lines 183 - 189, Restructure the
README so it stays within a single Diataxis quadrant: move configuration
reference, troubleshooting, and architecture explanation out of this page into
linked documentation pages, or convert the entire README into a focused
tutorial/how-to and remove the unrelated sections. Keep the bootstrap, setup,
and doctor commands within the chosen scope.

Source: Coding guidelines

🤖 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.

Nitpick comments:
In `@plugins/nemo-iron-swarm/README.md`:
- Around line 183-189: Restructure the README so it stays within a single
Diataxis quadrant: move configuration reference, troubleshooting, and
architecture explanation out of this page into linked documentation pages, or
convert the entire README into a focused tutorial/how-to and remove the
unrelated sections. Keep the bootstrap, setup, and doctor commands within the
chosen scope.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 48ef44b7-ea9d-4e84-aef6-b4220be037c3

📥 Commits

Reviewing files that changed from the base of the PR and between 91f762b and e3a32b2.

📒 Files selected for processing (5)
  • plugins/nemo-iron-swarm/README.md
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/manifests.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/runs.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/provisioning.py
  • web/packages/studio/src/components/ironSwarm/swarm/useSwarmEvents.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • web/packages/studio/src/components/ironSwarm/swarm/useSwarmEvents.ts
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/cli/provisioning.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/runs.py
  • plugins/nemo-iron-swarm/src/nemo_iron_swarm_plugin/api/v2/manifests.py

@parkanzky parkanzky left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Plugin looks idiomatic enough to me. When tests pass I think we should just merge this to get it in and allow people to interact with it. However, I am not familiar enough with typescript to review the studio pieces. Finding someone to review that now.

@marcusds marcusds left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Rather than including changes directly into studio I think this would benefit from using the new support studio has for plugins.

This PR has details: #594

Reach out if would like support on this. There some agent instructions that should be helpful.

Edit: since this would be the first real consumer of the studio plugin architecture I took a look at what moving it over would look like. For the most part it'll be straightforward but there are some changes I am going to make on our end to make it better. Edit: This is in #1180 and #1192

@koralchapnik

Copy link
Copy Markdown
Author

Working on this — moving the whole Studio UI into the plugin, as suggested. Thanks for #1180 and #1192! they're what made it possible.

The port is done locally and verified against a running platform: the UI now ships in the plugin wheel as a Studio bundle (Root + navItems via the nemo.studio entry point), its API client is generated inside the plugin (fetch-based, so no second axios), and web/packages/studio is back to matching main — no feature flag, no routes, no side-nav entry.

It sits on top of #1192, so I'll push once that merges and I can rebase .. pushing before then would pull its file moves into this PR's diff. Two plugin-API gaps came up while testing in the browser - commented on #1192.

Signed-off-by: Koral Chapnik Verbun <kchapnikverb@nvidia.com>
Signed-off-by: Koral Chapnik Verbun <kchapnikverb@nvidia.com>
Signed-off-by: Koral Chapnik Verbun <kchapnikverb@nvidia.com>
Signed-off-by: Koral Chapnik Verbun <kchapnikverb@nvidia.com>
Signed-off-by: Koral Chapnik Verbun <kchapnikverb@nvidia.com>
Signed-off-by: Koral Chapnik Verbun <kchapnikverb@nvidia.com>
Signed-off-by: Koral Chapnik Verbun <kchapnikverb@nvidia.com>
Signed-off-by: Koral Chapnik Verbun <kchapnikverb@nvidia.com>
Signed-off-by: Koral Chapnik Verbun <kchapnikverb@nvidia.com>
koralchapnik and others added 19 commits August 12, 2026 22:17
Events without a numeric id fell back to Date.now(), and that value then
advanced the poll cursor. An epoch-millisecond cursor sits far above any
real event id, so `after` matched nothing and the feed stopped for the
rest of the run. It also produced duplicate React keys in MessageFeed.

Skip events with no numeric id and advance the cursor from the highest
real id seen.

Signed-off-by: Koral Chapnik Verbun <kchapnikverb@nvidia.com>
- secrets_file was a client-supplied path handed straight to `init`, which
  reads it on the platform host. A caller could name any readable file
  (/proc/self/environ, another tenant's dotenv) and fold it into their
  manifest. Constrain it to the uploaded project directory.
- Log entries interpolated user-controlled names and workspaces, so a value
  containing newlines could forge log records (CodeQL log injection, 7 hits).
  Route them through nemo_platform_plugin's existing sanitize_for_log, as
  nemo-evaluator already does.
- `setup` echoed index_url verbatim; Artifactory embeds an access token in
  that URL. Reuse redact_index_url, which checks.py already applies.

Signed-off-by: Koral Chapnik Verbun <kchapnikverb@nvidia.com>
The install line fetched install.sh from the mutable main branch and piped
it to sh, so the code executed could change between readings. Pin it to
v0.0.92 — the version the deployments plugin already requires — matching
how the root README and CONTRIBUTING pin the uv installer.

Signed-off-by: Koral Chapnik Verbun <kchapnikverb@nvidia.com>
Wrapping the manifest id in sanitize_for_log pushed the warning past the
configured width. `ruff check` was clean, so only `ruff format --check`
caught it.

Signed-off-by: Koral Chapnik Verbun <kchapnikverb@nvidia.com>
- launch_synth_service documented RuntimeError; _await_ready raises
  IronSwarmRunError.
- The README said agent-source manifests are re-resolved on every run.
  They are frozen targets; an agent edit reaches one only through
  POST /manifests/{name}/refresh.
- `init`'s rendered file was described as inspection-only and as re-rendered
  from the agent ref. `run --config` accepts it, and project sources have no
  agent ref at all.

Signed-off-by: Koral Chapnik Verbun <kchapnikverb@nvidia.com>
Every other Studio feature flag is in the sample, so a developer copying it
had no way to discover this one. Defaults to false, matching the flag's
off-by-default definition.

Signed-off-by: Koral Chapnik Verbun <kchapnikverb@nvidia.com>
The test named both the local log and the downloaded member `events.jsonl`,
so it passed no matter where the fallback wrote. The log is per-run
(`_events_path` -> `<safe-run-name>.jsonl`) and `_save_events_fileset`
uploads that exact file, so the member carries the same basename. Name both
after the run so the test covers the contract rather than a coincidence.

Signed-off-by: Koral Chapnik Verbun <kchapnikverb@nvidia.com>
`agent_fileset` is uploaded by the service, but `project_fileset` is passed
in by the caller and stored verbatim, and nothing stops two manifests naming
the same bundle. Deleting either manifest destroyed the other's target.

Delete only the fileset we created; the uploader owns the project bundle.

Signed-off-by: Koral Chapnik Verbun <kchapnikverb@nvidia.com>
Main promoted AccessibleTitle, AccordionSection, FileUpload,
ExpandableMessage, ErrorPanel, CancelJobButton, DeleteConfirmationModal,
ConfirmationModal, and QuickActionsMenuRoot out of @studio/components into
@nemo/common, and dropped react-router-dom in favour of react-router. The
iron-swarm files still referenced the old specifiers, so they failed to
resolve once the branch moved onto current main.

Signed-off-by: mschwab <mschwab@nvidia.com>
The Iron Swarm UI lived in Studio: routes, dataViews, components, a feature
flag, side-nav entry, and a generated client in the shared SDK. Studio carried
all of it whether or not the plugin was installed.

It now ships with the plugin, loaded at runtime through the Studio plugin
system. Studio keeps no iron-swarm-specific code — the side-nav entry comes
from the plugin's navItems() joining the existing "Governance" group, and
routing from the plugin's own <Routes> under its splat mount.

Plugin web bundle:
- Shared singletons stay external (react, react-dom, react-router, KUI,
  react-query, @nemo/common), verified against the built artifact.
- The iron-swarm client is generated by orval from the plugin's own
  openapi.yaml, replacing the entry in the Studio SDK. Its mutator reads the
  access token the host hands the plugin rather than Studio's OIDC axios
  interceptor, which does not cross the plugin boundary.
- Platform calls go through host.sdk.platform. CreateSecretModal and the agent
  picker are reimplemented here: Studio's copies call the SDK directly and live
  outside @nemo/common, and promoting them would pull the SDK into the shared
  vendor bundle every plugin loads.
- useWorkspaceFromPath/useToast/useBreadcrumbs are shimmed over the host handle
  so the moved components keep their call shapes.
- eslint, vitest, and typecheck mirror Studio's setup. eslint needs the TS 6 API
  (typescript-eslint does not support TS 7), which is why this root pins the
  TS 6 build the way web/'s root does.

Adds host.apiBaseUrl to the plugin contract. A plugin calling its own service
needs it: Studio's dev-server /apis proxy is opt-in, so a relative request hits
the dev server whenever VITE_PLATFORM_BASE_URL is set.

Adds ErrorPanel, CancelJobButton, ControlledTextArea, ENTITY_NAME_HELP, and
entityNameSchema to the @nemo/common plugin surface.

Signed-off-by: mschwab <mschwab@nvidia.com>
Studio's Tailwind scans web/packages/** and nothing else, so a utility class
Studio does not already emit has no CSS once the code lives under plugins/.
Nothing catches this: the build, typecheck, and tests all pass, and it surfaces
only as unstyled UI. 28 of the 32 palette classes the migrated UI used were in
that state, along with ~20 layout utilities (pr-1, top-2, w-32, leading-5,
divide-y, lg:grid-cols-2, and the arbitrary sizes).

Neutral greys map onto Studio's semantic tokens (bg-surface-*, border-base,
text-subtle/primary). Categorical and status colours move to src/theme.ts,
which binds Studio's --text-color-accent-* / --text-color-feedback-*
properties — these are global and theme-aware, where a palette class is
neither. Studio writes the same values as text-[color:var(--...)] utilities,
but a plugin must not copy that form: those classes exist only while some file
under web/packages/** still uses them.

Layout follows the same rule — KUI Grid replaces lg:grid-cols-2, borders
replace divide-y, and fixed sizes become styles. The live-feed panel is no
longer a KUI Card: bounding .nv-card-content needs a descendant selector, and
an arbitrary-variant class is never emitted for a plugin.

Every class the plugin now uses is verified present in Studio's scanner input.
Inline style is no longer banned by the plugin's eslint config, since binding
theme custom properties is the only theme-aware option a plugin has.

Signed-off-by: mschwab <mschwab@nvidia.com>
react-diff-viewer-continued moved to the iron-swarm plugin's own pnpm root, but
only Studio's package.json was updated — the lockfile still carried it and its
transitives (emotion, refractor, prismjs types, hastscript). Regenerated with
the pinned pnpm; `--frozen-lockfile` now passes.

Signed-off-by: mschwab <mschwab@nvidia.com>
Adding ErrorPanel and CancelJobButton to @nemo/common's plugin surface pulled
axios into the vendor bundle — ErrorPanel through api/common/utils,
CancelJobButton through @nemo/sdk/generated/platform/api. axios resolves to its
Node build there, so vendor/common.js emitted bare imports of crypto, http,
https, url, events, stream and zlib. The browser cannot resolve those, so the
dynamic import of the plugin threw:

  [plugins] Failed to load plugin "iron-swarm": TypeError: Failed to resolve
  module specifier "crypto"

loadPlugin catches that, logs a warning and returns null, so the only symptom
is a missing nav item. This broke every plugin's bundle, not just iron-swarm —
common.js is shared.

Both exports are removed, with comments recording why. ErrorPanel was dead
weight anyway: its only caller was ironSwarmRoutes.tsx, deleted when the UI
moved. CancelJobButton is reimplemented in the plugin on host.sdk.platform,
the same way CreateSecretModal already was and for the same reason.

Every served vendor bundle now imports only import-map specifiers.

Signed-off-by: mschwab <mschwab@nvidia.com>
The host.apiBaseUrl paragraph landed in #1270 without a worked example, because
the plugin it describes was not on main yet.

Signed-off-by: mschwab <mschwab@nvidia.com>
Both of these existed in the plugin only because Studio had no shareable
version when the UI moved. Main now has both.

CreateSecretModal moved into @nemo/common in #1263, kept SDK-free by taking an
onCreate callback — so the caller owns the mutation and the barrel stays clear
of axios. The plugin's ~150-line copy is deleted; the mutation now comes off
host.sdk.platform and result messages route through host.notifications.notify
via onNotify, since ToastProvider is not shared across the plugin boundary.

host.sdk gained an `agents` client, so the hand-rolled paginating fetch against
this plugin's own fetcher is replaced by useAgentsForSelect, which walks
pagination on Studio's authenticated axios and shared cache. The plugin no
longer reimplements auth for a service it does not own.

Bundle drops to 814 kB. Verified the plugin bundle still imports only
import-map specifiers, and that no vendor bundle emits a Node builtin.

Signed-off-by: mschwab <mschwab@nvidia.com>
Asserts get_studio_spec() returns the iron-swarm spec and that the built
bundle it points at actually ships, so a missing dist/index.js fails the
suite rather than the browser.

Signed-off-by: Koral Chapnik Verbun <kchapnikverb@nvidia.com>
The UI ships with the plugin now, so studio.feature_flags.iron_swarm_enabled
no longer exists — the README still told users to set it. Also documents the
web bundle: how to rebuild it, and the Tailwind constraint that only shows up
as unstyled UI.

Signed-off-by: Koral Chapnik Verbun <kchapnikverb@nvidia.com>
The five ConfirmationModal/DeleteConfirmationModal call sites did not pass
onNotify, so every success and error message was dropped with a console
warning instead of reaching Studio's toaster — useNotify's fallback made it
silent rather than fatal. Also points the typecheck script at tsc6, the
binary the pinned @typescript/typescript6 actually installs.

Signed-off-by: Koral Chapnik Verbun <kchapnikverb@nvidia.com>
@koralchapnik
koralchapnik force-pushed the iron-swarm-plugin/koralchapnik branch from 635d205 to 377db23 Compare August 13, 2026 07:21
…t docstring change

335bd4d reworded delete_manifest's docstring but left the committed spec
stale, so lint-openapi — which regenerates and diffs — failed.

Signed-off-by: Koral Chapnik Verbun <kchapnikverb@nvidia.com>
A plugin's Studio UI ships as a minified bundle committed next to its Python
package so the wheel can install it. Scanning it surfaced five high-severity
alerts (ReDoS, unanchored regexes, double escaping) that all belong to bundled
third-party code — prismjs/refractor via react-diff-viewer-continued — which we
cannot patch. The sources that produce the bundle are still scanned.

Signed-off-by: Koral Chapnik Verbun <kchapnikverb@nvidia.com>
@koralchapnik
koralchapnik requested a review from a team as a code owner August 13, 2026 07:39
import { FC, useCallback, useEffect, useRef, useState } from 'react';
import { useParams } from 'react-router';

type AttackIntensity = 'light' | 'standard' | 'thorough';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Since this file is so large, I would split the types into a types.ts and the consts in a consts.ts

}
};

const downloadCsv = () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Similarly to the consts and types, I would move some of these functions out to a utils.ts file to make this component more maintainable. Would require a little work on to move some items to arguments, but an agent should be able to do it really easily for you.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Though in this specific example we actually have a shared triggerDownload you should be able to use.

}
/>

{gen.active ? (

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I would personally look into how you could split this component up into sub components (in a dir like ./components), its better for performance and maintability.


const NAME_PATTERN = /^[a-z0-9][a-z0-9-]*$/;

const schema = z.object({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We usually by practice put schema's in their own files.

: !lastHitlogFileset)) ||
(benignSource === 'upload' && (!benignFileset || uploadBenign.isPending))
}
onSubmit={() => start()}

@marcusds marcusds Aug 13, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Should have a e.preventDefault() here

onSubmit={(e) => {
    e.preventDefault();
    start();
}


// Re-seed the launch dialog's config from the manifest default each time it opens, so per-run tweaks
// are ephemeral (they never persist unless the user hits "Save as default").
useEffect(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Any manifest refetch while the dialog is could wipe user's entries. I'd move this logic into openRunDialog

<Button color="brand" type="submit" disabled={isCreating}>
{isCreating ? 'Creating…' : 'Create Manifest'}
</Button>
<Button kind="tertiary" onClick={onReset}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

add a type="button" so we don't submit the form.

// so the job can resume benign-suite synthesis.
export const InterviewPanel: FC<InterviewPanelProps> = ({ prompt, loading, onSubmit }) => {
const [answers, setAnswers] = useState<Record<string, string>>(() =>
Object.fromEntries(prompt.questions.map((q) => [q.gap, defaultAnswer(q.options)]))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Could this cause state to get reused in additional rounds?

},
});

return { report: query.data, isLoading: Boolean(jobName) && !hasReport, hasReport };

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If a job fails, will it have a report? If so, will this always be stuck loading if failed?

query: {
enabled: Boolean(jobName),
refetchInterval: (query) =>
query.state.data?.data?.some((result) => result.name === MITIGATIONS_RESULT)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Perhaps add a job status related escape hatch?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants