Ring:Declaring Plugin Permissions logo

Ring:Declaring Plugin Permissions

Organization
LerianStudio
ring:declaring-plugin-permissions

Interactively authoring an access-manager permission-declaration manifest (permissions.yaml) for the access-manager "inversão de responsabilidade": drives a plugin team through discovering its real Authorize() surface, normalizing every action to the SEMANTIC standard (never HTTP verbs), declaring roles/group grants and the M2M contract, then emits and validates a manifest that matches the lib-auth/v3 auth/declaration schema. Use when a plugin must publish its own permissions at boot (WireFromEnv) instead of the access-manager seed owning them, or when writing/fixing a permissions.yaml. It also bumps the repo's github-actions-shared-workflows CI pin to the release carrying the permission-manifest nudge. Skip when the plugin has no auth guards, or you only need the wiring (see WireFromEnv) and the manifest already exists.

Overview

PublisherLerianStudio
Repositoryring
Skill namering:declaring-plugin-permissions
Stars
215
Forks
28
Bundled files
1
LicenseApache-2.0
Links
  • Markdown instructions

    A SKILL.md file the model loads on demand, so it only costs tokens when a request actually matches.

  • Works with any LLM

    AI skills are plain Markdown, not provider-specific code, so this works with GPT, Claude, Gemini, Grok, or a local model.

  • 1 bundled files

    Scripts, templates, and references the model can read while it works. Files are read-only and never executed.

  • Open source

    Published by LerianStudio on GitHub. Read the source before you install it.

Installation

Install the Ring:Declaring Plugin Permissions AI skill in TypingMind to use it with any LLM, or drop it into another agent that reads SKILL.md.

1

Install in TypingMind

TypingMind installs a skill straight from its GitHub folder — it reads SKILL.md, bundles the resource files, and stores the result locally.

  1. Open the app and go to Plugins → Skills.
  2. Choose "Install from GitHub".
  3. Paste the skill folder URL below and confirm.
  4. Enable the skill in any chat where you want it available.
Plugins → Skills → Add skill → From GitHub URL, then paste the folder URL and press Continue.
2

Install in another agent

Any agent that reads the Agent Skills format can use this skill — copy the folder into that agent's skills directory.

Claude Code — .claude/skills
git clone --depth 1 https://github.com/LerianStudio/ring.git /tmp/ring
mkdir -p .claude/skills
cp -r /tmp/ring/dev-team/skills/declaring-plugin-permissions .claude/skills/lerianstudio-ring-declaring-plugin-permissions
Restart Claude Code after copying so it picks up the new skill.

Use it in TypingMind

Enable Ring:Declaring Plugin Permissions in any TypingMind chat and the model takes it from there. Its name and description sit in the system prompt, and the moment a request matches, the model loads the full instructions itself — you never invoke it by hand, and it costs no tokens until it is actually used.

The model loads Ring:Declaring Plugin Permissions on its own as soon as a request matches it.

Works with any AI model

AI skills are plain Markdown instructions rather than provider-specific code, so Ring:Declaring Plugin Permissions is not tied to the model it was written for. Install it once in TypingMind and use it with GPT-5, Claude, Gemini, Grok, DeepSeek, Mistral, Llama, or a local model you run yourself — all on your own API keys.

  • Loaded only when it is needed

    The system prompt carries just the name and description. The instructions are fetched on the first matching request, so an idle skill costs nothing.

  • Switch models mid-chat

    Because the skill is instructions rather than code, changing model does not break it — the next model reads the same SKILL.md.

Skill instructions

This is the SKILL.md content the model loads. Read it before installing — a skill is instructions your model will follow.

Declaring Plugin Permissions

Drive a Lerian plugin team through authoring permissions.yaml — the client-side, SEMANTIC declaration the plugin PUBLISHES at boot under the access-manager inversion. This is an interactive workflow: follow the steps in order and use AskUserQuestion to gather every choice. Do not free-hand a manifest.

Overview

Schema authority: lib-auth/v3 auth/declaration/manifest.go (>= v3.4.0-beta.1), mirrored server-side by plugin-access-manager identity/pkg/model/declaration.go. The reconciler validates this exact shape at boot and refuses a bad manifest.

THE CRITICAL INVARIANT: every (resource, action) pair in the manifest MUST exactly match a real AuthClient.Authorize(service, resource, action) guard in the plugin (lib-auth auth/middleware/middleware.go). If they diverge, authz silently breaks — the guard demands a permission the manifest never declared. Adopting the semantic standard therefore means the route guards AND the manifest move together.

  • CANONICAL model (semantic, complies): br-sisbajud (internal/auth/declaration/permissions.yaml).
  • ANTI-EXAMPLE (HTTP verbs, do NOT copy): midaz-fees — its guards pass Authorize("plugin-fees","estimates","post"). Legacy. Never emit verb actions.

When to use

  • A plugin must publish its own permissions at boot (inversion) — authoring a new permissions.yaml.
  • Fixing or migrating a plugin whose guards/manifest use HTTP verbs to the semantic standard.

Skip when

  • The plugin has no Authorize(...) guards / no RBAC surface.
  • Only the wiring is needed and the manifest already exists — point to authdecl.WireFromEnv and stop.

The naming standard (non-negotiable)

The schema doc comment says verbatim: "The action is SEMANTIC (create/read/update/delete), never an HTTP verb."

HTTP verb (FORBIDDEN)Semantic action (REQUIRED)
postcreate
getread
put / patchupdate
delete (method)delete / remove

Domain verbs are first-class and encouraged where CRUD does not fit: rotate, trigger, justify, generate, reprocess, read_pii, request_read, request_write, receive. Keep them; do not force them into CRUD.

This is REQUIRED — and nothing downstream will hold it for you. The HTTP-verb reject was removed from BOTH lib-auth and identity (lib-auth#145 / plugin-access-manager#310), so a manifest with action: get validates, boots and publishes without complaint — several in production do exactly that. What holds the standard is review plus the check-manifest-actions Makefile guard you add in Step 9. Only delete among HTTP methods is allowed — it is also a valid semantic action. Never emit post/get/put/patch as an action.

Manifest schema (author against THIS)

Top-level YAML: service (str, REQUIRED), version (int, REQUIRED), permissions (list), roles (list), m2m (object). All bare-name rules below: the server composes the prefix — never pre-prefix.

FieldRule
serviceREQUIRED, non-empty, no ./.. segment. KEEP any plugin- prefix. MUST equal the M2M app slug AND DisplayName (BOLA, enforced at boot). Also the 1st arg of Authorize.
versionREQUIRED int >= 1. ADVISORY — excluded from content hash; bumping alone is a no-op publish.
permissions[].resourceREQUIRED, BARE (server composes {service}/).
permissions[].actionREQUIRED, SEMANTIC — never an HTTP verb.
permissions[].effectallow ONLY. A deny is REJECTED — see below.
permissions[].roles>= 1 BARE role name, each MUST be declared in roles:.
roles[].nameREQUIRED, BARE. / allowed as hierarchy separator (fees/editor).
roles[].granted_tolist of { group: <bare-name> }. GROUP-ONLY — there is no user grantee. Server composes the {owner}/ prefix.
m2m.exposedbool — this plugin is callable as an M2M target.
m2m.needslist of target service slugs this plugin CALLS via M2M (e.g. midaz).

Why deny is refused and not merely discouraged. The manifest used to accept it and the reconciler wrote it to Casdoor as a real permission, but no decision point ever applied it: every evaluator reads an effect other than allow as "did not match" and carries on, authorizing on the first allow that does match. There is no deny-wins pass. So a deny was a refusal you could read in the manifest, in review, and in the stored permission — while the runtime granted. Validation now refuses it at boot (fail-closed, lib-auth#183) and the declaration PUT answers 422 (plugin-access-manager#448).

Only the effect field is constrained. deny is still a fine ACTION name: br-sfn/services/spb declares { resource: str-emission-approvals, action: deny, effect: allow }, because approving or denying an STR emission is that domain's verb.

Composed names the server builds: permission {service}/{resource}:{action}, role {service}/{name}, group {owner}/{group}.

House style: write permissions: in FLOW style

The schema is indifferent to YAML style, the org is not. Write each permission as a single flow-mapping line and align the columns:

yaml
permissions:
  - { resource: account-types, action: read,   effect: allow, roles: [editor, contributor, viewer] }
  - { resource: account-types, action: create, effect: allow, roles: [editor, contributor] }
  - { resource: account-types, action: delete, effect: allow, roles: [editor] }

not the five-line block form. This is what the existing manifests do — midaz, tracer, plugin-access-manager, streaming-hub, reporter, lender, br-sisbajud, billing-worker and every br-sfn/services/* — and a real manifest is 20-100 pairs, where flow gives one readable line and one clean diff hunk per permission instead of five. Keep roles: and m2m: in block style, as those same manifests do.

See template.permissions.yaml in this folder for a compact, valid, commented example.


Interactive workflow — follow in order

Step 1 — Determine service

Grep the plugin for its product/slug constant before asking:

bash
grep -rn -iE "ProductName|ApplicationName|ModuleName|feesApplicationName|Slug\s*=" \
  --include="*.go" <plugin-root> | head

Propose the found constant as the default. Confirm with AskUserQuestion, and warn: it MUST equal the M2M app DisplayName and be the first arg of every Authorize(...) call (BOLA). Keep any plugin- prefix.

Step 2 — Discover the REAL authorization surface

Enumerate what the code actually enforces today:

bash
grep -rn "\.Authorize(" --include="*.go" <plugin-root>

Extract each (service, resource, action) triple from the guard chains (and route tables). Present the full list. This list is ground truth — the manifest must cover exactly these pairs (Step 8 re-checks).

Step 3 — Normalize actions to the SEMANTIC standard

For EACH discovered action:

  • If it is an HTTP verb (post/get/put/patch/delete-method) → propose the semantic equivalent from the table AND flag that the route guard must change too (guard + manifest move together — this is a code change, not just YAML).
  • If it is already semantic → keep it.
  • If CRUD does not fit → offer the relevant domain verb.

Use AskUserQuestion per action/resource to let the engineer confirm or rename, offering sensible options (e.g. for delete: delete vs remove). Record the final semantic (resource, action) set. If any guard currently passes a verb, list those guards explicitly as follow-up code edits the team owns — do not silently emit verb actions to make the mismatch "go away".

Step 4 — Roles and group grants

Ask (via AskUserQuestion):

  • Which roles exist? Default viewer + editor; offer domain roles (operator/investigator/compliance-officer/…) as in br-sisbajud.
  • Which BARE group(s) grant each role? (Groups are group-only, bare.)
  • Grant mapping — offer this DEFAULT, let them override: read/query actions → viewer + editor; mutating actions → editor-only.

Every permission MUST list >= 1 role, and every listed role MUST be declared.

Step 5 — M2M contract

Ask:

  • exposed: is this plugin an M2M target (callable by other plugins)?
  • needs: which services does it CALL via M2M (e.g. midaz)? Omit the block if neither applies.

Step 6 — Emit permissions.yaml

Write the manifest to the plugin's declaration dir (mirror br-sisbajud: internal/auth/declaration/permissions.yaml). Bare resources/groups/roles, semantic actions, valid effects, >= 1 role per permission, permissions: entries in FLOW style (see "House style" above). Then show the wiring the plugin adds (manifest authoring is the focus; this is the glue):

go
//go:embed permissions.yaml
var Manifest []byte

// ...at startup (authdecl = github.com/LerianStudio/lib-auth/v3/auth/declaration, >= v3.4.0-beta.1):
stop, err := authdecl.WireFromEnv(ctx, authdecl.WireInput{
    Slug:     <service>,   // MUST equal manifest.service (BOLA)
    Manifest: Manifest,
    Logger:   logger,
})

Deployment sets the FIXED env contract (default OFF, fail-open). A NEW adopter creates these vars with the canonical IDP_ names from the start. The four RI/D7-declaration vars carry the product-wide IDP_ prefix (identity provider, lib-auth #4232 — shared across every plugin, NOT a per-plugin prefix): IDP_DECLARATION_ENABLED, IDP_HOST, IDP_M2M_CLIENT_ID, IDP_M2M_CLIENT_SECRET, plus the token-minter vars PLUGIN_AUTH_ENABLED, PLUGIN_AUTH_HOST (out of scope for #4232, unchanged).

The IDP_ names require lib-auth ≥ v3.4.0-beta.6 (the release that carries #4232). This is a LATER threshold than the >= v3.4.0-beta.1 manifest-schema pin above. For ONE release after #4232 the old names (DECLARATION_ENABLED, PLUGIN_IDENTITY_HOST, M2M_CLIENT_ID, M2M_CLIENT_SECRET) still work as deprecated aliases (canonical IDP_ wins; WireFromEnv WARNs when only the alias is set), so a plugin pinned to an older lib-auth keeps booting — migrate to the IDP_ names before the following release drops the aliases.

Step 7 — Validate

Run structural checks against every rule below, and if a Go toolchain + lib-auth (>= v3.4.0-beta.1) are available, verify against the REAL validator (parse+Validate, zero network) with a tiny throwaway program:

go
// authdecl "github.com/LerianStudio/lib-auth/v3/auth/declaration"
// _, err := authdecl.New(authdecl.Config{Slug: "<service>", Manifest: raw, /* IdentityAddr, ClientID/Secret dummy */})
// New parses + Validate()s the manifest eagerly and enforces slug==service (BOLA); a
// non-manifest error means the manifest itself is structurally valid.

Or a YAML lint + this checklist. Validation rules (all aggregated at boot):

  • service non-empty and not ./...
  • version >= 1.
  • each action is non-empty. The SEMANTIC standard above is a CONVENTION, not a boot check: the HTTP-verb reject was removed from both lib-auth and identity (lib-auth#145 / plugin-access-manager#310), so a manifest with action: get publishes without complaint. Hold the standard in review and with the Makefile guard below — nothing downstream will hold it for you.
  • each permission: non-empty resource and action; effect is allow;

    = 1 role; every role reference is a DECLARED role.

  • no duplicate composed permission {service}/{resource}:{action}.
  • no duplicate composed role {service}/{name}.
  • no Casdoor-safe-name collision: chars / ? : # & % = + ; and whitespace collapse to - (lossy), so two different names can collide — and a name that derives to empty is rejected.

Step 8 — Alignment gate (BLOCKING)

Re-confirm every declared (resource, action) maps to a real Authorize(...) call (Step 2 list) and vice-versa. Enumerate ANY mismatch as blocking:

  • guard exists, manifest missing → add the permission.
  • manifest declares a pair no guard uses → remove it or add the guard.
  • guard still passes an HTTP verb → the team MUST update the guard to the semantic action so both sides use it (do not "fix" it by declaring the verb).

Do not consider the manifest done while any mismatch remains.

Step 9 — Scaffold the durable CI guard (Makefile)

The alignment gate above is a one-time check. Lock the semantic standard in so a future edit that reintroduces an HTTP verb FAILS the build. For the MANIFEST's action names there is exactly ONE automated layer, and it is the one you add here. Guard alignment is NOT automated at all: the target below reads $(MANIFEST) and nothing else, so a guard still passing get against a manifest declaring read is caught by Steps 3 and 8 and by review — nowhere else.

  • Boot-time (lib-auth): none. The validator used to reject post/get/put/patch, and that reject was removed from lib-auth and identity (lib-auth#145 / plugin-access-manager#310). A manifest with a verb action starts and publishes fine — nothing catches the regression at runtime.
  • CI (Makefile): the check-manifest-actions guard below. Cheap, lives in the plugin's own repo, and is the only thing that fails a build on a verb reintroduced in the manifest.

Check the plugin's Makefile for the existing check-* convention (most Lerian plugins wire check-tests, check-migrations, … into a ci:/check aggregate — grep ^check- and ^ci:). Add a check-manifest-actions target matching that idiom and wire it into the aggregate:

makefile
MANIFEST ?= internal/auth/declaration/permissions.yaml

.PHONY: check-manifest-actions
# Fail if the manifest is missing/unreadable, or if it uses HTTP-verb actions.
# 'delete' is allowed (also a valid semantic action). This is the ONLY automated
# check for the semantic standard — lib-auth no longer rejects verbs at boot. It
# reads the manifest only: a mismatched Authorize() guard is Step 8's job.
# The pattern matches BOTH styles: flow `- { ..., action: post, ... }` (the house
# style) and block `action: post` on its own line, with or without quotes and with
# or without a trailing `# comment`. It deliberately does NOT match a `#` comment
# line that merely mentions an action, and does not fire on a resource or a longer
# action that starts with a verb (`repost`, `getaway`, `read_pii`).
check-manifest-actions:
	@test -r "$(MANIFEST)" || { echo "ERROR: manifest not found or unreadable: $(MANIFEST)"; exit 1; }
	@echo "Checking manifest actions are semantic (not HTTP verbs)..."
	@if grep -inE '^([[:space:]]*-[[:space:]]*\{(.*[{,])?)?[[:space:]]*action:[[:space:]]*["'\'']?(post|get|put|patch)["'\'']?[[:space:]]*([,}#]|$$)' "$(MANIFEST)"; then \
		echo "ERROR: HTTP-verb action in $(MANIFEST) — use a SEMANTIC action (create/read/update/delete or a domain verb). 'delete' is allowed."; \
		exit 1; \
	fi
	@echo "OK: manifest actions are semantic."

Add check-manifest-actions to the ci:/check: prerequisite list (and .PHONY). If the plugin has no check-*/ci idiom, still add the target and call it where tests run. Confirm it FAILS on a seeded action: post — seeded in the SAME style the manifest uses — and PASSES on the real manifest before finishing.

Step 10 — Bump the shared-workflows CI pin

The org shared CI (LerianStudio/github-actions-shared-workflows, reusable go-pr-validation.yml) now carries a NON-BLOCKING permission-manifest-nudge that reminds any lib-auth repo still missing a permissions.yaml. You are already touching this repo — bump its pin so the pipeline is current.

  • Find the consumer pins: grep -rn 'LerianStudio/github-actions-shared-workflows' .github/workflows. Expect exact-tag uses: …@vX.Y.Z on go-pr-validation.yml / go-release.yml / routine.yml. Leave any …@v1 major-float pins as-is.
  • Resolve the latest release tag: gh release view --repo LerianStudio/github-actions-shared-workflows --json tagName -q .tagName (or gh api repos/LerianStudio/github-actions-shared-workflows/releases/latest -q .tag_name). It must be >= the release that introduced permission-manifest-nudge.
  • Bump EVERY exact-tag shared-workflows pin in .github/workflows/*.yml to that tag, keeping all of them on the SAME version. Do not touch unrelated uses: lines.

This is hygiene, not a gate: for THIS repo — which now declares a manifest — the nudge reports "compliant" and posts nothing. The bump only keeps the shared pipeline current. Confirm the target tag exists before writing, and preserve the pin format.


Red Flags — STOP

  • An action is post/get/put/patch/delete-the-method → it is an HTTP verb.
  • A resource, role, or group carries a {service}/ or {owner}/ prefix → it will double-prefix; write it BARE.
  • A granted_to entry has a user: key → there is no user grantee; groups only.
  • A permission lists zero roles, or a role not in roles: → validation fails.
  • You are emitting a verb action to sidestep a guard mismatch → fix the guard instead.
  • service differs from the M2M app DisplayName / the Authorize 1st arg → BOLA break.

All of these mean: stop and correct before writing/finishing the manifest.

Anti-Rationalization

RationalizationWhy it's WRONGRequired action
"The guard passes post, so I'll declare post to match."Freezes the legacy anti-pattern; the standard is semantic on BOTH sides.Rename guard AND manifest to the semantic action together.
"I'll pre-prefix the resource with the service to be safe."Server composes the prefix; you get {service}/{service}/….Write resources/roles/groups BARE.
"A user grantee would be convenient here."The schema has no user grantee.Use a group; grant the group to the role.
"Version bump publishes the new content."Version is excluded from the content hash — bump alone is a no-op.Change the actual permissions/roles content.
"The manifest is valid, so we're done."Structural validity ≠ alignment with real guards.Pass Step 8; every pair must map to an Authorize call.

Bundled files

The model reads these on demand while the skill is loaded. They are exposed as readable files and are never executed.

Frequently asked questions

What does the Ring:Declaring Plugin Permissions AI skill do?

Interactively authoring an access-manager permission-declaration manifest (permissions.yaml) for the access-manager "inversão de responsabilidade": drives a plugin team through discovering its real Authorize() surface, normalizing every action to the SEMANTIC standard (never HTTP verbs), declaring roles/group grants and the M2M contract, then emits and validates a manifest that matches the lib-auth/v3 auth/declaration schema. Use when a plugin must publish its own permissions at boot (WireFromEnv) instead of the access-manager seed owning them, or when writing/fixing a permissions.yaml. It...

Why use Ring:Declaring Plugin Permissions on TypingMind?

Because you install it once and use it with any model. Ring:Declaring Plugin Permissions is plain Markdown rather than provider-specific code, so the same skill runs on GPT-5, Claude, Gemini, Grok, or a local model — and you can switch model mid-chat without it breaking. TypingMind runs on your own API keys, so you pay providers directly instead of a per-seat subscription, and your skills and chats stay in your own storage.

How do I install Ring:Declaring Plugin Permissions in TypingMind?

Open Plugins → Skills → Install from GitHub in TypingMind and paste https://github.com/LerianStudio/ring/tree/main/dev-team/skills/declaring-plugin-permissions. TypingMind reads its SKILL.md and bundles its files and installs it as a skill you can enable per chat.

Which AI models can use Ring:Declaring Plugin Permissions?

Any model you connect in TypingMind. AI skills are plain Markdown instructions rather than provider-specific code, so GPT, Claude, Gemini, Grok, and local models can all load this skill when a request matches it.

How many AI models can I use with Ring:Declaring Plugin Permissions?

As many as you like. As long as a model supports skills, you can use Ring:Declaring Plugin Permissions with it — GPT, Claude, Gemini, Grok, DeepSeek, Mistral, Llama and more — all on TypingMind with your own API keys.

Is the Ring:Declaring Plugin Permissions AI skill free?

Yes. It is published on GitHub by LerianStudio under the Apache-2.0 license. You only pay your own AI provider for the tokens you use.

What are AI skills?

An AI skill is a reusable instruction bundle that teaches an AI model how to do one specific task. It follows the open Agent Skills format: a SKILL.md file with a name and description, plus any scripts, templates or reference files the model may need. The model reads the instructions only when your request matches the skill, so an installed skill costs nothing until it is used.

How are AI skills different from plugins or MCP servers?

A plugin or MCP server gives a model new tools to call — code that runs somewhere and returns a result. An AI skill gives the model knowledge and process instead: how to approach a task, which steps to follow, what good output looks like. Skills are plain Markdown, so they need no server, no API key and no runtime, and they work with any model.

View all

Set up your own AI workspace now

Get notified about new features and future giveaways by subscribing to our newsletter 👇