Governance: policy, roles, audit and notifications — bRRAIn Docs
Ask the Security Policy Engine before acting, handle role lookup safely, record actions in the audit chain, and send notifications from a Platform SDK extension.
Governance: policy, roles, audit and notifications
Your extension is a non-human actor with its own service identity. The internal API authorizes the extension, not the person using it, so when your extension acts for a user it must ask, act and record:
- resolve the actor from trusted identity;
Policy().Check; on deny, audit the denial and stop;- perform the action;
Audit().Appendthe outcome.
Policy checks
var ErrDenied = errors.New("denied by policy")
// Allowed asks the Security Policy Engine; any error is a deny.
func Allowed(ctx context.Context, c *platformsdk.Client, kind, actorRole, resource string) error {
d, err := c.Policy().Check(ctx, platformsdk.PolicyCheck{
Kind: kind,
Actor: actorRole, // a lowercase role tier, e.g. "operator"
Resource: resource,
})
if err != nil {
return fmt.Errorf("policy engine unavailable, failing closed: %w", err)
}
if !d.Allow {
return fmt.Errorf("%w: %s", ErrDenied, d.Reason)
}
return nil
}
How the engine on current pods decides:
Actoris a role tier:sovereign,architect,librarian,operator,contributor,observerorguest, in lowercase. Anything else, including a user ID orOperatorwith a capital O, is treated asguest.- Read-like kinds (containing
read,list,search,queryand similar) are allowed for every tier. - Destructive kinds (containing
delete,wipeordestroy) requirelibrarianor above. operatorandcontributormay read and perform graph curation kinds (graph.tag,graph.link, ...); a genericvault.writeis denied for them.observer,guestand unknown actors may only read.- A
Classifiedvalue ofa0,sovereignortop-secretrequireslibrarianor above.
Use dotted kinds that describe the action (vault.write, vault.delete, llm.call) and reuse them in your audit events. Show the engine's Reason when it denies.
Where the role comes from. The engine evaluates the role you pass; it cannot verify it. Take it from the identity the console stamps on proxied requests from signed-in users, never from the request body, a query parameter, or a public route.
Roles and Control().RoleOf
bRRAIn has seven canonical tiers (Sovereign, Architect, Librarian, Operator, Contributor, Observer, Guest); custom roles are capped at Operator, and a custom role's permission list replaces its tier's defaults. Control().RoleOf(ctx, userID, scope) asks the Control Plane for a user's tier. On current brain pods it returns APIError 501. Wrap it so it fails closed:
// TierOf looks up a user's tier and fails closed when the pod cannot
// answer (current brain pods return 501 for this endpoint).
func TierOf(ctx context.Context, c *platformsdk.Client, userID, orgID string) (string, error) {
r, err := c.Control().RoleOf(ctx, userID, orgID)
var apiErr *platformsdk.APIError
if errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusNotImplemented {
return "", fmt.Errorf("role lookup not available on this pod; treating user as unprivileged: %w", err)
}
if err != nil {
return "", err
}
return r.Tier, nil
}
The audit chain
Audit().Append adds an event to the platform's single, tamper-evident audit chain. nil means recorded; an error means not recorded, which is a compliance gap your code must report.
| Field | Convention | Example |
| --- | --- | --- |
| Actor | Your extension's service identity | ext:hello-notes |
| Action | The kind you passed to the policy check | vault.delete |
| Object | The exact path or object ID | projects/Pier-Refit/drafts/old.md |
| Result | A small fixed vocabulary | ok, denied, failed |
| Extras | The person and the context | on_behalf_of, reason, model_id |
Never put secrets, tokens, credentials or file contents in any field.
// DeleteNote is the check, act, record pattern for a user-requested action.
func DeleteNote(ctx context.Context, c *platformsdk.Client, actorRole, userID, path, reason string) error {
if err := Allowed(ctx, c, "vault.delete", actorRole, path); err != nil {
_ = c.Audit().Append(ctx, platformsdk.AuditEvent{
Actor: "ext:hello-notes", Action: "vault.delete", Object: path,
Result: "denied", Extras: map[string]any{"on_behalf_of": userID},
})
return err
}
if err := c.Vault().Delete(ctx, platformsdk.VaultDeleteRequest{
Path: path,
Soft: true, // always explicit: the zero value is a hard delete
Reason: reason,
}); err != nil {
return err
}
if err := c.Audit().Append(ctx, platformsdk.AuditEvent{
Actor: "ext:hello-notes",
Action: "vault.delete",
Object: path,
Result: "ok",
Extras: map[string]any{"on_behalf_of": userID, "reason": reason, "soft": true},
}); err != nil {
return fmt.Errorf("deleted %s but audit append failed: %w", path, err)
}
return nil
}
Notifications
Notifier().Dispatch sends a message through the platform's Notifier; your extension names a channel and the platform owns delivery. On current pods, Channel: "email" goes to the email relay (To as recipients, Title as subject), and any other channel goes to the console notification relay. If a pod has no relay configured, the message is written to a log file in the vault instead of being delivered, so test the channels you depend on.
func NotifyOwner(ctx context.Context, c *platformsdk.Client, owner, path string) error {
return c.Notifier().Dispatch(ctx, platformsdk.NotifierMessage{
Channel: "email",
Title: "Your meeting digest is ready",
Body: "A new digest was written to " + path,
To: []string{owner},
})
}
Send pointers, not payloads: a notification leaves the governed vault.
Least privilege
- Declare in your manifest's
platform_sdk.consumesonly the sub-clients your code calls. - Write only in your namespace and the project folders your feature serves.
- Scheduled jobs that act for nobody in particular must not invent a role to get past the engine; record each run as a system action in the audit chain.