M6-07: Permission hints and expiry overview¶
Status: ready | Depends on: M1-30, M6-04 | Milestone: M6
Goal¶
After this ticket owners (and managers for their spaces) see two lists that 07 9.4 and 9.5 require, in the desktop administration and in the console: "Permission hints" and "Expiring soon". The server computes them in authz and serves them through two new operations with the read action access.hints.read (R-AZ-34, already in the core contract): listPermissionHints returns, per instance or per space, the hint H1 for every agent A and every person P in its trigger set T(A) that may not read a channel K of its read horizon H(A) ("P can learn content of K through A", 07 6), grants to disabled principals, grants trigger whose triggering is blocked by SR2 or SR3, and agents in mode personal with their trigger set; listExpiringAccess returns grants and group memberships that expire within authz.expiry_reminder_lead and API tokens of agents and apps that expire within the same lead, each with subject, target, expiry and the action "Extend" (revoke plus a new grant with a new reason, 07 9.4) or "Renew token". Both lists are computed from the same permission state as authorize (no second rule set), never name content, and show only what the caller may administer. An integration test proves the H1 hint with an agent whose read horizon is larger than the permissions of a triggering person. Why: H1 is a hint, not a block (07 6), so the owner must see it somewhere outside the grant wizard; expiring access must not end unnoticed (07 9.4).
Read first¶
docs/spec/07-permissions.md: 2.5 (validity of a grant), 2.6 (managers within their scope), 2.9 to 2.11 (agents, tool class), 4.3, 6 (read horizon H(A), trigger set T(A), hint H1 and where it appears), 7 (permission state), 9.1 (source kindvia_agent), 9.3 (hints in the preview), 9.4 (expiry reminder and "Extend"), 9.5 ("Permission hints" list: H1, grants to disabled principals,triggerwithout possible triggering (SR2, SR3), agents withpersonaland their trigger set), SR1, SR2, SR3.docs/spec/05-actions.mdandactions.yaml(core contract, read only):access.hints.read(R-AZ-34),grant.read,grant.revoke,grant.create,token.list,token.create(renewal),token.revoke,access.explain.docs/spec/03-data-model.md(core contract, read only):grants(expires_at,expiry_reminded_at),group_memberships,api_tokens(expires_at),agents(billing_mode).docs/spec/08-identity.md12 (API tokens, their expiry).docs/spec/01-product.md5.2 (administration in desktop and console).docs/DECISIONS.md: D-W-100 and its extension to M5 and M6 (D-W-120); D-W-123 adds R-AZ-34; section 10 (format ofD-M6-07-<n>rows).- Briefs (merged names win, D-W-100):
M1-28-authz-core.md(snapshot,Scope),M1-29-authz-invariants.md,M1-30-authz-expiry.md(expiry job,access.expiring),M1-49-authz-explain.md(via_agentsource, H1 in explain and preview),M1-44-identity-api-tokens.md(tokens),M1-54-chat-operators.md(operators of a channel),M6-04-console-admin.md(console hosting ofadmin_ui), the briefs of M4-27 and M4-28 (grant wizard, agents and tokens screens). - Code on the base branch:
services/server/internal/authz,internal/identity(tokens),internal/chat(operators),internal/agents,packages/admin_ui,apps/console,apps/client(administration).
This ticket touches no table, no event and no configuration key.
Fixed requirements that apply¶
Owner's fixed build rules (apply verbatim):
- No MVP. New from scratch and directly in the final state; stages are only the order.
- Strictly modular: clear module and service boundaries, dependencies only through defined interfaces, no cross access.
- Documentation in code: every public module, class, function and interface with a docstring/doc comment (purpose, parameters, return value, errors, example where useful); comments explain the why, not the what. API documentation is generated from code (OpenAPI from the routes, dartdoc, Python API docs in MkDocs).
- Documentation in the repo: README per module/service, ADR for every architecture decision, CHANGELOG per release, keep the documentation site current. A change without matching documentation is not done.
- Quality gates (SonarQube, linters, type checking, security scanners, test coverage) block the merge.
Rules for every brief:
- Test first: write the failing test, run it and keep the red output for the report, then make it green, then clean up.
- No en dash and no em dash in any file you write; the
text-rulespre-commit hook rejects them, and the ARB test of M4-06 rejects them in interface texts. Use a hyphen or rephrase. - White label: examples and test data use
example.comand the people Alex, Sam and Robin; no company, host or personal names. Interface texts come frompackages/l10n. - Windows only through named configuration keys (
authz.expiry_reminder_lead); code comments name the key, never the number. - Specification first: the two operations go into
docs/api/openapi.yamlfirst; 07 9.5 names them. The core contract is not edited by this ticket.
Product requirements implemented here:
- Hints from the same permission state that
authorizeuses; no second rule set (07 7; B7). - H1 per (agent, person, channel) exactly as 07 6 defines it; a hint, never a block.
- The list "Permission hints" contains exactly the four kinds of 07 9.5.
- "Expiring soon": grants, memberships and agent and app tokens within
authz.expiry_reminder_lead; "Extend" is a revoke plus a new grant with a new reason (07 9.4); tokens are renewed by creating a new token (shown once) and revoking the old one. - Scope: the instance for owners; a space for its managers, limited to targets within their
managescope (07 2.6); a channel or space the caller cannot read gives404, readable withoutmanage403. - No content and no names beyond what the administration lists already show (principal display names, target names).
Scope¶
All server paths under services/server/ unless they start with docs/. Every new Go file starts with // SPDX-License-Identifier: Apache-2.0.
Create:
internal/authz/domain/hints.go: pure computation of the four hint kinds from a permission state, the operator assignments and the agent facts (Design note 1).internal/authz/app/hints.goandexpiring.go: the use cases withauthorize(p, access.hints.read, instance | space), the scope limit, paging with the cursor codec of M1-18 (Design notes 2 and 3).- Ports where missing:
identity.TokenExpiry(agent and app tokens expiring before a time, without the token values),chat.Operators(operators per channel, M1-54) as used by M1-49. internal/authz/adapters/httpapi/hints.go: handlerslistPermissionHintsandlistExpiringAccess.packages/admin_ui/lib/src/hints/: the two list screens with filters (kind, space), the "Explain" link to A3, "Extend" through the grant wizard of M4-27 with the old grant prefilled and a new reason required, "Renew token" through the token screen of M4-28 (Design note 4).- Tests (Tests).
Change:
docs/api/openapi.yaml: operationslistPermissionHints(GET /api/v1/access/hints, queryspace_id,kind, cursor, limit) andlistExpiringAccess(GET /api/v1/access/expiring, queryspace_id, cursor, limit), bothx-authz-action: access.hints.read,x-rate-limit-class: rl.read; schemasPermissionHint(kindh1_read_horizon,grant_to_disabled,trigger_blocked,agent_personal; the IDs of agent, person, channel, grant, rule and the trigger set where the kind has them),PermissionHintPage,ExpiringAccessItem(kindgrant,membership,token; IDs,expires_at),ExpiringAccessPage;make api-lint,make api-checks,make codegen-check;api/operations.yamlenables them.apps/clientdesktop administration navigation andapps/consoleroutes: entries "Permission hints" and "Expiring soon" (console navigation of 01 5.5 under "Groups, grants").packages/l10nARB files (hint texts from templates, like the preview of 07 9.3).- Docs:
docs/spec/07-permissions.md9.4 and 9.5 (the operations and the list contents),internal/authz/README.md,packages/admin_ui/README.md, root and serverCHANGELOG.mdunder[Unreleased],docs/DECISIONS.mdsection 10 rowsD-M6-07-1toD-M6-07-5.
Out of scope¶
- Changing a hint into a block (07 6 decides: hint).
- Emails or pushes about expiry (07 9.4: the event
access.expiringexists; no email). - Changes to the core contract.
Boundaries¶
- Work in a dedicated git worktree on the branch
feat/M6-07-permission-hints; one writer per worktree. - Conventional Commits with the module or package as scope (for example
feat(authz): ...,feat(admin-ui): ...) and a DCO sign-off on every commit (git commit -s); squash into meaningful commits before the merge (docs/quality/push-discipline.md). - Never push to
main, never tag and never create remote repositories; the only remote steps are the branch push and the pull request ofAGENTS.md. - Do not modify files outside the scope above, except the CHANGELOG, README and
docs/DECISIONS.mdentries named in this brief. - Follow the host rules supplied by the launcher (ports, machine rules).
- Tests bind only
127.0.0.1and port 0; PostgreSQL only throughpgtest; client tests use the fake server of M4-17. Temporary directories only viamktemp -d, removed at the end. Stop only processes you started, by PID. - Never read or print secrets; token values never leave identity (the port returns only IDs and expiry).
- Never lower a threshold, disable a gate, delete a test, raise
repo.max_file_kbor extendquality/licenses/allowed.txt. - No permission logic in the client (15 1 principle 1).
- If the core contract or
docs/api/lacks something this ticket needs, stop withVerdict: BLOCKED <reason>instead of building a deviation (P-11). - This brief was written before milestone M1 closed (D-W-100, extended to M5 and M6 by D-W-120). Where the merged code of an earlier ticket names a package, type, function, file, provider or make target differently from this brief, follow the code and list each difference in the report; where it behaves differently from the specification, stop with
Verdict: BLOCKED <reason>.
Design notes¶
-
H1 and the other kinds (07 6, 9.5; Ruling for docs/DECISIONS.md). For each agent A: H(A) = channels A may read (its grants plus the operator rule), from the permission state of A; T(A) = people with effective
triggeron a target A operates and no guard rule blocking it (SR2, SR3), from the states of those people. For each P in T(A) and K in H(A) with P not allowed to read K: one hinth1_read_horizon (A, P, K). Reuse the function M1-49 uses for thevia_agentsource; if M1-49 computed it inline, move it toauthz/domainwith its tests and call it from both.grant_to_disabled: grants whose subject is a disabled principal.trigger_blocked: grants with leveltriggerwhere SR2 or SR3 blocks every triggering for that subject on that target.agent_personal: agents withbilling_mode = personalwith their trigger set (the I1 holder and anyone else who may trigger, which I1 forbids; the hint shows the set the owner must keep at one person). -
Scope and cost (07 2.6). For a space: only agents operating a channel of the space, only channels of the space in H(A), only grants on targets within the caller's
managescope. The computation runs per request over the permission states held by the cache of M1-28; measure it with the test world scaled to 50 agents and 500 people and record the time; if it exceeds the request deadline of 02, page the list in a fixed order of kinds (h1_read_horizon,grant_to_disabled,trigger_blocked,agent_personal) and within a kind by its stable key (agent, person and channel for H1, the grant ID for the two grant kinds, the agent ID foragent_personal), and compute per page; the cursor carries the kind and the position within it, so every hint of every kind appears on exactly one page. -
Expiring (07 9.4). Grants and memberships with
expires_atwithinnow + authz.expiry_reminder_lead, not revoked; tokens of agents and apps (not personal tokens) withexpires_atin the same window, fromidentity.TokenExpiry; ordered by expiry. For a space manager only grants on targets within the scope and memberships of groups whose grants fall in the scope. -
Screens (07 9.3, 9.4). Each hint shows a sentence from a template in the caller's language ("Sam can learn content of Engineering/Intern through agent Texter."), the kind as icon and text, and actions "Explain" (A3 for the channel) and, for grants, "Revoke" or "Extend". "Extend" opens the grant wizard with the subject, target, level and capabilities of the old grant, an empty reason and a new expiry; confirming creates the new grant, then revokes the old one, both with step-up (one step-up covers both within
auth.step_up_max_age). -
Rulings for
docs/DECISIONS.md(D-M6-07-<n>): one H1 function for explain and hints (note 1); the definitions of the other three kinds (note 1); scope and paging (note 2); the expiring window and token kinds (note 3); "Extend" as create then revoke (note 4).
Tests (write them first)¶
Server (pgtest, full chain):
TestHints_H1(PLAN row): agent Texter operates Marketing and may read Marketing/Kampagnen and Marketing/Intern; Sam hastriggeron Marketing/Kampagnen only;listPermissionHintsfor the owner contains exactly oneh1_read_horizon (Texter, Sam, Intern); granting Samreadon Intern removes it; the same check throughexplainChannelAccessstill showsvia_agent(one function).TestHints_OtherKinds: a grant to a disabled person, atriggergrant blocked by SR2 (tool class) or SR3 (dev environment not ready, where SR3 data exists; otherwise the test uses the SR2 case only and the report says so), an agent in modepersonalwith its trigger set.TestHints_Scope: Sam as manager of Support sees only Support's hints; withspace_id = Engineering404or403as 07 4.1 decides; Robin withoutmanagegets403.TestExpiring: grants, memberships and tokens inside and outsideauthz.expiry_reminder_leadwith a fake clock; personal tokens never listed; token values never in the response.TestHints_Pagingand the measured time of Design note 2.
Client (widget and integration with the fake server): the two screens in light and dark, text scaling 1 and 2, de and en; "Extend" creates then revokes with one step-up; "Explain" opens A3.
Commands that must pass, from the repository root:
cd services/server && go test -race -count=1 ./internal/authz/... ./internal/identity/... ./internal/chat/... ./internal/wire/...cd packages/admin_ui && flutter analyze --fatal-infos && flutter test --coveragemake codegen-check,make api-lint,make api-checks,make arch,make register-check,make catalog-check,make lint,make fmt-check,make coveragemake check
Acceptance criteria¶
- Integration test with an agent whose read horizon is larger than the permissions of a triggering person gives the H1 hint.
- The list contains exactly the four kinds of 07 9.5, from the permission state of
authorize; scope and errors follow 07 2.6 and 4.1. - "Expiring soon" lists grants, memberships and agent and app tokens within
authz.expiry_reminder_lead; "Extend" creates then revokes. - The operations are in
openapi.yaml; the screens are in the desktop administration and the console. - 07 9.4 and 9.5, READMEs, ARB files, CHANGELOGs and the rulings are updated.
-
make checkis green.
Definition of done¶
- Code and tests as specified; doc comments on every public Go and Dart symbol; comments explain why.
- READMEs of
authzandadmin_uiupdated in English. - Rulings of Design note 5 recorded in
docs/DECISIONS.mdsection 10. - Root and server
CHANGELOG.mdentries under 'Unreleased'. - An ADR only if you take an architecture decision beyond this brief (MADR 4 as in
docs/adr/0000-template-madr-4.md, headings and text in English, next free number,make adr-index). make checkgreen.- An independent code review over the diff, then the red team gate (
docs/quality/red-team-gate.md, verdictJAorNEIN); onNEINfix with a test per finding and have the delta checked again. Record both verdicts. - Land the change as
AGENTS.md("How work lands onmain") describes: a pull request from this branch, squash-merged only when review, red team andmake check,ci-okand the review threads are green; thengit worktree remove.
Report¶
Write to {{REPORT_PATH}}:
- First line exactly
Verdict: DONEorVerdict: BLOCKED <reason>. - Commits (SHA and subject), changed files with line counts, test results with counts (the red run before the change and the green run after), the summary of
make check. - Review and red team verdicts and the findings fixed.
- The measured computation time of the hints for the scaled world.
- What is not proven.
- Follow-ups that could not be done in this scope, each with its reason, every difference to the names of this brief (D-W-100), and spec gaps found.
Then write {{DONE_PATH}} with exit=0 sha=<sha> or exit=1 <reason>.