Tenuo Agent Authorization
tenuo-ai/tenuo
Add or retrofit Tenuo authorization for AI-agent tools and effects.
Concepts, external interfaces, and conventions for Gram's role-based access control (RBAC) subsystem — scopes, grants, principals, system roles, and the authz.Engine.Require enforcement path used…
$ npx skills add speakeasy-api/gram --skill gram-rbac -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install speakeasy-api/gram gram-rbac --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/speakeasy-api/gram.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.agents/skills/gram-rbac .claude/skills/gram-rbac && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "gram-rbac" agent skill from https://github.com/speakeasy-api/gram/tree/main/.agents/skills/gram-rbac into .claude/skills/gram-rbac/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "gram-rbac", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/speakeasy-api/gram/tree/main/.agents/skills/gram-rbacType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add speakeasy-api/gram --skill gram-rbac -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install speakeasy-api/gram gram-rbac --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/speakeasy-api/gram.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.agents/skills/gram-rbac .agents/skills/gram-rbac && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "gram-rbac" agent skill from https://github.com/speakeasy-api/gram/tree/main/.agents/skills/gram-rbac into .agents/skills/gram-rbac/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "gram-rbac", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add speakeasy-api/gram --skill gram-rbac -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install speakeasy-api/gram gram-rbac --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/speakeasy-api/gram.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.agents/skills/gram-rbac .cursor/skills/gram-rbac && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "gram-rbac" agent skill from https://github.com/speakeasy-api/gram/tree/main/.agents/skills/gram-rbac into .cursor/skills/gram-rbac/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "gram-rbac", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/speakeasy-api/gram.git --path .agents/skills/gram-rbac--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add speakeasy-api/gram --skill gram-rbac -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install speakeasy-api/gram gram-rbac --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/speakeasy-api/gram.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.agents/skills/gram-rbac .gemini/skills/gram-rbac && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "gram-rbac" agent skill from https://github.com/speakeasy-api/gram/tree/main/.agents/skills/gram-rbac into .gemini/skills/gram-rbac/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "gram-rbac", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install speakeasy-api/gram gram-rbacInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add speakeasy-api/gram --skill gram-rbac -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/speakeasy-api/gram.git skills-src && mkdir -p .github/skills && cp -r skills-src/.agents/skills/gram-rbac .github/skills/gram-rbac && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "gram-rbac" agent skill from https://github.com/speakeasy-api/gram/tree/main/.agents/skills/gram-rbac into .github/skills/gram-rbac/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "gram-rbac", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add speakeasy-api/gram --skill gram-rbac -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install speakeasy-api/gram gram-rbac --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/speakeasy-api/gram.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.agents/skills/gram-rbac .opencode/skills/gram-rbac && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "gram-rbac" agent skill from https://github.com/speakeasy-api/gram/tree/main/.agents/skills/gram-rbac into .opencode/skills/gram-rbac/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "gram-rbac", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
gram-rbacConcepts, external interfaces, and conventions for Gram's role-based access control (RBAC) subsystem — scopes, grants, principals, system roles, and the authz.Engine.Require enforcement path used…
Gram Rbac is an agent skill from speakeasy-api/gram. Concepts, external interfaces, and conventions for Gram's role-based access control (RBAC) subsystem — scopes, grants, principals, system roles, and the authz.Engine.Require enforcement path used inside handlers. Activate whenever the task involves authorization (adding or modifying a scope or resource type, declaring a new role or grant, gating a handler, changing scope inheritance, exposing RBAC state through the dashboard).
Its SKILL.md is about 9.8k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Backend & APIs, covering Authorization and RBAC. It works with Model Context Protocol. The repository describes itself as: Securely scale AI usage across your organization. A single stack to Connect, Secure, Observe and Distribute agents, MCPs, and Skills within your company. The licence is AGPL-3.0.
5 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit ad78247. It shows what the files ask for, not the result of running them.
Pre-approves nothing: there is no allowed-tools line, so your agent's usual permission prompts apply.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
miseFrom the folder's file list and the shell code blocks in SKILL.md.
No URLs in SKILL.md.
From URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Gram Rbac loads about 9.8k tokens when it runs. Until then it costs about 111 tokens; SKILL.md has 4,024 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check found no risky patterns in SKILL.md.
Automated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from speakeasy-api/gram at commit ad78247, republished under its AGPL-3.0 licence (© speakeasy-api). 4,024 words, ~9,774 tokens.
.claude/skills/gram-rbac/SKILL.md (or your agent's skills folder).Gram's RBAC is a scope-and-selector model. The server ships with a fixed set of scopes grouped into system roles (admin, member). A grant binds a scope to a selector (a Kubernetes-style map[string]string of resource_kind, resource_id, plus optional narrowing dimensions like tool or disposition) for a given principal (user or custom role). Handlers enforce scopes by calling authz.Engine.Require(ctx, authz.Check{...}); the dashboard renders the same scope vocabulary through a matching TypeScript union that is hand-maintained in lockstep with the server.
Scope. A named permission that authorizes an operation on a particular kind of resource.
Resource type. The kind of resource a scope protects — for example org, project, assistant, or mcp. Every scope has exactly one resource type.
Scope expansion. Higher-privilege scopes satisfy lower-privilege ones. In the read/write/connect family the privilege order is write > read > connect: mcp:write satisfies a mcp:read check, and either mcp:read or mcp:write satisfies a mcp:connect check (connect is the broadest, easiest-to-satisfy gate). The mapping lives in scopeExpansions in authz/scopes.go — key = required scope, value = higher-privilege scopes that also satisfy it.
Selector. A map[string]string of constraints attached to a grant or check. Always carries resource_kind and resource_id (both required); MCP scopes additionally allow tool and disposition. Wildcards are explicit values — {"resource_kind":"*","resource_id":"*"}, never empty {}. Defined in server/internal/authz/selector.go.
Selector matching. A grant selector satisfies a check selector when, for every key the grant constrains, either the values are equal or the grant value is "*". Keys present on the grant but absent from the check are skipped — this is what lets a disposition-scoped grant ({"disposition":"read_only"}) still satisfy a connection-level check that doesn't constrain disposition.
Grant. A tuple of {Scope, Selector} held by a principal. The API-visible forms are RoleGrant (carrying Selectors []Selector) and ListRoleGrant (which also carries the transitively-implied sub_scopes). Use authz.NewGrant(scope, resourceID) to construct one — it derives the selector's resource_kind from the scope family.
Principal. Who holds a grant — a urn.Principal with a type (user, role, service account) and an id.
Principal precedence. Blocklist exclusions (*:blocked_*) normally subtract from allows regardless of which principal holds either. One exception: a grant made directly to the user (user:<id>) that names a concrete resource (resource_id not *) outranks a block the user inherits from a role or user:all. Agent grants never outrank, because agent owners can write them without being administrators. The user's own blocks always apply, wildcard direct grants never outrank, roles and user:all share one level, a narrowed direct grant outranks only as far as it reaches, and risk_policy:bypass is never outranked. The rule lives in server/internal/authz/precedence.go (IsDirectGrant, DirectOverrideGrants, ExclusionYieldsToDirectGrants) and is mirrored in ListAccessibleMCPServersForUser / ListAccessibleSkillsForUser, runtimepolicy.DelegationContained, access.listGrants (direct_selectors), and the dashboard's useRBAC and per-server access list. Any new code that evaluates exclusions outside authz.Engine must apply it too.
Dimensions. Optional narrowing keys on a Check beyond resource_id. Today: tool and disposition for MCP scopes (see server/internal/authz/checks.go and MCPToolCallCheck), and project_id for MCP, environment, and assistant scopes. A project_id dimension is how a per-resource check inherits project-wide grants: the check names the resource and its project, so {"resource_id":"*","project_id":P} covers every resource in P while {"resource_id":R} covers only R (MCPCheck, AssistantCheck). Allowed keys per scope family are enforced by ValidateSelector; new dimensions must be added to allowedSelectorKeys in selector.go.
Disposition. A snake_case bucket derived from MCP tool annotation hints — read_only, destructive, idempotent, open_world. Constants live in authz/selector.go; conv.DispositionFromAnnotations(annotations) is the canonical conversion from *types.ToolAnnotations.
System role. A built-in role shipped with the server. Gram defines two: admin (every scope) and member (the read-and-connect subset). Constants authz.SystemRoleAdmin and authz.SystemRoleMember.
Enforcement. Inside a handler, authorization is an explicit one-line check: the handler names the scope (and resource, if project-scoped) it needs, and the RBAC engine either allows the call or returns a forbidden error.
Auth context invariant. Organization-scoped handlers can assume ActiveOrganizationID is populated. During login, session authentication can briefly produce an org-less context; Engine.PrepareContext handles that middleware boundary by installing zero grants instead of trying organization-scoped principal resolution. Endpoints already exposed during login keep their explicit org-less behavior: for example, access.ListGrants returns no grants, productfeatures.GetProductFeatures returns unauthorized, and auth.Info continues the login flow. Do not spread defensive empty-org checks into other handlers.
RBAC is split across two Go packages: server/internal/authz/ holds the enforcement primitives, and server/internal/access/ implements the Goa management service that exposes them over HTTP. When adding new authorization primitives (scopes, checks, enforcement logic) edit authz; when adding or changing the management API (role/member endpoints) edit access.
authz package — the enforcerScope vocabulary, grant types, and enforcement logic are defined here. authz's imports are deliberately minimal (DB, logger, cache, WorkOS, urn) so any package that gates on RBAC can depend on it without import cycles — never add an import to authz from a package that transitively depends on authz, since the split exists specifically to prevent the cycles that motivated it.
Scope declarations. Scope type and every Scope* constant live in server/internal/authz/scopes.go. Constants follow the Scope<Name> pattern; string values follow <resource>:<verb> (e.g. mcp:read, org:admin). ScopeRoot is reserved for service-internal superadmin overrides. The file also holds the scopeExpansions map and computes the inverse scopeSubScopes in init(). CalculateSubScopes(scope) exposes the inverse to callers.
System role grants. SystemRoleGrants in server/internal/authz/grants.go — admin and member defaults. Adding a scope usually means adding it to admin, and optionally to member if end users should get it by default. SeedSystemRoleGrants upserts the full set; SyncGrants upserts grants for a single role slug.
authz.Engine. The central enforcer. Methods: PrepareContext, Require(ctx, checks...), RequireAny(ctx, checks...), Filter(ctx, scope, ids), ShouldEnforce, InvalidateRoleCache, InvalidateAllRoleCaches, GetScopeOverrides. Constructed in server/cmd/gram/start.go via authz.NewEngine(logger, db, chDB, challengeLogging, membership, opts...) and injected into every service that gates on RBAC. RBAC is always enforced for eligible authenticated requests; the MembershipFetcher is the WorkOS client used for role-slug lookups.
Organization provisioning. authz.Provisioner seeds both built-in roles and their grants for every organization created through Gram. ProvisionOrganizationAdmin performs that seed and assigns the first user to SystemRoleAdmin in one transaction. The identity leaf package never imports authz. WorkOS organization event reconciliation calls SeedSystemRoleGrantsTx for organizations discovered through WorkOS.
authz.Check. {Scope, ResourceKind, ResourceID, Dimensions} — the thing a handler asks Require to enforce. For the common single-resource case, leave ResourceKind: "" (auto-derived from the scope family) and Dimensions: nil; exhaustruct requires every field at every call site. ResourceID is typically authCtx.ProjectID.String() for project-scoped scopes. Defined in server/internal/authz/access.go.
authz.Filter for list endpoints. When a handler lists resources the caller might only partially own, s.authz.Filter(ctx, scope, candidateIDs) ([]string, error) returns the subset of IDs the caller holds the scope for. The standard pattern is: gather candidate IDs from the repo, call Filter, then rebuild the response from the allowed IDs. Prefer this over a post-hoc per-item Require loop. Canonical call sites: server/internal/projects/impl.go (projects list) and server/internal/toolsets/impl.go (toolsets list).
Auth context accessor. contextvalues.GetAuthContext(ctx) returns the current *AuthContext. RBAC-relevant fields: ActiveOrganizationID, ProjectID, UserID, Email, IsAdmin, APIKeyID, SessionID. AccountType is billing metadata and does not control RBAC enforcement.
Scope overrides. A local-dev/superadmin header can inject a restricted grant set for the request, parsed in override.go and surfaced via Engine.GetScopeOverrides. access.ListGrants returns the override set verbatim when active so the dashboard reflects what the engine will enforce.
Error model. errors.go defines sentinel errors (ErrDenied, ErrMissingGrants, ErrNoChecks, ErrInvalidCheck) and typed errors (DeniedError, InvalidCheckError). The engine maps these to oops codes — ErrDenied → oops.CodeForbidden, everything else → oops.CodeUnexpected with a logged message.
Grant loading. LoadGrants(ctx, db, orgID, principals) reads the principal URN set and returns the flattened []Grant. Called by both Engine.PrepareContext (middleware path) and access.ListGrants (user-facing). Each row's selectors JSONB is parsed via SelectorFromRow. It also supplies unrestricted assistant:read/assistant:write defaults for the built-in Admin and Member role principals (role:global:<id>), because organizations seeded before the assistant scopes have no stored rows for them and system-role grants are immutable.
Sync semantics. SyncGrants distinguishes nil from empty: RoleGrant{Selectors: nil} writes a single wildcard row; RoleGrant{Selectors: []Selector{}} writes nothing (no access). Each non-nil selector is validated by ValidateSelector before insert.
access package — the management APIserver/internal/access/ implements the Goa access service on top of authz. Every handler calls s.authz.Require(...) with the appropriate scope before doing work. The package also owns queries.sql and the generated server/internal/access/repo/ SQLc package that both access and authz use to read and write grant rows.
Scope metadata. ListScopes in server/internal/access/impl.go returns one {Slug, Description, ResourceType} entry per scope; this is what the dashboard consumes to render the scope picker.
Full-access grant list. ListGrants returns a hard-coded full-access scope list when enforcement is intentionally skipped, such as sessionless or API-key requests. That inline list in server/internal/access/impl.go must grow whenever a new scope is added. The parallel test expectation is expectedFullAccessScopes in server/internal/access/listusergrants_test.go.
System role gating. isSystemRole(slug) in impl.go checks against authz.SystemRoleAdmin and authz.SystemRoleMember. System roles cannot be renamed, deleted, or have their grant set edited; only member assignment is allowed.
| File | Purpose |
|---|---|
server/design/access/design.go | Goa design for the access service. Regenerates server/gen/access/ and server/gen/http/access/ via mise run gen:goa-server. |
server/internal/authz/access.go | The Check type and its expansion logic. |
server/internal/authz/checks.go | Pre-built Check builders for multi-dimensional checks (e.g. MCPToolCallCheck, MCPToolCallDimensions). |
server/internal/authz/context.go | Request-context helpers for grants (GrantsToContext, GrantsFromContext). |
server/internal/authz/engine.go | The Engine type — central RBAC enforcer, role-slug caching, and override resolution. |
server/internal/authz/errors.go | Package sentinel errors and typed errors. |
server/internal/authz/grants.go | Grant/RoleGrant/ScopedGrant types, SystemRoleGrants, SyncGrants, SeedSystemRoleGrants, GrantsForRole, GrantsToScopedGrants. |
server/internal/authz/load.go | Principal grant loading from the database. |
server/internal/authz/override.go | Scope override plumbing (header parsing, override-to-grants conversion). |
server/internal/authz/precedence.go | Principal precedence: direct concrete grants outrank blocklist exclusions inherited from roles or user:all. |
server/internal/authz/provisioner.go | New-organization provisioning for built-in role grants and the initial Admin assignment. |
server/internal/authz/scopes.go | Scope type, constants, and expansion rules. |
server/internal/authz/selector.go | Selector type, matching rules, NewSelector/NewGrant helpers, ValidateSelector, ResourceKindForScope, disposition vocabulary, SelectorFromRow. |
server/internal/authztest/helpers.go | Test helpers other packages reuse for RBAC setup, including WithExactGrants. |
server/internal/access/impl.go | Implementation of the /rpc/access.* Goa service. |
server/internal/access/queries.sql | SQLc queries for principals, grants, roles, and members. Regenerates server/internal/access/repo/ via mise run gen:sqlc-server. |
| Path | Generator |
|---|---|
server/gen/access/, server/gen/http/access/ | mise run gen:goa-server from server/design/access/design.go. |
server/internal/access/repo/ | mise run gen:sqlc-server from server/internal/access/queries.sql (via the access stanza in server/database/sqlc.yaml). |
Scope and resource-type changes on the server ripple into the generated SDK types (via gen:sdk) and a few hand-maintained docs and tests — adding a scope is not purely a server-package change.
HTTP routes (design: server/design/access/design.go):
/rpc/access.listScopes — every scope the server knows about, with resource_type and description./rpc/access.listRoles, getRole, createRole, updateRole, deleteRole — custom role CRUD./rpc/access.listMembers, updateMemberRole — org membership and role assignment./rpc/access.listUserGrants — the caller's effective grants.Three-place enum lockstep. server/design/access/design.go repeats the scope slug enum in three places — RoleGrantModel.scope, ListRoleGrantModel.scope, and its sub_scopes element — plus ScopeModel.slug for the listing endpoint. All three must stay synchronized with authz/scopes.go, and ScopeModel.resource_type must contain every resource type in use. Adding a new resource type also means adding it to SelectorModel.resource_kind's enum (project, mcp, org, *) — the model that backs RoleGrant.selectors and ListRoleGrant.selectors.
Generated SDK types. client/dashboard/src/sdk/src/models/components/scopedefinition.ts, rolegrant.ts, listrolegrant.ts, selector.ts, etc. Regenerated by mise run gen:sdk after every design change.
UI-owned access-page types. client/dashboard/src/pages/access/types.ts owns the dashboard-only abstractions layered over the generated SDK: the ResourceType string-literal union, AnnotationHint, CustomTab, ActivePanel, PolicyEffect, ScopeRule, a UI RoleGrant interface (selectors: Selector[] | null, distinct from the SDK's RoleGrant), and toRoleSlug. It also owns the ANNOTATION_TO_DISPOSITION / DISPOSITION_TO_ANNOTATION maps that mirror the disposition vocabulary in authz/selector.go — keep these in lockstep when adding or renaming dispositions. It imports Scope / Selector / SelectorDisposition from the SDK for local use in those definitions.
The dashboard pages under client/dashboard/src/pages/access/ render membership and role management on top of the generated SDK and the listScopes response. RBAC-aware UI across the rest of the dashboard gates itself through a shared hook and component.
useRBAC hook. client/dashboard/src/hooks/useRBAC.ts wraps the generated useGrants React Query hook and exposes hasScope(scope, resourceId?, projectId?), hasAllScopes(scopes, resourceId?, projectId?), hasAnyScope(scopes, resourceId?, projectId?), plus isLoading, grants, and error. Returns false from the has* checks while grants are loading. The module also exports selectorMatches(grant, check) and resourceKindForScope(scope) — direct mirrors of the server-side helpers in authz/selector.go — for code that needs parity with backend matching outside the standard hasScope flow.
RequireScope component. client/dashboard/src/components/require-scope.tsx is the primary rendering gate. Props: scope: Scope | Scope[], all?: boolean (AND vs OR when multiple scopes), resourceId?: string, projectId?: string (the project the resource belongs to, for scope families with a project_id dimension), level: "page" | "section" | "component", children, and level-specific extras (fallback for page/section, reason/className for component).
level="page" — renders a full Unauthorized fallback page when the scope is missing.level="section" — hides the children entirely.level="component" — renders disabled with a tooltip explaining why (good for buttons and inputs).Scope vocabulary import. useRBAC, RequireScope, and the access pages import the Scope union directly from the generated SDK (@gram/client/models/components/rolegrant.js). That union is regenerated from the server scope enum by gen:sdk, so keeping server/design/access/design.go in lockstep with authz/scopes.go is what makes the client gates type-check.
Dashboard grant reference. docs/rbac.md contains the "Dashboard Grant Reference" table that maps dashboard pages and actions to the grants required to use them. Whenever adding a new scope, changing a system-role grant default, adding a new dashboard RBAC gate, or changing the grant required by a dashboard feature, update that table in the same change. The table is the first place to answer questions like "what grant is required to create a project?"
| File | Purpose |
|---|---|
client/dashboard/src/components/require-scope.tsx | RequireScope gating component — page, section, and component-level rendering gates. |
client/dashboard/src/hooks/useRBAC.ts | useRBAC hook — scope checks and raw grants for the dashboard. |
client/dashboard/src/pages/access/Access.tsx | Top-level access page shell. |
client/dashboard/src/pages/access/ChangeRoleDialog.tsx, CreateRoleDialog.tsx, DeleteRoleDialog.tsx | Role and member-role mutation dialogs. |
client/dashboard/src/pages/access/MembersTab.tsx, RolesTab.tsx | The two tabs of the access page. |
client/dashboard/src/pages/access/ScopePickerPopover.tsx | Scope selection UI. |
client/dashboard/src/pages/access/types.ts | UI-only access-page types (ResourceType, ScopeRule, UI RoleGrant) and disposition maps. (See "Server-client contract".) |
*authz.Engine into the service struct (if it isn't already) and keep it on s.authz.s.authz.Require(ctx, authz.Check{Scope: authz.Scope<Name>, ResourceKind: "", ResourceID: authCtx.ProjectID.String(), Dimensions: nil}) and return the error as-is. The exhaustruct linter requires every Check field — leave ResourceKind empty to auto-derive from the scope family and Dimensions nil unless you're narrowing by tool/disposition.*:read for GET/list, *:write for mutations, *:connect for runtime usage. Scope expansions mean write callers are still permitted to read.RequireAny instead of Require when a single handler legitimately satisfies multiple equivalent scopes.oops.CodeForbidden response, and one case that builds the context with the scope via authztest.WithExactGrants(t, ctx, authz.NewGrant(authz.Scope<Name>, resourceID)). Construct grants with authz.NewGrant (or authz.NewGrantWithSelector for non-trivial selectors) — never set Grant.Selector by hand.Use this when the resource type is already represented (e.g. adding a new verb on mcp).
Scope<Name> constant in server/internal/authz/scopes.go.scopeExpansions in the same file. Usually: the new scope is the upper or lower end of an existing read/write/connect triple.SystemRoleGrants in server/internal/authz/grants.go: admin always receives the new scope. Member receives it if and only if end users should have it by default (read and connect, yes; write, no).{Slug, Description, ResourceType} entry to ListScopes in server/internal/access/impl.go.ListGrants (same impl.go) so callers without grants loaded still see the complete catalogue.server/design/access/design.go that have to stay in lockstep.Scope union — it is regenerated in the SDK (@gram/client/models/components/rolegrant.js) by gen:sdk in step 10. Confirm the new slug appears there after regeneration.expectedFullAccessScopes in server/internal/access/listusergrants_test.go and the require.Len(t, result.Scopes, N) assertion in server/internal/access/listscopes_test.go.mise run gen:goa-server, then mise run gen:sdk.mise run lint:server and mise run test:server.Use this when introducing a resource type that doesn't exist yet (e.g. the first foo:* scopes).
ScopeModel.resource_type in server/design/access/design.go.ResourceType union in client/dashboard/src/pages/access/types.ts.Use this when adjusting what admin or member gets out of the box. Prefer additive changes — removing a grant from a shipped role is an observable permissions change for existing users.
SystemRoleGrants in server/internal/authz/grants.go.expectedFullAccessScopes in server/internal/access/listusergrants_test.go if the admin set changed.mise run lint:server and mise run test:server.Use this when a single handler should authorize per-tool — e.g. private MCP tool calls where a grant might allow only read_only tools. The canonical call site is server/internal/mcp/rpc_tools_call.go.
authz/checks.go rather than a raw map: authz.MCPToolCallDimensions{Tool: params.Name, Disposition: disposition}. Zero-value fields are dropped automatically.*types.ToolAnnotations via conv.DispositionFromAnnotations(annotations) — priority order is read_only > destructive > idempotent > open_world; missing or nil annotations yield an empty string (which gets dropped).authz.MCPToolCallCheck(toolsetID, dims). For new dimension shapes, add a fresh helper to authz/checks.go rather than scattering raw Check{Dimensions: …} literals across services.allowedSelectorKeys in authz/selector.go, otherwise ValidateSelector will reject any role grant that uses it. New disposition values must also be added to validDispositions and to the disposition enum on SelectorModel in server/design/access/design.go.mcp:connect with no tool key still satisfies a check that names a specific tool. This is intentional; it lets less-narrow grants cover more checks.Use this whenever a list* handler would otherwise return resources the caller has no grant for. projects.List and toolsets.List are the canonical examples.
[]string.allowedIDs, err := s.authz.Filter(ctx, authz.Scope<Name>, candidateIDs). Return the error as-is.allowedIDs and rebuild the response by walking the original rows, keeping only the ones whose ID is in the set. Preserves repo ordering without a second query.Require loop — Filter exists specifically to avoid N authorization round-trips.Dashboard code should never hand-roll scope checks — use the shared primitives so a change to useRBAC or <RequireScope> flows through the whole app.
Rendering gates — use <RequireScope>. Pick the level that matches what you want the un-entitled user to see:
level="page" around a full route component renders an Unauthorized fallback page.level="section" around a block hides it entirely.level="component" around a button or input renders disabled with a tooltip reason.<RequireScope scope="org:admin" level="component" reason="Admin only">
<Button onClick={() => setDialogOpen(true)}>New API key</Button>
</RequireScope>Multi-scope gates. Pass an array and set all to switch between OR (default) and AND logic: <RequireScope scope={["org:read", "org:admin"]} level="page">.
Resource-specific gates. Pass resourceId when the scope only applies to a specific resource: <RequireScope scope="mcp:write" resourceId={toolsetId} level="component">. For scope families with a project_id dimension (assistants), also pass projectId so project-wide grants match and grants for other projects do not: hasScope("assistant:write", assistant.id, project.id) for one assistant, hasScope("assistant:write", project.id, project.id) for project-level actions such as create, and hasScope("assistant:read", undefined, project.id) for "any assistant in this project".
Imperative checks — use useRBAC. When you need the scope result as a value (to compute a class name, skip an effect, pick a label), pull from the hook instead of wrapping markup:
const { hasScope, isLoading } = useRBAC();
const canEdit = hasScope("mcp:write", toolsetId);The Scope string you pass must match the server. Import Scope directly from the generated SDK (@gram/client/models/components/rolegrant.js). If TypeScript complains about an unknown scope, you're missing the union update from the server scope add (see "How to add a new scope to an existing resource type").
Update the dashboard grant reference. Any new or changed dashboard gate must update the "Dashboard Grant Reference" table in docs/rbac.md, including page-level access, component/action-level access, resource selector target, and any notable server-side check that differs from the visible UI gate.
const { grants } = useRBAC(); returns the raw RoleGrant[]. Prefer hasScope for gating; reach for grants only when you need to render them (the access page itself, diagnostics, dev overlays).authz.GrantsFromContext(ctx) returns the grants on the request context after the engine's PrepareContext middleware has run.GET /rpc/access.listUserGrants returns the caller's effective grants.admin — every scope. Write implies read via scopeExpansions, so admins can exercise every read operation transitively.member — the read-and-connect subset.{"resource_kind":"project","resource_id":"proj_123"}) or wildcards it ({"resource_kind":"*","resource_id":"*"} via authz.WildcardResource). A grant value of * matches anything for that selector key.root (authz.ScopeRoot) — held only by service-internal overrides; satisfies every check.| Task | Purpose |
|---|---|
mise run gen:goa-server | Regenerate server/gen/access/** after editing server/design/access/design.go. |
mise run gen:sdk | Regenerate the SDK and OpenAPI so dashboard/CLI consumers see the new scope vocabulary. |
mise run gen:sqlc-server | Regenerate server/internal/access/repo/ when queries.sql changes. Requires mise run infra:start (sqlc connects to the local Postgres to type-check queries). |
mise run lint:server | Catches exhaustruct violations in the scope/grant structs. |
mise run test:server | Runs the scope-count assertions and RBAC tests. Filter with ./internal/authz/... ./internal/access/... when iterating. |
This file documents conventions that evolve over time. Adding a new scope, resource type, or tweaking system-role defaults is already covered by "Jobs to be done" — those don't require skill edits. Structural changes do. Update this skill in the same commit when you make any of the following kinds of changes:
<resource>:<verb> scope naming convention.admin and member.authz.Engine as the central enforcer, or changing its method set (Require, RequireAny, Filter, PrepareContext, ShouldEnforce, etc.) or constructor signature.access or into a new package — the authz / access split is deliberate and load-bearing for import-cycle reasons.Check struct shape (currently {Scope, ResourceKind, ResourceID, Dimensions}) or the Selector type's matching rules.tool, disposition for MCP) — including changes to allowedSelectorKeys or validDispositions in authz/selector.go, or to the matching SelectorModel enums in the design file.scopeSubScopes is computed from scopeExpansions, or introducing transitive expansion). The expansion algorithm currently emits one entry per scope level (relying on selector matching to handle wildcards) — switching back to per-scope×per-resource enumeration would change the perf profile and is worth re-documenting.access.ListGrants and mirrored by expectedFullAccessScopes in tests), or where ListScopes is populated.client/dashboard/src/pages/access/types.ts, or changing the three-place-enum-lockstep count in the design file. Same applies if the ANNOTATION_TO_DISPOSITION / DISPOSITION_TO_ANNOTATION maps move out of that file.ActiveOrganizationID becomes optional, or a new invariant field is added.useRBAC return shape (including selectorMatches/resourceKindForScope helpers), <RequireScope> levels/props, or the SDK hook the dashboard reads grants from.authz.NewGrant, authz.NewGrantWithSelector, authz.NewSelector) — every test in the codebase is wired through these.authztest (e.g. renaming WithExactGrants or adding a new canonical helper tests should use).gram-management-api — the access service itself, and every service that gates handlers with authz.Require, follows that skill's flow.gram-audit-logging — role and member mutations emit audit events via server/internal/audit/access.go; subjects are access_role and access_member.golang — error handling through oops, the no-defensive-checks rule for ActiveOrganizationID, the setup_test.go / black-box test conventions used by RBAC tests.frontend — everything under client/dashboard/src/pages/access/ (component structure, cn()/design-system styling, React Query usage).postgresql — the principal_grants (with selectors JSONB NOT NULL), roles, and related tables backing the access/repo SQLc package.mise-tasks — when modifying the .mise-tasks/gen/*.sh scripts referenced above.© speakeasy-api, AGPL-3.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in .agents/skills/gram-rbac of speakeasy-api/gram.
Open the folder on GitHubat commit ad78247
Gram Rbac next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Gram Rbac this skillspeakeasy-api/gram | 272 | — | ~9.8k | Automated safety check: Pass | AGPL-3.0 | |
| Tenuo Agent Authorizationtenuo-ai/tenuo | 102 | — | ~2.3k | Automated safety check: Pass | Apache-2.0 | |
| Frontmcp Auth UIagentfront/frontmcp | 146 | — | ~3.7k | Automated safety check: Pass | Apache-2.0 | |
| Frontmcp Authoritiesagentfront/frontmcp | 146 | — | ~7.1k | Automated safety check: Pass | Apache-2.0 | |
| Workosusenotra/notra | 256 | — | ~6.2k | Automated safety check: Pass | AGPL-3.0 | |
| Manage Dashboard WidgetsPostHog/posthog | 40k | — | ~2.3k | Automated safety check: Pass | Custom licence |
tenuo-ai/tenuo
Add or retrofit Tenuo authorization for AI-agent tools and effects.
agentfront/frontmcp
A skill your agent uses when customizing, branding, or replacing the built-in FrontMCP OAuth pages (the login, consent, federated-select, incremental-authorization, and error pages) with your own…
agentfront/frontmcp
A skill your agent uses when implementing authorization and access control for FrontMCP tools, resources, prompts, or skills, deciding who may invoke what.
usenotra/notra
A skill your agent uses when the user asks for a WorkOS docs URL, term, or dashboard field (Sign-in endpoint, initiateloginuri, Redirect URI, WORKOS env vars), or is implementing, debugging, or…
PostHog/posthog
Guides PostHog engineers through dashboard widget platform work — ship a new widgettype (WIDGETREGISTRY, catalog, runwidgets, WidgetCard) or update a shipped type (config, query, layout, RBAC, tile…
aws/agent-toolkit-for-aws
Create managed Iceberg tables using Amazon S3 Tables (s3tables API namespace) with automatic compaction and snapshot management.
speakeasy-api/gram
A skill your agent uses when automating the Gram dashboard in a browser, capturing screenshots, inspecting pages.
speakeasy-api/gram
A skill your agent uses when adding, changing, restyling, reviewing, validating, or previewing a Gram/Speakeasy transactional email, in Go or in LMX/MJML — a template<name.go, a TemplateKey…
speakeasy-api/gram
A skill your agent uses when adding, changing, or styling UI in client/admin (the Gram admin dashboard) that touches shadcn/ui — a button, dialog, table, sidebar, badge, select, tabs, tooltip, card…
speakeasy-api/gram
A skill your agent uses when adding, editing, reviewing, testing, or locating a reviewed skill distributed with the Platform MCP plugin; triggers include "Platform MCP skill", "platformmcpskills"…
speakeasy-api/gram
A skill your agent uses when changing or reviewing Gram ClickHouse schemas, migrations, queries, inserts, access principals, bootstrap SQL, Cloud compatibility, partial migration failures, or…
speakeasy-api/gram
A skill your agent uses when gating a feature behind a flag, dogfooding or gradually rolling out a change, choosing between productfeatures and PostHog feature flags, adding or checking a product…
Works with
Categories
Concepts, external interfaces, and conventions for Gram's role-based access control (RBAC) subsystem — scopes, grants, principals, system roles, and the authz.Engine.Require enforcement path used…. Gram Rbac is an agent skill from speakeasy-api/gram.Require enforcement path used inside handlers.
Gram Rbac fits situations like: tasks that involve Authorization and RBAC.
Run `npx skills add speakeasy-api/gram --skill gram-rbac -a claude-code`. Or copy the skill folder (.agents/skills/gram-rbac in speakeasy-api/gram) into .claude/skills/gram-rbac in your project. Claude Code loads it when a task matches its description.
Run `npx skills add speakeasy-api/gram --skill gram-rbac -a codex`. Or copy the skill folder (.agents/skills/gram-rbac in speakeasy-api/gram) into .agents/skills/gram-rbac in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add speakeasy-api/gram --skill gram-rbac -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/gram-rbac, .gemini/skills/gram-rbac, .github/skills/gram-rbac and .opencode/skills/gram-rbac in your project.
Going by SKILL.md and its folder, Gram Rbac needs the command-line tools its instructions call (mise).
SKILL.md contains no URLs. Any network use would come from the scripts or tools the agent runs. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found no risky patterns, such as piping downloads into a shell, reading credential files or hidden Unicode. It is not a guarantee. Review the folder before installing.
Gram Rbac is published under the AGPL-3.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 9.8k tokens (SKILL.md is roughly 39k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Gram Rbac: Tenuo Agent Authorization (tenuo-ai/tenuo, 102 stars), Frontmcp Auth UI (agentfront/frontmcp, 146 stars), Frontmcp Authorities (agentfront/frontmcp, 146 stars) and Workos (usenotra/notra, 256 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
speakeasy-api (a GitHub organization) maintains it in speakeasy-api/gram, which has 272 GitHub stars. The repository holds 39 skills in this directory. The repository was last updated on October 8, 2026.
Source: speakeasy-api/gram on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.