From 47db4b17588e329ced3505840c4efef6914d1846 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Sun, 30 Aug 2026 15:57:46 -0700 Subject: [PATCH] feat(seo): add agent permissions guide --- frontend/scripts/generate-seo-pages.mjs | 8 +- frontend/scripts/generate-seo-pages.test.mjs | 35 ++- frontend/src/components/landing/GuidePage.tsx | 14 +- frontend/src/content/guides.json | 230 +++++++++++++++++- frontend/src/v2/__tests__/V2Login.test.tsx | 17 +- 5 files changed, 295 insertions(+), 9 deletions(-) diff --git a/frontend/scripts/generate-seo-pages.mjs b/frontend/scripts/generate-seo-pages.mjs index bc73ad4b03..4fdbf258cb 100644 --- a/frontend/scripts/generate-seo-pages.mjs +++ b/frontend/scripts/generate-seo-pages.mjs @@ -319,7 +319,13 @@ const renderGuide = (guide) => { const sections = guide.sections.map((section) => `

${escapeHtml(section.title)}

- ${(section.paragraphs || []).map((paragraph) => `

${escapeHtml(paragraph)}

`).join('')} + ${(section.paragraphs || []).map((paragraph) => { + if (typeof paragraph === 'object' && paragraph.strong) { + return `

${escapeHtml(paragraph.strong)}

`; + } + + return `

${escapeHtml(paragraph)}

`; + }).join('')} ${section.bullets?.length ? list(section.bullets) : ''} ${section.orderedItems?.length ? `
    ${section.orderedItems.map((item) => `
  1. ${escapeHtml(item)}
  2. `).join('')}
` : ''} ${section.tables?.map(renderGuideTable).join('') || ''} diff --git a/frontend/scripts/generate-seo-pages.test.mjs b/frontend/scripts/generate-seo-pages.test.mjs index 734cab289a..7a99e2d4af 100644 --- a/frontend/scripts/generate-seo-pages.test.mjs +++ b/frontend/scripts/generate-seo-pages.test.mjs @@ -19,7 +19,7 @@ test('emits a canonical crawlable page for every public route', async () => { const guides = JSON.parse(guideText); const pages = buildPageDefinitions({ landing: translations.landing, compare: translations.compare, useCases, guides }); - assert.equal(pages.length, 20); + assert.equal(pages.length, 21); assert.deepEqual(pages.map((page) => page.path), [ '/', '/compare/', @@ -41,6 +41,7 @@ test('emits a canonical crawlable page for every public route', async () => { '/guides/how-to-build-an-ai-agent-team/', '/guides/agent-to-agent-messaging/', '/guides/self-hosted-ai-agent-platform/', + '/guides/ai-agent-permissions-and-tokens/', ]); assert.deepEqual(pages[0].schema['@graph'].map((item) => item['@type']), [ 'Organization', @@ -76,7 +77,7 @@ test('emits a canonical crawlable page for every public route', async () => { '/guides/ai-agent-task-management/', '/guides/connect-claude-codex-shared-workspace/', ]); - assert.equal(guidePages.length, 10); + assert.equal(guidePages.length, 11); for (const guide of guidePages) { assert.equal(guide.ogType, 'article'); const article = guide.schema['@graph'].find((item) => item['@type'] === 'Article'); @@ -185,6 +186,26 @@ test('emits a canonical crawlable page for every public route', async () => { assert.match(selfHostedHtml, /

Choose the responsibility you are ready to operate<\/h2>

These gaps are not reasons to avoid self-hosting\./); assert.match(selfHostedHtml, /href="\/guides\/connect-claude-codex-shared-workspace\//); assert.match(selfHostedHtml, /href="\/guides\/ai-agent-memory\//); + const permissionsGuide = guidePages.find((page) => page.path === '/guides/ai-agent-permissions-and-tokens/'); + assert.equal(permissionsGuide.title, 'AI Agent Permissions and Tokens: Scope Runtime Access Safely | Commonly'); + const permissionsHtml = renderStaticPage(guideTemplate, permissionsGuide); + assert.match(permissionsHtml, /An AI agent runtime token is a bearer credential that identifies a specific agent installation/); + assert.match(permissionsHtml, /Commonly \(commonly\.me\), the shared workspace where humans and AI agents work together/); + assert.match(permissionsHtml, /

Separate authentication, authorization, and tool permissions<\/h2>\s*

These three ideas[^<]*They are different layers:<\/p>\s*

/); + assert.match(permissionsHtml, /

Why the layers must stay separate<\/h2>/); + assert.match(permissionsHtml, /https:\/\/csrc\.nist\.gov\/glossary\/term\/least_privilege/); + assert.match(permissionsHtml, /https:\/\/cheatsheetseries\.owasp\.org\/cheatsheets\/Secrets_Management_Cheat_Sheet\.html/); + assert.match(permissionsHtml, /It does authorize meaningful work[^<]*<\/p>\s*

Commonly documents two agent-related token categories[^<]*<\/p>\s*

/); + assert.match(permissionsHtml, /Adding an agent to a pod is also an access decision for its runtime token\.<\/strong>/); + assert.match(permissionsHtml, /Before adding an existing agent[^<]*questions:<\/p>\s*
    /); + assert.match(permissionsHtml, /Once issued, a runtime request uses the token in the authorization header:<\/p>\s*
    /);
    +  assert.match(permissionsHtml, /Commonly’s documentation recommends an environment variable[^<]*:<\/p>\s*
    /);
    +  assert.match(permissionsHtml, /Use a controlled test sequence:<\/p>\s*
      /); + assert.match(permissionsHtml, /A practical response sequence is:<\/p>\s*
        /); + assert.match(permissionsHtml, /

        Sharing a token between installations<\/h2>/); + assert.match(permissionsHtml, /

        Assuming token scope governs local tools<\/h2>/); + assert.match(permissionsHtml, /href="\/guides\/connect-claude-codex-shared-workspace\//); + assert.match(permissionsHtml, /href="\/guides\/ai-agent-task-management\//); for (const guidePath of [ '/guides/multi-agent-collaboration-platform/', '/guides/ai-agent-workspace/', @@ -211,6 +232,16 @@ test('emits a canonical crawlable page for every public route', async () => { const html = renderStaticPage(guideTemplate, pages.find((page) => page.path === guidePath)); assert.match(html, /href="\/guides\/self-hosted-ai-agent-platform\//); } + for (const guidePath of [ + '/guides/connect-claude-codex-shared-workspace/', + '/guides/ai-agent-task-management/', + '/guides/how-to-build-an-ai-agent-team/', + '/guides/self-hosted-ai-agent-platform/', + '/guides/agent-to-agent-messaging/', + ]) { + const html = renderStaticPage(guideTemplate, pages.find((page) => page.path === guidePath)); + assert.match(html, /href="\/guides\/ai-agent-permissions-and-tokens\//); + } const guidesIndex = pages.find((page) => page.path === '/guides/'); assert.equal(guidesIndex.title, 'Guides for teams working with AI agents | Commonly'); assert.equal(guidesIndex.schema['@graph'].find((item) => item['@type'] === 'WebPage').name, 'Guides for teams working with AI agents'); diff --git a/frontend/src/components/landing/GuidePage.tsx b/frontend/src/components/landing/GuidePage.tsx index 02e383e6fc..e81c8e0524 100644 --- a/frontend/src/components/landing/GuidePage.tsx +++ b/frontend/src/components/landing/GuidePage.tsx @@ -14,9 +14,15 @@ import commonlyLogo from '../../assets/commonly-logo.png'; import guides from '../../content/guides.json'; import { guidePalette, useGuideCanvas } from './guideShell'; +interface GuideStrongParagraph { + strong: string; +} + +type GuideParagraph = string | GuideStrongParagraph; + interface GuideSection { title: string; - paragraphs?: string[]; + paragraphs?: GuideParagraph[]; bullets?: string[]; orderedItems?: string[]; codeBlocks?: Array<{ @@ -138,7 +144,11 @@ const GuidePage: React.FC = () => { {section.title} - {section.paragraphs?.map((paragraph) => {paragraph})} + {section.paragraphs?.map((paragraph) => ( + + {typeof paragraph === 'string' ? paragraph : {paragraph.strong}} + + ))} {section.bullets && ( diff --git a/frontend/src/content/guides.json b/frontend/src/content/guides.json index 3136100d46..ce10592702 100644 --- a/frontend/src/content/guides.json +++ b/frontend/src/content/guides.json @@ -511,6 +511,10 @@ "label": "Learn about shared memory for AI agents", "path": "/guides/ai-agent-memory/" }, + { + "label": "Learn about AI agent permissions and tokens", + "path": "/guides/ai-agent-permissions-and-tokens/" + }, { "label": "Explore agent collaboration", "path": "/use-cases/agent-collab/" @@ -718,6 +722,10 @@ "label": "Evaluate a self-hosted AI agent platform", "path": "/guides/self-hosted-ai-agent-platform/" }, + { + "label": "Learn about AI agent permissions and tokens", + "path": "/guides/ai-agent-permissions-and-tokens/" + }, { "label": "Explore agent collaboration", "path": "/use-cases/agent-collab/" @@ -1517,7 +1525,8 @@ { "label": "Learn about shared memory for AI agents", "path": "/guides/ai-agent-memory/" }, { "label": "Learn about human-in-the-loop review", "path": "/guides/human-in-the-loop-ai-agents/" }, { "label": "Learn about AI agent handoffs", "path": "/guides/ai-agent-handoffs/" }, - { "label": "Learn about agent-to-agent messaging", "path": "/guides/agent-to-agent-messaging/" } + { "label": "Learn about agent-to-agent messaging", "path": "/guides/agent-to-agent-messaging/" }, + { "label": "Learn about AI agent permissions and tokens", "path": "/guides/ai-agent-permissions-and-tokens/" } ], "cta": { "title": "Build for legible work, not autonomous theater", @@ -1708,7 +1717,8 @@ { "label": "Learn about shared memory for AI agents", "path": "/guides/ai-agent-memory/" }, { "label": "Learn about human-in-the-loop review", "path": "/guides/human-in-the-loop-ai-agents/" }, { "label": "Learn about AI agent handoffs", "path": "/guides/ai-agent-handoffs/" }, - { "label": "Learn how to build an AI agent team", "path": "/guides/how-to-build-an-ai-agent-team/" } + { "label": "Learn how to build an AI agent team", "path": "/guides/how-to-build-an-ai-agent-team/" }, + { "label": "Learn about AI agent permissions and tokens", "path": "/guides/ai-agent-permissions-and-tokens/" } ], "cta": { "title": "Give the team a place to continue", @@ -1948,7 +1958,8 @@ { "label": "Read the multi-agent collaboration guide", "path": "/guides/multi-agent-collaboration-platform/" }, { "label": "Learn about AI agent workspaces", "path": "/guides/ai-agent-workspace/" }, { "label": "Connect Claude Code and Codex to one workspace", "path": "/guides/connect-claude-codex-shared-workspace/" }, - { "label": "Learn about shared memory for AI agents", "path": "/guides/ai-agent-memory/" } + { "label": "Learn about shared memory for AI agents", "path": "/guides/ai-agent-memory/" }, + { "label": "Learn about AI agent permissions and tokens", "path": "/guides/ai-agent-permissions-and-tokens/" } ], "cta": { "title": "Choose the responsibility you are ready to operate", @@ -1956,5 +1967,218 @@ "primary": { "label": "Create a shared workspace", "path": "/v2/register" }, "secondary": { "label": "Explore Commonly’s guides", "path": "/guides/" } } + }, + "ai-agent-permissions-and-tokens": { + "eyebrow": "Guide", + "titleTag": "AI Agent Permissions and Tokens: Scope Runtime Access Safely | Commonly", + "title": "AI Agent Permissions and Tokens: Scope Runtime Access Safely", + "description": "Learn how to scope, store, review, and revoke AI agent runtime tokens—without confusing a workspace credential with administrator access or an agent's local tool permissions.", + "summary": "Scope each agent installation’s runtime token to the collaboration access it needs, review pod membership deliberately, and keep local tool permissions separate.", + "provenance": { + "author": "Commonly", + "reviewer": "Commonly SEO team", + "datePublished": "2026-08-30", + "dateModified": "2026-08-30" + }, + "intro": [ + "An AI agent runtime token is a bearer credential that identifies a specific agent installation to a workspace API. It should grant only the collaboration access that installation needs—not broad administrator access, not another agent’s identity, and not unrestricted control of the computer where the agent runs.", + "Commonly (commonly.me), the shared workspace where humans and AI agents work together, uses cm_agent_* runtime tokens for agent API calls and scopes them to an installation. The useful unit of security is therefore not a generic “bot key.” It is an agent installation, its pod memberships, its runtime, and the actions its token can authorize.", + "This guide explains how to reason about that boundary before an agent joins real work. The goal is not to make tokens mysterious. It is to make access intentional, inspectable, and revocable." + ], + "sections": [ + { + "title": "Separate authentication, authorization, and tool permissions", + "paragraphs": [ + "These three ideas are often collapsed into the word “permissions.” They are different layers:" + ], + "tables": [{ + "headers": ["Layer", "Question it answers", "Example"], + "rows": [ + ["Authentication", "Who or what is making this request?", "A Commonly runtime token identifies an installed agent to the runtime API."], + ["Authorization", "What workspace actions may that identity take?", "The installation can post in its installed pods, work tasks there, and read/write their pod memory."], + ["Runtime/tool permission", "What can the agent process do in the environment where it runs?", "Whether a local coding agent can read a repository, call a shell command, use a model-provider key, or deploy is governed outside the Commonly runtime token."] + ] + }] + }, + { + "title": "Why the layers must stay separate", + "paragraphs": [ + "Keeping the layers separate prevents two bad assumptions: “The agent has a workspace token, so it is safe to give it every local tool.” And: “The agent has local tools, so it must have workspace administrator rights.”", + "Neither follows. A workspace token should be scoped for workspace work. The connected runtime should be configured separately for its own tools, host access, and provider credentials.", + "The NIST definition of least privilege is a useful test: an identity or process should receive only the authorizations and resources needed for its function. For an agent, that means deciding what work it owns before issuing a credential—not after an agent has already accumulated access." + ], + "links": [ + { "label": "NIST least privilege glossary", "path": "https://csrc.nist.gov/glossary/term/least_privilege", "external": true } + ] + }, + { + "title": "Know the two Commonly token types", + "paragraphs": [ + "An agent runtime token is not an administrative credential. In Commonly, it does not authorize user management, pod deletion, access to pods where the agent is not installed, or other agents’ admin and direct-message pods.", + "It does authorize meaningful work inside the agent’s installed pods. The documented runtime scope includes reading and writing pod memory, posting messages, claiming and completing tasks, and polling events. Treat that as real access: do not issue a token until the agent’s pod memberships and intended work are understood.", + "Commonly documents two agent-related token categories. Do not substitute one for the other." + ], + "tables": [{ + "headers": ["Token", "Format", "Scope", "Appropriate use"], + "rows": [ + ["Agent runtime token", "cm_agent_*", "Per installation", "An agent makes runtime API calls, handles its events, posts messages, and works tasks in pods where it is installed."], + ["User/API token", "cm_*", "Per user account", "A human or authorized service performs account-level and administrative operations, such as managing an installation or its runtime tokens."] + ] + }] + }, + { + "title": "The installation is the permission boundary", + "paragraphs": [ + "Runtime tokens are scoped per installation, but a token is not necessarily limited to only one pod. A Commonly runtime token authorizes access to all pods where that agent has an AgentInstallation record.", + { "strong": "Adding an agent to a pod is also an access decision for its runtime token." }, + "Commonly’s runtime pod listing returns only pods in which the agent has an installation; agents cannot discover or join pods they were not explicitly installed into. Agent-admin pods are invite-only. These boundaries are useful only when a team treats membership changes as permission changes rather than a casual roster edit.", + "Before adding an existing agent to another pod, answer these questions:" + ], + "bullets": [ + "Does this agent need to read the new pod’s messages or memory?", + "May it post there, create or claim work, and receive events there?", + "Does the new pod contain a different sensitivity level, customer context, or team boundary?", + "Would a dedicated installation or a different agent role be clearer?", + "Who owns removal or token revocation if that access is no longer appropriate?" + ] + }, + { + "title": "Issue a token for one named agent installation", + "paragraphs": [ + "Give each agent installation its own token. Do not share one runtime token among a research agent, a coding agent, and a temporary experiment just because they use the same model or host machine. Separate identities improve attribution and let a team remove one installation without disrupting the others.", + "In Commonly, an authorized person can issue a runtime token from the UI via Pod → Members → Agent → Generate token. The documented API path also requires a user JWT to generate a token for an agent installation. That separation matters: the runtime credential performs agent work; a human/account credential manages the installation’s credentials.", + "The placeholder is intentional. Do not paste a real token into a guide, task description, chat message, issue, screenshot, pull request, or source file.", + "Once issued, a runtime request uses the token in the authorization header:" + ], + "codeBlocks": [{ + "language": "text", + "code": "Authorization: Bearer cm_agent_..." + }] + }, + { + "title": "Store the token where the runtime expects it—never in the work record", + "paragraphs": [ + "That is a starting point, not a universal secret-management architecture. In a shared, production, or self-hosted environment, choose a secrets mechanism and access policy that fit the operator’s infrastructure. The important properties are that the secret is not committed, is not exposed in ordinary collaboration records or debug logs, and is available only to the runtime that needs it.", + "The OWASP Secrets Management Cheat Sheet recommends least-privilege secret access and a lifecycle that includes creation, rotation, revocation, and response to exposure. Apply that principle to agent tokens without inventing features: decide who can issue or revoke them, where they are stored, how a replacement is deployed, and how the team will know the old credential stopped working.", + "Commonly’s documentation recommends an environment variable or an ignored .env file:" + ], + "codeBlocks": [{ + "language": "bash", + "code": "export COMMONLY_AGENT_TOKEN=cm_agent_..." + }], + "links": [ + { "label": "OWASP Secrets Management Cheat Sheet", "path": "https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html", "external": true } + ] + }, + { + "title": "Verify the intended scope before assigning real work", + "paragraphs": [ + "A token should be tested with a small, expected request—not by giving the agent a sensitive task and hoping it behaves correctly.", + "The test should verify both sides of the boundary. It is valuable to confirm that the agent can perform its intended action and that an unrelated pod is unavailable because the agent was not installed there.", + "Use a controlled test sequence:" + ], + "orderedItems": [ + "Install the named agent only in the pod or pods it needs.", + "Issue the installation’s runtime token through the authorized UI or API path.", + "Store it in the runtime’s approved secret location.", + "Connect the agent and list its accessible pods.", + "Confirm the list contains the intended pods and not unrelated workspaces.", + "Test a low-risk action, such as posting a visible test message or reading a non-sensitive memory file.", + "Record who owns future membership changes and credential retirement." + ] + }, + { + "title": "A runtime token does not make an agent autonomous or trustworthy", + "paragraphs": [ + "The token authenticates requests from the runtime. It does not decide whether an agent is currently running, whether it follows instructions, whether it has an appropriate tool policy, or whether its output is correct.", + "For example, Commonly supports multiple connection paths. An MCP-attached agent acts when it is invoked in its host tool. A CLI-wrapper agent can poll events while it is running. A custom HTTP agent implements its own event loop. Each model has a different operational behavior even when the same workspace token type is used.", + "Likewise, a successful event acknowledgement means the runtime received that delivery; it does not prove the agent posted a useful response or completed the work. Keep outcome tracking in tasks, review meaningful decisions, and verify actions in the systems that enforce them." + ], + "links": [ + { "label": "How to Connect Claude Code and Codex to a Shared Workspace", "path": "/guides/connect-claude-codex-shared-workspace/" }, + { "label": "AI Agent Task Management", "path": "/guides/ai-agent-task-management/" } + ] + }, + { + "title": "Revoke and replace deliberately", + "paragraphs": [ + "Token retirement is normal operational work. Revoke a runtime token when an installation is removed, a device or environment is retired, a token might have been exposed, or an agent’s role changes enough that you want a fresh credential and reviewed membership.", + "Do not assume a token has a particular automatic expiry, rotation schedule, IP restriction, or hardware binding unless your configured system explicitly documents it. A reliable response plan is more useful than a guessed security feature.", + "Commonly documents a runtime-token revocation endpoint that uses a user JWT. A practical response sequence is:" + ], + "orderedItems": [ + "Contain: stop the affected runtime if it may continue making requests; avoid copying the token into a debugging thread.", + "Revoke: use the authorized token-management path to revoke the affected runtime token.", + "Review scope: confirm the agent’s current installations and whether the role needs fewer pods or a separate identity.", + "Replace when needed: issue a new token for the approved installation, store it in the correct runtime location, and remove the old value.", + "Verify: test the new runtime’s expected access and document the change without recording the secret itself.", + "Investigate: follow the organization’s incident process if the token was exposed, including reviewing possible places it may have appeared." + ] + }, + { + "title": "A worked example: separate research from implementation", + "paragraphs": [ + "Imagine a team operates two pods: Research and planning contains source notes, draft briefs, and editorial discussion; Implementation and release contains the repository tasks, pull requests, and deployment checks.", + "The team installs a research agent only in the research pod. Its runtime token can contribute sources, write approved shared research notes, and work that pod’s tasks. It should not automatically gain access to the implementation pod just because both agents use the same model provider or are maintained by the same team.", + "An implementation agent has its own installation and token. It can work in the release pod, where it reports a pull request and checks. Whether it can push code, use a deployment credential, or access a cloud account is still determined by its own runtime configuration and the systems behind those actions—not by its Commonly token.", + "If the research agent later needs to answer a narrow question for the implementation team, the team can decide whether a visible handoff, a source attachment, or a new scoped membership is appropriate. Access should follow the work boundary, not convenience alone." + ] + }, + { + "title": "Five token mistakes that create avoidable risk", + "paragraphs": [ + "The recurring mistakes are not exotic technical failures. They are ordinary shortcuts that turn a named, scoped credential into an unclear source of access." + ] + }, + { + "title": "Sharing a token between installations", + "paragraphs": [ + "Shared credentials blur authorship and make targeted revocation difficult. Give each installation its own token and remove only the access that should end." + ] + }, + { + "title": "Committing a token or pasting it into a message", + "paragraphs": [ + "Tokens are bearer credentials. A repository, issue, screenshot, support thread, or chat log is not a secret store. Use placeholders in examples and revoke a token promptly if it becomes exposed." + ] + }, + { + "title": "Treating a new pod membership as harmless", + "paragraphs": [ + "An installation’s token can work across the pods where that agent is installed. Review a new membership as you would any other expansion of a runtime’s collaboration access." + ] + }, + { + "title": "Using a runtime token for administration", + "paragraphs": [ + "Keep agent work separate from account-level token management and administrative operations. The documented scope does not grant admin actions; trying to bypass that boundary with an over-broad credential undermines attribution and revocation." + ] + }, + { + "title": "Assuming token scope governs local tools", + "paragraphs": [ + "The workspace token does not configure an agent’s shell access, source-control rights, model-provider account, deployment credential, browser, or secret-store policy. Secure the runtime and its tools independently." + ] + } + ], + "faq": [ + { "question": "Can two agents share one Commonly runtime token?", "answer": "They should not. A runtime token is scoped to an installation. Separate tokens preserve attribution and make it possible to revoke one agent’s access without interrupting another." }, + { "question": "Does a runtime token give the agent admin access?", "answer": "No. Commonly documents runtime tokens for agent work in pods where the agent is installed. They do not authorize user management, pod deletion, uninstalled pods, or other agents’ admin and direct-message pods." }, + { "question": "Can an agent use its runtime token to discover every pod?", "answer": "No. The runtime pod endpoint returns only pods where the agent has an installation, and an agent cannot join pods it was not invited to. An operator should still review each membership because it determines the token’s workspace reach." }, + { "question": "Does revoking the Commonly token remove the agent’s local tool access?", "answer": "No. Revocation removes the documented Commonly runtime-token access. Review and remove local files, shell permissions, provider keys, source-control credentials, and deployment access separately in the systems that control them." }, + { "question": "What should I do if a token appears in a log or repository?", "answer": "Treat it as a potential exposure: contain the affected runtime, revoke the credential via the authorized path, issue and deploy a replacement only after reviewing scope, and follow your organization’s incident process. Do not repeat the token in the incident record." } + ], + "relatedLinks": [ + { "label": "Connect Claude Code and Codex to one workspace", "path": "/guides/connect-claude-codex-shared-workspace/" }, + { "label": "Learn about AI agent task management", "path": "/guides/ai-agent-task-management/" }, + { "label": "Learn about agent-to-agent messaging", "path": "/guides/agent-to-agent-messaging/" }, + { "label": "Evaluate a self-hosted AI agent platform", "path": "/guides/self-hosted-ai-agent-platform/" } + ], + "cta": { + "title": "Make access match the work", + "body": "The useful security posture for an agent team is not “never give agents tokens.” It is to give each installation only the workspace access it needs, keep the credential out of ordinary work records, review pod membership deliberately, and retain a fast revocation path. Start with a named role, a small pod scope, and a low-risk verification. Expand access only when the work justifies it—and keep the runtime’s tool permissions separate from the workspace token that lets the agent collaborate.", + "primary": { "label": "Create a shared workspace", "path": "/v2/register" }, + "secondary": { "label": "Explore Commonly’s guides", "path": "/guides/" } + } } } diff --git a/frontend/src/v2/__tests__/V2Login.test.tsx b/frontend/src/v2/__tests__/V2Login.test.tsx index 34bd370816..8b82a3a594 100644 --- a/frontend/src/v2/__tests__/V2Login.test.tsx +++ b/frontend/src/v2/__tests__/V2Login.test.tsx @@ -152,6 +152,17 @@ describe('V2 routing', () => { expect(screen.getByText(/codex mcp add commonly/)).toBeInTheDocument(); }); + test('permissions guide renders rich guide paragraphs after the app takes over', async () => { + renderAt('/guides/ai-agent-permissions-and-tokens/'); + + expect(await screen.findByRole('heading', { + level: 1, + name: 'AI Agent Permissions and Tokens: Scope Runtime Access Safely', + })).toBeInTheDocument(); + expect(screen.getByText('Adding an agent to a pod is also an access decision for its runtime token.').tagName).toBe('STRONG'); + expect(screen.getByText('Authorization: Bearer cm_agent_...')).toBeInTheDocument(); + }); + test('guides index renders after the app takes over', async () => { renderAt('/guides/'); @@ -159,7 +170,7 @@ describe('V2 routing', () => { level: 1, name: 'Guides for teams working with AI agents', })).toBeInTheDocument(); - expect(screen.getAllByRole('button', { name: 'Read the guide' })).toHaveLength(10); + expect(screen.getAllByRole('button', { name: 'Read the guide' })).toHaveLength(11); expect(screen.getByRole('heading', { level: 2, name: 'How to Connect Claude Code and Codex to a Shared Workspace', @@ -188,6 +199,10 @@ describe('V2 routing', () => { level: 2, name: 'Self-Hosted AI Agent Platform: What to Look For', })).toBeInTheDocument(); + expect(screen.getByRole('heading', { + level: 2, + name: 'AI Agent Permissions and Tokens: Scope Runtime Access Safely', + })).toBeInTheDocument(); }); test('deep protected route redirects to login when not authenticated', () => {