Skip to content

Fix #478: required Scientist execution fails closed, never PASS on WARN - #490

Merged
richard-devbot merged 4 commits into
mainfrom
fix-478-scientist-fail-closed
Jul 30, 2026
Merged

Fix #478: required Scientist execution fails closed, never PASS on WARN#490
richard-devbot merged 4 commits into
mainfrom
fix-478-scientist-fail-closed

Conversation

@richard-devbot

Copy link
Copy Markdown
Owner

Summary

Closes #478 (P0, Wave 0 of the #476 autonomy-hardening epic). sdlc_validate's sandbox-execution step degraded ANY unverified result (no runtime, no configured command, sandbox disabled) to a WARN check — and a WARN never changed the overall verdict, so the builder's self-reported tests_run stood unchallenged. A well-formed builder contract could PASS a "required" stage with no command ever actually executed. Worse, when a container DID run but hit an infrastructure problem (image pull failure, daemon hiccup) rather than a real test failure, the non-zero exit was recorded as an ordinary FAIL — burning a builder retry for something that was never the builder's fault.

  • sandbox.js: new execution_policy (required|optional|prohibited, default optional — a zero-config install's behavior is byte-for-byte unchanged). resolveExecutionPolicy() resolves task-level > per-stage > project-global, same precedence family as resolveSandboxCommand. A per-stage config entry may now set execution_policy alone, without its own command.
  • sandbox.js: new looksLikeInfraFailure() heuristic — a non-zero exit with NO stdout at all and a known Docker/Podman error signature in stderr (Cannot connect to the Docker daemon, pull access denied, Unable to find image, etc.) means the command never actually ran. Reclassified to the SAME unverified/observed shape as "no runtime present" instead of FAIL — a real test failure (stdout present, or no matching signature) is untouched.
  • rstack-sdlc.ts: runValidationExecution now resolves the policy and, when execution is required but unverified for ANY reason, returns infraBlocked: true with a BLOCKED_INFRA check status instead of WARN. sdlc_validate's claim critical section short-circuits on infraBlocked before writing validation.json or touching task.status/the claim nonce — the task stays exactly as claimed, so re-running sdlc_validate once the infra issue is fixed re-attempts the SAME claim for free. This deliberately never reaches classifyRetryDecision, so an infra block can never consume an attempt-budget slot. Emits a distinguishable execution_blocked_infra event and a structured tool response naming the exact reason and the recovery action.
  • config-validation.js: validateSandboxConfig recognizes execution_policy at both the project-global and per-stage levels, flags invalid values, and no longer requires a per-stage entry to carry a command when it sets a valid execution_policy instead.

A genuine test FAIL/PASS is untouched by any of this regardless of policy — required only changes what happens when a real result couldn't be obtained at all.

Test plan

  • npx tsx --test tests/sandbox-execution-452.test.js — the infra-failure heuristic (matching/non-matching signatures, stdout-present exemption, timeout exemption) and resolveExecutionPolicy's precedence chain
  • npx tsx --test tests/sandbox-validate-452.test.js — the full runValidationExecution required/optional/prohibited matrix (no runtime, no command, disabled, infra-suspect failure, real FAIL, real PASS) and validateSandboxConfig's new execution_policy checks
  • npx tsx --test tests/execution-policy-478.test.js — new end-to-end test via the real extension: a required task with no authoritative command blocks on infra (never PASS, never a builder attempt — claim/nonce/task.status untouched, validation.json never written, a second infra block recorded distinctly), and the default optional policy is provably unchanged from pre-[P0][Scientist] Required execution must fail closed as BLOCKED_INFRA, never PASS on WARN #478 behavior
  • npm test — 1651/1651
  • npm run typecheck — clean
  • npm run lint — clean (pre-existing unrelated warnings only)
  • npm run validate — clean
  • node scripts/security-audit.mjs — clean (branch includes the Fix Security baseline gate: override brace-expansion in eslint's dependency tree #473 brace-expansion fix so CI is green regardless of merge order)

Scope notes (read before merge)

  • Default stays optional, deliberately. The issue's acceptance criteria describe required semantics precisely; making it the global default would be a breaking change for every existing zero-config install (many currently-passing tests assert WARN-coexists-with-PASS behavior when no runtime/command exists). An operator opts a stage/task into the hard guarantee via sandbox.execution_policy, sandbox.per_stage.<id>.execution_policy, or task.execution_policy.
  • The infra-vs-real-failure heuristic is exactly that — a heuristic over stable, well-known Docker/Podman CLI error text, not an exhaustive parser. False negatives (an infra failure whose stderr doesn't match a known signature) fall through to the existing FAIL path — the same behavior as before this fix, not a regression.
  • Not addressed here (explicitly out of scope, left for [P0][Durability] Add immutable attempt ledger, BUILT state, leases, CAS transitions, and transactional outbox #481/[P0][Integrity] Bind builder, Scientist, validators, artifacts, and destructive actions to the exact attempt #482 per the epic's own dependency map): container image digest-pinning, binding the Scientist's result to a run/task/attempt/claim/input-snapshot identity envelope, and Business Hub UI changes to visually distinguish BLOCKED_INFRA from FAIL/PASS (the execution_blocked_infra event is emitted and ready for a future dashboard card, but no UI work is included in this PR).
  • No new task-status or validator-contract schema values were introduced — BLOCKED_INFRA only ever appears as a check-level status (already free-text per the validator-contract schema) and is never written as task.status or validation.status, so no schema changes were needed and the blast radius stays contained to the sandbox/validate wiring.

🤖 Generated with Claude Code

richardsongunde and others added 3 commits July 26, 2026 17:21
…#472)

npm audit reported 5 high-severity advisories, all tracing to one root:
a DoS/ReDoS bug in brace-expansion (GHSA-3jxr-9vmj-r5cp,
GHSA-mh99-v99m-4gvg), reachable via eslint -> minimatch@3.1.5 ->
brace-expansion@1.1.16. npm's only reported fix was eslint@10
(isSemVerMajor), which pulls in a new no-useless-assignment rule
in @eslint/js's recommended config, flagging 15 pre-existing
dead-store patterns across src/ -- too invasive for a security-only
fix.

Instead: add a root `overrides` entry (this repo already uses the
same mechanism for esbuild/protobufjs/ws) forcing brace-expansion to
the patched 5.0.8 line. Verified this reaches eslint's own tree
(minimatch@3.1.5's brace-expansion now 5.0.8) while, as documented,
leaving @earendil-works/pi-coding-agent's shrinkwrapped copy (5.0.6)
untouched -- npm honors a shrinkwrap absolutely, so overrides can't
reach it regardless.

That shrinkwrapped copy is the only remaining high-severity finding,
and it's exactly the node this gate's BUNDLED_ROOTS/
TOLERATED_BUNDLED_ADVISORIES allowlist already exists to tolerate --
added the second GHSA id to that allowlist (the gate is deliberately
strict per-advisory-id, so a newly surfaced GHSA for an
already-tolerated package still fails until consciously added).

Verified: npm run lint unchanged (0 errors, same 4 pre-existing
warnings -- confirms minimatch@3.1.5 works fine with the newer
brace-expansion at runtime), npm run typecheck clean, gate logic
simulated against real npm audit output now reports 0 blocking.
Qodo flagged four issues on the eslint/brace-expansion security fix:
- no test coverage for the TOLERATED_BUNDLED_ADVISORIES allowlist change
- brace-expansion@5.0.8 declares engines.node "20 || >=22", excluding the
  Node 18 CI lane
- the ">=5.0.8" override is unbounded and can float to a future major
- the script comment mis-attributed the version bump to an eslint 9->10
  bump that never happened (the real mechanism is the root override)

Fix: bound the override to an exact "5.0.8" pin; export the pure
tolerate/block decision logic from security-audit.mjs (evaluateAdvisories,
isFullyBundled, advisoryIds) behind a main-guard so it's unit-testable
without shelling out to npm audit, and add tests/security-audit.test.js
covering the tolerate/block/allowlist-miss/severity-threshold paths, plus
a pinned-allowlist regression test; correct the stale comment to document
the real mechanism and the accepted Node 18 engines trade-off (no
brace-expansion release fixes both GHSAs while keeping Node 18 support;
it's a devDependency-only path and no engine-strict is set, so this
doesn't fail CI in practice).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
sdlc_validate's sandbox-execution step degraded ANY unverified result (no
runtime, no configured command, sandbox disabled) to a WARN check — and a
WARN never changed the overall verdict, so the builder's self-reported
tests_run stood unchallenged. A well-formed builder contract could PASS a
"required" stage with no command ever actually executed. Worse, when a
container DID run but hit an infrastructure problem (image pull failure,
daemon hiccup) rather than a real test failure, the non-zero exit was
recorded as an ordinary FAIL — burning a builder retry for something that
was never the builder's fault.

- sandbox.js: new `execution_policy` (required|optional|prohibited,
  DEFAULT_SANDBOX_CONFIG default "optional" — a zero-config install's
  behavior is byte-for-byte unchanged). resolveExecutionPolicy() resolves
  task-level > per-stage > project-global, same precedence family as
  resolveSandboxCommand. A per-stage config entry may now set
  execution_policy alone, without its own command (opting a stage into
  "required" while still relying on the project-global/task command).
- sandbox.js: new looksLikeInfraFailure() heuristic — a non-zero exit with
  NO stdout at all and a known Docker/Podman error signature in stderr
  ("Cannot connect to the Docker daemon", "pull access denied", "Unable to
  find image", etc.) means the command never actually ran. Reclassified to
  the SAME unverified/observed shape as "no runtime present" instead of
  FAIL — a real test failure (stdout present, or no matching signature)
  is untouched.
- rstack-sdlc.ts: runValidationExecution now resolves the policy and, when
  execution is "required" but unverified for ANY reason (disabled,
  unconfigured, no runtime, infra-suspect failure), returns
  `infraBlocked: true` with a `BLOCKED_INFRA` check status instead of WARN.
  sdlc_validate's claim critical section short-circuits on infraBlocked
  BEFORE writing validation.json or touching task.status/the claim nonce —
  the task stays exactly as claimed, so re-running sdlc_validate once the
  runtime/image/daemon issue is fixed re-attempts the SAME claim for free.
  This deliberately never reaches classifyRetryDecision, so an infra block
  can never consume an attempt-budget slot. Emits a distinguishable
  `execution_blocked_infra` event and a structured tool response naming
  the exact reason and the recovery action.
- config-validation.js: validateSandboxConfig recognizes execution_policy
  at both the project-global and per-stage levels, flags invalid values,
  and no longer requires a per-stage entry to carry a command when it sets
  a valid execution_policy instead.

A genuine test FAIL/PASS is untouched by any of this regardless of policy
— required only changes what happens when a real result couldn't be
obtained at all. Test coverage: the infra-failure heuristic (matching and
non-matching signatures, stdout-present exemption, timeout exemption),
resolveExecutionPolicy's precedence chain, config validation, the full
runValidationExecution required/optional/prohibited matrix, and an
end-to-end sdlc_build_next → sdlc_validate integration test proving the
claim/nonce/attempt survive an infra block untouched and a second infra
block is recorded distinctly. 1651/1651 tests, typecheck clean, lint
clean, validate clean, security-audit clean.

Fixes #478.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@strix-security

strix-security Bot commented Jul 30, 2026

Copy link
Copy Markdown

Strix Security Review

All previously reported security findings have been resolved.

1 resolved finding
Review summary

Reviewed all security-relevant changes in the PR, including src/core/harness/sandbox.js, src/core/harness/config-validation.js, src/integrations/pi/rstack-sdlc.ts, the dependency override updates, and the scripts/security-audit.mjs advisory-gating changes. The previously reported stderr-spoofing issue is fixed in the current diff: the infra-failure heuristic now relies on a trusted start sentinel emitted before the untrusted sandbox command runs, so repository-controlled test code can no longer suppress a real failure by printing Docker/Podman error text to stderr. No new security vulnerabilities were identified in the changed code.

Updated for 10ffafa.


Reviewed by Strix
Re-run review · Configure security review settings

@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@richard-devbot, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 46 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 04b6f5e7-be62-4d11-96a6-bcff9cf4222b

📥 Commits

Reviewing files that changed from the base of the PR and between cce8808 and 10ffafa.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • package.json
  • scripts/security-audit.mjs
  • src/core/harness/config-validation.js
  • src/core/harness/sandbox.js
  • src/integrations/pi/rstack-sdlc.ts
  • tests/execution-policy-478.test.js
  • tests/sandbox-execution-452.test.js
  • tests/sandbox-validate-452.test.js
  • tests/security-audit.test.js

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Fail-closed sandbox execution: required policy blocks on unverified + infra heuristics

🐞 Bug fix ✨ Enhancement 🧪 Tests ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Add execution_policy to require verified sandbox runs and block when unverified.
• Reclassify Docker/Podman infra errors as unverified, not builder-caused FAIL.
• Pin brace-expansion to patched version and unit-test the security audit allowlist.
Diagram

graph TD
  A["sdlc_validate (tool)"] --> B["runValidationExecution"] --> P["resolveExecutionPolicy"] --> R["runInSandbox"] --> C{"infraBlocked?"}
  C -->|"yes"| D["Return execution_blocked_infra (no state change)"]
  C -->|"no"| E["Write validation.json + update task status"]
  H["validateSandboxConfig"] --> P
  R --> T[("tasks.json / validation.json")]
  subgraph Legend
    direction LR
    _proc["Process/Module"] ~~~ _dec{"Decision"} ~~~ _store[("State files")]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Classify infra failures by exit codes only (e.g., Docker 125)
  • ➕ Simpler than stderr signature matching
  • ➕ Less sensitive to CLI message text changes
  • ➖ Misses many real infra errors that return generic exit codes
  • ➖ Still risks misclassifying genuine test failures as infra based on code alone
2. Pre-flight runtime checks (ping daemon, pull image) before running command
  • ➕ More deterministic separation between infra and test failures
  • ➕ Can provide targeted recovery messages earlier
  • ➖ Adds extra runtime cost (daemon check/pull) to every validate run
  • ➖ Requires more logic per runtime (docker/podman) and may still be flaky under load
3. Enforce required execution via config-time validation failure
  • ➕ Fails fast at configuration load time for inconsistent required setups
  • ➕ Avoids mixing policy decisions into execution result classification
  • ➖ Doesn’t address transient infra outages (daemon down) where retry should be free
  • ➖ Harder to express “required but temporarily blocked” without mutating task state

Recommendation: Current approach is the best fit for the stated goals: it preserves zero-config backward compatibility (default optional), introduces an explicit fail-closed mode for required stages, and prevents infra unavailability from consuming attempt budgets by short-circuiting before task/validation writes. The stderr-signature heuristic is a pragmatic middle ground; pre-flight checks could be a future enhancement if signature drift becomes a problem.

Files changed (10) +717 / -82

Enhancement (2) +148 / -46
security-audit.mjsRefactor security audit gate into testable decision logic +62/-36

Refactor security audit gate into testable decision logic

• Exports bundled roots/allowlist and factors advisory evaluation into a pure evaluateAdvisories() helper. Adds a proper main guard for CLI execution and expands the brace-expansion GHSA allowlist documentation.

scripts/security-audit.mjs

sandbox.jsAdd execution_policy support and infra-failure reclassification +86/-10

Add execution_policy support and infra-failure reclassification

• Introduces EXECUTION_POLICIES and resolves global/per-stage executionPolicy in resolveSandboxConfig, including policy-only per-stage entries. Adds a heuristic to detect container runtime infra failures and degrades them to unverified/observed results instead of FAIL, plus resolveExecutionPolicy() for precedence resolution.

src/core/harness/sandbox.js

Bug fix (2) +79 / -11
config-validation.jsValidate sandbox execution_policy (global + per-stage) +21/-4

Validate sandbox execution_policy (global + per-stage)

• Adds execution_policy/executionPolicy to known sandbox keys and validates allowed values using EXECUTION_POLICIES. Loosens per-stage validation to allow policy-only entries (command optional when a valid policy is present).

src/core/harness/config-validation.js

rstack-sdlc.tsBlock required unverified execution without consuming claim/attempts +58/-7

Block required unverified execution without consuming claim/attempts

• Extends runValidationExecution to compute execution policy, return infraBlocked, and map unverified required results to BLOCKED_INFRA. Updates sdlc_validate to short-circuit on infraBlocked before writing validation.json or mutating task status/claim nonce, and emits an execution_blocked_infra event with structured recovery guidance.

src/integrations/pi/rstack-sdlc.ts

Tests (4) +473 / -1
execution-policy-478.test.jsEnd-to-end tests for required execution_policy infra blocks +144/-0

End-to-end tests for required execution_policy infra blocks

• Adds tool-level integration tests proving that required execution blocks on missing command, never records a verdict, never changes claim/nonce, and can be retried for free. Also verifies optional/default policy remains backward compatible (no execution_blocked_infra).

tests/execution-policy-478.test.js

sandbox-execution-452.test.jsUnit tests for infra failure heuristic and policy resolution +97/-1

Unit tests for infra failure heuristic and policy resolution

• Adds coverage for Docker/Podman infra error reclassification to unverified, including negative cases (stdout present, timeouts, unrelated stderr). Adds tests for EXECUTION_POLICIES, resolveExecutionPolicy precedence, and policy-only per-stage config handling.

tests/sandbox-execution-452.test.js

sandbox-validate-452.test.jsValidation tests for required/optional/prohibited execution policies +120/-0

Validation tests for required/optional/prohibited execution policies

• Adds runValidationExecution tests ensuring required policy returns BLOCKED_INFRA when unverified (no runtime/command/disabled/infra-suspect failure) but preserves true PASS/FAIL semantics. Adds config-validation tests for execution_policy key/value correctness and per-stage policy-only entries.

tests/sandbox-validate-452.test.js

security-audit.test.jsUnit tests for security audit allowlist evaluation +112/-0

Unit tests for security audit allowlist evaluation

• Adds tests for evaluateAdvisories/isFullyBundled/advisoryIds using synthetic npm-audit JSON shapes. Pins expected brace-expansion GHSA allowlist contents to prevent accidental weakening of the security baseline gate.

tests/security-audit.test.js

Other (2) +17 / -24
package-lock.jsonUpdate lockfile for brace-expansion 5.0.8 override +15/-23

Update lockfile for brace-expansion 5.0.8 override

• Updates resolved dependency tree to pull brace-expansion 5.0.8 (and balanced-match 4.x) and drops concat-map from that path. Also includes minor lockfile metadata normalization (e.g., bin path, peer flags).

package-lock.json

package.jsonAdd brace-expansion override for security advisory fix +2/-1

Add brace-expansion override for security advisory fix

• Adds an npm overrides pin for brace-expansion 5.0.8 to remediate high-severity advisories without upgrading eslint major versions.

package.json

@qodo-code-review

qodo-code-review Bot commented Jul 30, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📜 Skill insights (3)

Context used
✅ Compliance rules (platform): 300 rules
✅ Skills: 17 invoked
  code-review-pr
  claude-api
  documentation-writing
  pptx
  docx
  performance-monitoring
  security-compliance
  cso
  plan-eng-review
  design-review
  prompt-engineering
  mcp-builder
  qa-testing
  code-patterns
  xlsx
  security-owasp
  testing-qa

Grey Divider


Action required

1. Infra heuristic false positives ✓ Resolved 🐞 Bug ≡ Correctness
Description
looksLikeInfraFailure() assumes an empty stdout implies the test command never ran, so a real
command that writes only to stderr (or prints a matching signature) can be reclassified from FAIL to
unverified/observed. That unverified record becomes a WARN via executionCheck(), so under the
default optional policy a genuine failing execution may no longer fail validation and can still PASS
based on the builder’s self-reported tests_run signal.
Code

src/core/harness/sandbox.js[R185-189]

+function looksLikeInfraFailure({ timedOut, exitCode, stdout, stderr }) {
+  if (timedOut || exitCode === 0) return false;
+  if (stdout.trim()) return false; // the command produced output — it ran
+  return INFRA_FAILURE_SIGNATURES.some((pattern) => pattern.test(stderr));
+}
Relevance

●● Moderate

Heuristic could misclassify real failures; semantic behavior change may be debated without clear
precedent.

PR-#461

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The heuristic only checks for non-zero exit + empty stdout + matching stderr signature, then forces
an unverified/observed record; executionCheck converts unverified/observed into WARN; and validation
only flips the testsRunOk signal on PASS/FAIL, so WARN preserves self-report behavior (and status
may remain PASS).

src/core/harness/sandbox.js[165-189]
src/core/harness/sandbox.js[263-279]
src/core/harness/sandbox.js[455-462]
src/integrations/pi/rstack-sdlc.ts[2577-2580]
src/integrations/pi/rstack-sdlc.ts[2663-2666]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`looksLikeInfraFailure()` uses `stdout.trim()` as its only evidence that the command executed, which is not reliable because many commands produce output only on stderr. If stderr happens to contain one of the Docker/Podman signature substrings, a real failing test run can be downgraded to `{status:'observed', tier:'unverified'}` and turned into a WARN, changing the overall verdict behavior under `execution_policy=optional`.

### Issue Context
This was added to avoid consuming builder attempts on daemon/image failures, but the current heuristic can also match container stderr produced by the command itself.

### Fix Focus Areas
- src/core/harness/sandbox.js[165-291]

### Suggested fix approaches (pick one)
1) **Sentinel marker (recommended):** wrap the executed command so the shell prints a known marker to stdout *before* running the user command (e.g. `echo __RSTACK_SB_STARTED__ ; <command>`), then:
  - Treat infra-failure only when the marker is absent and stderr matches a signature.
  - Strip the marker from `stdout_tail` so evidence stays clean.
2) **Tighten conditions to avoid false positives:** only classify infra failures for exit codes strongly associated with `docker run` failures (e.g. 125/126/127) and/or require docker/podman-prefixed error formatting (reduces spoofability).

Either approach should bias toward **false negatives** (treat ambiguous cases as real FAIL), not false positives.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. runValidationExecution() uses any 📜 Skill insight ✧ Quality
Description
The updated runValidationExecution signature and return type use any, which weakens type safety
and violates the TypeScript typing requirement. This can hide real shape mismatches for task,
config, deps, and the returned check/record objects.
Code

src/integrations/pi/rstack-sdlc.ts[R1158-1161]

export async function runValidationExecution(
  { projectRoot, task, stageIds = [], config, deps = {} }:
  { projectRoot: string; task: any; stageIds?: string[]; config?: any; deps?: any; },
-): Promise<{ record: any | null; check: any }> {
+): Promise<{ record: any | null; check: any; policy: string; infraBlocked: boolean }> {
Relevance

●● Moderate

Removing any may require larger typing refactor; no close precedent showing strict no-any
enforcement here.

PR-#152
PR-#230

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1399957 forbids using any in TypeScript. The changed function type annotations
include task: any, config?: any, deps?: any, and `Promise<{ record: any | null; check: any;
... }>` in the modified lines.

src/integrations/pi/rstack-sdlc.ts[1158-1161]
Skill: code-patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`runValidationExecution` introduces/retains `any` in its public TypeScript types, violating the rule that forbids `any`.

## Issue Context
The function now returns additional fields (`policy`, `infraBlocked`), so this is a good moment to define explicit interfaces/types for the function input and return shapes.

## Fix Focus Areas
- src/integrations/pi/rstack-sdlc.ts[1158-1161]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. PII in owner comment 📜 Skill insight ⛨ Security
Description
New test-file header comments include a full personal name (Richardson Gunde), which is PII in a
code comment. This violates the requirement to avoid exposing sensitive data (including PII) in
documentation/comments.
Code

tests/execution-policy-478.test.js[7]

+ * owner: RStack developed by Richardson Gunde
Relevance

●● Moderate

Repo has prior comments with the same personal name; unclear if maintainers treat it as forbidden
PII.

PR-#317

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The added header comments explicitly name an individual (Richardson Gunde), which is PII in a
documentation-like comment block. The compliance rule forbids exposing sensitive data (including
PII) in documentation/comments.

tests/execution-policy-478.test.js[1-8]
tests/security-audit.test.js[1-11]
Skill: documentation-writing

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A full personal name is added in code comments (PII). The compliance rule requires documentation/code comments not expose sensitive data including PII.

## Issue Context
The PR adds header comments with `owner: RStack developed by Richardson Gunde` in multiple new/changed test files.

## Fix Focus Areas
- tests/execution-policy-478.test.js[1-8]
- tests/security-audit.test.js[1-11]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Policy validation case mismatch ✓ Resolved 🐞 Bug ≡ Correctness
Description
validateSandboxConfig() checks execution_policy values case-sensitively, while
resolveSandboxConfig() lowercases before validating; the same mismatch exists for per-stage
execution_policy. This can cause config validation to report an operator’s policy as invalid even
though it will be accepted and applied at runtime.
Code

src/core/harness/config-validation.js[R163-166]

+  const executionPolicy = sandbox.execution_policy ?? sandbox.executionPolicy;
+  if (executionPolicy != null && !EXECUTION_POLICIES.includes(executionPolicy)) {
+    issues.push({ field: 'sandbox.execution_policy', problem: `must be one of ${EXECUTION_POLICIES.join(' | ')}, got ${JSON.stringify(executionPolicy)} — "optional" applies` });
+  }
Relevance

●●● Strong

Validator/resolver inconsistency is a straightforward correctness fix; similar harness config
hardening changes were accepted.

PR-#152

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The validator compares the raw execution_policy value directly against EXECUTION_POLICIES, while the
resolver trims and lowercases both global and per-stage values before checking membership, so
mixed-case values will pass runtime resolution but fail validation.

src/core/harness/config-validation.js[159-166]
src/core/harness/config-validation.js[199-210]
src/core/harness/sandbox.js[350-355]
src/core/harness/sandbox.js[364-377]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Config validation for `sandbox.execution_policy` and `sandbox.per_stage.<stage>.execution_policy` is currently case-sensitive, but the runtime resolver normalizes via `trim().toLowerCase()`.

### Issue Context
This creates inconsistent behavior and confusing operator feedback (e.g., `"Required"` gets flagged by the validator but is applied as `required` at runtime).

### Fix Focus Areas
- src/core/harness/config-validation.js[159-166]
- src/core/harness/config-validation.js[199-210]
- src/core/harness/sandbox.js[353-355]
- src/core/harness/sandbox.js[365-377]

### Suggested fix
In `validateSandboxConfig`:
- Normalize `executionPolicy` and `entryPolicy` the same way as the resolver: `String(value).trim().toLowerCase()` (only when the value is a string).
- Validate against `EXECUTION_POLICIES` after normalization.
- Preserve the original raw value only for error messages (optional).

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. security-audit.mjs uses console.log 📜 Skill insight ✧ Quality
Description
The script includes console.log statements, which are disallowed by the debug-logging compliance
rule. Keeping these can introduce unwanted output in CI and violates the requirement to remove debug
logging statements before merge.
Code

scripts/security-audit.mjs[R108-123]

+  if (tolerated.length) {
+    console.log('Tolerated advisories (vendored/bundled, unpatchable downstream — track upstream):');
+    for (const t of tolerated) console.log(`  - ${t.name} (${t.severity}) [${t.ids.join(', ')}]`);
+  }
+
+  if (blocking.length) {
+    console.error('\nBlocking high/critical advisories in our dependency tree:');
+    for (const b of blocking) {
+      console.error(`  - ${b.name} (${b.severity})`);
+      for (const n of b.nodes || []) console.error(`      ${n}`);
+    }
+    console.error('\nRun `npm audit` for details, then bump or override the offending package.');
+    process.exit(1);
  }
-  console.error('\nRun `npm audit` for details, then bump or override the offending package.');
-  process.exit(1);
+
+  console.log(`\nSecurity baseline OK — ${tolerated.length} tolerated bundled advisory(ies), 0 blocking.`);
Relevance

●●● Strong

Build/audit scripts likely must avoid console.log; team previously tightened this script’s
compliance logic.

PR-#439

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1400056 forbids debug logging statements like console.log. The modified main()
function prints tolerated advisories and the success summary using console.log.

scripts/security-audit.mjs[108-123]
Skill: testing-qa

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Changed code includes `console.log(...)`, which is disallowed by the compliance rule banning debug logging statements.

## Issue Context
This is a CI-facing script; if output is necessary, prefer a sanctioned logging approach (or move informational output to `console.error` if allowed by project conventions).

## Fix Focus Areas
- scripts/security-audit.mjs[108-123]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


6. Magic number 300 in slice() ✓ Resolved 📜 Skill insight ⚙ Maintainability
Description
The new infra-failure evidence string truncates stderr with a hardcoded 300 character limit. This
violates the requirement to replace magic numbers with named constants, reducing readability and
making future tuning error-prone.
Code

src/core/harness/sandbox.js[274]

+          evidence: `container infrastructure failure (exit ${exitCode}): ${stderr.text.trim().slice(0, 300) || '(no stderr captured)'} — execution not verified`,
Relevance

●●● Strong

Replacing a literal with a named constant is a low-risk maintainability cleanup; similar small
harness hardening accepted.

PR-#399

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
PR Compliance ID 1400588 requires replacing literal numeric values with named constants. The added
code truncates stderr using the literal 300 in slice(0, 300) when composing evidence output.

src/core/harness/sandbox.js[266-276]
Skill: code-patterns

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A hardcoded truncation length (`300`) is used when building the infra-failure evidence string, which is a magic number and should be a named constant.

## Issue Context
The evidence message in `runInSandbox()` truncates stderr via `stderr.text.trim().slice(0, 300)`. This should be replaced with a module-level constant (e.g., `INFRA_FAILURE_EVIDENCE_MAX_CHARS`) so the limit is self-documenting and easy to adjust.

## Fix Focus Areas
- src/core/harness/sandbox.js[266-276]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

* never consume a builder attempt or the claim itself — end-to-end through
* the real sdlc_build_next / sdlc_validate tools.
*
* owner: RStack developed by Richardson Gunde

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Pii in owner comment 📜 Skill insight ⛨ Security

New test-file header comments include a full personal name (Richardson Gunde), which is PII in a
code comment. This violates the requirement to avoid exposing sensitive data (including PII) in
documentation/comments.
Agent Prompt
## Issue description
A full personal name is added in code comments (PII). The compliance rule requires documentation/code comments not expose sensitive data including PII.

## Issue Context
The PR adds header comments with `owner: RStack developed by Richardson Gunde` in multiple new/changed test files.

## Fix Focus Areas
- tests/execution-policy-478.test.js[1-8]
- tests/security-audit.test.js[1-11]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines 1158 to +1161
export async function runValidationExecution(
{ projectRoot, task, stageIds = [], config, deps = {} }:
{ projectRoot: string; task: any; stageIds?: string[]; config?: any; deps?: any; },
): Promise<{ record: any | null; check: any }> {
): Promise<{ record: any | null; check: any; policy: string; infraBlocked: boolean }> {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. runvalidationexecution() uses any 📜 Skill insight ✧ Quality

The updated runValidationExecution signature and return type use any, which weakens type safety
and violates the TypeScript typing requirement. This can hide real shape mismatches for task,
config, deps, and the returned check/record objects.
Agent Prompt
## Issue description
`runValidationExecution` introduces/retains `any` in its public TypeScript types, violating the rule that forbids `any`.

## Issue Context
The function now returns additional fields (`policy`, `infraBlocked`), so this is a good moment to define explicit interfaces/types for the function input and return shapes.

## Fix Focus Areas
- src/integrations/pi/rstack-sdlc.ts[1158-1161]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +108 to +123
if (tolerated.length) {
console.log('Tolerated advisories (vendored/bundled, unpatchable downstream — track upstream):');
for (const t of tolerated) console.log(` - ${t.name} (${t.severity}) [${t.ids.join(', ')}]`);
}

if (blocking.length) {
console.error('\nBlocking high/critical advisories in our dependency tree:');
for (const b of blocking) {
console.error(` - ${b.name} (${b.severity})`);
for (const n of b.nodes || []) console.error(` ${n}`);
}
console.error('\nRun `npm audit` for details, then bump or override the offending package.');
process.exit(1);
}
console.error('\nRun `npm audit` for details, then bump or override the offending package.');
process.exit(1);

console.log(`\nSecurity baseline OK — ${tolerated.length} tolerated bundled advisory(ies), 0 blocking.`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

3. security-audit.mjs uses console.log 📜 Skill insight ✧ Quality

The script includes console.log statements, which are disallowed by the debug-logging compliance
rule. Keeping these can introduce unwanted output in CI and violates the requirement to remove debug
logging statements before merge.
Agent Prompt
## Issue description
Changed code includes `console.log(...)`, which is disallowed by the compliance rule banning debug logging statements.

## Issue Context
This is a CI-facing script; if output is necessary, prefer a sanctioned logging approach (or move informational output to `console.error` if allowed by project conventions).

## Fix Focus Areas
- scripts/security-audit.mjs[108-123]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment thread src/core/harness/sandbox.js Outdated
Comment thread src/core/harness/sandbox.js Outdated
Comment thread src/core/harness/config-validation.js

@strix-security strix-security Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Strix flagged 2 new security findings below. See the pinned summary comment for the full PR status.

Comment on lines +174 to +189
const INFRA_FAILURE_SIGNATURES = [
/cannot connect to the docker daemon/i,
/unable to find image/i,
/no such image/i,
/manifest (unknown|for .* not found)/i,
/pull access denied/i,
/oci runtime (create|exec) failed/i,
/error response from daemon/i,
/image .* (not found|not exist)/i,
];

function looksLikeInfraFailure({ timedOut, exitCode, stdout, stderr }) {
if (timedOut || exitCode === 0) return false;
if (stdout.trim()) return false; // the command produced output — it ran
return INFRA_FAILURE_SIGNATURES.some((pattern) => pattern.test(stderr));
}

@strix-security strix-security Bot Jul 30, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Spoofable stderr heuristic lets untrusted sandbox code suppress real test failures

Resolved — no longer flagged as of 10ffafa.

Comment on lines +1188 to +1191
const baseCheck = executionCheck(record, resolved.command);
const unverified = !record || record.tier === "unverified" || record.status === "observed";
const infraBlocked = policy === "required" && unverified;
const check = infraBlocked ? { ...baseCheck, status: "BLOCKED_INFRA" } : baseCheck;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Optional policy preserves the downgraded result as a non-blocking check

…re signal

Strix (HIGH, CWE-807) and qodo both found the same real gap in the
infra-failure heuristic: it inferred "the command never ran" from "stdout
is empty", but (a) a genuine test failure that only writes to stderr also
has empty stdout — a false positive that could silently downgrade a real
FAIL to a WARN — and (b) since the command runs via `sh -c` INSIDE the
container, repository-controlled test code could deliberately print one
of the known Docker/Podman error phrases to stderr and exit non-zero to
suppress its own real failure (the untrusted code influencing a trust
decision about itself).

Fixed with an unspoofable signal instead of an inference: buildSandboxArgv
now wraps the untrusted command so a TRUSTED start sentinel
(`__rstack_sandbox_started__`) prints to stdout before the command ever
runs. looksLikeInfraFailure now checks for the sentinel's presence in the
RAW (untruncated) stdout buffer — not "is stdout empty" — so:
- a real failure that writes only to stderr still shows the sentinel on
  stdout and correctly stays FAIL;
- untrusted code has no way to make the sentinel ABSENT, since the
  trusted wrapper's printf already ran and was already captured before
  the untrusted command got control — the only lever left to the
  untrusted code is printing the SAME sentinel too, which is harmless
  (still proves the command started, still a real FAIL either way).
The sentinel check runs on the full buffer before tail() truncates it, so
a long-output command whose sentinel would otherwise fall out of the
8KB tail window is still correctly recognized as started.

Also (qodo, real bug): validateSandboxConfig compared execution_policy
values case-sensitively while sandbox.js's resolveExecutionPolicy
normalizes via trim().toLowerCase() before comparing — an operator's
"Required" would validate as invalid even though it's accepted and
applied correctly at runtime. Added a matching normalizeExecutionPolicy
helper used by both the top-level and per-stage checks.

Also (qodo, minor): extracted the hardcoded `300` stderr-truncation length
in the infra-failure evidence string into a named
INFRA_FAILURE_EVIDENCE_MAX_CHARS constant.

Test coverage: updated the pinned argv test for the new wrapped command
format, two new tests proving a genuine stderr-only failure and a
deliberately daemon-phrase-mimicking real failure both stay FAIL, a
dedicated spoofing-attempt regression test, and case/whitespace
insensitivity coverage for the config validator matching the resolver.
1654/1654 tests, typecheck clean, lint clean.

Skipped as out of scope for this PR (originate from the already-open
#473 branch merged in for CI): PII-in-owner-comment finding (the
attribution line is this codebase's established, intentional convention,
naming the real project owner — not accidental PII), console.log usage
in security-audit.mjs. Skipped as not applicable given this file's
pre-existing convention: `any` typing on runValidationExecution.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@richard-devbot

Copy link
Copy Markdown
Owner Author

@strix-security please re-run — the spoofable stderr infra-heuristic reported in the previous review has been fixed with an unspoofable trusted start-sentinel.

@richard-devbot
richard-devbot merged commit 5c1de6a into main Jul 30, 2026
10 checks passed
@richard-devbot
richard-devbot deleted the fix-478-scientist-fail-closed branch July 30, 2026 18:03
richard-devbot pushed a commit that referenced this pull request Jul 31, 2026
…ache

Strix (CWE-863, LOW — live-reproduced): the semantic decision key omitted
the project dimension, so equal (runId, taskId, artifact) tuples in two
different roots cross-suppressed — a queue card in project A could swallow
project B's blocked gate. approvalDecisionKey now leads with
projectId ?? projectRoot, the gate-side key passes the gate's project, and
the Studio deck matches items to the run by project as well as run id for
BOTH listing and gate suppression (legacy records without project info
keep run-id matching). Two cross-project regression tests, verified
failing first.

Qodo (reliability, PR #143 precedent): canonicalPath/canonicalRoot
swallowed every realpath failure — EACCES/ELOOP/EIO now log to stderr
before degrading to the lexical form; only not-found stays quiet. The
memoization cache is removed outright (realpath is microseconds, the root
set is tiny, and a cache would freeze identities across symlink retargets)
— which also resolves the staleness and vague-perf-claim review notes.

Qodo (convention, PR #490 precedent): the browser test's timeouts are
named constants.

Refs #494 #505 (PR #509 review follow-up).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
richard-devbot pushed a commit that referenced this pull request Jul 31, 2026
Qodo (correctness, real): morphChildren used a plain object as the
data-key → node map, so a state-derived key like "__proto__" silently
failed to store (assignment hits the prototype slot) — the node lost
keyed matching and its identity on reorder. Object.create(null) removes
every special-key pitfall; a __proto__ round-trip case is pinned in the
morph browser test, verified failing on the plain-object map first.

Convention items (PR #490 precedent): browser-test timeouts are named
constants; test names carry their #495 attribution.

Refs #495 (PR #504 review follow-up).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
richard-devbot pushed a commit that referenced this pull request Jul 31, 2026
Qodo (convention, PR #490 precedent): humanizeDurationMs and
formatSnapshotAge now branch on named constants (MS_PER_MINUTE,
MINUTES_PER_HOUR, HOURS_PER_DAY) — the two formatters are meant to stay
in step, and named thresholds make drift visible at a glance. Browser-test
timeouts are named constants in both new test files.

Refs #491 #492 #493 (PR #508 review follow-up).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
richard-devbot pushed a commit that referenced this pull request Jul 31, 2026
Qodo (convention, PR #490 precedent): the events-timeline depth cap and
the rail-label goal length are named constants (EVENTS_TIMELINE_CAP —
matching the rollup index's own timeline depth — and
PROOF_SOURCE_GOAL_CHARS).

Refs #496 #498 (PR #511 review follow-up).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
richard-devbot pushed a commit that referenced this pull request Jul 31, 2026
Qodo (convention, #490/#509/#511 precedent): the settle guard's 120ms
slack is now COUNT_UP_SETTLE_BUFFER_MS with its rationale.

Refs #516 (PR #520 review follow-up).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[P0][Scientist] Required execution must fail closed as BLOCKED_INFRA, never PASS on WARN

2 participants