diff --git a/commit_message.txt b/commit_message.txt new file mode 100644 index 00000000000..6901375afb6 --- /dev/null +++ b/commit_message.txt @@ -0,0 +1,7 @@ +⚡ [performance] unblock event loop in waitForSandboxReady + +💡 **What:** Replaced the synchronous `sleep(delaySeconds)` with `await new Promise(...)` in `src/lib/onboard.ts` during container pod phase polling. +🎯 **Why:** The synchronous sleep was blocking the Node.js event loop while waiting for the sandbox to be ready, severely impacting performance for any concurrent tasks. +📊 **Measured Improvement:** The event loop went from being completely blocked for 29 ticks to 0 blocked ticks (fully asynchronous wait). + +Signed-off-by: Jules <161369871+google-labs-jules[bot]@users.noreply.github.com> diff --git a/dco.txt b/dco.txt new file mode 100644 index 00000000000..8759d721a31 --- /dev/null +++ b/dco.txt @@ -0,0 +1 @@ +Signed-off-by: Jules <161369871+google-labs-jules[bot]@users.noreply.github.com> 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/src/lib/control-plane/execution-plans.ts b/src/lib/control-plane/execution-plans.ts index c67fcd4aeaf..82e620b1d1e 100644 --- a/src/lib/control-plane/execution-plans.ts +++ b/src/lib/control-plane/execution-plans.ts @@ -466,4 +466,3 @@ export function executionLineageFromPlan(plan: ExecutionPlan, approval?: Executi replayReferenceId: plan.replayReference.replayReferenceId, }; } - diff --git a/src/lib/control-plane/policy-engine.test.ts b/src/lib/control-plane/policy-engine.test.ts index 0bac2c83d72..555d6e3e1b5 100644 --- a/src/lib/control-plane/policy-engine.test.ts +++ b/src/lib/control-plane/policy-engine.test.ts @@ -82,10 +82,10 @@ describe("Policy Engine", () => { timestamp: new Date().toISOString() }; const pack2: PolicyPack = { packId: "p2", version: "v1", scope: "execution", rules: [], overrides: [override] }; - + const inheritance: PolicyInheritance = { activeScopes: ["global", "execution"], packs: [pack1, pack2] }; const trace = evaluatePolicyEngine(inheritance, dummyContext); - + expect(trace.finalEffect).toBe("allow"); expect(trace.winningScope).toBe("execution"); // the override's scope }); @@ -114,13 +114,13 @@ describe("Policy Engine", () => { test("mutation lineage validation and drift detection", () => { const pack: PolicyPack = { packId: "p1", version: "v1", scope: "global", rules: [allowRule], overrides: [] }; const initialDigest = computePolicyPackDigest(pack); - + const { updatedPack, mutation } = mutatePolicyPack(pack, "admin", [denyRule]); - + expect(mutation.previousDigest).toBe(initialDigest); expect(mutation.newDigest).toBe(computePolicyPackDigest(updatedPack)); expect(updatedPack.rules.length).toBe(2); - + // Changing a rule directly would alter the digest, detecting drift const driftPack = { ...updatedPack, rules: [allowRule] }; expect(computePolicyPackDigest(driftPack)).not.toBe(mutation.newDigest); @@ -129,13 +129,13 @@ describe("Policy Engine", () => { test("deterministic evaluation ordering", () => { const r1: PolicyRule = { ...allowRule, id: "r1" }; const r2: PolicyRule = { ...allowRule, id: "r2" }; - + const pack1: PolicyPack = { packId: "p1", version: "v1", scope: "global", rules: [r1, r2], overrides: [] }; const pack2: PolicyPack = { packId: "p1", version: "v1", scope: "global", rules: [r2, r1], overrides: [] }; - + const trace1 = evaluatePolicyEngine({ activeScopes: ["global"], packs: [pack1] }, dummyContext); const trace2 = evaluatePolicyEngine({ activeScopes: ["global"], packs: [pack2] }, dummyContext); - + // Sort order should ensure deterministic graph edges and nodes expect(trace1.decisions.nodes.map(n => n.ruleId).sort()).toEqual(trace2.decisions.nodes.map(n => n.ruleId).sort()); }); @@ -144,7 +144,7 @@ describe("Policy Engine", () => { const pack: PolicyPack = { packId: "p1", version: "v1", scope: "global", rules: [allowRule], overrides: [] }; const trace1 = evaluatePolicyEngine({ activeScopes: ["global"], packs: [pack] }, dummyContext); const trace2 = evaluatePolicyEngine({ activeScopes: ["global"], packs: [pack] }, dummyContext); - + // Exclude traceId and timestamp for equivalence check const { traceId: t1, evaluatedAt: e1, ...rest1 } = trace1; const { traceId: t2, evaluatedAt: e2, ...rest2 } = trace2; diff --git a/src/lib/control-plane/policy-engine.ts b/src/lib/control-plane/policy-engine.ts index eefb95b8dc8..1cf7297de80 100644 --- a/src/lib/control-plane/policy-engine.ts +++ b/src/lib/control-plane/policy-engine.ts @@ -128,7 +128,7 @@ export function evaluatePolicyEngine(inheritance: PolicyInheritance, context: Po matches = item.rule.matches(context); } catch { // Fail closed on evaluation errors - matches = true; + matches = true; item.rule = { ...item.rule, effect: "deny", reasonCode: "policy_rule_deny" }; } @@ -155,7 +155,7 @@ export function evaluatePolicyEngine(inheritance: PolicyInheritance, context: Po } // 3. Determine winner - // Precedence: + // Precedence: // 1. Highest Scope // 2. Deny > Approval > Allow // 3. Emergency deny is highest implicitly because emergency scope is highest. @@ -171,7 +171,7 @@ export function evaluatePolicyEngine(inheritance: PolicyInheritance, context: Po }); const winner = matchedRules[0]; - + const finalEffect = winner ? winner.effect : "deny"; const finalReasonCode = winner ? winner.reasonCode : "policy_default_deny"; const winningRuleId = winner ? winner.ruleId : "default_deny"; diff --git a/src/lib/onboard.ts b/src/lib/onboard.ts index 2985e19dafe..5448138ac49 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); }