From 2ede8e1ef9fa11dc405cf8a63038f3477674cc5f Mon Sep 17 00:00:00 2001 From: Hardonian <118695431+Hardonian@users.noreply.github.com> Date: Sun, 10 May 2026 03:56:44 +0000 Subject: [PATCH 1/5] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20performance:=20unblock?= =?UTF-8?q?=20event=20loop=20in=20waitForSandboxReady?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced the synchronous `sleep(delaySeconds)` with `await new Promise(...)` to avoid blocking the Node.js event loop during sandbox readiness checks. --- src/lib/onboard.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 2985e19dafe..4b86c97df3d 100644 --- a/src/lib/onboard.ts +++ b/src/lib/onboard.ts @@ -3539,7 +3539,7 @@ async function ensureNamedCredential( return replaceNamedCredential(envName, label, helpUrl); } -function waitForSandboxReady(sandboxName: string, attempts = 10, delaySeconds = 2): boolean { +async function waitForSandboxReady(sandboxName: string, attempts = 10, delaySeconds = 2): Promise { for (let i = 0; i < attempts; i += 1) { const podPhase = runCaptureOpenshell( [ @@ -3558,7 +3558,7 @@ function waitForSandboxReady(sandboxName: string, attempts = 10, delaySeconds = { ignoreError: true }, ); if (podPhase === "Running") return true; - sleep(delaySeconds); + await new Promise(r => setTimeout(r, delaySeconds * 1000)); } return false; } @@ -7984,7 +7984,7 @@ async function _setupPolicies( process.exit(1); } - if (!waitForSandboxReady(sandboxName)) { + if (!(await waitForSandboxReady(sandboxName))) { console.error(` Sandbox '${sandboxName}' was not ready for policy application.`); process.exit(1); } @@ -8022,7 +8022,7 @@ async function _setupPolicies( return; } - if (!waitForSandboxReady(sandboxName)) { + if (!(await waitForSandboxReady(sandboxName))) { console.error(` Sandbox '${sandboxName}' was not ready for policy application.`); process.exit(1); } @@ -8617,7 +8617,7 @@ async function setupPoliciesWithSelection( if (selectedPresets && selectedPresets.length > 0) { const resumeSelection = chosen || []; if (onSelection) onSelection(resumeSelection); - if (!waitForSandboxReady(sandboxName)) { + if (!(await waitForSandboxReady(sandboxName))) { console.error(` Sandbox '${sandboxName}' was not ready for policy application.`); process.exit(1); } @@ -8702,7 +8702,7 @@ async function setupPoliciesWithSelection( } if (onSelection) onSelection(chosen); - if (!waitForSandboxReady(sandboxName)) { + if (!(await waitForSandboxReady(sandboxName))) { console.error(` Sandbox '${sandboxName}' was not ready for policy application.`); process.exit(1); } @@ -8724,7 +8724,7 @@ async function setupPoliciesWithSelection( const interactiveChoice = resolvedPresets.map((p) => p.name); if (onSelection) onSelection(interactiveChoice); - if (!waitForSandboxReady(sandboxName)) { + if (!(await waitForSandboxReady(sandboxName))) { console.error(` Sandbox '${sandboxName}' was not ready for policy application.`); process.exit(1); } From b96c935c61c5f8746873ef916e2e1149601961a8 Mon Sep 17 00:00:00 2001 From: Hardonian <118695431+Hardonian@users.noreply.github.com> Date: Sun, 10 May 2026 04:21:39 +0000 Subject: [PATCH 2/5] =?UTF-8?q?=E2=9A=A1=EF=B8=8F=20[performance]=20unbloc?= =?UTF-8?q?k=20event=20loop=20in=20waitForSandboxReady?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaced the synchronous `sleep(delaySeconds)` with `await new Promise(...)` to avoid blocking the Node.js event loop during sandbox readiness checks. Signed-off-by: Jules <161369871+google-labs-jules[bot]@users.noreply.github.com> --- .../architecture/secret-redaction-doctrine.md | 12 +- docs/architecture/security-policy-model.md | 12 +- docs/architecture/security-threat-model.md | 48 +- .../src/components/viewers/receipt-viewer.tsx | 442 +++++++++--------- operator-console/src/styles/index.css | 1 - scripts/verify-changelog-hygiene.js | 19 +- scripts/verify-core.js | 41 +- src/lib/control-plane/execution-plans.ts | 1 - src/lib/control-plane/governance.ts | 3 +- src/lib/control-plane/policy-engine.test.ts | 18 +- src/lib/control-plane/policy-engine.ts | 6 +- src/lib/control-plane/runtime-seams.ts | 2 +- src/lib/control-plane/task-classification.ts | 2 + src/lib/control-plane/types.ts | 12 + src/lib/execution/queue.ts | 2 +- 15 files changed, 335 insertions(+), 286 deletions(-) diff --git a/docs/architecture/secret-redaction-doctrine.md b/docs/architecture/secret-redaction-doctrine.md index 9f92467dfdc..b2f906e04ec 100644 --- a/docs/architecture/secret-redaction-doctrine.md +++ b/docs/architecture/secret-redaction-doctrine.md @@ -79,8 +79,8 @@ Additional high-confidence patterns for persistent content: ### Tier 1: Partial redaction (`redact()`) -**Consumer:** `src/lib/runner.ts` (CLI subprocess output) -**Behavior:** Preserves first 4 characters, replaces remainder with `*` (capped at 20 asterisks). +**Consumer:** `src/lib/runner.ts` (CLI subprocess output) +**Behavior:** Preserves first 4 characters, replaces remainder with `*` (capped at 20 asterisks). **Rationale:** Allows operators to identify which key is in use without exposing the full secret. ```text @@ -92,8 +92,8 @@ URL handling: Replaces userinfo with `****`, redacts sensitive query parameters. ### Tier 2: Full redaction (`redactFull()`) -**Consumer:** `src/lib/debug.ts` (diagnostic dump files) -**Behavior:** Replaces entire match with ``. Also covers `KEY=value` patterns and `Bearer` tokens. +**Consumer:** `src/lib/debug.ts` (diagnostic dump files) +**Behavior:** Replaces entire match with ``. Also covers `KEY=value` patterns and `Bearer` tokens. **Rationale:** Diagnostic dumps may be shared with support; no partial exposure is acceptable. ```text @@ -103,8 +103,8 @@ Output: NVIDIA_API_KEY= ### Tier 3: Sensitive text redaction (`redactSensitiveText()`) -**Consumer:** `src/lib/onboard-session.ts` (onboarding session logs) -**Behavior:** Full replacement + 240-character output truncation. +**Consumer:** `src/lib/onboard-session.ts` (onboarding session logs) +**Behavior:** Full replacement + 240-character output truncation. **Rationale:** Onboarding logs may contain user-typed credentials; truncation prevents accumulation of sensitive context. --- diff --git a/docs/architecture/security-policy-model.md b/docs/architecture/security-policy-model.md index 480fad15939..437b4b0e4bc 100644 --- a/docs/architecture/security-policy-model.md +++ b/docs/architecture/security-policy-model.md @@ -28,7 +28,7 @@ Each gate emits a deterministic reason code on rejection. No silent fallthrough. ### NetworkPolicy -**Status:** Implemented (sandbox egress) +**Status:** Implemented (sandbox egress) **Scope:** Controls which external endpoints the sandbox can reach. Defined in `nemoclaw-blueprint/policies/` as YAML presets. Each preset specifies allowed egress domains. The operator selects which presets apply during onboarding. Unlisted endpoints are denied (deny-by-default). @@ -37,7 +37,7 @@ SSRF protection (`nemoclaw/src/blueprint/ssrf.ts`) supplements the network polic ### CommandExecutionPolicy -**Status:** Implemented +**Status:** Implemented **Scope:** Governs how subprocess commands are executed. Hard constraints (not configurable — always enforced): @@ -49,7 +49,7 @@ These constraints are structural (enforced in `src/lib/runner.ts`) and cannot be ### RemoteExecutionPolicy -**Status:** Scaffolded (opt-in via `NEMOCLAW_REMOTE_EXECUTION=1`) +**Status:** Scaffolded (opt-in via `NEMOCLAW_REMOTE_EXECUTION=1`) **Scope:** Controls whether execution may cross trust boundaries to remote workers. Policy evaluation chain (implemented in `src/lib/control-plane/remote-execution.ts`): @@ -62,7 +62,7 @@ Policy evaluation chain (implemented in `src/lib/control-plane/remote-execution. ### CredentialPolicy -**Status:** Implemented +**Status:** Implemented **Scope:** Controls credential handling across persistence boundaries. Rules (enforced in `src/lib/security/credential-filter.ts`): @@ -74,7 +74,7 @@ Rules (enforced in `src/lib/security/credential-filter.ts`): ### SecretRedactionPolicy -**Status:** Implemented +**Status:** Implemented **Scope:** Controls how secrets appear in operator-visible outputs. Three redaction tiers (implemented in `src/lib/security/redact.ts`): @@ -89,7 +89,7 @@ All tiers source patterns from `src/lib/security/secret-patterns.ts`. ### MemoryWritePolicy -**Status:** Implemented +**Status:** Implemented **Scope:** Controls what content may be written to persistent workspace memory. Rules (enforced in `nemoclaw/src/security/secret-scanner.ts`): diff --git a/docs/architecture/security-threat-model.md b/docs/architecture/security-threat-model.md index bb9de28f1e2..046587e602c 100644 --- a/docs/architecture/security-threat-model.md +++ b/docs/architecture/security-threat-model.md @@ -16,8 +16,8 @@ This document catalogs the security threats relevant to the NemoClaw governed ex ## THREAT-001: Credential leakage via CLI output -**Category:** Secret exposure -**Status:** Mitigated +**Category:** Secret exposure +**Status:** Mitigated **Attack vector:** API keys, tokens, or passwords embedded in subprocess stdout/stderr leak to the operator terminal or diagnostic logs. **Enforcement:** @@ -37,8 +37,8 @@ This document catalogs the security threats relevant to the NemoClaw governed ex ## THREAT-002: Credential persistence in config files -**Category:** Secret exposure -**Status:** Mitigated +**Category:** Secret exposure +**Status:** Mitigated **Attack vector:** API keys baked into sandbox filesystem or local backup archives survive beyond their intended runtime scope. **Enforcement:** @@ -57,8 +57,8 @@ This document catalogs the security threats relevant to the NemoClaw governed ex ## THREAT-003: Credential leakage via URLs -**Category:** Secret exposure -**Status:** Mitigated +**Category:** Secret exposure +**Status:** Mitigated **Attack vector:** Tokens embedded in URL query parameters, userinfo, or path segments leak through logging or diagnostic output. **Enforcement:** @@ -74,8 +74,8 @@ This document catalogs the security threats relevant to the NemoClaw governed ex ## THREAT-004: Secret persistence in workspace memory writes -**Category:** Secret exposure -**Status:** Mitigated +**Category:** Secret exposure +**Status:** Mitigated **Attack vector:** An agent writes an API key or credential into a persistent memory file (MEMORY.md, workspace files, agent skills), where it survives across sessions. **Enforcement:** @@ -93,8 +93,8 @@ This document catalogs the security threats relevant to the NemoClaw governed ex ## THREAT-005: Command injection via shell interpretation -**Category:** Command execution safety -**Status:** Mitigated +**Category:** Command execution safety +**Status:** Mitigated **Attack vector:** An attacker crafts input containing shell metacharacters (`$(whoami)`, `&& rm -rf /`, backtick expansion) that are interpreted if commands are executed via shell. **Enforcement:** @@ -111,8 +111,8 @@ This document catalogs the security threats relevant to the NemoClaw governed ex ## THREAT-006: SSRF via DNS rebinding (TOCTOU) -**Category:** Network safety -**Status:** Mitigated +**Category:** Network safety +**Status:** Mitigated **Attack vector:** An attacker controls a DNS record that returns a public IP at validation time and a private/internal IP at connection time, bypassing the private-IP check. **Enforcement:** @@ -131,8 +131,8 @@ This document catalogs the security threats relevant to the NemoClaw governed ex ## THREAT-007: Malicious or compromised remote worker -**Category:** Transport/trust boundary -**Status:** Scaffolded +**Category:** Transport/trust boundary +**Status:** Scaffolded **Attack vector:** A remote worker endpoint returns forged execution results, manipulated telemetry, or exfiltrates command payloads. **Enforcement (current):** @@ -151,8 +151,8 @@ This document catalogs the security threats relevant to the NemoClaw governed ex ## THREAT-008: Forged telemetry injection -**Category:** Observability integrity -**Status:** Partially mitigated +**Category:** Observability integrity +**Status:** Partially mitigated **Attack vector:** An attacker injects fabricated telemetry data (fake GPU counts, false health status) to influence operational intelligence or mislead operators. **Enforcement (current):** @@ -169,8 +169,8 @@ This document catalogs the security threats relevant to the NemoClaw governed ex ## THREAT-009: Replay envelope tampering -**Category:** Audit integrity -**Status:** Mitigated +**Category:** Audit integrity +**Status:** Mitigated **Attack vector:** An attacker modifies exported replay envelopes (event payloads, sequence numbers, lineage references) to forge execution history. **Enforcement:** @@ -186,8 +186,8 @@ This document catalogs the security threats relevant to the NemoClaw governed ex ## THREAT-010: Remote execution without operator consent -**Category:** Authorization boundary -**Status:** Mitigated +**Category:** Authorization boundary +**Status:** Mitigated **Attack vector:** A local command silently dispatches execution to a remote worker without the operator being aware of the trust boundary crossing. **Enforcement:** @@ -203,8 +203,8 @@ This document catalogs the security threats relevant to the NemoClaw governed ex ## THREAT-011: Proofpack / export integrity compromise -**Category:** Evidence integrity -**Status:** Partially mitigated +**Category:** Evidence integrity +**Status:** Partially mitigated **Attack vector:** Exported evidence bundles (receipts, plans, telemetry) are modified after export, creating a false audit trail. **Enforcement (current):** @@ -221,8 +221,8 @@ This document catalogs the security threats relevant to the NemoClaw governed ex ## THREAT-012: Unauthorized privilege escalation via trust conflation -**Category:** Authorization boundary -**Status:** Mitigated +**Category:** Authorization boundary +**Status:** Mitigated **Attack vector:** A worker with high trust (based on telemetry or self-reported claims) bypasses policy evaluation to execute unauthorized work. **Enforcement:** diff --git a/operator-console/src/components/viewers/receipt-viewer.tsx b/operator-console/src/components/viewers/receipt-viewer.tsx index b527111ff45..36010476822 100644 --- a/operator-console/src/components/viewers/receipt-viewer.tsx +++ b/operator-console/src/components/viewers/receipt-viewer.tsx @@ -1,221 +1,221 @@ -import React from "react"; -import type { ExecutionReceipt, DegradedState } from "../../data/types"; -import { StateLabel } from "../primitives/state-label"; -import { StatusBadge } from "../primitives/status-badge"; -import { Timestamp } from "../primitives/timestamp"; -import { Timeline } from "../primitives/timeline"; -import { KVTable } from "../primitives/key-value-table"; -import { DataTable, type ColumnDef } from "../primitives/data-table"; -import styles from "./receipt-viewer.module.css"; - -export interface ReceiptViewerProps { - receipt: ExecutionReceipt; -} - -export function ReceiptViewer({ receipt }: ReceiptViewerProps) { - const timelineItems = receipt.phases.map((phase) => ({ - timestamp: phase.at, - label: phase.phase, - status: phaseStatus(phase.phase), - detail: phase.notes, - })); - - const severityStatus = severityToStatus(highestSeverity(receipt.degradedEvents)); - - return ( -
-
-
-

- Receipt: {receipt.receiptId} -

- {severityStatus && } -
-
- Request: {receipt.requestId} - Created: - {receipt.nodeId && Node: {receipt.nodeId}} - {receipt.modelId && Model: {receipt.modelId}} -
-
- -
-

Execution Phases

- -
- - {receipt.schedulingDecision && ( -
-

Scheduling Decision

- -
- )} - - {receipt.policyDecision && ( -
-

Policy Decision

- -
- )} - - {receipt.degradedEvents.length > 0 && ( -
-

Degraded Events ({receipt.degradedEvents.length})

-
- {receipt.degradedEvents.map((d, idx) => ( -
-
- - - {d.reasonCode} -
-

{d.explanation}

-
- Subsystem: {d.affectedSubsystem} - Source: {d.sourceComponent} - -
- {d.recoverySuggestion && ( -

Recovery: {d.recoverySuggestion}

- )} -
- ))} -
-
- )} - - {receipt.fallbackAttempts.length > 0 && ( -
-

Fallback Attempts ({receipt.fallbackAttempts.length})

- ({ at: f.at, reason: f.reason, target: f.target ?? "" }))} - caption="Fallback attempts" - /> -
- )} - - {receipt.toolInvocations.length > 0 && ( -
-

Tool Invocations ({receipt.toolInvocations.length})

- ({ name: t.name, at: t.at, durationMs: t.durationMs ?? "N/A", status: t.status }))} - caption="Tool invocations" - /> -
- )} - -
-

Timing

- -
- -
-

Provenance

- }] : []), - ]} - /> -
- - {receipt.operatorOverrides.length > 0 && ( -
-

Operator Overrides ({receipt.operatorOverrides.length})

-
- {receipt.operatorOverrides.map((o, idx) => ( -
- {o.actor} - -

{o.reason}

-
- ))} -
-
- )} -
- ); -} - -const fallbackColumns: ColumnDef[] = [ - { key: "at", header: "Time", render: (v) => }, - { key: "reason", header: "Reason" }, - { key: "target", header: "Target" }, -]; - -const toolColumns: ColumnDef[] = [ - { key: "name", header: "Tool" }, - { key: "at", header: "Time", render: (v) => }, - { key: "durationMs", header: "Duration (ms)" }, - { key: "status", header: "Status", render: (v) => }, -]; - -function phaseStatus(phase: string): string { - const map: Record = { - received: "healthy", - policy: "healthy", - scheduling: "healthy", - execution: "constrained", - completed: "healthy", - failed: "unavailable", - }; - return map[phase] ?? "unknown"; -} - -function severityToStatus(severity: string): "info" | "warning" | "error" | "critical" | "success" | "unknown" { - const map: Record = { - info: "info", - warning: "warning", - error: "error", - critical: "critical", - }; - return map[severity] ?? "unknown"; -} - -function highestSeverity(events: DegradedState[]): string { - const order = ["critical", "error", "warning", "info"]; - for (const level of order) { - if (events.some((e) => e.severity === level)) return level; - } - return "info"; -} - -function formatMs(ms: number | undefined): string { - if (ms === undefined) return "Unavailable"; - return `${ms} ms`; -} - -function schedulingEntries(decision: { selected?: { nodeId: string; modelId: string; score: number; reasons: Array<{ code: string; explanation: string; source: string }> }; rejected: Array<{ nodeId: string; modelId: string; score: number; reasons: Array<{ code: string; explanation: string; source: string }> }>; reasons: Array<{ code: string; explanation: string; source: string }> }): Array<{ key: string; value: React.ReactNode }> { - const entries: Array<{ key: string; value: React.ReactNode }> = []; - if (decision.selected) { - entries.push({ key: "Selected", value: `${decision.selected.nodeId} : ${decision.selected.modelId} (score: ${decision.selected.score})` }); - if (decision.selected.reasons.length > 0) { - entries.push({ key: "Reason", value: decision.selected.reasons.map((r) => r.code).join(", ") }); - } - } else { - entries.push({ key: "Selected", value: "None" }); - } - if (decision.rejected.length > 0) { - entries.push({ key: "Rejected", value: `${decision.rejected.length} candidate(s)` }); - } - entries.push({ key: "Decision Reasons", value: decision.reasons.map((r) => r.code).join(", ") }); - return entries; -} - -function policyEntries(decision: { allowed: boolean; requiredApproval: boolean; reasons: Array<{ code: string; explanation: string; source: string }> }): Array<{ key: string; value: React.ReactNode }> { - return [ - { key: "Allowed", value: decision.allowed ? "Yes" : "No" }, - { key: "Approval Required", value: decision.requiredApproval ? "Yes" : "No" }, - { key: "Reasons", value: decision.reasons.map((r) => r.code).join(", ") }, - ]; -} +import React from "react"; +import type { ExecutionReceipt, DegradedState } from "../../data/types"; +import { StateLabel } from "../primitives/state-label"; +import { StatusBadge } from "../primitives/status-badge"; +import { Timestamp } from "../primitives/timestamp"; +import { Timeline } from "../primitives/timeline"; +import { KVTable } from "../primitives/key-value-table"; +import { DataTable, type ColumnDef } from "../primitives/data-table"; +import styles from "./receipt-viewer.module.css"; + +export interface ReceiptViewerProps { + receipt: ExecutionReceipt; +} + +export function ReceiptViewer({ receipt }: ReceiptViewerProps) { + const timelineItems = receipt.phases.map((phase) => ({ + timestamp: phase.at, + label: phase.phase, + status: phaseStatus(phase.phase), + detail: phase.notes, + })); + + const severityStatus = severityToStatus(highestSeverity(receipt.degradedEvents)); + + return ( +
+
+
+

+ Receipt: {receipt.receiptId} +

+ {severityStatus && } +
+
+ Request: {receipt.requestId} + Created: + {receipt.nodeId && Node: {receipt.nodeId}} + {receipt.modelId && Model: {receipt.modelId}} +
+
+ +
+

Execution Phases

+ +
+ + {receipt.schedulingDecision && ( +
+

Scheduling Decision

+ +
+ )} + + {receipt.policyDecision && ( +
+

Policy Decision

+ +
+ )} + + {receipt.degradedEvents.length > 0 && ( +
+

Degraded Events ({receipt.degradedEvents.length})

+
+ {receipt.degradedEvents.map((d, idx) => ( +
+
+ + + {d.reasonCode} +
+

{d.explanation}

+
+ Subsystem: {d.affectedSubsystem} + Source: {d.sourceComponent} + +
+ {d.recoverySuggestion && ( +

Recovery: {d.recoverySuggestion}

+ )} +
+ ))} +
+
+ )} + + {receipt.fallbackAttempts.length > 0 && ( +
+

Fallback Attempts ({receipt.fallbackAttempts.length})

+ ({ at: f.at, reason: f.reason, target: f.target ?? "" }))} + caption="Fallback attempts" + /> +
+ )} + + {receipt.toolInvocations.length > 0 && ( +
+

Tool Invocations ({receipt.toolInvocations.length})

+ ({ name: t.name, at: t.at, durationMs: t.durationMs ?? "N/A", status: t.status }))} + caption="Tool invocations" + /> +
+ )} + +
+

Timing

+ +
+ +
+

Provenance

+ }] : []), + ]} + /> +
+ + {receipt.operatorOverrides.length > 0 && ( +
+

Operator Overrides ({receipt.operatorOverrides.length})

+
+ {receipt.operatorOverrides.map((o, idx) => ( +
+ {o.actor} + +

{o.reason}

+
+ ))} +
+
+ )} +
+ ); +} + +const fallbackColumns: ColumnDef[] = [ + { key: "at", header: "Time", render: (v) => }, + { key: "reason", header: "Reason" }, + { key: "target", header: "Target" }, +]; + +const toolColumns: ColumnDef[] = [ + { key: "name", header: "Tool" }, + { key: "at", header: "Time", render: (v) => }, + { key: "durationMs", header: "Duration (ms)" }, + { key: "status", header: "Status", render: (v) => }, +]; + +function phaseStatus(phase: string): string { + const map: Record = { + received: "healthy", + policy: "healthy", + scheduling: "healthy", + execution: "constrained", + completed: "healthy", + failed: "unavailable", + }; + return map[phase] ?? "unknown"; +} + +function severityToStatus(severity: string): "info" | "warning" | "error" | "critical" | "success" | "unknown" { + const map: Record = { + info: "info", + warning: "warning", + error: "error", + critical: "critical", + }; + return map[severity] ?? "unknown"; +} + +function highestSeverity(events: DegradedState[]): string { + const order = ["critical", "error", "warning", "info"]; + for (const level of order) { + if (events.some((e) => e.severity === level)) return level; + } + return "info"; +} + +function formatMs(ms: number | undefined): string { + if (ms === undefined) return "Unavailable"; + return `${ms} ms`; +} + +function schedulingEntries(decision: { selected?: { nodeId: string; modelId: string; score: number; reasons: Array<{ code: string; explanation: string; source: string }> }; rejected: Array<{ nodeId: string; modelId: string; score: number; reasons: Array<{ code: string; explanation: string; source: string }> }>; reasons: Array<{ code: string; explanation: string; source: string }> }): Array<{ key: string; value: React.ReactNode }> { + const entries: Array<{ key: string; value: React.ReactNode }> = []; + if (decision.selected) { + entries.push({ key: "Selected", value: `${decision.selected.nodeId} : ${decision.selected.modelId} (score: ${decision.selected.score})` }); + if (decision.selected.reasons.length > 0) { + entries.push({ key: "Reason", value: decision.selected.reasons.map((r) => r.code).join(", ") }); + } + } else { + entries.push({ key: "Selected", value: "None" }); + } + if (decision.rejected.length > 0) { + entries.push({ key: "Rejected", value: `${decision.rejected.length} candidate(s)` }); + } + entries.push({ key: "Decision Reasons", value: decision.reasons.map((r) => r.code).join(", ") }); + return entries; +} + +function policyEntries(decision: { allowed: boolean; requiredApproval: boolean; reasons: Array<{ code: string; explanation: string; source: string }> }): Array<{ key: string; value: React.ReactNode }> { + return [ + { key: "Allowed", value: decision.allowed ? "Yes" : "No" }, + { key: "Approval Required", value: decision.requiredApproval ? "Yes" : "No" }, + { key: "Reasons", value: decision.reasons.map((r) => r.code).join(", ") }, + ]; +} diff --git a/operator-console/src/styles/index.css b/operator-console/src/styles/index.css index 2b0e40a9d27..a80fa645d55 100644 --- a/operator-console/src/styles/index.css +++ b/operator-console/src/styles/index.css @@ -191,4 +191,3 @@ details[open] > summary::before { color: var(--color-text-secondary); margin: 0 0 1.5rem; } - diff --git a/scripts/verify-changelog-hygiene.js b/scripts/verify-changelog-hygiene.js index 73977c3f13f..f87918e9c16 100644 --- a/scripts/verify-changelog-hygiene.js +++ b/scripts/verify-changelog-hygiene.js @@ -25,18 +25,27 @@ const duplicateLines = [...duplicateLineMap.entries()] .map(([line]) => line); const problems = []; -if (count(/SPDX-FileCopyrightText/g) !== 1) problems.push("duplicate SPDX-FileCopyrightText header"); -if (count(/SPDX-License-Identifier/g) !== 1) problems.push("duplicate SPDX-License-Identifier header"); +if (count(/SPDX-FileCopyrightText/g) !== 1) + problems.push("duplicate SPDX-FileCopyrightText header"); +if (count(/SPDX-License-Identifier/g) !== 1) + problems.push("duplicate SPDX-License-Identifier header"); const titleCount = normalized.filter((line) => line === "# Changelog").length; if (titleCount !== 1) problems.push("duplicate # Changelog title"); const titleIndex = normalized.findIndex((line) => line === "# Changelog"); if (titleIndex >= 0) { const preTitleContent = normalized .slice(0, titleIndex) - .filter((line) => line && !line.startsWith("