Skip to content

feat(builder): add cedar builder in js#137

Open
lizradway wants to merge 1 commit into
cedar-policy:mainfrom
lizradway:builder
Open

feat(builder): add cedar builder in js#137
lizradway wants to merge 1 commit into
cedar-policy:mainfrom
lizradway:builder

Conversation

@lizradway

@lizradway lizradway commented Jun 12, 2026

Copy link
Copy Markdown

Description of changes

Adds js/cedar-agent-policy-builder/ — a TypeScript library that generates Cedar policies, entities, and schemas for agent authorization from declarative configuration. Integrates with @cedar-policy/mcp-schema-generator-wasm for schema generation and @cedar-policy/cedar-wasm for policy validation.

Usage

Builder API:

import { CedarAgentPolicyBuilder } from 'cedar-agent-policy-builder'

const { policies, entities, schema } = new CedarAgentPolicyBuilder()
  // Identity: resolve principal from invocationState.user_id as type "User"
  .principal({ key: 'user_id', type: 'User' })
  // Roles: admins can use all tools, analysts can only use search and query_database
  .role('admin', ['*'])
  .role('analyst', ['search', 'query_database'])
  // Restriction: analysts can only query the analytics or reporting databases
  .restrict('query_database', { allowedValues: { database: ['analytics', 'reporting'] } })
  // Rate limit: max 3 send_email calls per session
  .rateLimit('send_email', 3)
  // Time window: tools only allowed between 9am-5pm UTC
  .timeWindow({ hourStart: 9, hourEnd: 17 })
  // Environment denial: deny delete_record in production
  .denyToolsInEnv('production', ['delete_record'])
  // Consent: send_email and delete_file require human approval before executing
  .consent(['send_email', 'delete_file'])
  // Tools: MCP tool definitions for Cedar schema generation (enables build-time validation)
  .tools([
    { name: 'search', inputSchema: { type: 'object', properties: { query: { type: 'string' } }, required: ['query'] } },
    { name: 'query_database', inputSchema: { type: 'object', properties: { database: { type: 'string' }, query: { type: 'string' } }, required: ['database', 'query'] } },
  ])
  .build()

// policies: Generated Cedar permit/forbid rules as a string
// entities: Cedar entities array (Role entities + McpServer resource entity)
// schema: Cedar schema generated from tool inputSchemas (for build-time policy validation)

From config (JSON/object):

import { fromConfig } from 'cedar-agent-policy-builder'

const { policies, entities } = fromConfig({
  principal: { key: 'user_id', type: 'User' },
  roles: { admin: ['*'], analyst: ['search', 'query_database'] },
  restrictions: { query_database: { allowedValues: { database: ['analytics', 'reporting'] } } },
  rateLimits: { send_email: 3 },
  consent: { send_email: [], delete_file: [] },
})

.tools() accepts MCP tool definitions (framework-agnostic):

// From any MCP server's list_tools response:
.tools(mcpServer.listTools().tools)

// From Strands tool specs:
.tools(allTools.map(t => t.spec))

// From OpenAI Agents:
.tools(myTools.map(t => ({ name: t.name, inputSchema: t.parameters })))

Full Strands integration (with CedarAuthorization intervention handler):

import { Agent, tool } from '@strands-agents/sdk'
import { CedarAuthorization } from '@strands-agents/sdk/vended-interventions/cedar'
import { CedarAgentPolicyBuilder } from 'cedar-agent-policy-builder'
import { z } from 'zod'

const searchTool = tool({
  name: 'search',
  description: 'Search for information',
  inputSchema: z.object({ query: z.string() }),
  callback: (input) => `Results for: ${input.query}`,
})

const queryDatabase = tool({
  name: 'query_database',
  description: 'Query a database',
  inputSchema: z.object({ database: z.string(), query: z.string() }),
  callback: (input) => `[${input.database}] ${input.query} → 42 rows`,
})

const allTools = [searchTool, queryDatabase]

// Generate policies from declarative config, schema from tool definitions
const { policies, schema } = new CedarAgentPolicyBuilder()
  .principal({ key: 'user_id', type: 'User' })
  .role('admin', ['*'])
  .role('analyst', ['search', 'query_database'])
  .restrict('query_database', { allowedValues: { database: ['analytics', 'reporting'] } })
  .tools(allTools.map(t => t.spec))
  .build()

// CedarAuthorization evaluates the generated policies at runtime
const cedar = new CedarAuthorization({
  policies,
  schema,
  contextEnricher: ({ invocationState }) => ({
    role: String(invocationState.role ?? 'none'),
  }),
})

const agent = new Agent({
  tools: allTools,
  interventions: [cedar],
})

await agent.invoke('Search for reports', { invocationState: { user_id: 'bob', role: 'analyst' } })
// → allowed

await agent.invoke('Query the secrets database', { invocationState: { user_id: 'bob', role: 'analyst' } })
// → denied (analyst restricted to analytics/reporting)

How it integrates with the MCP schema generator:

.tools() definitions ──────────┐
                               ├──► @cedar-policy/mcp-schema-generator-wasm ──► Cedar schema
.principal()/.resource() ──────┘                                                     │
                                                                                     ▼
.role()/.restrict()/.consent() ──► Policy generator ──► Cedar policies ──► validate(policies, schema)
                                                                            catches typos/type errors
                                                                            at build time

Issue #, if available

N/A — supporting the Cedar authorization and Intervention Primitive design proposals:

Checklist for requesting a review

The change in this PR is:

  • A backwards-compatible change requiring a minor version bump to any crates in this repository (e.g., addition of a new API).
    • cedar-agent-policy-builder (new package)

I confirm that this PR:

  • Updates the "Unreleased" section of the CHANGELOG with a description of my change (required for major/minor version bumps).

Additional Context

  • Aligns with cedar-policy-mcp-schema-generator semantics: actions named directly after tools, context.input.* for tool arguments, context.session.* for runtime state
  • Uses @cedar-policy/mcp-schema-generator-wasm for schema generation from MCP tool definitions
  • .tools() accepts the standard MCP tool definition format ({ name, inputSchema }) — works with any framework (Strands, OpenAI Agents, raw MCP servers)
  • 64 tests including adversarial injection tests and real Cedar evaluation via @cedar-policy/cedar-wasm
  • Build-time warnings when tool names in restrict/consent/denyToolsInEnv don't match declared roles

@lizradway lizradway force-pushed the builder branch 3 times, most recently from 5a52d2a to a50f346 Compare June 12, 2026 16:46
Adds js/cedar-agent-policy-builder/ — a TypeScript library that generates
Cedar policies, entities, and schemas for agent authorization from
declarative configuration.

Features:
- Fluent builder API: .role(), .restrict(), .rateLimit(), .timeWindow(),
  .denyToolsInEnv(), .consent(), .resource(), .namespace()
- fromConfig() for JSON/object-based configuration
- Schema generation via @cedar-policy/mcp-schema-generator-wasm integration
- McpServer resource entity generation (aligns with MCP schema generator)
- Consent-gated policies (permit when context.session.user_consent == true)
- Build-time warnings for undeclared tool references
- Edge case handling: empty allowedValues = deny, denyToolsInEnv() without
  tools = deny all, rateLimit(0) = always deny, timeWindow(n,n) = deny all

Tests: 64 tests including adversarial injection tests and real Cedar
evaluation via @cedar-policy/cedar-wasm.

Signed-off-by: Liz <91279165+lizradway@users.noreply.github.com>
@lizradway lizradway changed the title feat(builder): add cedar builder in ts feat(builder): add cedar builder in js Jun 12, 2026
@lizradway lizradway marked this pull request as ready for review June 12, 2026 16:49
@victornicolet victornicolet self-requested a review June 12, 2026 18:11

@victornicolet victornicolet left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, I have some comments.

We should also have a README.md with documentation and some CI on this package.

if (tools.includes('*')) {
// Deny all tools in this environment
policies.push(
`forbid(\n principal,\n action,\n resource\n) when { context.session.environment == "${escapeCedarString(env)}" };`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the policy checkcontext.session has environment here just to be safe? Same on other uses of context.sessions.<field name>.

if (forRole) {
this._config.consent[tool].push(forRole)
} else {
this._config.consent[tool] = ['*']

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that .consent([tool_foo], 'admin').consent(['tool_foo', 'tool_bar']) for example results in having consent for tool_foo for all roles. I think it's worth documenting (for this API and others) how permissions / deny get added/transformed when builder methods are chained (i.e. in this case, ommitting the role results in overwriting all previous consent instructions for a tool).

}
if (this._config.consent) {
for (const tool of Object.keys(this._config.consent)) referenced.add(tool)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this also add tool references in this._config.rateLimits?

@lizradway

Copy link
Copy Markdown
Author

cc: @victornicolet

We should also have a README.md with documentation and some CI on this package.

Sounds good! can add README.md, is it alright to add CI in a follow up PR on Monday to get an MVP through?

@github-actions

Copy link
Copy Markdown

Coverage Report

Head Commit: 6502909638658bfd9e8c011c99cb418b141a885a

Base Commit: 759b2b2812a780ece7c1a152810abd33baeaa87a

Download the full coverage report.

Coverage of Added or Modified Lines of Rust Code

Required coverage: 80.00%

Actual coverage: 100.00%

Status: PASSED ✅

Details
File Status Covered Coverage Missed Lines

Coverage of All Lines of Rust Code

Required coverage: 80.00%

Actual coverage: 90.91%

Status: PASSED ✅

Details
Package Status Covered Coverage Base Coverage
cedar-policy-mcp-schema-generator 🟢 2525/2740 92.15% 92.15%
cedar-policy-mcp-schema-generator-wasm 🟢 147/161 91.30% 91.30%
mcp-tools-sdk 🟢 1601/1799 88.99% 88.99%

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.

2 participants