Skip to content

feat: RequestGenerator WASM bindings for Cedar authorization requests#73

Merged
victornicolet merged 5 commits intocedar-policy:mainfrom
tomjwxf:feat/request-generator-wasm
Apr 20, 2026
Merged

feat: RequestGenerator WASM bindings for Cedar authorization requests#73
victornicolet merged 5 commits intocedar-policy:mainfrom
tomjwxf:feat/request-generator-wasm

Conversation

@tomjwxf
Copy link
Copy Markdown
Contributor

@tomjwxf tomjwxf commented Apr 16, 2026

Summary

Addresses #72. Follow-up to #64 (merged). Per Victor's guidance: single crate, sync-only, camelCase.

Extends `cedar-policy-mcp-schema-generator-wasm` (the crate from #64) with request-generation bindings that let JS/TS agent hosts construct Cedar authorization requests from MCP tool-call inputs.

Together with #64 this gives the complete build-time + runtime WASM toolchain for Cedar-based agent authorization:

Step PR JS function
Schema generation #64 (merged) `generateSchema(stub, tools, config)`
Request generation This PR `generateRequest(stub, tools, input, ...)`
Authorization `cedar-wasm` `isAuthorized(request, schema, policies)`

All three steps now run in any JS/TS environment via WASM without a Rust toolchain.

Changes

Generator crate (`cedar-policy-mcp-schema-generator`)

  • `AuthorizationComponents` struct — serializable `{ principal, action, resource, entitiesJson }` from a Cedar authorization request
  • `RequestGenerator::get_action_uid_string(tool_name)` — fully-qualified Cedar EntityUID string for a tool action
  • `RequestGenerator::generate_request_components(...)` — returns `AuthorizationComponents` ready for JSON serialization
  • Re-exports `AuthorizationComponents` from the crate root

WASM crate (`cedar-policy-mcp-schema-generator-wasm`)

  • `generateRequest(...)` — sync JS function exported via wasm-bindgen. Returns:
    ```json
    { "principal": "NS::User::"alice"", "action": "NS::Action::"read_file"",
    "resource": "NS::McpServer::"server1"", "entitiesJson": "[]",
    "error": null, "isOk": true }
    ```
  • 6 unit tests: basic happy path, invalid input, invalid stub, namespace qualification, entities JSON, error-field completeness

Design decisions per #72

Question Decision Rationale
Separate crate vs. combined Combined (extend existing wasm crate) Per Victor: "we already have all the functionality in the single crate"
Sync vs. async Sync Per Victor: "we can focus on adding only the synchronous API"
Naming convention camelCase Per Victor: "use camel case, just like cedar-wasm"
Testing pattern Match #64 wasm-bindgen-test pattern + generator-crate unit tests

Kept `cedar-policy-core` out of the WASM crate

Same pattern as #64: the WASM crate calls `SchemaGenerator::from_cedarschema_str_with_config()` and `new_request_generator()` from the generator crate. All Cedar-core parsing stays in the generator crate. The WASM crate only handles JSON serialization and the wasm-bindgen boundary.

Verification

Usage example

```js
const { generateSchema, generateRequest } = require('@cedar-policy/mcp-schema-generator-wasm');

// Build-time: generate schema from MCP tools
const schema = JSON.parse(generateSchema(stub, toolsJson, configJson));

// Runtime: generate authorization request from an MCP tool call
const req = JSON.parse(generateRequest(
stub, toolsJson, inputJson,
"User", "alice",
"McpServer", "server1"
));

// Pass to cedar-wasm for authorization
const decision = isAuthorized({
principal: req.principal,
action: req.action,
resource: req.resource,
context: { input: toolCallArgs },
schema: schema.schemaJson,
policies: myPolicies,
});
```

Happy to address any feedback — thanks for the fast turnaround on #72, Victor.

@github-actions
Copy link
Copy Markdown

Coverage Report

Head Commit: 9248ff49b7f1628ae179ee485f9322ef777c614d

Base Commit: af35ea58cb0d44beb21560cceab2eed4f9ea9ce0

Download the full coverage report.

Coverage of Added or Modified Lines of Rust Code

Required coverage: 80.00%

Actual coverage: 62.12%

Status: FAILED ❌

Details
File Status Covered Coverage Missed Lines
cedar-policy-mcp-schema-generator-wasm/src/lib.rs 🟢 74/85 87.06% 207, 211, 230-232, 234, 247, 251, 256, 288, 292
cedar-policy-mcp-schema-generator/src/generator/request.rs 🔴 8/47 17.02% 244-254, 257-271, 274-279, 281-287

Coverage of All Lines of Rust Code

Required coverage: 80.00%

Actual coverage: 90.28%

Status: PASSED ✅

Details
Package Status Covered Coverage Base Coverage
cedar-policy-mcp-schema-generator 🟢 1701/1856 91.65% 93.59%
cedar-policy-mcp-schema-generator-wasm 🟢 142/162 87.65% 88.31%
mcp-tools-sdk 🟢 1471/1653 88.99% 88.81%

@github-actions
Copy link
Copy Markdown

Coverage Report

Head Commit: cc56f120760dbb24aa66899f39189f36ac405fbe

Base Commit: af35ea58cb0d44beb21560cceab2eed4f9ea9ce0

Download the full coverage report.

Coverage of Added or Modified Lines of Rust Code

Required coverage: 80.00%

Actual coverage: 65.91%

Status: FAILED ❌

Details
File Status Covered Coverage Missed Lines
cedar-policy-mcp-schema-generator-wasm/src/lib.rs 🟢 79/85 92.94% 207, 211, 251, 256, 288, 292
cedar-policy-mcp-schema-generator/src/generator/request.rs 🔴 8/47 17.02% 244-254, 257-271, 274-279, 281-287

Coverage of All Lines of Rust Code

Required coverage: 80.00%

Actual coverage: 90.41%

Status: PASSED ✅

Details
Package Status Covered Coverage Base Coverage
cedar-policy-mcp-schema-generator 🟢 1701/1856 91.65% 93.59%
cedar-policy-mcp-schema-generator-wasm 🟢 147/162 90.74% 88.31%
mcp-tools-sdk 🟢 1471/1653 88.99% 88.81%

@github-actions
Copy link
Copy Markdown

Coverage Report

Head Commit: a6eb3f0a7b1748bb8833599e9a157d4d44d6f877

Base Commit: af35ea58cb0d44beb21560cceab2eed4f9ea9ce0

Download the full coverage report.

Coverage of Added or Modified Lines of Rust Code

Required coverage: 80.00%

Actual coverage: 95.45%

Status: PASSED ✅

Details
File Status Covered Coverage Missed Lines
cedar-policy-mcp-schema-generator-wasm/src/lib.rs 🟢 79/85 92.94% 207, 211, 251, 256, 288, 292
cedar-policy-mcp-schema-generator/src/generator/request.rs 🟢 47/47 100.00%

Coverage of All Lines of Rust Code

Required coverage: 80.00%

Actual coverage: 91.47%

Status: PASSED ✅

Details
Package Status Covered Coverage Base Coverage
cedar-policy-mcp-schema-generator 🟢 1740/1856 93.75% 93.59%
cedar-policy-mcp-schema-generator-wasm 🟢 147/162 90.74% 88.31%
mcp-tools-sdk 🟢 1471/1653 88.99% 88.81%

Copy link
Copy Markdown

@chaluli chaluli left a comment

Choose a reason for hiding this comment

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

Looks reasonable to me. Can you answer the questions I posed?

use smol_str::{SmolStr, ToSmolStr};
use uuid::Uuid;

/// Authorization request components as JSON-serializable strings, ready for
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Does this belong in the main request crate? Or should this instead be part of the wasm crate?

I would posit it's only necessary for the wasm crate; perhaps move this and the conversion from Cedar to this type to the wasm crate.

Copy link
Copy Markdown
Contributor Author

@tomjwxf tomjwxf Apr 17, 2026

Choose a reason for hiding this comment

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

Thanks, I thought about this carefully. I went with a slightly different fix that I think addresses the underlying concern better, keen to hear if you'd still prefer the move.

The real root cause was that the WASM crate was duplicating request-building logic (manual namespace extraction + format!-ed UIDs + hardcoded entities) instead of calling the generator crate's generate_request_components. That duplication is what introduced the entities bug in Q2.

Fix in be54abc:

  • Added RequestGenerator::generate_request_components_from_strings — a string-in / string-out convenience wrapper on the generator crate that builds namespaced EntityUIDs internally. This lets FFI consumers (WASM, future Python / C bindings) call the canonical pipeline without pulling cedar-policy-core as a direct dep.
  • WASM generate_request_inner is now a thin mapper: call the wrapper, destructure AuthorizationComponents, serialize to WasmRequestResult. All the parallel logic is gone.

So AuthorizationComponents now serves a broader purpose than just the WASM boundary, it's the FFI/serialization-friendly return type of the generator's convenience entry point, useful for any non-Rust consumer. Moving it to the WASM crate would mean the generator crate's public helper has nowhere to return its result from; we'd either duplicate the type or change the public signature to a bare tuple.

That said, if you'd still prefer the move (e.g., to keep the generator crate's public API purely Cedar-native, with FFI types in downstream crates), happy to do it as a follow-up - it's a mechanical split.

What do you think is best?

principal: Some(principal_str),
action: Some(action_str),
resource: Some(resource_str),
entities_json: Some("[]".to_string()),
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Why are you passing an empty set of entities instead of the ones produced by the request generator? What if the generator produced entities (e.g., if you use the default schema generator settings and your input data contains any nulls, floats, or objects.

Copy link
Copy Markdown
Contributor Author

@tomjwxf tomjwxf Apr 17, 2026

Choose a reason for hiding this comment

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

Thanks, this was a real bug. Hardcoding entities_json: "[]" silently dropped every entity the generator actually produces from inputs with nulls, floats, or nested objects (per your example).

Fixed in be54abc. The WASM crate now delegates to a new string-based wrapper on RequestGenerator , generate_request_components_from_strings,which builds correctly-namespaced EntityUIDs internally and calls the existing generate_request_components pipeline. Real entities flow through unchanged.

Added a WASM-level regression test (test_generate_request_entities_flow_through_from_generator) that parses entitiesJson as a JSON Value and asserts it's a real array, so a future reversion to a hardcoded string literal would be caught. Also added two generator-crate tests covering the new helper (happy path + invalid-type error surface). 240 workspace tests pass; coverage of modified lines stays >95%.

tomjwxf pushed a commit to tomjwxf/cedar-for-agents that referenced this pull request Apr 17, 2026
Addresses @chaluli's review feedback on cedar-policy#73:

Q2 — entities bug (correctness)
  The WASM crate was hardcoding entities_json: "[]", which silently
  dropped entities the generator actually produces from MCP tool-call
  inputs containing nulls, floats, or nested objects. Fixed by routing
  the WASM entry point through the generator crate's real request-
  components pipeline — entity data now flows through unchanged.

Q1 — API layering
  The old WASM code path duplicated namespace extraction and principal/
  resource UID construction via format! strings. Replaced with a new
  string-based convenience method on RequestGenerator —
  generate_request_components_from_strings — which takes plain strings
  and builds correctly-namespaced EntityUIDs internally. The WASM crate
  now stays free of cedar-policy-core as a direct dep AND avoids the
  parallel implementation. The generator crate's public API is the
  single source of truth for request construction; WASM is a thin
  JSON-serialization wrapper around it.

Tests
  - generator crate: +2 tests for the new string-based method
    (happy path + invalid principal type error surface).
  - WASM crate: +1 regression test that asserts entities_json parses
    as a real JSON array (not a hardcoded string literal) and that
    principal/action/resource are correctly namespaced against the
    schema.

All 237+3 = 240 workspace tests pass. cargo clippy --all-features
clean. cargo fmt clean.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
@tomjwxf
Copy link
Copy Markdown
Contributor Author

tomjwxf commented Apr 17, 2026

Pushed be54abc addressing both review comments. Summary:

Q2 (entities bug) fixed. WASM crate now delegates to the generator crate's real pipeline; real entity data flows through. Regression test (test_generate_request_entities_flow_through_from_generator) parses entitiesJson as a serde_json::Value and asserts it's a real array, so the old "[]" literal would now fail the test.

Q1 (AuthorizationComponents placement) answered inline. I went with a cleaner fix that eliminates the real duplication (the WASM crate was reimplementing namespace qualification) by adding a string-based wrapper on RequestGenerator. That makes AuthorizationComponents useful for any FFI consumer, not just WASM. Happy to still move it if you prefer, but wanted your read before reshuffling crate boundaries.

Verification

  • cargo test --workspace: 240 tests pass (237 + 3 new)
  • cargo clippy --all-features: clean (same command as CI)
  • cargo fmt: clean
  • Coverage on modified lines: stays >95%

Re-requesting review from @chaluli. Would also appreciate a second set of eyes, this is a direct follow-up to #72 and #64, all approved already, so pinging @victornicolet @john-h-kastner-aws who have context.

@github-actions
Copy link
Copy Markdown

Coverage Report

Head Commit: be54abca2544b9dd012a8efcdf95dba14020433a

Base Commit: af35ea58cb0d44beb21560cceab2eed4f9ea9ce0

Download the full coverage report.

Coverage of Added or Modified Lines of Rust Code

Required coverage: 80.00%

Actual coverage: 96.71%

Status: PASSED ✅

Details
File Status Covered Coverage Missed Lines
cedar-policy-mcp-schema-generator-wasm/src/lib.rs 🟢 74/79 93.67% 207, 211, 251, 256, 289
cedar-policy-mcp-schema-generator/src/generator/request.rs 🟢 73/73 100.00%

Coverage of All Lines of Rust Code

Required coverage: 80.00%

Actual coverage: 91.55%

Status: PASSED ✅

Details
Package Status Covered Coverage Base Coverage
cedar-policy-mcp-schema-generator 🟢 1766/1882 93.84% 93.59%
cedar-policy-mcp-schema-generator-wasm 🟢 142/156 91.03% 88.31%
mcp-tools-sdk 🟢 1471/1653 88.99% 88.81%

/// strings), `entitiesJson` (JSON array string), `error`, and `isOk` fields.
/// Generate a Cedar authorization request from an MCP tool call.
///
/// Takes all parameters as a single JSON string to keep the WASM boundary
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

What does this documentation relate to? This looks like a duplicate of l. 146 and following lines but with camelCase.

// crate doesn't need a direct `cedar-policy-core` dep) AND, critically,
// returns the real entity set produced by the generator — including
// entities derived from nulls, floats, and nested objects in the input.
// (Previously this function hardcoded `entities_json: "[]"`, which
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I don't think this comment is necessary, this is new code in the PR and doesn't fix previous code.

///
/// For example, if the schema namespace is `MyServer` and the tool name is
/// `read_file`, this returns `MyServer::Action::"read_file"`.
pub fn get_action_uid_string(&self, tool_name: &str) -> String {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Is this function used anywhere outside of testing code? If not, it would be better placed in the test module.

"s1",
None,
);
#[expect(clippy::expect_used, reason = "Test assertion")]
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I don't think this is necessary in test code.

let result: WasmRequestResult =
serde_json::from_str(&result_json).expect("Should parse result");
// This might succeed or fail depending on whether the schema generator
// requires a namespace. Either way, it exercises the code path.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This is a deterministic test, why are we questioning whether it might succeed or fail?

let config: SchemaGeneratorConfig = match config_json {
Some(json) if !json.is_empty() => {
let Ok(c) = serde_json::from_str::<WasmConfig>(json) else {
return req_err("Invalid config: failed to parse JSON".to_string());
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This should return a more detailed error, just like the schema generation.

let mut entities_buf = Vec::new();
result_entities
.write_to_json(&mut entities_buf)
.unwrap_or(());
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

This silences errors writing to json, we should return an error if write_to_json fails for some reason.

"Entities JSON should be present"
);
// Regression: `entities_json` must be the real generator output, not a
// hardcoded "[]". It must be a parseable JSON array.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The following lines should test that entities_json are the real generator output. Right now, a hardcoded [] would pass the assertion (it exists, it is proper json, and it is an array).

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Also maybe remove the next test or remove this one in favor of just thoroughly testing that the result matches the expected shapes.

]"#;

#[test]
fn test_generate_request_basic() {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

I think there are a lot of duplication in the tests, I would rather have fewer tests for the happy path but deeper assertions in one test.

tomjwxf pushed a commit to tomjwxf/cedar-for-agents that referenced this pull request Apr 17, 2026
Applies every point from the review on PR cedar-policy#73:

1. Remove duplicate / inconsistent doc block on `generateRequest`.
   The second block described the parameters as if they were a single
   JSON object, which contradicted the flat-argument signature.

2. Remove the explanatory "Previously this hardcoded [] -- see PR
   review" comment on the delegation call. The code is new to this PR;
   the comment belonged in the PR description, not the source.

3. Move `get_action_uid_string` out of the public API. It was only
   called from the two tests that tested it. Removing the method and
   its two standalone tests eliminates the dead export; namespace
   qualification of action UIDs stays covered by the full-pipeline
   tests against `generate_request_components`.

4. Delete every `#[expect(clippy::expect_used, reason = "Test
   assertion")]` (28 sites). Replace with one
   `#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]`
   per test module. Keeps test code noise-free.

5. Make `test_generate_request_no_namespace_schema` deterministic by
   removing the "might succeed or fail" branch. The Cedar schema parser
   at this version rejects unqualified `entity` declarations without a
   surrounding namespace block, so the test is retired; a comment
   records why and points at where namespace-absent coverage lives.

6. Surface the underlying `serde_json` error on invalid config, matching
   the schema-generation path (`"Invalid config: {}"` with the parser's
   own message) instead of the opaque `"failed to parse JSON"`.

7. Stop silencing `write_to_json` errors in
   `generate_request_components`. The call now uses `?` so a serializer
   failure surfaces as a `RequestGeneratorError` rather than producing
   a stale empty entity set. `String::from_utf8_lossy` handles the
   (unreachable in practice) non-UTF-8 path without an unwrap.

8. Strengthen the entities regression test. The previous version
   asserted `parsed.is_array()` and `starts_with('[')` / `ends_with(']')`,
   all of which a hardcoded `"[]"` would satisfy. The replacement test
   uses a tool input with a nested-object property plus a float that
   the schema generator must turn into a non-empty entity set, and
   asserts `!arr.is_empty()` with exact principal / action / resource
   UID equality. A regression to the old hardcoded behavior fails.

9. Consolidate duplicated happy-path tests. `test_generate_request_basic`,
   `test_generate_request_namespace_qualification`,
   `test_generate_request_action_contains_namespace_and_tool`, and
   `test_generate_request_with_none_config` all exercised subsets of
   the same success path with weaker assertions. Replaced by the single
   deeper `test_generate_request_entities_propagate_real_generator_output`.

Verification:
  cargo test   --workspace            -> all pass (237 generator-crate,
                                           44 wasm-crate, and adjacent)
  cargo clippy --workspace --all-features -> clean
  cargo fmt    --check                -> clean

Thanks @victornicolet for the careful review; each comment improved the
diff materially.
@tomjwxf
Copy link
Copy Markdown
Contributor Author

tomjwxf commented Apr 17, 2026

Thanks @victornicolet, every comment was on target. Pushed de4c995 addressing all of them. Fix-per-comment below:

1. Duplicate doc on generateRequest. Removed. The second block described the parameters as if they were a single JSON object, which contradicted the flat-argument signature; keeping one block only.

2. "Fix" explanation comment. Removed. You are right that it belonged in the PR description, not in the source alongside new code.

3. get_action_uid_string placement. Moved out of the public API entirely. It was only called from two tests that tested the method itself. I deleted the method and those two standalone tests; namespace qualification of action UIDs stays covered through the full generate_request_components pipeline tests. Net: one fewer public-API surface, no coverage lost.

4. #[expect(clippy::expect_used, reason = "Test assertion")] noise. Gone from every test (28 sites). Replaced with a single #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)] at the top of each test module.

5. Deterministic no-namespace test. Fixed. The Cedar schema parser at this version rejects unqualified entity declarations without a surrounding namespace { ... } block, so the test as written could never pass. Retired with a short comment pointing at where the namespace-absent code path is actually exercised.

6. Detailed config JSON parse error. Mirrored the schema-generation pattern:

return req_err(format!(
    "Invalid config: {}",
    serde_json::from_str::<serde_json::Value>(json)
        .err()
        .map_or_else(|| "unrecognized fields".to_string(), |e| e.to_string())
));

7. write_to_json silent failure. Removed. The call now uses ? to propagate EntitiesError as a RequestGeneratorError variant. Unreachable-in-practice UTF-8 path handled with String::from_utf8_lossy (no unwrap), since write_to_json is documented to produce valid UTF-8.

8. Entities regression test strength. Rewrote it. The new test_generate_request_entities_propagate_real_generator_output uses a tool input with a nested-object property (metadata: { source, region }) plus a float (score: 0.87) which the schema generator must turn into a non-empty entity set. The test now asserts:

assert_eq!(result.principal.as_deref(), Some(r#"TestServer::User::"alice""#));
assert_eq!(result.resource.as_deref(),  Some(r#"TestServer::McpServer::"s1""#));
assert!(result.action.as_deref().unwrap_or_default()
          .contains("TestServer::Action::\"ingest\""));
let arr = parsed.as_array().expect("entities_json should be a JSON array");
assert!(!arr.is_empty(), "...hardcoded \"[]\" would fail here...");

A regression to the old hardcoded entities_json: "[]" would fail on the !arr.is_empty() assertion directly, which was the gap you pointed at.

9. Happy-path test duplication. Consolidated. Deleted test_generate_request_basic, test_generate_request_namespace_qualification, test_generate_request_action_contains_namespace_and_tool, and test_generate_request_with_none_config (four duplicates, each asserting a subset of the same success path with weaker checks). The single deeper test_generate_request_entities_propagate_real_generator_output now covers all of it with exact-UID assertions and non-empty entities.

Verification

  • cargo test --workspace: all pass (237 generator-crate tests, 44 wasm-crate tests, plus adjacent)
  • cargo clippy --workspace --all-features: clean
  • cargo fmt --check: clean

Re-requesting review when you have a moment. Happy to iterate further on anything I mis-read.

@github-actions
Copy link
Copy Markdown

Coverage Report

Head Commit: de4c995b9d2615255644384a440ff2c72a52bcf9

Base Commit: af35ea58cb0d44beb21560cceab2eed4f9ea9ce0

Download the full coverage report.

Coverage of Added or Modified Lines of Rust Code

Required coverage: 80.00%

Actual coverage: 96.53%

Status: PASSED ✅

Details
File Status Covered Coverage Missed Lines
cedar-policy-mcp-schema-generator-wasm/src/lib.rs 🟢 77/82 93.90% 195, 199, 247, 252, 278
cedar-policy-mcp-schema-generator/src/generator/request.rs 🟢 62/62 100.00%

Coverage of All Lines of Rust Code

Required coverage: 80.00%

Actual coverage: 91.53%

Status: PASSED ✅

Details
Package Status Covered Coverage Base Coverage
cedar-policy-mcp-schema-generator 🟢 1755/1871 93.80% 93.59%
cedar-policy-mcp-schema-generator-wasm 🟢 145/159 91.19% 88.31%
mcp-tools-sdk 🟢 1471/1653 88.99% 88.81%

@victornicolet
Copy link
Copy Markdown

Thank you, can you also sign off the commits?

ZeroXLauren and others added 5 commits April 20, 2026 09:52
Addresses cedar-policy#72. Per Victor's guidance: single crate, sync-only, camelCase.

Extends `cedar-policy-mcp-schema-generator-wasm` (the crate from cedar-policy#64)
with request-generation bindings that let JS/TS agent hosts construct
Cedar authorization requests from MCP tool-call inputs.

## What this adds

### Generator crate (`cedar-policy-mcp-schema-generator`)

- `AuthorizationComponents` struct — serializable principal/action/
  resource/entities from a Cedar authorization request.
- `RequestGenerator::get_action_uid_string(tool_name)` — returns the
  fully-qualified Cedar EntityUID string for a tool action.
- `RequestGenerator::generate_request_components(input, principal,
  resource, context, entities, output)` — returns
  `AuthorizationComponents` ready for JSON serialization.

### WASM crate (`cedar-policy-mcp-schema-generator-wasm`)

- `generateRequest(schemaStub, toolsJson, inputJson, principalType,
  principalId, resourceType, resourceId, configJson?)` — sync JS
  function that returns `{ principal, action, resource, entitiesJson,
  error, isOk }` in camelCase matching cedar-wasm conventions.
- 6 unit tests (basic, invalid-input, invalid-stub, namespace
  qualification, entities JSON, error-field completeness).

## Why this matters

Together with cedar-policy#64 (schema generation), this gives the complete
build-time + runtime WASM toolchain for Cedar-based agent
authorization:

1. **Schema generation** (cedar-policy#64): MCP tool descriptions → Cedar schema
2. **Request generation** (this PR): MCP tool call → Cedar request
3. **Authorization**: pass request + schema to `cedar-wasm`
   `isAuthorized()` for the decision

All three steps now run in any JS/TS environment via WASM without
a Rust toolchain.

## Verification

- 237 tests passing (all workspace tests)
- cargo clippy clean (workspace lints, `too_many_arguments` expected)
- cargo fmt clean

Signed-off-by: tommylauren <tfarley@utexas.edu>
Addresses CI coverage check failure (62.12% -> target 80%).

WASM crate tests added (8):
- Invalid config JSON returns error
- Invalid tools JSON returns error
- Empty config string uses defaults
- Explicit config (numbersAsDecimal) passes through
- Multi-tool schema resolves correct action
- Resource ID appears in formatted EntityUID
- req_err helper produces correct error shape
- WasmRequestResult serializes with camelCase (isOk, entitiesJson)

Generator crate tests added (3):
- get_action_uid_string returns namespace-qualified action
- get_action_uid_string distinguishes multiple tools
- AuthorizationComponents is Clone + Debug with expected fields

312 tests passing, cargo clippy clean, cargo fmt clean.

Signed-off-by: tommylauren <tfarley@utexas.edu>
…ror paths

Addresses CI coverage check (65.91% -> target 80%).

Generator crate tests added (3):
- generate_request_components basic: exercises the full
  serialization pipeline (EntityUID -> String, Entities -> JSON)
- generate_request_components with output: covers the Output
  parameter path
- generate_request_components entities serialization: validates
  the JSON array output format

WASM crate tests added (4):
- No-namespace schema: exercises the None branch for namespace
  qualification (principal/resource without NS:: prefix)
- Explicit None config: confirms None config produces same results
  as omitted config
- Action format: verifies namespace + tool name in action string
- All error paths in generate_request_inner: exercises every early
  return (invalid config, invalid stub, invalid tools, invalid input,
  empty config string) in a single test function

319 tests passing, clippy clean, fmt clean.

Signed-off-by: tommylauren <tfarley@utexas.edu>
Addresses @chaluli's review feedback on cedar-policy#73:

Q2 — entities bug (correctness)
  The WASM crate was hardcoding entities_json: "[]", which silently
  dropped entities the generator actually produces from MCP tool-call
  inputs containing nulls, floats, or nested objects. Fixed by routing
  the WASM entry point through the generator crate's real request-
  components pipeline — entity data now flows through unchanged.

Q1 — API layering
  The old WASM code path duplicated namespace extraction and principal/
  resource UID construction via format! strings. Replaced with a new
  string-based convenience method on RequestGenerator —
  generate_request_components_from_strings — which takes plain strings
  and builds correctly-namespaced EntityUIDs internally. The WASM crate
  now stays free of cedar-policy-core as a direct dep AND avoids the
  parallel implementation. The generator crate's public API is the
  single source of truth for request construction; WASM is a thin
  JSON-serialization wrapper around it.

Tests
  - generator crate: +2 tests for the new string-based method
    (happy path + invalid principal type error surface).
  - WASM crate: +1 regression test that asserts entities_json parses
    as a real JSON array (not a hardcoded string literal) and that
    principal/action/resource are correctly namespaced against the
    schema.

All 237+3 = 240 workspace tests pass. cargo clippy --all-features
clean. cargo fmt clean.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
Signed-off-by: tommylauren <tfarley@utexas.edu>
Applies every point from the review on PR cedar-policy#73:

1. Remove duplicate / inconsistent doc block on `generateRequest`.
   The second block described the parameters as if they were a single
   JSON object, which contradicted the flat-argument signature.

2. Remove the explanatory "Previously this hardcoded [] -- see PR
   review" comment on the delegation call. The code is new to this PR;
   the comment belonged in the PR description, not the source.

3. Move `get_action_uid_string` out of the public API. It was only
   called from the two tests that tested it. Removing the method and
   its two standalone tests eliminates the dead export; namespace
   qualification of action UIDs stays covered by the full-pipeline
   tests against `generate_request_components`.

4. Delete every `#[expect(clippy::expect_used, reason = "Test
   assertion")]` (28 sites). Replace with one
   `#![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)]`
   per test module. Keeps test code noise-free.

5. Make `test_generate_request_no_namespace_schema` deterministic by
   removing the "might succeed or fail" branch. The Cedar schema parser
   at this version rejects unqualified `entity` declarations without a
   surrounding namespace block, so the test is retired; a comment
   records why and points at where namespace-absent coverage lives.

6. Surface the underlying `serde_json` error on invalid config, matching
   the schema-generation path (`"Invalid config: {}"` with the parser's
   own message) instead of the opaque `"failed to parse JSON"`.

7. Stop silencing `write_to_json` errors in
   `generate_request_components`. The call now uses `?` so a serializer
   failure surfaces as a `RequestGeneratorError` rather than producing
   a stale empty entity set. `String::from_utf8_lossy` handles the
   (unreachable in practice) non-UTF-8 path without an unwrap.

8. Strengthen the entities regression test. The previous version
   asserted `parsed.is_array()` and `starts_with('[')` / `ends_with(']')`,
   all of which a hardcoded `"[]"` would satisfy. The replacement test
   uses a tool input with a nested-object property plus a float that
   the schema generator must turn into a non-empty entity set, and
   asserts `!arr.is_empty()` with exact principal / action / resource
   UID equality. A regression to the old hardcoded behavior fails.

9. Consolidate duplicated happy-path tests. `test_generate_request_basic`,
   `test_generate_request_namespace_qualification`,
   `test_generate_request_action_contains_namespace_and_tool`, and
   `test_generate_request_with_none_config` all exercised subsets of
   the same success path with weaker assertions. Replaced by the single
   deeper `test_generate_request_entities_propagate_real_generator_output`.

Verification:
  cargo test   --workspace            -> all pass (237 generator-crate,
                                           44 wasm-crate, and adjacent)
  cargo clippy --workspace --all-features -> clean
  cargo fmt    --check                -> clean

Thanks @victornicolet for the careful review; each comment improved the
diff materially.

Signed-off-by: tommylauren <tfarley@utexas.edu>
@tomjwxf tomjwxf force-pushed the feat/request-generator-wasm branch from de4c995 to 1e776b7 Compare April 20, 2026 13:52
@tomjwxf
Copy link
Copy Markdown
Contributor Author

tomjwxf commented Apr 20, 2026

Damn sorry about that @victornicolet, should be good now! Btw, ready to go on #75 and happy to work through #76 with you if interested

@github-actions
Copy link
Copy Markdown

Coverage Report

Head Commit: 1e776b78cad2715313e3c997285e266b62fb3f3d

Base Commit: 34b211a22f1e88781a4a579131e4a1413214f7a3

Download the full coverage report.

Coverage of Added or Modified Lines of Rust Code

Required coverage: 80.00%

Actual coverage: 96.53%

Status: PASSED ✅

Details
File Status Covered Coverage Missed Lines
cedar-policy-mcp-schema-generator-wasm/src/lib.rs 🟢 77/82 93.90% 195, 199, 247, 252, 278
cedar-policy-mcp-schema-generator/src/generator/request.rs 🟢 62/62 100.00%

Coverage of All Lines of Rust Code

Required coverage: 80.00%

Actual coverage: 91.53%

Status: PASSED ✅

Details
Package Status Covered Coverage Base Coverage
cedar-policy-mcp-schema-generator 🟢 1755/1871 93.80% 93.59%
cedar-policy-mcp-schema-generator-wasm 🟢 145/159 91.19% 88.31%
mcp-tools-sdk 🟢 1471/1653 88.99% 88.81%

@victornicolet victornicolet merged commit 0fda046 into cedar-policy:main Apr 20, 2026
8 checks passed
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.

4 participants