Policies
Policies
A policy is src/access_control/policy/+<name>.policy.ts, named by its file. It holds grants per collection, the automations its holders may start, capabilities beyond collections, and limits. Teams map to policies in src/access_control/team/+team.ts.
// src/access_control/policy/+helper.policy.ts (templates/on-demand-services, excerpt)
import { policy } from '@norbital-ai/bolt';
const SELF = { user: { eq: { actor: 'id' } } } as const;
const OWN = { helper: { is: SELF } } as const;
/**
* A helper: their own visits and the customer details needed to reach them, their own record, and the position their
* app reports. They answer the shift check and move a visit through its day; everything else is the scheduler's.
*/
export default policy({
description: 'A helper’s own visits, shift check answers, completion and reported position.',
capabilities: { apps: ['helper'] },
grants: {
helpers: {
read: { where: SELF, fields: ['id', 'user', 'name', 'phone', 'skills', /* … */ 'last_location_at'] },
update: { where: SELF, fields: ['last_location'] }
},
helper_time_off: { read: { where: OWN } },
services: { read: { fields: ['id', 'name', 'duration_minutes', 'description'] } },
visits: {
read: { where: OWN, fields: ['id', 'number', 'booking', 'helper', 'slot', /* … */ 'completion_notes'] },
update: { where: OWN, fields: ['status', 'shift_check', 'mc', 'completion_notes'] },
moves: { status: ['scheduled->in_progress', 'in_progress->done'] },
actions: ['confirm_shift', 'decline_shift']
},
customers: {
read: {
where: { bookings: { some: { visits: { some: OWN } } } },
fields: ['id', 'name', 'email', 'phone', 'address']
}
}
// …
},
limits: { act: '120/min', read: '600/min', agent: '50/h' }
}); Grants
A grant names a collection and its arms: read, history, create, update, delete, queries, actions and moves (the state edges a holder may take, or 'all'). Each arm is true, a Where scope, or { where, fields }, where fields narrows what a read shows or a write may set. { via: '<relation>' } inherits the parent’s grant.
A Where may use operands such as { actor: 'id' }, { actor: 'teams' }, { today: '' } or { startOf: 'month' }, and crosses relations with is, some, none and every. On an update, where applies to the row before and after the write; with previous, where applies to the result and previous to the stored row.
// src/access_control/policy/+suspicion_review_automation.policy.ts (templates/field-operations, excerpt)
const UNCHECKED = { suspicion_checked_at: { isNull: true } } as const;
grants: {
job_assignments: {
read: true,
update: {
previous: UNCHECKED, // the stored row before the write
where: { suspicion_checked_at: { isNull: false } }, // the row as it will be
fields: ['suspicion_checked_at']
}
},
photo_evidence: {
read: true,
update: {
// a photo awaiting inspection, or a hashed one (seeded, bank-loaded) still without a scene
previous: { or: [{ sha256: { eq: '' } }, { scene_embedding: { isNull: true } }] },
fields: [
'sha256',
'inspection_failed_at',
'inspection_failure_reason',
'perceptual_embedding',
'scene_embedding',
'flags',
'matched_evidence_ids'
]
}
}
} That is how a transition is granted exactly once: the review above may stamp suspicion_checked_at only on a job that has none, and fill a photo’s facts only while its hash is empty or it has no scene yet.
Unions and masks
A member holds the union of their policies, and for each row the least constraining grant admits. Fields a read grant does not list are masked: absent from rows, filters, sorts, labels and downloads.
Capabilities and automations
capabilities.apps opens apps (a group name opens everything under it), and tools, mcp and skills extend the agent. automations lists the automations — and <collection>.integration runs — a holder may start.
Limits
limits rate-limit act, read, agent, upload, register, a single query or action ('<c>.<name>'), and for envoys envoys.receive and envoys.unrecognised. A rate is written '600/min' or '1/15min', and is counted per actor by default, per IP on a public app, and per sender or per envoy on the envoy keys.
Teams
// src/access_control/team/+team.ts (templates/on-demand-services)
import { team } from '@norbital-ai/bolt';
/** Source access roles: stable handles are membership and approval identities. */
export default team({
"team-operations": {
"name": "Operations",
"parent": null,
"policies": [
"operations"
]
},
"team-helpers": {
"name": "Helpers",
"parent": null,
"policies": [
"helper"
]
}
}); +team.ts maps team handles to the policies each holds; a member’s source team is a column of their row, naming a handle. TeamName comes from this file, so a misspelt approver in an approval step is a type error. Teams have a parent: the tree scopes data ({ actor: 'teamTree' }) and never passes policies down.