PolicyEngineService¶
Target
This page describes intended behaviour or structure. No implementation is claimed unless a section is explicitly marked Current implementation or Evidence.
| Bounded context | BC-18 Policy |
| Platform / plane | Governance / Trust & Operations |
| Deployment granularity | module of host · host GovernanceHost |
| Status | target · ConnectSoft.Factory.Governance.Policy |
| Built from | connectsoft-microservice · libraries: connectsoft-extensions-compliance, connectsoft-extensions-saas-abstractions, connectsoft-extensions-messaging-model, connectsoft-extensions-persistence-model-nhibernate, connectsoft-extensions-audit-net |
| Decisions | ADR-0015, ADR-0021, ADR-0022 |
| Traceability | FR-01 FR-04 FR-05 FR-06 FUX-01 FUX-05 NFR-01 NFR-04 XL-04 XL-05 XL-10 |
Purpose and responsibilities¶
The original design's policy question (§43) is: WHO can run WHICH AGENT using WHICH MODELS against WHICH DATA with WHICH TOOLS in WHICH ENVIRONMENT under WHICH BUDGET. PolicyEngineService answers it deterministically from bundles — Published policy assets (BC-04) typed into rules per domain (run acceptance, model access, tool execution, knowledge access, memory promotion, connection grants, deployment, spend, data handling) for the platform, a tenant or a project, where each narrower scope may only tighten the wider one (D18) — and records every decision immutably with its effect (allow, deny, require-approval, prohibited), obligations, matched rules and reasons (policy-decision-v1). Runs pin the active revision at acceptance; a decision under a pinned revision is reproducible.
It absorbs PolicyDefinitionService, PolicyEvaluationService, MemoryPolicyService and TenantIsolationPolicyService from the June-2026 catalog: one engine, several domains.
Context and boundaries¶
flowchart LR
S["PolicyEngineService"]
RunControlService["RunControlService"] -->|grpc| S
AgentRuntimeService["AgentRuntimeService"] -->|grpc| S
ModelRouterService["ModelRouterService"] -->|grpc| S
ToolAdapterService["ToolAdapterService"] -->|grpc| S
ContextBuilderService["ContextBuilderService"] -->|grpc| S
MemoryPromotionService["MemoryPromotionService"] -->|grpc| S
IntegrationService["IntegrationService"] -->|grpc| S
StudioBff["StudioBff"] -->|sync-http| S
S -->|event| AgentRegistryService["AgentRegistryService"]
S -->|grpc| ProjectService["ProjectService"]
S -->|grpc| EnvironmentService["EnvironmentService"]
S -->|event| AuditService["AuditService"]
S -. "PolicyRevisionActivated" .-> E_PolicyRevisionActivated(("event"))
S -. "PolicyRevisionRetired" .-> E_PolicyRevisionRetired(("event"))
S -. "PolicyDecisionRecorded" .-> E_PolicyDecisionRecorded(("event"))
Example bundle (the §43 YAML, typed):
scope: project:booking-saas
parent: tenant:connectsoft
rules:
- id: shell-read
domain: ToolExecution
match: { action: "tool:shell-sandbox", environmentClass: Production, operation: read }
effect: Allow
- id: shell-write-prod
domain: ToolExecution
match: { action: "tool:shell-sandbox", environmentClass: Production, operation: write }
effect: RequireApproval
obligations: { actionClass: production }
- id: db-update
domain: ToolExecution
match: { action: "tool:database", operation: update }
effect: Prohibited
- id: deploy-staging
domain: Deployment
match: { environmentClass: Staging }
effect: Allow
- id: deploy-prod
domain: Deployment
match: { environmentClass: Production }
effect: RequireApproval
obligations: { actionClass: production, approverRule: release-managers }
- id: pii-external-models
domain: ModelAccess
match: { dataClass: Restricted, privacyClass: External }
effect: Prohibited
Domain model¶
PolicyBundle — Runtime read model of a Published policy asset — a versioned set of rules per domain for one scope (the YAML of the original design §43 made typed), with the revision id runs pin at acceptance. (catalog entry)
| Field | Type | Required | Description |
|---|---|---|---|
revision |
string |
yes | Policy revision id pinned by runs (ADR-0014). |
tenantId |
string |
yes | |
scope |
enum:PolicyScope |
yes | |
scopeId |
string |
yes | Tenant id or project id; platform for platform scope. |
releaseId |
string |
yes | Published asset release (BC-04). |
parentRevision |
string |
no | The wider-scope revision this bundle tightens. |
rulesRef |
string |
yes | Artifact reference of the rule document. |
rulesHash |
hash |
yes | |
domains |
list:string |
yes | |
active |
bool |
yes | |
activatedAt |
datetime |
no |
Invariants:
- A tenant bundle may only tighten the platform bundle and a project bundle may only tighten the tenant bundle; activation validates monotonic tightening per rule and refuses otherwise.
- Bundles are immutable per revision; a change is a new Published release and a new revision; accepted runs keep their pinned revision.
- Prohibited effects cannot be overridden by any approval; RequireApproval names the action class the approval must bind to (D21).
Events: PolicyRevisionActivated, PolicyRevisionRetired · Tables: PolicyBundles, PolicyRules · Repository: PolicyBundleRepository
PolicyDecision — An immutable recorded decision (policy-decision-v1) for one subject, action and resource under one revision — effect, obligations, matched rules and reasons. (catalog entry)
| Field | Type | Required | Description |
|---|---|---|---|
decisionId |
guid |
yes | |
tenantId |
string |
yes | |
projectId |
string |
no | |
environmentId |
string |
no | |
runId |
guid |
no | |
subject |
json |
yes | Identity kind and id (user, agent release, workload), roles, delegated scope |
domain |
enum:PolicyDomain |
yes | |
action |
string |
yes | Action identifier, for example tool:propose-change-set or model:invoke:code-agent |
actionClass |
string |
yes | Side-effect class after environment raising. |
resource |
json |
yes | Resource descriptor: tool, profile, source, connection, environment, data class |
inputsHash |
hash |
no | |
policyRevision |
string |
yes | |
effect |
enum:PolicyEffect |
yes | |
obligations |
json |
no | Redaction, limits, approver rule reference, logging requirements |
matchedRules |
list:string |
yes | |
reasons |
list:string |
yes | |
evaluatedAt |
datetime |
yes |
Invariants:
- Decisions are immutable and reference the exact revision; the same inputs under the same revision give the same effect (deterministic evaluation).
- Every effect of RequireApproval names the action class; the approval that follows (BC-03, BC-20) binds to this decision id.
- Decisions never contain prompt text, secrets or payloads; they reference hashes.
Events: PolicyDecisionRecorded · Tables: PolicyDecisions · Repository: PolicyDecisionRepository
erDiagram
PolicyBundle {
string revision PK
string tenantId
enum_PolicyScope scope
string scopeId
string releaseId
string parentRevision
string rulesRef
hash rulesHash
list_string domains
bool active
datetime activatedAt
}
PolicyDecision {
guid decisionId PK
string tenantId
string projectId
string environmentId
guid runId
json subject
enum_PolicyDomain domain
string action
string actionClass
json resource
hash inputsHash
string policyRevision
enum_PolicyEffect effect
json obligations
list_string matchedRules
list_string reasons
datetime evaluatedAt
}
PolicyRule {
string ruleId
enum_PolicyDomain domain
json match
enum_PolicyEffect effect
json obligations
int priority
}
PolicyBundle ||--o{ PolicyRule : contains
Entities and value objects¶
PolicyRule (entity, in PolicyBundle) — One typed rule of a domain.
| Field | Type | Required | Description |
|---|---|---|---|
ruleId |
string |
yes | |
domain |
enum:PolicyDomain |
yes | |
match |
json |
yes | Subject, action, resource and environment predicates |
effect |
enum:PolicyEffect |
yes | |
obligations |
json |
no | |
priority |
int |
yes |
Data model and storage¶
| Store | Role | Engine / profile | Tables |
|---|---|---|---|
| Governance store | primary | azure-sql / nhibernate-sqlserver | PolicyBundles, PolicyRules, PolicyDecisions |
| Immutable audit store | blob | blob / azure-blob-immutable | — |
| Cache and coordination | cache | redis / azure-cache | — |
| Table | Owner | Columns | Indexes |
|---|---|---|---|
dbo.PolicyBundles |
PolicyBundle | 11 | 1 |
dbo.PolicyRules |
PolicyRule | 8 | 0 |
dbo.PolicyDecisions |
PolicyDecision | 17 | 2 |
Full DDL: appendix.
APIs¶
| Method | Path | Operation | Scope | Side effects | Idempotency | Gateway | Summary |
|---|---|---|---|---|---|---|---|
GET |
/factory/policies |
ListPolicyBundles | factory.policies.read |
read | natural | GW-03 | Active and retired bundles per scope with revisions |
GET |
/factory/policies/{revision} |
GetPolicyBundle | factory.policies.read |
read | natural | GW-03 | The rules of a revision (typed view of the YAML). |
POST |
/factory/projects/{projectId}/policy/validate |
ValidateProjectPolicy | factory.policies.write |
sandbox | natural | GW-03 | Validate a project override against the tenant bundle: every rule must tighten, never loosen (D18). |
POST |
/factory/policies/simulate |
SimulatePolicy | factory.policies.write |
sandbox | natural | GW-03 | What-if evaluation of a candidate bundle against recorded decisions of a period (sandbox; later-stage depth). |
GET |
/factory/runs/{runId}/policy-decisions |
ListRunDecisions | factory.runs.read |
read | natural | GW-03 | Decisions recorded for a run with effects |
| gRPC service | Method | Streaming | Visibility | Side effects | Summary |
|---|---|---|---|---|---|
Policy |
Decide |
none | internal | read | Evaluate subject |
Policy |
GetActiveRevision |
none | internal | read | The revision a new run must pin for its scope. |
Policy |
ValidateTightening |
none | internal | read | Check that a candidate bundle only tightens its parent; used at publication of policy assets. |
Messages¶
| Event | Topic | Scope / v | Aggregate | Consumers | Summary |
|---|---|---|---|---|---|
| PolicyRevisionActivated | policy.bundles.v1.policy-revision-activated |
integration / 1 | PolicyBundle | ApprovalPolicyService, RunControlService, StudioBff, AuditService, DashboardService | A bundle revision is active for its scope; new runs pin it. |
| PolicyRevisionRetired | policy.bundles.v1.policy-revision-retired |
integration / 1 | PolicyBundle | StudioBff, AuditService | A revision is no longer active; pinned runs finish on it. |
| PolicyDecisionRecorded | policy.decisions.v1.policy-decision-recorded |
integration / 1 | PolicyDecision | AuditService, DashboardService | A decision was evaluated and recorded. |
PolicyRevisionActivated payload:
| Field | Type | Required | Description |
|---|---|---|---|
revision |
string |
yes | |
tenantId |
string |
yes | |
scope |
enum:PolicyScope |
yes | |
scopeId |
string |
yes | |
rulesHash |
hash |
yes |
PolicyRevisionRetired payload:
| Field | Type | Required | Description |
|---|---|---|---|
revision |
string |
yes | |
tenantId |
string |
yes | |
scope |
enum:PolicyScope |
yes |
PolicyDecisionRecorded payload:
| Field | Type | Required | Description |
|---|---|---|---|
decisionId |
guid |
yes | |
tenantId |
string |
yes | |
runId |
guid |
no | |
domain |
enum:PolicyDomain |
yes | |
action |
string |
yes | |
actionClass |
string |
yes | |
effect |
enum:PolicyEffect |
yes | |
policyRevision |
string |
yes |
| Command | Handled by | Result events | Summary |
|---|---|---|---|
ActivateRevision |
PolicyBundle | PolicyRevisionActivated, PolicyRevisionRetired | |
Decide |
PolicyDecision | PolicyDecisionRecorded |
| Event | Producer | Handler | Purpose |
|---|---|---|---|
| AssetReleasePublished | AgentRegistryService | ActivatePolicyBundle |
Activate Published policy assets after tightening validation. |
| AssetReleaseRevoked | AgentRegistryService | RetirePolicyBundle |
Retire revoked policy revisions. |
Workers and jobs¶
| Worker | Trigger | Output events | Retry | Idempotency key | Summary |
|---|---|---|---|---|---|
| DecisionRetentionSweeper | schedule 0 2 * * * |
— | fixed | decisionId |
Moves decisions older than the hot window to the immutable audit store and keeps the 7-year evidence copy. |
Actors and grains¶
None modelled
No grains are modelled for this item yet.
Sagas and long-running flows¶
None modelled
No sagas are modelled for this item yet.
sequenceDiagram
participant X as Caller (runtime, gateway, executor…)
participant P as PolicyEngineService
participant A as AuditService
X->>P: Decide (subject, domain, action, class, resource, revision)
P->>P: resolve bundle chain platform → tenant → project
P->>P: match rules by priority, tightening semantics
P->>P: record decision (effect, obligations, reasons)
P-)A: PolicyDecisionRecorded
P-->>X: decision id, effect, obligations
Options and configuration¶
| Section | Key | Type | Default | Reloadable | Secret | Summary |
|---|---|---|---|---|---|---|
Governance:Policy |
DecisionCacheSeconds |
int | 30 |
yes | no | Cache identical decisions per revision for hot paths; never across revisions. |
Governance:Policy |
PlatformSpendThresholdUsd |
decimal | 10 |
yes | no | Platform spend threshold referenced by Spend rules (D21); projects may lower it. |
Metrics and observability¶
| Metric | Instrument | Unit | Dimensions | Summary |
|---|---|---|---|---|
connectsoft.factory.policy.decisions |
counter | {decision} | tenant, domain, effect | Decisions by effect. |
connectsoft.factory.policy.decision_latency |
histogram | s | domain | Evaluation latency. |
connectsoft.factory.policy.active_revisions |
updown | {revision} | scope | Active revisions per scope. |
connectsoft.factory.policy.tightening_rejections |
counter | {bundle} | tenant | Overrides refused for loosening. |
Security¶
| Authentication | client-credentials |
| Scopes | factory.policies.read (Read bundles and decisions.); factory.policies.write (Validate and simulate (Workspace admin); factory.runs.read (Decisions of a run.) |
| Roles | Reviewer, Operator, WorkspaceAdmin, PlatformAdmin, Observer |
| Tenant isolation | row-filter |
| Data classification | restricted |
| Max side-effect class | sandbox |
| Secrets | — |
Dependencies¶
Outbound
| Target | Kind | Purpose | Failure mode | Required |
|---|---|---|---|---|
| AgentRegistryService | event | Policy bundles as Published assets. | queue | yes |
| ProjectService | grpc | Scope and membership roles of subjects. | fail-fast | yes |
| EnvironmentService | grpc | Environment class for class raising. | fail-fast | yes |
| AuditService | event | Decisions as evidence. | queue | yes |
Inbound
| Target | Kind | Purpose | Failure mode | Required |
|---|---|---|---|---|
| RunControlService | grpc | Acceptance decisions | — | yes |
| AgentRuntimeService | grpc | Decisions before gated calls. | — | yes |
| ModelRouterService | grpc | Decisions per invocation. | — | yes |
| ToolAdapterService | grpc | Decisions per tool call. | — | yes |
| ContextBuilderService | grpc | Classification and redaction obligations. | — | yes |
| MemoryPromotionService | grpc | Promotion policy. | — | yes |
| IntegrationService | grpc | Grant policy. | — | yes |
| StudioBff | sync-http | Policy views and validation. | — | yes |
Failure and recovery¶
| Scenario | Detection | Recovery | Data impact | RTO (min) |
|---|---|---|---|---|
| Rule document unreadable | activation validation fails | revision not activated; the previous stays active | none | 0 |
| Decision latency above budget | latency histogram | per-revision decision cache; more replicas; callers keep their timeouts and fail closed | none | 15 |
NFR targets¶
| Target | Class value (pilot-interactive) | Override |
|---|---|---|
| latency_p95_ms | 2000 | — |
| availability_pct | 99.5 | — |
| throughput_rps | 50 | — |
| rpo_minutes | 15 | — |
| rto_minutes | 240 | — |
| cost_class | low | — |
Deployment¶
| Granularity | module of host · host GovernanceHost |
| Topologies | saas-default, hybrid, on-prem, hybrid-compute |
| Runtime | —; replicas —–— |
| Ports | — |
Current implementation¶
The MCP governance toolkit integration offers policy paths, fail-closed and audit options for MCP tools only; ConnectSoft.Extensions.ResourceServer offers capability-based authorization. No bundle, revision, domain model or recorded decision exists.
Later capabilities¶
Policy simulation against recorded decisions at depth; policy drift detection across tenants; agent-authored policy proposals reviewed by Platform admins; per-run policy explanations in the Studio.
Decisions and traceability¶
| Family | Ids |
|---|---|
| FR | FR-01, FR-04, FR-05, FR-06 |
| FUX | FUX-01, FUX-05 |
| NFR | NFR-01, NFR-04 |
| XL | XL-04, XL-05, XL-10 |