Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 42 additions & 65 deletions docs/content/modeling/agents/task-based-authorization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
ProductName,
ProductNameFormat,
RelatedSection,
TupleViewer,
} from '@components/Docs';

# Modeling Task-Based Authorization for Agents
Expand Down Expand Up @@ -47,23 +48,13 @@ A `tool` represents a capability (e.g., `slack_send_message`), and a `tool_resou

For example, you can grant `task:1` access to send any Slack message, while restricting `task:2` to a specific channel:

```yaml
tuples:
# Any task can list Slack channels
- user: task:*
relation: can_call
object: tool:slack_list_channels

# task:1 can send any Slack message
- user: task:1
relation: can_call
object: tool:slack_send_message

# task:2 can only send messages to channel XGA14FG
- user: task:2
relation: can_call
object: tool_resource:slack_send_message/XGA14FG
```
<TupleViewer
tuples={[
{ user: 'task:*', relation: 'can_call', object: 'tool:slack_list_channels' },
{ user: 'task:1', relation: 'can_call', object: 'tool:slack_send_message' },
{ user: 'task:2', relation: 'can_call', object: 'tool_resource:slack_send_message/XGA14FG' },
]}
/>

When checking whether `task:2` can call `tool_resource:slack_send_message/XGA14FG`, send a [contextual tuple](../../interacting/contextual-tuples.mdx) linking the resource to its tool. This avoids storing a tuple for every channel — you provide the tool-to-resource relationship at query time.

Expand Down Expand Up @@ -103,11 +94,11 @@ type organization
type project
relations
define organization: [organization]

# relations for users
define owner: [user]
define member: [user]

# relations for tasks
define read: [task]
define write: [task]
Expand Down Expand Up @@ -168,21 +159,13 @@ The `can_call` relation accepts three types of assignments:

Each task is linked to its session and agent when created:

```yaml
tuples:
# Link task to its agent and session
- user: task:1
relation: task
object: agent:1
- user: task:1
relation: task
object: session:1

# Grant session-level access
- user: session:1#task
relation: can_call
object: tool:slack_send_message
```
<TupleViewer
tuples={[
{ user: 'task:1', relation: 'task', object: 'agent:1' },
{ user: 'task:1', relation: 'task', object: 'session:1' },
{ user: 'session:1#task', relation: 'can_call', object: 'tool:slack_send_message' },
]}
/>

## Expiration and call count

Expand All @@ -209,27 +192,24 @@ condition max_call_count(max_tool_calls: int, current_tool_count: int) {

The `expiration` condition grants access for a fixed duration from the grant time. The `max_call_count` condition limits how many times the tool can be called. When writing the tuple, you provide the condition parameters:

```yaml
tuples:
# task:1 can call the tool for 10 minutes
- user: task:1
relation: can_call
object: tool:slack_send_message
condition:
name: expiration
context:
grant_time: "2026-03-22T00:00:00Z"
grant_duration: 10m

# task:2 can call the tool up to 2 times
- user: task:2
relation: can_call
object: tool:slack_send_message
condition:
name: max_call_count
context:
max_tool_calls: 2
```
<TupleViewer
tuples={[
{
user: 'task:1',
relation: 'can_call',
object: 'tool:slack_send_message',
condition: { name: 'expiration', context: { grant_time: '2026-03-22T00:00:00Z', grant_duration: '10m' } },
},
]}
rightColumnTuples={[
{
user: 'task:2',
relation: 'can_call',
object: 'tool:slack_send_message',
condition: { name: 'max_call_count', context: { max_tool_calls: 2 } },
},
]}
/>

Comment thread
aaguiarz marked this conversation as resolved.
When checking access, pass the current time or current call count in the request context.

Expand All @@ -255,15 +235,12 @@ type tool

The `can_call` relation requires both that the task has been granted access **and** that the agent making the call is linked to the task. When the task is created, link it to its agent:

```yaml
tuples:
- user: task:1
relation: task
object: agent:1
- user: task:1
relation: can_call
object: tool:slack_send_message
```
<TupleViewer
tuples={[
{ user: 'task:1', relation: 'task', object: 'agent:1' },
{ user: 'task:1', relation: 'can_call', object: 'tool:slack_send_message' },
]}
/>

At check time, send a contextual tuple identifying the calling agent. If the agent is linked to the task, the check returns `true`:

Expand Down Expand Up @@ -336,6 +313,6 @@ Mapping user intent to the right set of permissions is an active area of researc
title: 'Contextual Tuples',
description: 'Learn how to use contextual tuples to send dynamic context at query time',
link: '../../interacting/contextual-tuples',
}
},
]}
/>
220 changes: 220 additions & 0 deletions src/components/Docs/SnippetViewer/TupleViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
/**
* TupleViewer — displays OpenFGA relationship tuples in a styled code block.
*
* - Renders tuples with column-aligned keys in italic muted color.
* - Exposes CSS hook classes so shared styles can control the block surface, keys, copy button, and responsive layout.
* - Copy button writes valid YAML list format to the clipboard.
* - Optional `rightColumnTuples` renders two columns via CSS grid with a mobile fallback.
*/
import React, { useMemo, useRef } from 'react';
import { usePrismTheme } from '@docusaurus/theme-common';
import { CodeBlockContextProvider, createCodeBlockMetadata } from '@docusaurus/theme-common/internal';
import CopyButton from '@theme/CodeBlock/Buttons/CopyButton';
import type { RelationshipCondition } from '../RelationshipTuples/Viewer';

Comment on lines +11 to +14
interface Tuple {
user: string;
relation: string;
object: string;
condition?: RelationshipCondition;
}

interface TupleViewerProps {
tuples: Tuple[];
rightColumnTuples?: Tuple[];
}

const PAD = 'condition'.length;
const INNER_PAD = 'context'.length;
const keyStyle: React.CSSProperties = { fontStyle: 'italic' };

function Key({ name, pad }: { name: string; pad: number }): JSX.Element {
return (
<span className="tuple-viewer__key" style={keyStyle}>
{name.padEnd(pad)}
</span>
);
}

function formatDisplayValue(value: unknown): string {
return typeof value === 'string' ? value : JSON.stringify(value);
}

function formatYamlValue(value: unknown): string {
if (value === null) {
return 'null';
}

if (typeof value === 'string') {
return JSON.stringify(value);
}

if (typeof value === 'number' || typeof value === 'boolean') {
return String(value);
}

return JSON.stringify(value);
}

function getTupleKey(tuple: Tuple): string {
return JSON.stringify([tuple.user, tuple.relation, tuple.object, tuple.condition ?? null]);
}

function TupleBlock({ tuple }: { tuple: Tuple }): JSX.Element {
const contextEntries = Object.entries(tuple.condition?.context ?? {});

return (
<div className="tuple-viewer__tuple-block">
<div>
<Key name="user" pad={PAD} /> : {tuple.user}
</div>
<div>
<Key name="relation" pad={PAD} /> : {tuple.relation}
</div>
<div>
<Key name="object" pad={PAD} /> : {tuple.object}
</div>
{tuple.condition && (
<>
<div>
<Key name="condition" pad={PAD} /> :
</div>
<div className="tuple-viewer__nested-block">
<div>
<Key name="name" pad={INNER_PAD} /> : {tuple.condition.name}
</div>
{contextEntries.length > 0 && (
<>
<div>
<Key name="context" pad={INNER_PAD} /> :
</div>
<div className="tuple-viewer__nested-block tuple-viewer__context-block">
{contextEntries.map(([key, value]) => (
<div key={key}>
<Key name={key} pad={0} /> : {formatDisplayValue(value)}
</div>
))}
</div>
</>
)}
</div>
</>
)}
</div>
);
}

function TupleList({ tuples }: { tuples: Tuple[] }): JSX.Element {
const seenTuples = new Map<string, number>();

return (
<>
{tuples.map((tuple) => {
const baseKey = getTupleKey(tuple);
const count = seenTuples.get(baseKey) ?? 0;
seenTuples.set(baseKey, count + 1);
const tupleKey = count === 0 ? baseKey : `${baseKey}:${count}`;

return <TupleBlock key={tupleKey} tuple={tuple} />;
})}
</>
);
}

function formatYaml(tuple: Tuple): string {
const lines = [`- user: ${tuple.user}`, ` relation: ${tuple.relation}`, ` object: ${tuple.object}`];

if (tuple.condition) {
lines.push(` condition:`);
lines.push(` name: ${tuple.condition.name}`);

Comment thread
aaguiarz marked this conversation as resolved.
const contextEntries = Object.entries(tuple.condition.context ?? {});
if (contextEntries.length > 0) {
lines.push(` context:`);
}

for (const [key, value] of contextEntries) {
lines.push(` ${key}: ${formatYamlValue(value)}`);
}
}
Comment on lines +124 to +139

return lines.join('\n');
}

function TupleViewerCopyButton({ text }: { text: string }): JSX.Element {
const codeBlockRef = useRef<HTMLPreElement>(null);
const metadata = useMemo(
() =>
createCodeBlockMetadata({
code: text,
className: 'language-yaml',
language: 'yaml',
defaultLanguage: undefined,
metastring: undefined,
magicComments: [],
title: null,
showLineNumbers: false,
}),
[text],
);

return (
<CodeBlockContextProvider
metadata={metadata}
wordWrap={{
codeBlockRef,
isEnabled: false,
isCodeScrollable: false,
toggle: () => undefined,
}}
>
<div className="tuple-viewer__button-group">
<CopyButton className="tuple-viewer__copy-button" />
</div>
</CodeBlockContextProvider>
);
}

export function TupleViewer({ tuples, rightColumnTuples }: TupleViewerProps): JSX.Element {
const { plain } = usePrismTheme();
const allTuples = rightColumnTuples ? [...tuples, ...rightColumnTuples] : tuples;
const yaml = allTuples.map(formatYaml).join('\n');

return (
<div
className="theme-code-block tuple-viewer"
style={{
position: 'relative',
marginBottom: 'var(--ifm-leading)',
color: plain.color,
borderRadius: 'var(--ifm-code-border-radius)',
}}
>
<div
className="tuple-viewer__body"
style={{
padding: 'var(--ifm-pre-padding)',
fontFamily: 'var(--ifm-font-family-monospace)',
fontSize: 'var(--ifm-code-font-size)',
lineHeight: 'var(--ifm-pre-line-height)',
overflow: 'auto',
whiteSpace: 'pre',
}}
>
{rightColumnTuples ? (
<div className="tuple-viewer__grid">
<div className="tuple-viewer__column">
<TupleList tuples={tuples} />
</div>
<div className="tuple-viewer__column tuple-viewer__column--secondary">
<TupleList tuples={rightColumnTuples} />
</div>
</div>
) : (
<TupleList tuples={tuples} />
)}
</div>
<TupleViewerCopyButton text={yaml} />
</div>
);
}
1 change: 1 addition & 0 deletions src/components/Docs/SnippetViewer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export { SupportedLanguage, languageLabelMap } from './SupportedLanguage';
export * from './StreamedListObjectsRequestViewer';
export * from './WriteRequestViewer';
export * from './WriteAuthzModelViewer';
export * from './TupleViewer';
Loading
Loading