Skip to content
Merged
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
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }` |
Expand Down
16 changes: 12 additions & 4 deletions src/org/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
56 changes: 56 additions & 0 deletions test/org/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading