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:

  1. resolve the actor from trusted identity;
  2. Policy().Check; on deny, audit the denial and stop;
  3. perform the action;
  4. Audit().Append the 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:

  • Actor is a role tier: sovereign, architect, librarian, operator, contributor, observer or guest, in lowercase. Anything else, including a user ID or Operator with a capital O, is treated as guest.
  • Read-like kinds (containing read, list, search, query and similar) are allowed for every tier.
  • Destructive kinds (containing delete, wipe or destroy) require librarian or above.
  • operator and contributor may read and perform graph curation kinds (graph.tag, graph.link, ...); a generic vault.write is denied for them.
  • observer, guest and unknown actors may only read.
  • A Classified value of a0, sovereign or top-secret requires librarian or 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.consumes only 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.

Next