feat(cli): manage agent grants and keep remote login on the public origin (#20323)

## Thinking Path

> - Paperclip lets people control their AI agents and permissions.
> - Remote operators need a browser-approved CLI login.
> - Proxies can send that login's approval link to an internal host.
> - Human member commands cannot grant an agent an explicit permission
such as commentary:view.
> - This change keeps approval on the canonical public origin and adds
Board-only single-grant commands.
> - The API checks company access and permission-management authority
and records each change.
> - An operator can sign in, grant one agent permission, and preserve
the agent's other grants.

## Linked Issues or Issue Description

**What happened?**

A proxied CLI login returned an approval URL on the upstream host. The
signed-in browser was on the public host. The CLI also had no command to
add an arbitrary explicit agent grant: member commands manage human
members, while agent permissions:update exposes fixed configuration
fields.

**Expected behavior**

Approval uses the configured canonical public origin. A Board operator
with users:manage_permissions can grant or revoke one explicit agent
permission without replacing other grants or activating an inactive
membership.

**Steps to reproduce**

Configure a public auth origin. Create a CLI challenge through a proxy
that replaces forwarded-host headers. Inspect the approval URL. Then try
to grant commentary:view to an agent through the CLI.

## What Changed

- Use the existing public-origin resolver for CLI approval URLs.
- Add agent permission:grant and permission:revoke commands.
- Add PATCH /api/agents/{id}/permission-grants with strict shared
validation and Board-only OpenAPI metadata.
- Serialize each mutation on the agent row. Preserve unrelated grants
and membership state. Commit the grant and its activity record together
and publish activity after commit.
- Document remote login and single-grant management.

## Verification

- The new approval-link regression failed before the fix and passes
after it.
- All 25 CLI auth route tests pass.
- All 72 affected CLI, login and OpenAPI tests pass.
- All 5 PostgreSQL grant tests pass, including actual commentary reads
(403 before grant, 200 after grant, 403 after revoke), preservation,
audit attribution, concurrent additions, invalid inputs, inactive or
missing membership, agent self-grants, cross-company denial, and
rollback on audit failure.
- Shared and CLI typechecks and server declaration-layer checks pass.
- pnpm verify requests broad CI coverage because doc/CLI.md is a global
verification input. Greptile reviewed final head
338c58fb24 at 5/5 with no findings. All 58
non-skipped checks pass on that head. CI run:
https://github.com/paperclipai/paperclip/actions/runs/42663640985. Live
target acceptance is in progress.

## Risks

This adds an explicit permission-management API. It requires Board
authentication, company access, and users:manage_permissions.
Foreign-company agents return 404. Granting a key intentionally makes
that key unscoped in its company; revocation removes that key. Other
keys and agent configuration remain unchanged. Inactive memberships and
unapproved or terminated agents cannot gain grants. No schema change is
needed. Revert this change to remove the new management surface;
existing grants retain their normal authorization rules.

## Model Used

OpenAI GPT-6 through Codex, with reasoning, code editing, shell tools,
and browser testing. The runtime does not expose a more specific model
ID or context window.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip authored and GitHub committed 2026-11-09 11:35:52 -06:00
1 parent 1b7fe33432
commit ff2d463eed
13 files changed
+304 -3

No files matched your search

+11
View File
@@ -131,6 +131,17 @@ describe("agent lifecycle commands", () => {
]);
});
it("grants and revokes one permission without replacing the grant list", async () => {
const fetchMock = vi.fn().mockImplementation(() => Promise.resolve(jsonResponse()));
vi.stubGlobal("fetch", fetchMock);
await run(["agent", "permission:grant", AGENT_ID, "commentary:view"]);
await run(["agent", "permission:revoke", AGENT_ID, "commentary:view"]);
expect(fetchMock.mock.calls.map(call => [call[0], call[1]?.method, JSON.parse(call[1]?.body)])).toEqual([
[`http://localhost:3100/api/agents/${AGENT_ID}/permission-grants`, "PATCH", { permissionKey: "commentary:view", enabled: true }],
[`http://localhost:3100/api/agents/${AGENT_ID}/permission-grants`, "PATCH", { permissionKey: "commentary:view", enabled: false }],
]);
});
it("refuses to delete an instructions file without confirmation", async () => {
const fetchMock = vi.fn().mockImplementation(() => Promise.resolve(jsonResponse()));
vi.stubGlobal("fetch", fetchMock);
+19
View File
@@ -6,6 +6,7 @@ import {
updateAgentInstructionsBundleSchema,
updateAgentInstructionsPathSchema,
updateAgentPermissionsSchema,
updateAgentPermissionGrantSchema,
updateAgentSchema,
upsertAgentInstructionsFileSchema,
wakeAgentSchema,
@@ -462,6 +463,24 @@ export function registerAgentCommands(program: Command): void {
}),
);
for (const [command, enabled] of [["permission:grant", true], ["permission:revoke", false]] as const) {
addCommonClientOptions(
agent.command(command)
.description(`${enabled ? "Grant" : "Revoke"} one agent permission (Board permission manager only)`)
.argument("<agentId>", "Agent UUID")
.argument("<permissionKey>", "Permission key, such as commentary:view")
.action(async (agentId: string, permissionKey: string, opts: BaseClientOptions) => {
try {
const ctx = resolveCommandContext(opts);
const payload = updateAgentPermissionGrantSchema.parse({ permissionKey, enabled });
printOutput(await ctx.api.patch(apiPath`/api/agents/${agentId}/permission-grants`, payload), { json: ctx.json });
} catch (err) {
handleCommandError(err);
}
}),
);
}
addCommonClientOptions(
agent
.command("configuration")