Solcis.Sdk
0.8.0
dotnet add package Solcis.Sdk --version 0.8.0
NuGet\Install-Package Solcis.Sdk -Version 0.8.0
<PackageReference Include="Solcis.Sdk" Version="0.8.0" />
<PackageVersion Include="Solcis.Sdk" Version="0.8.0" />
<PackageReference Include="Solcis.Sdk" />
paket add Solcis.Sdk --version 0.8.0
#r "nuget: Solcis.Sdk, 0.8.0"
#:package Solcis.Sdk@0.8.0
#addin nuget:?package=Solcis.Sdk&version=0.8.0
#tool nuget:?package=Solcis.Sdk&version=0.8.0
.NET PEP client
A reference Policy Enforcement Point in C#/.NET. It implements the same decide contract and the same
four security rules as the Python and Node clients. The full frozen decide contract ships with the SDK
sources rather than inside this package — ask your Solcis contact for it if you are writing a client
of your own.
SolcisPep.DecideAndEnforce(action, policyBin: …) execs policy decide --json - via
ProcessStartInfo + ArgumentList (UseShellExecute = false), writes the request to its stdin, reads the nested decision.decision,
and enforces fail-closed (PROCEED / BLOCK / HOLD). Decide() never throws. It holds no private
key and imports no proprietary code.
Install
Solcis.Sdk is published to NuGet.org. No source configuration and no token — it resolves from the gallery your SDK already uses:
dotnet add package Solcis.Sdk
That adds the newest release. To stay on your Solcis release's minor, give it as a floating version:
<PackageReference Include="Solcis.Sdk" Version="0.8.*" /> for Solcis 0.8 (see
Which version to pin).
The same gallery supplies the transitive crypto dependencies (NSec.Cryptography, Blake3), so a
cold restore needs nothing beyond the default source.
Which version to pin
The package carries your Solcis release's major.minor: Solcis.Sdk 0.8.x goes with Solcis 0.8. The
patch is the package's own, so a client fix ships without a Solcis release. Use the newest patch for your
release's minor, and move to the next minor when you upgrade Solcis; before 1.0, a new minor can change
the client's API. The client and the engine also have to agree on the decision contract, which the
client verifies on every call and enforces by BLOCKing against an engine built to a different one.
Note: the package is branded Solcis and its environment variables use the
SOLCIS_prefix. The credentials it handles are minted under thesolcis_prefix too: a channel pre-shared key (PSK) readssolcis_pep_…and an operating credential readssolcis_ac_…. A credential issued earlier may carry the product's former prefix, which is still accepted; the SDK passes a credential through without checking its prefix.
The four security rules
- argv, never a shell string —
policyruns from an argument list with no shell, and the request goes to its stdin (policy decide --json -), never into argv, where every local account could read it. Action data is never evaluated by a shell. No command injection. - decision from the JSON, never the exit code — the engine exits
0even when it denies. The outcome is read fromdecision.decision; the exit status is never trusted. - fail closed — any problem (non-zero exit, empty or garbled output, missing decision, timeout,
an exception) maps to
BLOCK. A broken check must never accidentally allow. - no signing key — the client verifies signatures and never produces them; it needs only the public trust anchor and the bundle. It does hold two secrets you must protect if you use those features: the serve channel's pre-shared key, and the agent credential you supply to enrolment.
Build + test
Requires the .NET SDK 8.0+ to build and test:
cd oss/pep-sdk/dotnet
dotnet test tests/SolcisPep.Tests.csproj
The tests drive the shared ../test-support/stub-policy.sh (a bash
stub — Linux/macOS, or WSL/bash on Windows) and cover: the verdict mapping, fail-closed on every error
path (garble / empty / crash / missing / non-object JSON / timeout), argv-not-shell, decision-from-
JSON-not-exit-code, verify-before-obey, and no-key — mirroring the Python/Node gates one-for-one.
Use
using Solcis.Pep;
var action = new Dictionary<string, object?>
{
["action"] = "export", ["agent_id"] = "agent-1", ["user_id"] = "user-1",
["source"] = "case-db", ["destination"] = "workspace-y",
};
var (outcome, decision) = SolcisPep.DecideAndEnforce(
action, policyBin: "/path/to/policy", bundle: "/path/to/bundle", trust: "/path/to/trust");
if (outcome != SolcisPep.Proceed) { /* stop (BLOCK) or pause for a human (HOLD) */ }
Set the binary via the policyBin argument or the SOLCIS_POLICY_BIN env — an explicit path, never a
bare name found on a writable PATH. For a signed bundle pass bundle: + trust: (the PUBLIC anchor);
it is verified (policy verify) before any decision is trusted. Prefer DecideAndEnforce — it
decides and maps in one call, so the two steps can't be mis-wired.
With policyBin set but neither bundle nor trust configured, Decide() fails closed
(no_bundle_configured) rather than silently falling back to the engine's built-in demo policy — see
the reason-code table. To try the SDK with no bundle to
hand, opt into the demo engine explicitly: pass demoEngine: true, or set
SOLCIS_PEP_DEMO_ENGINE=true. Not for production or a shared environment — the demo engine
decides from a permissive sample policy, not one you authored. demoEngine is tri-state
(bool?): the default null defers to the env; true turns it on; false turns it off outright,
even with the env set — precedence is per-call > env > off.
The same opt-in also covers the demo engine's receipt anchor on the serve transport. policy serve --demo signs receipts with a key anyone can reproduce, so without the opt-in the client refuses that key.
Configured as SOLCIS_PDP_TRUST_ANCHOR, a serve decide fails closed with receipt_untrusted and
SolcisServe.ReportOutcome returns outcome_receipt_unverifiable, both before any connection; as a
decide's trust-on-first-use pin, the decide fails closed before any connection; and first seen on first
contact, it is never pinned and that decide fails closed once the response is read. Pass
demoEngine: true to DecideAndEnforceAuto or SolcisServe.ReportOutcome, or set
SOLCIS_PEP_DEMO_ENGINE=true, only when rehearsing against the demo engine: the same setting turns on the
process-exec demo policy, so set it only in a rehearsal shell — see
the demo receipt anchor.
Enforce — a Decision overload + ResultToOutcome
Enforce(string) maps a decision token to Proceed / Block / Hold. For call-site parity with the
Node/Python object-accepting enforce, Enforce also has a Decision-record overload and a named
ResultToOutcome(Decision) helper (both read decision.Verdict):
var decision = SolcisPep.Decide(action, policyBin: "/path/to/policy");
SolcisPep.Enforce(decision.Verdict); // the decision string
SolcisPep.Enforce(decision); // the whole Decision record — same outcome
SolcisPep.ResultToOutcome(decision); // the self-documenting named form
A non-blank unknown token maps to Block (fail closed). A null or blank decision string is API
misuse and throws ArgumentException (a development signal); DecideAndEnforce always passes a real,
non-blank Verdict, so it never throws.
Per-call enforcement mode
Set the enforcement mode for a single call with the enforcementMode: parameter — threaded verbatim
into the process-exec argv as --enforcement-mode <value>:
var (outcome, decision) = SolcisPep.DecideAndEnforce(
action, policyBin: "/path/to/policy", enforcementMode: "shadow");
Precedence: the per-call enforcementMode (when non-empty) overrides SOLCIS_ENFORCEMENT_MODE,
which overrides the engine default. It writes no environment variable, is never sourced from the
request/action/payload, and governs the process-exec transport only — on serve (DecideAndEnforceAuto
with SOLCIS_PDP_ADDR set) the daemon owns the mode (no effect, no error). It never softens a real
deny: routing keys solely on the authoritative decision.
Non-blocking async API
DecideAsync and DecideAndEnforceAsync are non-blocking async siblings of Decide /
DecideAndEnforce. Each runs the policy child on Process + WaitForExitAsync / ReadToEndAsync
under a CancellationTokenSource timeout, so a single-threaded host is not frozen for the whole
spawn, and the returned Task completes with the same Decision the sync path returns for the same
input:
var d = await SolcisPep.DecideAsync(action, policyBin: "/path/to/policy");
var (outcome, decision) = await SolcisPep.DecideAndEnforceAsync(action, policyBin: "/path/to/policy");
if (outcome != SolcisPep.Proceed) { /* stop (BLOCK) or pause for a human (HOLD) */ }
They are additive — the sync Decide / DecideAndEnforce are unchanged — and govern the
process-exec transport only (the serve transport is already async network I/O; for the
config-selected front door use the sync DecideAndEnforceAuto). They preserve the exact fail-closed
semantics of the sync path — every fail path maps to the identical coded token, and a slow child is
killed at the timeout ⇒ decide_timeout — and the same decided-request / BoundFields binding, so
AssertExecutedRequest behaves identically on an async result.
The Task never faults. It always completes RanToCompletion with a fail-closed Block — even a
fault in the SDK's own async plumbing resolves to Block, never faults the Task (an unobserved fault
could be swallowed upstream and fail open). onDecision fires once, exactly like the sync twin.
When a bundle is configured and no skip-verify handle is presented, the async path runs policy verify
then policy decide as two sequential async spawns, so an un-skipped call costs up to ~2× the
per-spawn timeout budget (verify-timeout + decide-timeout). Verify once at boot with VerifyAtBoot
(below) and pass its handle as verified: to collapse this to a single spawn.
Bundle identity, skip-verify, provenance, and the executed→decided binding
Every Decision carries the engine's native-identity block — four additive, tolerant-parse
properties (absent ⇒ "", never fabricated), surfaced on both transports:
PolicyId— theid:of the policy that decided: the policy whose rule decided, or whose own policy-level check did (such as its large-input refusal). For a deployment the console publishes, it is the policy id the console shows. It is""when no single policy decided — an allow traced to no rule, an empty set of policies, or an engine-wide check such as intent alignment (on a deny, the rule id says which). When several policies objected, it names the one whose outcome was enforced; the decision log names them all. Identity is not an authz anchor: it is a non-authenticated author label for observability only; never grant or route onPolicyIdalone.VersionHash—blake3(policy.yaml), the revision of the combined set of policies enforced — not the version of the policyPolicyIdnames (distinct from the skip-verifybundle_hashand the serve Healthbundle_manifest_hash).Action— the decided action-subject the engine evaluated (e.g.bank_transfer_external).Verification—"verified"(the signature was checked this call) or"assumed"(skip-verify).
var d = SolcisPep.Decide(action, policyBin: "/path/to/policy", bundle: bundle, trust: trust);
// PolicyId is "" when no single policy decided.
var decidedBy = d.PolicyId.Length > 0 ? d.PolicyId : "(no single policy)";
Console.WriteLine($"decided by {decidedBy} {d.Action} {d.Verification}");
Read the block in this order: if Ok is false, ignore it; an empty VersionHash means the demo engine or no
bundle; an empty PolicyId with a VersionHash means no single policy decided.
Rules the engine could not evaluate
When the policy engine cannot evaluate a rule (an AI model it could not reach, a fact the request did
not supply, a policy module that failed), it fails closed, usually to a hold. ReasonCode alone cannot
tell that hold from one the policy asked for. EngineExceptions can: one record for each rule, or eval
within a rule, that could not be evaluated, on both transports. It is empty when the engine
evaluated every rule, when the engine predates the field, and when Ok is false.
EngineExceptionsOmitted counts any further records the engine left out, and is 0 when there are none.
| property | meaning |
|---|---|
RuleId |
the rule that could not be evaluated |
EvalId |
the eval within the rule, or null |
Cause |
why it could not be evaluated (below) |
Refusal |
when model routing refused, which check refused it: a reason token from the serve Health report's escalating_rules; otherwise null |
Capability |
the capability name the policy's author gave the rule's AI model, never an endpoint; otherwise null |
PolicyId |
the id: of the policy the rule belongs to; "" when no single policy owns it; null on the demo engine |
Deciding |
true on the one record, if any, whose failure is the outcome this decision took |
ObserveOnly |
true when the rule is observe-only, so its failure never decides |
Cause is currently one of model_disabled, model_not_configured, model_unavailable, model_no_capacity,
decision_deadline_passed, model_timed_out, model_error, model_input_missing,
judge_misconfigured, predicate_unevaluable, wasm_module_failed, rule_has_no_evaluator or
required_registry_empty. The list can grow: treat a value you do not recognise as a rule
that could not be evaluated, for a reason this version cannot name. A malformed record is skipped
rather than failing the decision.
var (outcome, d) = SolcisPep.DecideAndEnforce(action, policyBin: policyBin, bundle: bundle, trust: trust);
foreach (var e in d.EngineExceptions)
if (e.Deciding) Console.Error.WriteLine($"{e.RuleId} could not be evaluated ({e.Cause})");
Each record is an EngineExceptionRecord.
These records explain an outcome and never change it: route on the outcome (or Verdict) as always. The
onDecision record does not carry them, because an unbounded list suits a result and not a log line, so
read them from the result.
The decision's severity
The policy engine can rate a decision: how serious it judged what it decided. Severity carries that
rating on both transports, and is null when the decision has none. null means not rated and never means
"low". It is also null when Ok is false, and when the engine sent a rating this version cannot read. It
is the decision's highest rating, which may come from a rule other than RuleId: a deny from one rule
can carry a high rating from another rule that only observes. On a shadowed decision (Shadow is true)
it is the rating the decision would have carried, so check Shadow before acting on it.
Severity is a SeverityRating?:
| property | meaning |
|---|---|
Label |
the rating's word, compared with the SeverityLabels constants (Low, Medium, High, Critical). A string rather than an enum: the engine's cut-offs between labels are provisional, so an unknown label is passed through as sent. Rank by this, and do not recompute it from Score |
Score |
a whole number from 0 to 100, always present. For sorting and display, not for thresholds |
Confidence |
how sure the model was, from 0 to 1, normally only on an inference rating; null when the engine sends none |
Source |
what produced it: wasm (a WebAssembly policy module), inference (an AI model) or deterministic (a built-in check, or a score the policy's author wrote down). Open, like Label |
DeterministicCheck |
the built-in check that produced a deterministic rating, such as pii.credit-card; null on a deterministic rating the author wrote down, and on every other source |
A worked example: a policy denies a message with on_match: deny in a rule named FLAGGED-DENY, while an
observe-only rule named PII-OBSERVE rates the same message high with the pii.credit-card check. The
result reads Verdict == "deny", RuleId == "FLAGGED-DENY" and Severity of high, score 50, source
deterministic, DeterministicCheck pii.credit-card. The rating describes the decision, not the rule that
decided it.
Rank labels through an explicit map, and treat a label you do not recognise as at least High, so a label the
engine adds later is never quietly ignored:
static int Rank(string label) => label switch
{
SeverityLabels.Low => 0,
SeverityLabels.Medium => 1,
SeverityLabels.High => 2,
SeverityLabels.Critical => 3,
_ => 2, // an unknown label counts as at least High
};
var (outcome, d) = SolcisPep.DecideAndEnforce(action, policyBin: policyBin, bundle: bundle, trust: trust);
if (outcome == SolcisPep.Proceed && !d.Shadow && d.Severity is { } sev && Rank(sev.Label) >= Rank(SeverityLabels.High))
Console.Error.WriteLine("allowed with a high rating: " + JsonSerializer.Serialize(new { sev.Label, sev.Score, sev.Source, d.RuleId }));
Label and Source are strings the engine supplies, so escape them before writing them into a log line; serialising them with JsonSerializer.Serialize, as above, does that.
A rating never changes the outcome: route on the outcome (or Verdict) as always. Use Severity to allow and
log, to feed telemetry, or to send a request to a person. If a request with a rating should be blocked or
held, give the rule that decides it an on_match of deny or escalate in the policy, where the decision
is governed and recorded. A policy cannot gate a check on another rule's severity band.
On a serve allow whose receipt verified, Severity is the copy the engine signed into the receipt. Under trust-on-first-use (TOFU), where the client pins the receipt-signing key the first time it sees one instead of being given it, the very first receipt is checked against a key that is not pinned yet.
Everywhere else (process-exec, and a serve deny or hold) it is the engine's reply, which is not signed, so
treat it as a signal and not as evidence. The decision log holds the signed record.
SolcisPep.ParseSeverity(JsonElement) is public for a caller that holds a raw rating of its own, such as a
severity element from a decide reply it parsed itself. It applies the same rules as the result and returns
the rating, or null when the element is missing or malformed. A receipt's signed rating is already parsed, as
SolcisReceipt.ContentSeverity.
The DecisionRecord passed to onDecision carries Severity too, because a rating is fixed-size and suits a
log line.
The mandatory executed→decided binding
Decide reports the Action it evaluated; a PEP must bind the tool call it is about to execute to
that decided action. SolcisPep.AssertExecutedAction(result, executedAction) is that binding, and it is
the mandatory PEP pattern: decide the action you are about to execute, then assert the executed
action equals result.Action before acting. It returns Proceed only when the decision routed
to PROCEED, the result is a real engine decision, and the decided action equals the executed one — every
other case (including a matched-action deny) returns Block. A caller without this binding is outside
the trust envelope.
var (outcome, decision) = SolcisPep.DecideAndEnforce(action, policyBin: policyBin, bundle: bundle, trust: trust);
if (outcome == SolcisPep.Proceed && SolcisPep.AssertExecutedAction(decision, executedAction) == SolcisPep.Proceed)
{
// safe to execute exactly `executedAction`
}
When a signed policy denies on a request field (not just the action) — e.g. destination or
payload.amount — the decide result lists those fields in decision.BoundFields, and the PEP must bind
every one. SolcisPep.AssertExecutedRequest(result, executedRequest) is the generalisation of
AssertExecutedAction: it binds the action and every BoundFields entry, comparing each executed
value against the decided one byte-for-byte (no trim / case-fold / unicode / numeric normalisation),
so a decide-benign / execute-malicious dodge (decide destination="internal", execute "external")
fails closed to Block. Prefer it whenever your policies branch on request parameters. (It needs the
decided request the SDK captured, so it applies to the process-exec Decide result.)
var (outcome, decision) = SolcisPep.DecideAndEnforce(action, policyBin: policyBin, bundle: bundle, trust: trust);
if (outcome == SolcisPep.Proceed && SolcisPep.AssertExecutedRequest(decision, action) == SolcisPep.Proceed)
{
// safe to execute `action`: its action AND every bound field matched the decided request
}
Skip per-call re-verify (VerifyAtBoot)
Verify a bundle once at boot and skip the per-call signature re-verify (two policy spawns → one):
var handle = SolcisPep.VerifyAtBoot(policyBin, bundle, trust); // runs the real anchor verify, captures bundle_hash
var d = SolcisPep.Decide(action, policyBin: policyBin, bundle: bundle, trust: trust, verified: handle);
VerifyAtBoot runs the real Ed25519/anchor verify and, on success, captures the engine-reported
bundle_hash (h) into an opaque handle; Decide(verified:) then appends --assume-verified --expect-bundle-hash <h>. The exact security trade: it skips the Ed25519 signature check but the
engine still recomputes every file's blake3 and the full-signed-set h, refusing fail-closed on any
mismatch. It needs the operator-pinned hash — a skip requested without a successfully-minted handle
fails closed (assume_verified_requires_verify), and with no bundle configured
(assume_verified_requires_bundle). A hand-built handle never enables skip; on the serve transport it is
a no-op. Two-hash discipline: VersionHash (blake3(policy.yaml), identity) ≠ h (the signed-set
bundle_hash, integrity) — never pin VersionHash as the skip hash.
The decision-provenance hook
Pass an Action<DecisionRecord> as onDecision: to observe the finalized decision — Outcome,
Decision, ReasonCode, RuleId, PolicyId, VersionHash, Action, Verification, Shadow,
Severity, Transport, Ok — without a bespoke telemetry ring buffer. It fires exactly once, after the
decision is finalized, never influences routing, and a throw is swallowed (the observer must be
non-blocking):
SolcisPep.DecideAndEnforce(action, policyBin: policyBin, onDecision: record => Log(record));
The two transports (one API)
The Solcis.Pep namespace in the Solcis.Sdk package carries both transports: SolcisPep (the process-exec transport, above) and
SolcisServe (the framed-TCP serve transport), with SolcisReceipt verifying the signed serve receipt.
The example above uses the process-exec entry point (SolcisPep.DecideAndEnforce). For the unified
front door that auto-selects the transport by config, use SolcisPep.DecideAndEnforceAuto(action): it
enforces over the serve transport when SOLCIS_PDP_ADDR is set (a loopback host:port), else over
process-exec. SOLCIS_PDP_ADDR is the only selector — not SOLCIS_PDP (the testbed stub|api mode
enum) or SOLCIS_PDP_URL (an HTTP address). The serve transport verifies a signed receipt before
honouring any proceed-able decision.
Required —
SOLCIS_CHANNEL_AGENT_ID. The daemon channel-overwritesrequest.agent_idwith the channel id its PSK maps to, so the signed receipt is bound to that channel id. SetSOLCIS_CHANNEL_AGENT_IDto that channel id: the agent's id from the console, shown under the environment's name on the agent's page. There is no default. Unset or empty, every decide BLOCKs withchannel_agent_id_missingbefore any connection is opened, andReportOutcomeis refused with the same code. A wrong value fails closed too: a genuine allow fails the content-bind (receipt_tampered), and a legitimate outcome report surfaces asoutcome_receipt_unverifiable(recorded, but not provably the reporter's).
Serve env vocabulary (canonical names)
The serve transport is configured from the environment only (never from a request field). Each value
resolves with the precedence explicit call option > canonical SOLCIS_PDP_* env, read presence-based
(the resolver reads the raw Environment.GetEnvironmentVariable and branches on != null, so a
set-but-empty canonical stays empty and still fails closed).
| Concept | Canonical |
|---|---|
| Pre-shared key (channel credential) | SOLCIS_PDP_PSK |
| Receipt-signing trust anchor | SOLCIS_PDP_TRUST_ANCHOR |
| Connect+read timeout (ms) | SOLCIS_PDP_TIMEOUT_MS |
Daemon loopback host:port (selects serve) |
SOLCIS_PDP_ADDR |
SOLCIS_PDP_TRUST_ANCHOR is the receipt-signing anchor (it verifies the signed serve receipt) — a
distinct name from SOLCIS_TRUST, the bundle-verify (policy verify --trust) anchor on the
process-exec path. No env name or value ever appears in a fail-closed diagnostic line (coded reason
tokens only).
The full env surface and the serve wire are in
../README.md and
../SERVE_PROTOCOL.md.
What is this daemon enforcing? (SolcisServe.Health)
To render "this engine is currently enforcing X" without making a decision, read the enforced-identity
block off a serve Health report. SolcisServe.Health() returns a parsed HealthReport — or null
on any fault — and, on a bundle-serving daemon, that report carries PolicySetId — the id of the policy document enforced, composed: followed by your
enforcement point id for a policy set — and the same VersionHash / Verification a decision carries:
var h = SolcisServe.Health(); // null on any fault (unreachable / refused / malformed)
if (h is null)
{
// could not reach the daemon — render "unknown"
}
else if (h.PolicySetId is null)
{
// no bundle yet (demo engine, or awaiting its first pulled bundle)
}
else
{
Console.WriteLine($"enforcing {h.PolicySetId} @ {h.VersionHash} ({h.Verification})");
// on serve, h.Verification is always HealthReport.VerificationVerified today
}
Three rules keep this honest:
- Provenance, not permission.
PolicySetId/VersionHash/Verificationride outside the signed receipt, so they are labels to display and log — never an authorization anchor, and never a substitute for the pinned receipt trust anchor the serve decide path verifies. - Identity present is not fresh, unstale, or non-empty. A present identity is what the engine decides
with now; it does not say the bundle is current, that a reload did not fail, or that anything is
enforced (a signed zero-member bundle has an identity and enforces nothing). Freshness is the
operator-only
BundleManifestHash/BundleStale, absent (null) from a Pep report. nullmeans unknown.SolcisServe.Health()collapses every fault tonull. Render "unknown"; never fall back to a negative claim about enforcement.
Enrol — bind your agent's identity at startup (SolcisEnrol)
A cold-path helper, separate from the decide/enforce core, wraps the control-plane
POST /api/agent-environments/enrol: a running agent presents its solcis_ac_… operating credential and
its runtime did, and its declared instance flips declared -> active.
using Solcis.Pep;
try
{
// Each option falls back to its env var, so a fully-configured agent can pass no options.
EnrolResult instance = await SolcisEnrol.EnrolAsync(
new EnrolOptions { BaseUrl = baseUrl, Credential = credential, Did = did });
// instance.Id / instance.Did / instance.FirstSeenAt / instance.Status == "active"
}
catch (SolcisEnrolException e) when (e.Retryable)
{
// transient (control-plane down / rate-limited / 5xx) — safe to retry: a repeat enrol is idempotent
}
| Env var | Meaning |
|---|---|
SOLCIS_CONTROL_PLANE_URL |
The control-plane origin (management plane — not the PDP; unrelated to SOLCIS_PDP_ADDR/SOLCIS_PDP_URL). Must be https unless loopback. |
SOLCIS_AGENT_CREDENTIAL |
The solcis_ac_… operating credential (a bearer secret; never logged). |
SOLCIS_AGENT_DID |
The runtime did (not SOLCIS_CHANNEL_AGENT_ID, the serve channel id). |
Idempotent on the same did (a 200 no-op, FirstSeenAt preserved) — call it every boot; retry when
e.Retryable. It throws (it does not fail closed to BLOCK): SolcisEnrolUsageException on misuse
(missing config or a non-https base), SolcisEnrolException (with a closed .Code, a .Retryable flag, and
the HTTP .Status) on an HTTP/transport fault — surfaced on the awaited Task. It uses the BCL
HttpClient — no new dependency — and transits your credential to the control-plane, storing none. TLS
trust uses the OS certificate store.
Migration: ProjectReference → PackageReference
If your project still consumes this client by path — a
<ProjectReference Include=".../oss/pep-sdk/dotnet/SolcisPep.csproj" /> — follow
Install to add the package source, then replace that <ProjectReference> with a
<PackageReference Include="Solcis.Sdk" />.
The namespace is pinned, not restructured: using Solcis.Pep; and the call sites
(SolcisPep.DecideAndEnforce(...), or SolcisPep.DecideAndEnforceAuto(...) for transport auto-selection)
are unchanged. The package targets net8.0+ (also net10.0) with PackageLicenseExpression=MIT.
Breaking change: none to the namespace or the enforcement result. The only additions are the minimal
crypto runtime dependencies (NSec.Cryptography + Blake3, pulled transitively, used only by the
serve/receipt path — the process-exec path is pure-BCL) and the new serve transport + unified front
door. The .nupkg ships only this library's own DLLs + metadata; the crypto deps resolve transitively.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Blake3 (>= 1.1.0)
- NSec.Cryptography (>= 24.4.0)
-
net8.0
- Blake3 (>= 1.1.0)
- NSec.Cryptography (>= 24.4.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.8.0 | 91 | 10/16/2026 |