Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions src/OpenClaw.Shared/ExecApprovals/CanonicalCommandIdentity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
using System.Collections.Generic;

namespace OpenClaw.Shared.ExecApprovals;

// Architectural barrier produced by PR3.
// Equivalent to ExecHostValidatedRequest in the macOS reference, extended with resolution outputs.
// No module from PR4 onward may accept ValidatedRunRequest as direct input (research doc 05 line 439).
// Rail 15: a single canonical representation reused across evaluation, logging, prompting, execution.
public sealed class CanonicalCommandIdentity
{
// ── Normalization outputs ─────────────────────────────────────────────────

// Argv exactly as produced by PR2 (no trimming; coding contract process-argv-semantics).
public IReadOnlyList<string> Command { get; }

// Canonical display form generated from argv. Never rawCommand from the agent.
// Used by logging and prompting. Research doc 05 decision 2.
public string DisplayCommand { get; }

// Safe rawCommand for executable resolution. Null in Windows v1 (rawCommand not in
// system.run protocol; research doc 05 OQ-V4 / decision 10).
public string? EvaluationRawCommand { get; }

// ── Resolution outputs ────────────────────────────────────────────────────

// Singular resolution for the state machine (PR5).
// Null if the primary executable cannot be determined.
public ExecCommandResolution? Resolution { get; }

// Per-segment resolutions for the allowlist matcher (PR4/PR5).
// Empty list means fail-closed — no allowlist satisfaction possible.
public IReadOnlyList<ExecCommandResolution> AllowlistResolutions { get; }

// Suggested allowlist patterns for prompt/UI (PR6). Not a security decision.
public IReadOnlyList<string> AllowAlwaysPatterns { get; }

// ── Request context (carried from ValidatedRunRequest) ────────────────────

public string? Cwd { get; }
public int TimeoutMs { get; }
public IReadOnlyDictionary<string, string>? Env { get; }
public string? AgentId { get; }
public string? SessionKey { get; }

internal CanonicalCommandIdentity(
IReadOnlyList<string> command,
string displayCommand,
string? evaluationRawCommand,
ExecCommandResolution? resolution,
IReadOnlyList<ExecCommandResolution> allowlistResolutions,
IReadOnlyList<string> allowAlwaysPatterns,
string? cwd,
int timeoutMs,
IReadOnlyDictionary<string, string>? env,
string? agentId,
string? sessionKey)
{
Command = command;
DisplayCommand = displayCommand;
EvaluationRawCommand = evaluationRawCommand;
Resolution = resolution;
AllowlistResolutions = allowlistResolutions;
AllowAlwaysPatterns = allowAlwaysPatterns;
Cwd = cwd;
TimeoutMs = timeoutMs;
Env = env;
AgentId = agentId;
SessionKey = sessionKey;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
using System.Collections.Generic;

namespace OpenClaw.Shared.ExecApprovals;

// Either a CanonicalCommandIdentity (IsResolved=true) or a typed denial (IsResolved=false).
// Produced by ExecApprovalV2Normalizer; consumed by the coordinator pipeline (PR7).
public sealed class ExecApprovalV2NormalizationOutcome
{
public bool IsResolved { get; }
public CanonicalCommandIdentity? Identity { get; }
public ExecApprovalV2Result? Error { get; }

private ExecApprovalV2NormalizationOutcome(CanonicalCommandIdentity identity)
{
IsResolved = true;
Identity = identity;
}

private ExecApprovalV2NormalizationOutcome(ExecApprovalV2Result error)
{
IsResolved = false;
Error = error;
}

public static ExecApprovalV2NormalizationOutcome Ok(CanonicalCommandIdentity identity)
=> new(identity);

public static ExecApprovalV2NormalizationOutcome Fail(ExecApprovalV2Result error)
=> new(error);
}

// Rail 18 steps 2-4: normalize command form → resolve executable → build canonical identity.
// Stateless — safe to call concurrently.
public static class ExecApprovalV2Normalizer
{
public static ExecApprovalV2NormalizationOutcome Normalize(ValidatedRunRequest request)
{
var argv = request.Argv;
var cwd = request.Cwd;
var env = request.Env as IReadOnlyDictionary<string, string>;

// displayCommand is always derived from argv, never from rawCommand (research doc 05 decision 2).
var displayCommand = ShellQuoting.FormatExecCommand(argv);

// rawCommand is null in Windows v1 (system.run does not carry it; research doc 05 OQ-V4).
// EvaluationRawCommand stays null — correct and documented conservative output.
string? evaluationRawCommand = null;

// Singular resolution for state machine.
var resolution = ExecCommandResolver.Resolve(argv, cwd, env);

// Multi-segment resolution for allowlist.
// Empty list is fail-closed: no allowlist satisfaction possible (research doc 04 R2).
// An empty list is NOT itself a denial at this step — the evaluator decides.
var allowlistResolutions = ExecCommandResolver.ResolveForAllowlist(
argv, evaluationRawCommand, cwd, env);

// UX patterns for prompting.
var allowAlwaysPatterns = ExecCommandResolver.ResolveAllowAlwaysPatterns(argv, cwd, env);

// Rail 6: if argv is non-empty but resolution is entirely impossible, deny.
// "Ambiguous or inconsistent" → typed deny, not silent allow.
if (resolution is null && allowlistResolutions.Count == 0)
return Fail("executable-resolution-failed");

var identity = new CanonicalCommandIdentity(
argv,
displayCommand,
evaluationRawCommand,
resolution,
allowlistResolutions,
allowAlwaysPatterns,
cwd,
request.TimeoutMs,
env,
request.AgentId,
request.SessionKey);

return ExecApprovalV2NormalizationOutcome.Ok(identity);
}

private static ExecApprovalV2NormalizationOutcome Fail(string reason)
=> ExecApprovalV2NormalizationOutcome.Fail(
ExecApprovalV2Result.ResolutionFailed(reason));
}
Loading
Loading