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
67 changes: 50 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -402,17 +402,23 @@ const connections = await onecli.org.listConnections();
await onecli.org.renameConnection(connections[0].id, "prod");
await onecli.org.deleteConnection(connections[0].id);

// Org-wide rules (apply to every agent in the org)
await onecli.org.createRule({
// Org-wide policy rules (the policy engine — staged draft → publish).
// Writes publish automatically; pass { skipPublish: true } to stage and
// review first (a publish snapshots the WHOLE org draft, including changes
// staged by other users).
const { result: created } = await onecli.org.createPolicyRule({
name: "Block Gmail sends",
hostPattern: "gmail.googleapis.com",
pathPattern: "/gmail/v1/users/me/messages/send",
action: "block",
enabled: true,
targets: [{ kind: "app", provider: "gmail", tools: ["send_email"] }],
});
const rules = await onecli.org.listRules();
await onecli.org.updateRule(rules[0].id, { enabled: false });
await onecli.org.deleteRule(rules[0].id);
const draft = await onecli.org.listPolicyRules(); // "published" = enforced
await onecli.org.updatePolicyRule(created.id, { enabled: false });
await onecli.org.deletePolicyRule(created.id);
await onecli.org.publishPolicy(); // publish anything still staged

// Legacy org rules (deprecated): cloud deployments reject these writes with
// 410 Gone; pre-cutover self-hosted servers still accept them.
const legacyRules = await onecli.org.listRules();

// Watch manual-approval requests across every project in the org. Each request
// carries its own `projectId`, and the decision is routed back to that project.
Expand All @@ -426,10 +432,14 @@ const handle = onecli.org.configureManualApproval(
// handle.stop() when shutting down
```

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.
Policy rules carry structured `targets` (app / connection / secret /
network) and `identities` (org rules take `agentGroup`/`user`/`group`).
Rule responses use `PolicyRuleTarget` (arrays always present, unset scalars
`null`); inputs use `PolicyRuleTargetInput` (omit unused fields — the API
rejects `null`s on write). `updatePolicyRule` requires at least one field
(an empty input is rejected with 422). Legacy rule listings still mix
custom rows (with `hostPattern`/etc.) and read-only app-permission rows
identified by `metadata.provider` + `metadata.toolId`.

| Method | Endpoint | Returns |
|--------|----------|---------|
Expand All @@ -438,11 +448,20 @@ are identified by `metadata.provider` + `metadata.toolId` instead.
| `listConnections(provider?)` | `GET /v1/org/connections` | `OrgConnection[]` |
| `renameConnection(id, label)` | `PATCH /v1/org/connections/{id}` | `OrgConnection` |
| `deleteConnection(id)` | `DELETE /v1/org/connections/{id}` | `void` |
| `listRules()` | `GET /v1/org/rules` | `OrgRule[]` |
| `getRule(id)` | `GET /v1/org/rules/{id}` | `OrgRule` |
| `createRule(input)` | `POST /v1/org/rules` | `OrgRule` |
| `updateRule(id, input)` | `PATCH /v1/org/rules/{id}` | `{ success: boolean }` |
| `deleteRule(id)` | `DELETE /v1/org/rules/{id}` | `void` |
| `listPolicyRules(status?)` | `GET /v1/org/policy/rules` | `OrgPolicyRule[]` |
| `getPolicyRule(id)` | `GET /v1/org/policy/rules/{id}` | `OrgPolicyRule` |
| `createPolicyRule(input, opts?)` | `POST /v1/org/policy/rules` (+publish) | `PolicyWriteResult<OrgPolicyRule>` |
| `updatePolicyRule(id, input, opts?)` | `PATCH /v1/org/policy/rules/{id}` (+publish) | `PolicyWriteResult<OrgPolicyRule>` |
| `deletePolicyRule(id, opts?)` | `DELETE /v1/org/policy/rules/{id}` (+publish) | `PolicyWriteResult<null>` |
| `reorderPolicyRules(ids, opts?)` | `PUT /v1/org/policy/rules/order` (+publish) | `PolicyWriteResult<OrgPolicyRule[]>` |
| `getPolicyDefault(status?)` / `setPolicyDefault(action, opts?)` | `GET`/`PATCH /v1/org/policy/default` | `OrgPolicyRule` / `PolicyWriteResult<OrgPolicyRule>` |
| `publishPolicy()` | `POST /v1/org/policy/publish` | `PolicyPublishResult` |
| `getPolicyLastPublish()` | `GET /v1/org/policy/last-publish` | `PolicyLastPublish \| null` |
| `listRules()` *(legacy)* | `GET /v1/org/rules` | `OrgRule[]` |
| `getRule(id)` *(legacy)* | `GET /v1/org/rules/{id}` | `OrgRule` |
| `createRule(input)` *(deprecated — 410 on cloud)* | `POST /v1/org/rules` | `OrgRule` |
| `updateRule(id, input)` *(deprecated — 410 on cloud)* | `PATCH /v1/org/rules/{id}` | `{ success: boolean }` |
| `deleteRule(id)` *(deprecated — 410 on cloud)* | `DELETE /v1/org/rules/{id}` | `void` |
| `configureManualApproval(cb, options?)` | `GET /v1/org/approvals/pending` (long-poll) | `ManualApprovalHandle` |

---
Expand Down Expand Up @@ -476,6 +495,20 @@ import type {
OrgRuleCondition,
CreateOrgRuleInput,
UpdateOrgRuleInput,
OrgPolicyRule,
PolicyRuleAction,
PolicyRuleStatus,
PolicyRuleTarget,
PolicyRuleTargetInput,
PolicyRuleIdentity,
OrgPolicyRuleIdentityInput,
PolicyRuleConditionsInput,
CreateOrgPolicyRuleInput,
UpdateOrgPolicyRuleInput,
PolicyWriteOptions,
PolicyWriteResult,
PolicyPublishResult,
PolicyLastPublish,
} from "@onecli-sh/sdk";
```

Expand Down
14 changes: 14 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,18 @@ export type {
OrgRuleCondition,
CreateOrgRuleInput,
UpdateOrgRuleInput,
OrgPolicyRule,
PolicyRuleAction,
PolicyRuleStatus,
PolicyRuleIdentity,
OrgPolicyRuleIdentityInput,
PolicyRuleTarget,
PolicyRuleTargetInput,
PolicyRuleConditionsInput,
CreateOrgPolicyRuleInput,
UpdateOrgPolicyRuleInput,
PolicyWriteOptions,
PolicyWriteResult,
PolicyPublishResult,
PolicyLastPublish,
} from "./org/types.js";
179 changes: 179 additions & 0 deletions src/org/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,19 @@ import type {
} from "../approvals/types.js";
import type {
ConnectOrgAppInput,
CreateOrgPolicyRuleInput,
CreateOrgRuleInput,
GetOrgAuthorizeUrlOptions,
OrgConnection,
OrgPolicyRule,
OrgRule,
PolicyLastPublish,
PolicyPublishResult,
PolicyRuleAction,
PolicyRuleStatus,
PolicyWriteOptions,
PolicyWriteResult,
UpdateOrgPolicyRuleInput,
UpdateOrgRuleInput,
} from "./types.js";

Expand Down Expand Up @@ -258,6 +267,9 @@ export class OrgClient {

/**
* Create an organization rule.
*
* @deprecated Cloud deployments reject this write with 410 Gone — use
* {@link createPolicyRule}. Pre-cutover self-hosted servers still accept it.
*/
createRule = async (input: CreateOrgRuleInput): Promise<OrgRule> => {
return this.request<OrgRule>("POST", "/v1/org/rules", input);
Expand All @@ -266,6 +278,9 @@ export class OrgClient {
/**
* Update an organization rule. Nullable fields accept an explicit `null`
* to clear the stored value.
*
* @deprecated Cloud deployments reject this write with 410 Gone — use
* {@link updatePolicyRule}. Pre-cutover self-hosted servers still accept it.
*/
updateRule = async (
ruleId: string,
Expand All @@ -280,6 +295,9 @@ export class OrgClient {

/**
* Delete an organization rule.
*
* @deprecated Cloud deployments reject this write with 410 Gone — use
* {@link deletePolicyRule}. Pre-cutover self-hosted servers still accept it.
*/
deleteRule = async (ruleId: string): Promise<void> => {
await this.request<undefined>(
Expand All @@ -288,6 +306,167 @@ export class OrgClient {
);
};

// ── The policy engine (staged draft → publish) ────────────────────────────
//
// Writes land in the org's DRAFT and enforce only after a publish. By
// default each write method publishes immediately; note a publish
// snapshots the WHOLE org draft — including changes staged by other users
// — so pass `{ skipPublish: true }` and call `publishPolicy()` explicitly
// when several people edit policy. If the publish step fails after a
// successful write, the write IS staged and the PUBLISH request's error is
// rethrown unchanged (its `url` ends with `/publish`) — do NOT retry the
// write (that would stage a duplicate); inspect via
// `listPolicyRules("draft")` and retry `publishPolicy()` instead.

/**
* List the organization's policy-engine rules — the DRAFT working copy by
* default, or the enforced set with `"published"`.
*/
listPolicyRules = async (
status: PolicyRuleStatus = "draft",
): Promise<OrgPolicyRule[]> => {
return this.request<OrgPolicyRule[]>(
"GET",
`/v1/org/policy/rules?status=${status}`,
);
};

/**
* Get one DRAFT policy rule. Published row ids regenerate on every
* publish — list with `"published"` and match by `logicalId` instead.
*/
getPolicyRule = async (ruleId: string): Promise<OrgPolicyRule> => {
return this.request<OrgPolicyRule>(
"GET",
`/v1/org/policy/rules/${encodeURIComponent(ruleId)}`,
);
};

/**
* Create a policy rule (and publish, unless `skipPublish`).
*/
createPolicyRule = async (
input: CreateOrgPolicyRuleInput,
options?: PolicyWriteOptions,
): Promise<PolicyWriteResult<OrgPolicyRule>> => {
const rule = await this.request<OrgPolicyRule>(
"POST",
"/v1/org/policy/rules",
input,
);
return this.finishPolicyWrite(rule, options);
};

/**
* Update a DRAFT policy rule (and publish, unless `skipPublish`).
*/
updatePolicyRule = async (
ruleId: string,
input: UpdateOrgPolicyRuleInput,
options?: PolicyWriteOptions,
): Promise<PolicyWriteResult<OrgPolicyRule>> => {
const rule = await this.request<OrgPolicyRule>(
"PATCH",
`/v1/org/policy/rules/${encodeURIComponent(ruleId)}`,
input,
);
return this.finishPolicyWrite(rule, options);
};

/**
* Delete a DRAFT policy rule (and publish, unless `skipPublish`).
*/
deletePolicyRule = async (
ruleId: string,
options?: PolicyWriteOptions,
): Promise<PolicyWriteResult<null>> => {
await this.request<undefined>(
"DELETE",
`/v1/org/policy/rules/${encodeURIComponent(ruleId)}`,
);
return this.finishPolicyWrite(null, options);
};

/**
* Reorder the DRAFT (and publish, unless `skipPublish`). `orderedIds`
* must name EVERY non-default draft rule exactly once — take the full
* list from `listPolicyRules("draft")`; the server rejects a partial
* permutation with 409.
*/
reorderPolicyRules = async (
orderedIds: string[],
options?: PolicyWriteOptions,
): Promise<PolicyWriteResult<OrgPolicyRule[]>> => {
const rules = await this.request<OrgPolicyRule[]>(
"PUT",
"/v1/org/policy/rules/order",
{ orderedIds },
);
return this.finishPolicyWrite(rules, options);
};

/**
* Get the organization's terminal Default Rule (a virtual default is
* returned when none is persisted yet).
*/
getPolicyDefault = async (
status: PolicyRuleStatus = "draft",
): Promise<OrgPolicyRule> => {
return this.request<OrgPolicyRule>(
"GET",
`/v1/org/policy/default?status=${status}`,
);
};

/**
* Set the Default Rule's action (and publish, unless `skipPublish`).
*/
setPolicyDefault = async (
action: PolicyRuleAction,
options?: PolicyWriteOptions,
): Promise<PolicyWriteResult<OrgPolicyRule>> => {
const rule = await this.request<OrgPolicyRule>(
"PATCH",
"/v1/org/policy/default",
{ action },
);
return this.finishPolicyWrite(rule, options);
};

/**
* Publish the WHOLE org draft — every staged change, including other
* users' — into a new enforced generation.
*/
publishPolicy = async (): Promise<PolicyPublishResult> => {
return this.request<PolicyPublishResult>(
"POST",
"/v1/org/policy/publish",
{},
);
};

/**
* The most recent publish, or `null` when the org was never published.
*/
getPolicyLastPublish = async (): Promise<PolicyLastPublish | null> => {
return this.request<PolicyLastPublish | null>(
"GET",
"/v1/org/policy/last-publish",
);
};

/** Complete a policy write per the publish option (see the section note). */
private finishPolicyWrite = async <T>(
result: T,
options?: PolicyWriteOptions,
): Promise<PolicyWriteResult<T>> => {
if (options?.skipPublish) {
return { result, published: false, generation: null };
}
const publish = await this.publishPolicy();
return { result, published: true, generation: publish.generation };
};

/**
* Register a callback for manual approval requests across **every** project
* in the organization. Starts background long-polling to the gateway; the
Expand Down
Loading
Loading