From 147e250802901c68e3f1e52c5420a67d71c8490e Mon Sep 17 00:00:00 2001 From: Guy Ben Aharon Date: Sat, 4 Jul 2026 17:11:20 -0700 Subject: [PATCH] feat: mark OrgRule endpoint fields optional for app-permission rules Rule listings mix custom rules (which carry hostPattern/pathPattern/method) with app-permission rules, which omit those endpoint fields and are identified by metadata.provider + metadata.toolId. Type OrgRule accordingly, document the two kinds in the README, and pin the mixed-list decode in a test. Inputs (CreateOrgRuleInput/UpdateOrgRuleInput) are unchanged. Co-Authored-By: Claude Fable 5 --- README.md | 5 ++++ src/org/types.ts | 16 +++++++++--- test/org/client.test.ts | 56 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9bca212..320743d 100644 --- a/README.md +++ b/README.md @@ -415,6 +415,11 @@ await onecli.org.updateRule(rules[0].id, { enabled: false }); await onecli.org.deleteRule(rules[0].id); ``` +Rule listings mix two kinds of rules. Custom rules (like the one created +above) carry your `hostPattern`/`pathPattern`/`method`. App-permission rules +(managed through the app permissions surface) omit those endpoint fields and +are identified by `metadata.provider` + `metadata.toolId` instead. + | Method | Endpoint | Returns | |--------|----------|---------| | `connectApp(provider, input)` | `POST /v1/org/apps/{provider}/connect` | `{ success: boolean }` | diff --git a/src/org/types.ts b/src/org/types.ts index 24ec4d0..d05e336 100644 --- a/src/org/types.ts +++ b/src/org/types.ts @@ -49,13 +49,21 @@ export interface OrgRuleCondition { key?: string; } -/** An organization-scoped policy rule, applied to every agent in the org. */ +/** + * An organization-scoped policy rule, applied to every agent in the org. + * + * The endpoint fields (`hostPattern`/`pathPattern`/`method`) are present on + * custom (user-authored) rules only. App-permission rules — rows whose + * `metadata.source` is `"app_permission"` — omit them; those rules are + * identified by `metadata.provider` + `metadata.toolId` and managed via + * `PUT /v1/org/rules/permissions/{provider}`. + */ export interface OrgRule { id: string; name: string; - hostPattern: string; - pathPattern: string | null; - method: OrgRuleMethod | null; + hostPattern?: string; + pathPattern?: string | null; + method?: OrgRuleMethod | null; action: OrgRuleAction; enabled: boolean; rateLimit: number | null; diff --git a/test/org/client.test.ts b/test/org/client.test.ts index 9a8289a..fac1397 100644 --- a/test/org/client.test.ts +++ b/test/org/client.test.ts @@ -203,6 +203,62 @@ describe("OrgClient rules", () => { ); }); + it("decodes mixed rule listings: masked app-permission rules + custom rules", async () => { + vi.spyOn(globalThis, "fetch").mockResolvedValue( + new Response( + JSON.stringify([ + { + id: "app-1", + name: "Gmail: Send email", + action: "manual_approval", + enabled: true, + rateLimit: null, + rateLimitWindow: null, + scope: "organization", + metadata: { + source: "app_permission", + provider: "gmail", + toolId: "send_email", + }, + createdAt: "2026-07-04T00:00:00Z", + }, + { + id: "custom-1", + name: "Block deletes", + hostPattern: "api.example.com", + pathPattern: "/v1/*", + method: "DELETE", + action: "block", + enabled: true, + rateLimit: null, + rateLimitWindow: null, + scope: "organization", + metadata: null, + createdAt: "2026-07-04T00:00:00Z", + }, + ]), + { status: 200 }, + ), + ); + + const rules = await client().listRules(); + + const appRule = rules[0]!; + expect(appRule.hostPattern).toBeUndefined(); + expect(appRule.pathPattern).toBeUndefined(); + expect(appRule.method).toBeUndefined(); + expect(appRule.metadata).toEqual({ + source: "app_permission", + provider: "gmail", + toolId: "send_email", + }); + + const custom = rules[1]!; + expect(custom.hostPattern).toBe("api.example.com"); + expect(custom.pathPattern).toBe("/v1/*"); + expect(custom.method).toBe("DELETE"); + }); + it("lists, gets, updates, and deletes rules on the canonical paths", async () => { const fetchSpy = vi .spyOn(globalThis, "fetch")