Skip to content

fix(blueprint): use allowlist for plan.json persist and log - #3649

Closed
MayankSharmaCSE wants to merge 1 commit into
NVIDIA:mainfrom
MayankSharmaCSE:fix/plan-json-allowlist
Closed

fix(blueprint): use allowlist for plan.json persist and log#3649
MayankSharmaCSE wants to merge 1 commit into
NVIDIA:mainfrom
MayankSharmaCSE:fix/plan-json-allowlist

Conversation

@MayankSharmaCSE

@MayankSharmaCSE MayankSharmaCSE commented May 16, 2026

Copy link
Copy Markdown

Summary

actionPlan was logging credential_env to stdout and actionStatus was dumping the raw plan.json contents without any field filtering. Both paths relied on manually remembering to exclude sensitive fields — a pattern that breaks silently when new fields get added to RunPlan. This PR replaces that with an explicit allowlist so only safe fields ever get persisted or logged.

Related Issue

Closes #3648

Changes

  • Added SanitizedPlan interface and sanitizePlan() helper that defines exactly which fields are safe to write to disk or print to stdout
  • actionPlan now logs the sanitized view instead of the full plan object (previously included credential_env)
  • actionApply now constructs a SanitizedPlan directly instead of an inline object with a "don't forget to exclude secrets" comment — the type system enforces the boundary
  • actionStatus now parses plan.json and filters to allowlisted keys before logging, instead of dumping raw file contents

Type of Change

  • Code change (feature, bug fix, or refactor)

Verification

  • npm test passes
  • Tests added or updated for new or changed behavior
  • No secrets, API keys, or credentials committed

Signed-off-by: MayankSharmaCSE mayanksharmaCSE1@gmail.com

Signed-off-by: mayanksharmaCSE <mayanksharmacse1@gmail.com>
Copilot AI review requested due to automatic review settings May 16, 2026 17:35
@copy-pr-bot

copy-pr-bot Bot commented May 16, 2026

Copy link
Copy Markdown

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

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented May 16, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Added SanitizedPlan interface and sanitizePlan() helper to exclude sensitive credential fields from plan data persistence and logging. Updated actionPlan(), actionApply(), and actionStatus() to apply sanitization consistently when logging, writing, and reading plan state.

Changes

Plan Data Sanitization

Layer / File(s) Summary
Sanitization contract and helper
nemoclaw/src/blueprint/runner.ts
SanitizedPlan interface and sanitizePlan() function exclude credential-related fields from RunPlan and optionally add a timestamp.
Sanitization integration in action methods
nemoclaw/src/blueprint/runner.ts
actionPlan() logs the sanitized plan; actionApply() writes plan.json with sanitized state fields and timestamp; actionStatus() parses plan.json and logs a whitelisted subset of safe fields.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related issues

Poem

🐰 Credentials tucked away with care,
Sanitized plans float through the air,
Safe to log, secure to save,
Blueprints now are extra brave!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Title check ✅ Passed The title accurately summarizes the main change: implementing an allowlist approach for plan.json persistence and logging to fix security issues with credential exposure.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

Copilot AI 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.

Pull request overview

Replaces ad-hoc field exclusion for plan persistence and logging with an explicit allowlist, so that secret-bearing fields on RunPlan (currently credential_env, plus future additions) can never leak into stdout via actionPlan, into state/plan.json via actionApply, or be echoed back by actionStatus.

Changes:

  • Introduces a SanitizedPlan interface and sanitizePlan() helper enumerating the fields safe to persist/log.
  • actionPlan now logs sanitizePlan(plan) instead of the raw RunPlan.
  • actionApply constructs a SanitizedPlan for plan.json, and actionStatus defensively re-filters the file contents through a key allowlist before logging.
Comments suppressed due to low confidence (1)

nemoclaw/src/blueprint/runner.ts:758

  • The allowlist of safe keys is hard-coded here as a string literal array, separate from the SanitizedPlan interface and from sanitizePlan. If a new safe field is added to SanitizedPlan in the future, this list will silently drop it from status output. Consider deriving this list from a single source of truth (e.g., a shared const SAFE_PLAN_KEYS = [...] as const used both for the type and the runtime filter), so all three call sites stay in sync.
    const safe: Record<string, unknown> = {};
    for (const key of ["run_id", "profile", "sandbox_name", "inference", "policy_additions", "timestamp"]) {
      if (key in raw) safe[key] = raw[key];
    }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +692 to +705
const safeState: SanitizedPlan = {
run_id: rid,
profile,
sandbox_name: sandboxName,
inference: {
provider_type: inferenceCfg.provider_type,
provider_name: inferenceCfg.provider_name,
endpoint: inferenceCfg.endpoint,
model: inferenceCfg.model,
},
policy_additions: policyAdditions,
timestamp: new Date().toISOString(),
};
writeFileSync(join(stateDir, "plan.json"), JSON.stringify(safeState, null, 2));

@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 (2)
nemoclaw/src/blueprint/runner.ts (2)

692-705: ⚡ Quick win

Consider using sanitizePlan() helper for consistency.

The manual construction of safeState duplicates the sanitization logic from sanitizePlan(). If SanitizedPlan is extended in the future, both locations would need updates. Since all the required data (sandboxImage, forwardPorts, router config, etc.) is available in scope, you could construct a RunPlan object and call sanitizePlan(plan, timestamp) to centralize the sanitization logic.

♻️ Refactor to use sanitizePlan() helper
  progress(85, "Saving run state");
- const safeState: SanitizedPlan = {
-   run_id: rid,
-   profile,
-   sandbox_name: sandboxName,
-   inference: {
-     provider_type: inferenceCfg.provider_type,
-     provider_name: inferenceCfg.provider_name,
-     endpoint: inferenceCfg.endpoint,
-     model: inferenceCfg.model,
-   },
-   policy_additions: policyAdditions,
-   timestamp: new Date().toISOString(),
- };
+ const plan: RunPlan = {
+   run_id: rid,
+   profile,
+   sandbox: {
+     image: sandboxImage,
+     name: sandboxName,
+     forward_ports: forwardPorts,
+   },
+   inference: {
+     provider_type: inferenceCfg.provider_type,
+     provider_name: inferenceCfg.provider_name,
+     endpoint: inferenceCfg.endpoint,
+     model: inferenceCfg.model,
+     credential_env: inferenceCfg.credential_env,
+   },
+   router: {
+     enabled: blueprint.components?.router?.enabled === true,
+     port: blueprint.components?.router?.port ?? DEFAULT_ROUTER_PORT,
+     pool_config_path: blueprint.components?.router?.pool_config_path,
+   },
+   policy_additions: policyAdditions,
+   dry_run: false,
+ };
+ const safeState = sanitizePlan(plan, new Date().toISOString());
  writeFileSync(join(stateDir, "plan.json"), JSON.stringify(safeState, null, 2));
🤖 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 `@nemoclaw/src/blueprint/runner.ts` around lines 692 - 705, The manual
construction of safeState duplicates sanitization logic; instead build a RunPlan
object with the existing scope values (e.g. run_id rid, profile, sandbox_name
sandboxName, inferenceCfg fields, policyAdditions, plus sandboxImage,
forwardPorts, router config, etc.), then call sanitizePlan(plan, new
Date().toISOString()) to produce the SanitizedPlan and write that JSON to
join(stateDir, "plan.json") using writeFileSync; replace the safeState literal
with the sanitizedPlan result and ensure you import/ensure availability of
sanitizePlan and the RunPlan type where needed.

756-758: ⚡ Quick win

Leverage type system for the field whitelist.

The hardcoded field list should be tied to the SanitizedPlan interface for type safety. If SanitizedPlan is extended or modified, the current approach requires manually remembering to update this array. A type-annotated constant would catch mismatches at compile time.

♻️ Add type annotation to the whitelist
  try {
    const raw: Record<string, unknown> = JSON.parse(
      readFileSync(join(runDir, "plan.json"), "utf-8"),
    );
+   const SAFE_KEYS: (keyof SanitizedPlan)[] = [
+     "run_id",
+     "profile",
+     "sandbox_name",
+     "inference",
+     "policy_additions",
+     "timestamp",
+   ];
    const safe: Record<string, unknown> = {};
-   for (const key of ["run_id", "profile", "sandbox_name", "inference", "policy_additions", "timestamp"]) {
+   for (const key of SAFE_KEYS) {
      if (key in raw) safe[key] = raw[key];
    }
    log(JSON.stringify(safe, null, 2));
🤖 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 `@nemoclaw/src/blueprint/runner.ts` around lines 756 - 758, Replace the
hardcoded string array with a typed whitelist so the compiler verifies it
against the SanitizedPlan shape: declare a constant like const WHITELIST:
Array<keyof SanitizedPlan> = ["run_id", "profile", "sandbox_name", "inference",
"policy_additions", "timestamp"]; then iterate for (const key of WHITELIST) { if
(key in raw) safe[key] = raw[key]; } referencing SanitizedPlan and the new
WHITELIST constant in runner.ts to ensure future changes to SanitizedPlan
surface type errors.
🤖 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 `@nemoclaw/src/blueprint/runner.ts`:
- Around line 692-705: The manual construction of safeState duplicates
sanitization logic; instead build a RunPlan object with the existing scope
values (e.g. run_id rid, profile, sandbox_name sandboxName, inferenceCfg fields,
policyAdditions, plus sandboxImage, forwardPorts, router config, etc.), then
call sanitizePlan(plan, new Date().toISOString()) to produce the SanitizedPlan
and write that JSON to join(stateDir, "plan.json") using writeFileSync; replace
the safeState literal with the sanitizedPlan result and ensure you import/ensure
availability of sanitizePlan and the RunPlan type where needed.
- Around line 756-758: Replace the hardcoded string array with a typed whitelist
so the compiler verifies it against the SanitizedPlan shape: declare a constant
like const WHITELIST: Array<keyof SanitizedPlan> = ["run_id", "profile",
"sandbox_name", "inference", "policy_additions", "timestamp"]; then iterate for
(const key of WHITELIST) { if (key in raw) safe[key] = raw[key]; } referencing
SanitizedPlan and the new WHITELIST constant in runner.ts to ensure future
changes to SanitizedPlan surface type errors.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 59cdea0b-211b-41d6-bb2a-5eb2b0247ed3

📥 Commits

Reviewing files that changed from the base of the PR and between a8dff7a and 23dbc68.

📒 Files selected for processing (1)
  • nemoclaw/src/blueprint/runner.ts

@wscurran

Copy link
Copy Markdown
Contributor

@cjagwani cjagwani self-assigned this May 19, 2026
@cjagwani

cjagwani commented May 19, 2026

Copy link
Copy Markdown
Collaborator

Closing in favor of #3674, which lands the same allowlist direction (and credits this PR in the body) but additionally:

  • Removes credential_env from RunPlan.inference at the type level (runner.ts:461-466) so it can't reappear by accident.
  • Centralizes the allowlist into one builder per output context — addresses the open Copilot review comments here at runner.ts:692-705 (duplicate safeState literal) and runner.ts:756-758 (untyped string-array allowlist).
  • Recursively sanitizes nested sandbox.* / router.* keys in actionStatus (this PR only filtered top-level keys, so a leaked nested field in an older on-disk plan.json would still print).
  • Adds +179 lines of regression coverage including future-secret fields (token, authorization) and a corrupt-plan.json fallback test.

@cjagwani cjagwani closed this May 19, 2026
ericksoa added a commit that referenced this pull request May 19, 2026
## Summary
- remove `credential_env` from public blueprint plan output
- persist `plan.json` through an explicit allowlist builder instead of
ad-hoc credential exclusions
- make `status` parse `plan.json` and re-render only safe allowlisted
fields
- add regression coverage for credential env names, defaults, secret
values, token/auth fields, and future unallowlisted fields

Fixes #3648

## Related contributor work
- Related PR #3649 by @MayankSharmaCSE independently proposed the
explicit allowlist direction for plan logging and persisted `plan.json`.
This PR keeps that core approach while adding nested status
sanitization, preserving the public plan/status fields requested in the
issue, and adding regression tests.

## Validation
- `cd nemoclaw && npm test -- src/blueprint/runner.test.ts` (`91
passed`)
- `cd nemoclaw && npm run build`
- `cd nemoclaw && npm run check`
- `git diff --check`


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

## Summary by CodeRabbit

* **Bug Fixes**
* Plan outputs and stored run plans no longer expose credential-related
fields or secret values

* **Tests**
* Added comprehensive tests validating that sensitive credential
information and secret values are not exposed in plan output, persisted
plans, or status reports

<!-- review_stack_entry_start -->

[![Review Change
Stack](https://storage.googleapis.com/coderabbit_public_assets/review-stack-in-coderabbit-ui.svg)](https://app.coderabbit.ai/change-stack/NVIDIA/NemoClaw/pull/3674?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack)

<!-- review_stack_entry_end -->

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
@wscurran wscurran added bug-fix PR fixes a bug or regression and removed fix labels Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug-fix PR fixes a bug or regression

Projects

None yet

Development

Successfully merging this pull request may close these issues.

actionPlan logs credential_env to stdout; actionStatus logs raw plan.json without field allowlist

4 participants