Skip to content

Fine-grained keys, grant errors and CLI organization switching - #623

Draft
etbyrd wants to merge 10 commits into
mainfrom
rbac-keys-and-org-switching
Draft

etbyrd wants to merge 10 commits into
mainfrom
rbac-keys-and-org-switching

Conversation

@etbyrd

@etbyrd etbyrd commented Oct 24, 2026 •

Copy link
Copy Markdown
Member

Client side of fine-grained keys, grants and CLI organization switching. Draft until the matching server-side handlers are available. Every new path is feature-detected, so released servers keep working with this build.

Spec and SDKs

  • x-primitive-permission on every operation the server declares, plus grant_forbidden, the step-up codes and grant_forbidden detail fields in the error schema.
  • New operations: listCliOrgs, createKey, rotateKey, updateKey, getKeyBuilder, listCredentials, revokeCredential, listCredentialEvents, getCredentialPolicy, listRoles, listGroups, listMailboxAccess, getAccessMode.
  • StartCliLoginInput gains login_kind and requested_grant. The user login poll shape is a separate CliUserLoginPollResult schema, so the generated poll result type is unchanged.
  • whoami gains optional grant and check fields. The grant_forbidden detail fields are nullable, so Go decodes error bodies that carry null details. Its include, check and resource query parameters are described in the operation, not declared, because declaring the first parameter would change the generated Go Whoami signature.
  • Node: GrantForbiddenError (a PrimitiveApiError subclass, so existing checks still match), isGrantForbiddenError, and PERMISSIONS, Permission and OPERATION_PERMISSIONS, generated from the spec into permissions.generated.ts. Also a GrantSpec builder, client.keys, client.credentials, client.roles, client.account.whoami({ grant: true }) and client.account.checkPermission(). The operation manifest gains permissions.
  • Python: GrantForbiddenError(PrimitiveAPIError) with the detail fields.
  • Go: GrantForbiddenError, which embeds *APIError. errors.As matches both types.
  • Go and Python clients are regenerated, and only new types and fields were added.

CLI

  • primitive login sends login_kind: "user" and retries without it on 400 validation_error, so servers without user logins get today's org-bound login. The saved file for an org-bound login is byte-compatible with today's. A user login is saved as auth_method: "user_grant": the standard fields hold the current organization's token, and user_grant holds the identity token and a small per-org token cache.
  • primitive orgs lists your organizations, with the refusal reason for each one you cannot use. primitive org use <org> switches by id or name.
  • --org <org> (or PRIMITIVE_CLI_ORG) works on every command. bin/run.js strips it just before oclif parses the command, after the detached receiver and signal workers start, so they never inherit it. primitive memories keeps its own boolean --org. Organization names resolve against the full list, so a name two organizations share is refused. Parallel commands wait briefly for the credentials lock rather than failing.
  • Per-org tokens are exchanged when missing or expired. On a 401, the CLI exchanges again once and retries. A refused exchange comes back as a 403 whose code is the reason (not_a_member, cli_access_disabled_by_org, role_insufficient, cli_access_revoked). The identity token is refreshed when needed, and invalid_grant removes the saved login.
  • primitive logout on a user login revokes the refresh token, which signs out of every organization.
  • primitive login --permission <p> sends requested_grant. If the server refuses it (grant_not_available, or an older server's validation_error), the login stops. It is never retried with a wider grant.
  • primitive keys list shows API key metadata (id, prefix, name, grant, expiry, last use) and never a secret. The CLI has no command that creates, rotates, updates or revokes a key or other credential: API keys are created, rotated and revoked in the dashboard, and the help and README say so. The generated keys:create-key, keys:rotate-key, keys:update-key and credentials:revoke-credential commands are left out too, along with their entries in list-operations, describe and fish completion (DASHBOARD_ONLY_OPERATION_IDS in cli-operations.ts). The SDK client methods for keys stay.
  • primitive whoami shows organization, role and grant when the server reports them. --check <permission> prints the decision and exits 1 when the permission is not allowed. --grant adds the grant to --json.
  • primitive roles list|groups|mailbox-access|access-mode are read-only. roles list help names the built-in roles, operator included.

Built-in operator role

  • The spec gains BuiltinRoleKey: owner, admin, operator, member. operator has every permission admin has except managing members, changing roles, sending invitations and billing. Role.key, CliOrg.role and CliOrgAccessToken.role point to it and stay plain strings, so template, custom and unlisted keys still decode. CredentialPolicy.cli_access says owners_and_admins admits owner, admin and operator.
  • Node: BUILTIN_ROLE_KEYS (generated from the spec into permissions.generated.ts) and the BuiltinRoleKey type. Python: the generated BuiltinRoleKey enum. Go: BuiltinRoleKey, RoleOwner through RoleMember, BuiltinRoleKeys() and IsBuiltinRoleKey, with a test that pins them to the spec enum.
  • grant_forbidden errors print a hint naming the missing permission.

Unchanged: connected agent profiles, pconn_ detection, and API keys. With --org naming a different organization, an org-bound login, an API key or a connected profile refuses the command rather than act elsewhere.

Notes for review

  • The server answers keys list and the inventory and audit operations only for an owner or admin in a dashboard session. From a CLI login (an OAuth token) they show the server's 403.
  • --permission values are sent as space-separated permissions in requested_grant. The server accepts only primitive:api there, so narrowed logins are refused.

Tests

  • sdk-node/tests/api/grants.test.ts: GrantForbiddenError parsing (typed details, other refusals stay PrimitiveApiError, malformed details), the generated permissions, GrantSpec, and the keys, credentials, roles and whoami calls.
  • cli-node/tests/oclif/user-grant.test.ts runs against a mock HTTP server:
    • login start, user login kind and the old-server fallback;
    • --permission refusals;
    • end-to-end login with --org, and the org-bound save from an old server;
    • exchange on expiry;
    • one re-exchange for concurrent 401s, and a refused re-exchange carrying its reason;
    • every refusal reason;
    • --org by name with caching;
    • identity refresh and invalid_grant;
    • old-server unsupported_grant_type and feature_disabled;
    • org-bound logins unchanged, and --org refused for them and for API keys;
    • the orgs, org use, whoami and --check commands;
    • logout.
  • cli-node/tests/oclif/keys-roles.test.ts: --org argv handling, keys list, that no create, rotate, update or revoke command or generated operation is offered, and the roles commands with an operator row.
  • cli-node/tests/oclif/user-grant.test.ts also lists and switches to an organization where the role is operator.
  • cli-node/tests/oclif/commands-manifest-coverage.test.ts: every manifest operation has a command except exactly the dashboard-only ones.
  • sdk-node/tests/api/grants.test.ts, sdk-python/tests/test_grant_forbidden.py and sdk-go/roles_test.go: the built-in role keys include operator, and a key outside the list still parses.
  • sdk-go/grant_forbidden_test.go and sdk-python/tests/test_grant_forbidden.py.
  • make node-check-generated, go-check, python-check and shared-check all pass. sdk-node: 1277 tests. cli-node: lint and typecheck are clean, and the suite passes in a clean environment. A few notification, receipt and pending-mail tests time out under full-suite load on a busy machine; they are untouched here and the set that times out changes between runs. make cli-build succeeds.

Nothing was run against a deployed environment.

…spec and SDKs

The spec gains x-primitive-permission on every declared operation, the
grant_forbidden and step-up error codes, the whoami grant fields, the CLI
user login shapes, the org list, and the keys, credential inventory, audit
event, credential policy and role read operations.

Node, Python and Go get a typed GrantForbiddenError that still matches
their existing API error type. Node also gets the generated Permission
union and OPERATION_PERMISSIONS, a GrantSpec builder, and keys,
credentials and roles resources.
…es commands

primitive login asks for a login as the person (login_kind user) and falls
back to today's org-bound login when the server refuses the field. Such a
login lists organizations with primitive orgs, switches with primitive org
use, and takes --org on every command. Per-org tokens are exchanged when
missing or expired and once more on a 401; a refused exchange reports its
reason. Saved org-bound logins, API keys and connected agent profiles keep
working as before and refuse a different organization.

Also primitive keys create, list, rotate and revoke (with --expires,
--permission and --step-up), login --permission, whoami showing the grant
and answering --check, and read-only roles commands.
…hed workers, nullable grant details

- Wait briefly for the credentials lock during exchanges instead of failing,
  and use another process's fresh token after a 401.
- Take the 401 retry token from the request header.
- Resolve organization names only through the full list, so a shared name
  is refused instead of matching a cached organization.
- Rename the selection variable to PRIMITIVE_CLI_ORG, apply --org after the
  receiver and signal workers start, and keep memories:<cmd> --org intact.
- payloads push and pull use the per-org token for user logins and --org.
- doctor and the root sign-in line recognize user logins.
- whoami --check refuses a connected profile; the grant lines appear only
  when the server reports a grant, and --json without --grant makes no
  extra request.
- login checks a saved user login without an organization against the
  server before calling it valid.
- grant_forbidden detail fields are nullable, so the Go SDK decodes errors
  that carry null details.
- Neutral wording in docs and help text.
- Raise the agent network smoke script's output buffer for list-operations.
@etbyrd

etbyrd commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@greptileai review

@greptile-apps

greptile-apps Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no new blocking issues were found.

Reviews (7) · Last reviewed commit: "Keep the public built-in role list to ow..." · Reviewed by Greptile

Comment thread cli-node/src/oclif/user-grant.ts Outdated
Comment thread cli-node/src/oclif/global-org-flag.ts
Comment thread cli-node/src/oclif/user-grant.ts
…g for profile-bound agent commands, accept empty revoke responses
@etbyrd

etbyrd commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@greptileai review

Comment thread cli-node/src/oclif/agent-setup.ts Outdated
@etbyrd

etbyrd commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@greptileai review

@etbyrd

etbyrd commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

Summary finding (listener child inherits the organization selection) addressed in ea08619: listen with a connected agent credential refuses --org / PRIMITIVE_CLI_ORG before any listener state is written, so a background supervisor is never started with a selection its credential would refuse. @greptileai review

Comment thread cli-node/src/oclif/commands/listen.ts
@etbyrd

etbyrd commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

Summary finding (enrollment leaves receiving stopped) addressed in 66ab747: agent setup starts the profile's background listener with the organization selection removed from its environment (withoutOrgSelection), so agent enroll --org completes; a direct listen with a connected credential still refuses --org. @greptileai review

…n operator role

The CLI keeps keys list, which shows key metadata and never a secret, and
says keys are created, rotated and revoked in the dashboard. The generated
create, rotate, update and revoke commands are left out as well. The SDK
client methods stay.

The spec gains BuiltinRoleKey (owner, admin, operator, member, restricted).
Node exports BUILTIN_ROLE_KEYS and BuiltinRoleKey, Python generates the enum
and Go adds BuiltinRoleKey constants.
@etbyrd

etbyrd commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@greptileai review

Role keys stay plain strings, so a key outside the list still parses.
@etbyrd

etbyrd commented Oct 24, 2026

Copy link
Copy Markdown
Member Author

@greptileai review

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant