Policy and configuration
A sandbox policy sets what the workload may do: filesystem and process rules fixed at creation, and network rules that can change while it runs. When the workload is denied network access, the gateway can propose rules as a draft, in chunks, for a person (or your code) to review.
The policy types are the generated wire types from openshell.sandbox.v1 (Openshell.Sandbox.V1.SandboxPolicy, NetworkPolicyRule, ...), as in the TypeScript SDK; there is no policy builder.
Review draft chunks
client.Policy (IPolicyClient) reads the draft and approves, rejects, edits, or undoes its chunks. Each DraftChunk carries the proposed rule, the rationale, security notes, a confidence, and a ReviewToken that pins the inputs you reviewed: approving fails with Conflict when they changed since.
var draft = await client.Policy.GetDraftAsync("agent-1", statusFilter: "pending");
Console.WriteLine(draft.RollingSummary);
foreach (var chunk in draft.Chunks)
{
Console.WriteLine($"{chunk.RuleName} for {chunk.Binary}: {chunk.Rationale} (confidence {chunk.Confidence:P0})");
if (!string.IsNullOrEmpty(chunk.SecurityNotes))
{
// Rejection reasons are surfaced back to the agent in the sandbox.
await client.Policy.RejectChunkAsync("agent-1", chunk.Id, reason: chunk.SecurityNotes);
continue;
}
// The review token pins what you reviewed: approval fails with Conflict when the inputs changed since.
var change = await client.Policy.ApproveChunkAsync("agent-1", chunk.Id, chunk.ReviewToken);
Console.WriteLine($"policy version {change.PolicyVersion}");
}
A rejection reason is surfaced back to the agent in the sandbox. EditChunkAsync replaces the proposed rule of a pending chunk before approval, UndoChunkAsync reverses an approval (removing the merged rule from the active policy), and ClearChunksAsync drops every pending chunk. A watch reports draft changes as DraftPolicyUpdated events.
Approve in bulk
// Approves the pending chunks in one write. Chunks with security notes are skipped unless included;
// set Approvals to approve exactly the chunks (and review tokens) you reviewed.
var result = await client.Policy.ApproveAllAsync("agent-1", new ApproveAllOptions
{
IncludeSecurityFlagged = false,
RequestId = "approve-agent-1-draft-7",
});
Console.WriteLine($"approved {result.ChunksApproved}, skipped {result.ChunksSkipped}");
// Every approval, rejection and undo is recorded.
var history = await client.Policy.GetDraftHistoryAsync("agent-1");
foreach (var entry in history)
{
Console.WriteLine($"{entry.EventTime} {entry.EventType} {entry.ChunkId}: {entry.Description}");
}
Revisions
Every policy change creates a revision. GetStatusAsync reports the active version and the load status of a revision (PolicyStatusOptions.Version, or the latest); ListRevisions pages through them, newest first.
var status = await client.Policy.GetStatusAsync("agent-1");
Console.WriteLine($"active version {status.ActiveVersion}: {status.Revision?.Status}");
await foreach (var revision in client.Policy.ListRevisions("agent-1", new PolicyListOptions { PageSize = 20 }))
{
Console.WriteLine($"v{revision.Version} {revision.Status} {revision.PolicyHash} {revision.LoadError}");
}
Set the policy
client.Config (IConfigClient) writes the policy directly. At sandbox scope only the network policies may change; the static parts (filesystem, landlock, process) must match the create-time policy. SetPolicyOptions.Wait polls until the sandbox reports the new policy hash.
// Read the effective policy, change its network rules, and write it back. At sandbox scope only the
// network policies may change; the static parts must match the create-time policy.
var config = await client.Config.GetSandboxAsync("agent-1");
var policy = config.Policy!.Clone();
policy.NetworkPolicies.Remove("legacy-api");
var sandbox = await client.Sandboxes.GetAsync("agent-1");
var result = await client.Config.SetPolicyAsync("agent-1", policy, new SetPolicyOptions
{
ExpectedResourceVersion = sandbox.ResourceVersion, // optimistic concurrency
Wait = true, // until the sandbox reports the new hash
WaitTimeout = TimeSpan.FromSeconds(30),
});
Console.WriteLine($"policy v{result.Version} {result.PolicyHash}");
MergePolicyAsync applies incremental merge operations (Openshell.V1.PolicyMergeOperation) instead of a whole policy, and SetGlobalPolicyAsync replaces the gateway-global policy. Updates accept annotations, stored with the revision (never put secrets in them), and a request id for idempotency.
Settings
Settings are typed key-value pairs at sandbox or gateway-global scope. SettingValue converts implicitly from string, bool, and long; SettingValue.FromBytes takes bytes.
// Strings, booleans and integers convert to SettingValue implicitly.
await client.Config.SetSettingAsync("agent-1", "log_level", "debug");
await client.Config.SetGlobalSettingAsync("max_sandboxes", 50L);
var config = await client.Config.GetSandboxAsync("agent-1");
foreach (var (key, setting) in config.Settings)
{
// Scope says whether the value comes from the sandbox or from the gateway-global settings.
Console.WriteLine($"{key} = {setting.Value} ({setting.Scope})");
}
await client.Config.DeleteGlobalSettingAsync("max_sandboxes");
GetSandboxAsync returns the effective settings with the scope each value comes from, plus the policy, its version and hash, and the revisions of the sandbox configuration. GetGatewayAsync returns the global settings. Only global settings can be deleted.