<h1 align="center">
  <code>@onecli-sh/sdk</code>
</h1>

<p align="center">
  Official Node.js SDK for <a href="https://onecli.sh">OneCLI</a>. Route AI agent traffic through the OneCLI gateway — agents never see real credentials.
</p>

<p align="center">
  <a href="https://onecli.sh/docs/sdks/node">Documentation</a> &nbsp;|&nbsp;
  <a href="https://onecli.sh">Website</a> &nbsp;|&nbsp;
  <a href="https://github.com/onecli/node-sdk">GitHub</a>
</p>

<p align="center">
  <a href="https://www.npmjs.com/package/@onecli-sh/sdk">
    <img src="https://img.shields.io/npm/v/@onecli-sh/sdk.svg" alt="npm version" />
  </a>
  <a href="https://github.com/onecli/node-sdk/blob/main/LICENSE">
    <img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License" />
  </a>
  <a href="https://www.npmjs.com/package/@onecli-sh/sdk">
    <img src="https://img.shields.io/node/v/@onecli-sh/sdk.svg" alt="Node.js version" />
  </a>
</p>

---

## Installation

```bash
npm install @onecli-sh/sdk
# or
pnpm add @onecli-sh/sdk
# or
yarn add @onecli-sh/sdk
```

## Requirements

| SDK version | Node.js version |
| ----------- | --------------- |
| >= 0.1.0    | >= 20           |

## Quick Start

```typescript
import { OneCLI } from "@onecli-sh/sdk";

// Cloud (api.onecli.sh) — no url needed, it's the default
const onecli = new OneCLI({
  apiKey: "oc_your_api_key",
});

const args = ["run", "-i", "--rm", "--name", "my-agent"];

// Fetches container config and pushes -e / -v flags onto args
const active = await onecli.applyContainerConfig(args);

// args now contains HTTPS_PROXY, CA certs, and volume mounts
console.log(active); // true if OneCLI was reachable
```

> **Self-hosted?** Pass `url: "http://localhost:10254"` (or wherever your instance runs) to the constructor, or set the `ONECLI_URL` environment variable.

### Environment variables

Instead of passing options explicitly, set environment variables:

```bash
export ONECLI_API_KEY=oc_your_api_key

# Self-hosted only — cloud users can skip this (defaults to https://api.onecli.sh)
# export ONECLI_URL=http://localhost:10254
```

```typescript
import { OneCLI } from "@onecli-sh/sdk";

// Automatically reads from ONECLI_API_KEY (and ONECLI_URL if set)
const onecli = new OneCLI();
const active = await onecli.applyContainerConfig(args);
```

| Variable             | Description                                                              |
| -------------------- | ------------------------------------------------------------------------ |
| `ONECLI_API_KEY`     | API key (`oc_...` for project keys, `oc_org_...` for org keys)          |
| `ONECLI_URL`         | Base URL of the OneCLI instance. Defaults to `https://api.onecli.sh`    |
| `ONECLI_GATEWAY_URL` | Gateway URL for manual approval polling (auto-resolved if not set)      |
| `ONECLI_PROJECT_ID`  | Default project ID for org-level API keys                               |

### Organization API keys

Organization-level API keys (`oc_org_...`) grant access across all projects in an org. Pass a `projectId` to specify which project to target.

```typescript
import { OneCLI } from "@onecli-sh/sdk";

// Set a default project for all operations
const onecli = new OneCLI({
  apiKey: "oc_org_your_org_key",
  projectId: "proj-123",
});

await onecli.createAgent({ name: "Bot", identifier: "bot" });

// Override the project for a specific operation
await onecli.createAgent(
  { name: "Bot", identifier: "bot" },
  { projectId: "proj-456" },
);
```

Organization keys also unlock the org-level surface under `onecli.org` — connections and rules shared by every project (no `projectId` needed). See the `onecli.org` section in the API reference below.

---

## API Reference

### `OneCLI`

Main SDK client.

```typescript
new OneCLI(options?: OneCLIOptions)
```

| Option       | Type     | Default                          | Description                                                            |
| ------------ | -------- | -------------------------------- | ---------------------------------------------------------------------- |
| `apiKey`     | `string` | `ONECLI_API_KEY` env var         | API key (`oc_...` for project keys, `oc_org_...` for org keys)         |
| `url`        | `string` | `ONECLI_URL` or `https://api.onecli.sh` | Base URL of the OneCLI instance                                 |
| `timeout`    | `number` | `5000`                           | Request timeout in milliseconds                                        |
| `gatewayUrl` | `string` | `ONECLI_GATEWAY_URL` env var     | Gateway URL for manual approval polling (auto-resolved if not set)     |
| `projectId`  | `string` | `ONECLI_PROJECT_ID` env var      | Default project ID for org-level API keys (can be overridden per-operation) |

---

### Container configuration

#### `onecli.getContainerConfig(options?)`

Fetch the raw container configuration from OneCLI.

```typescript
const config = await onecli.getContainerConfig();
console.log(config.env);                        // { HTTPS_PROXY: "...", HTTP_PROXY: "...", ... }
console.log(config.caCertificate);              // PEM-formatted CA certificate
console.log(config.caCertificateContainerPath); // /tmp/onecli-proxy-ca.pem

// Fetch config for a specific agent
const agentConfig = await onecli.getContainerConfig({ agent: "my-agent" });

// With org-level API key, specify the target project
const config = await onecli.getContainerConfig({ projectId: "proj-123" });
```

| Option      | Type     | Description                                                              |
| ----------- | -------- | ------------------------------------------------------------------------ |
| `agent`     | `string` | Agent identifier to fetch config for (uses default agent if omitted)     |
| `projectId` | `string` | Project ID override for org-level API keys                               |

**Returns** `{ env, caCertificate, caCertificateContainerPath }`

**Throws** `OneCLIRequestError` on non-200 response.

#### `onecli.applyContainerConfig(args, options?)`

Fetch config and push Docker flags onto the `args` array. Returns `true` on success, or `false` if OneCLI is unreachable or unhealthy (network error or 5xx). Throws `OneCLIRequestError` on a 4xx response (e.g. an unknown agent identifier or invalid API key) — a real misconfiguration you should handle rather than launch an uncredentialed container.

```typescript
const args = ["run", "-i", "--rm", "my-image"];
const active = await onecli.applyContainerConfig(args, {
  combineCaBundle: true,
  addHostMapping: true,
});
```

| Option            | Type      | Default | Description                                    |
| ----------------- | --------- | ------- | ---------------------------------------------- |
| `combineCaBundle` | `boolean` | `true`  | Build combined CA bundle for system-wide trust  |
| `addHostMapping`  | `boolean` | `true`  | Add `host.docker.internal` mapping on Linux     |
| `agent`           | `string`  |         | Agent identifier to fetch config for            |
| `projectId`       | `string`  |         | Project ID override for org-level API keys      |

**What it does:**
1. Fetches container config from OneCLI with Bearer auth
2. Pushes `-e KEY=VALUE` for each environment variable
3. Writes the CA certificate to a temp file and mounts it with `-v`
4. Builds a combined CA bundle (system CAs + OneCLI CA) so all tools trust OneCLI
5. Adds `--add-host host.docker.internal:host-gateway` on Linux

If OneCLI is unreachable or unhealthy (network error or 5xx), returns `false` without mutating the args array. A 4xx response (e.g. the agent identifier isn't registered) throws `OneCLIRequestError` instead of failing silently.

---

### Agent management

#### `onecli.createAgent(input, options?)`

Create a new agent.

```typescript
const agent = await onecli.createAgent({
  name: "My Agent",
  identifier: "my-agent",
});

console.log(agent.id);         // Agent ID
console.log(agent.identifier); // "my-agent"
console.log(agent.createdAt);  // ISO 8601 timestamp
```

| Input        | Type     | Description                                                                  |
| ------------ | -------- | ---------------------------------------------------------------------------- |
| `name`       | `string` | Display name for the agent                                                   |
| `identifier` | `string` | Unique identifier (1-50 chars; lowercase letters, numbers, hyphens; starts with a letter or number) |

**Returns** `{ id, name, identifier, createdAt }`

#### `onecli.listAgents(options?)`

List all agents in the project.

```typescript
const agents = await onecli.listAgents();

for (const agent of agents) {
  console.log(agent.identifier, agent.isDefault);
}
```

**Returns** `Array<{ id, name, identifier, isDefault, createdAt }>`

#### `onecli.listAgentsWithGrants(options?)`

`listAgents` plus a per-agent summary of what's granted (which apps and
secrets, not the full tool lists) in one round-trip.

```typescript
const agents = await onecli.listAgentsWithGrants();

for (const agent of agents) {
  console.log(agent.identifier, agent.grantsSummary.total);
  for (const entry of agent.grantsSummary.entries) {
    // entry.kind: "app" (a connection) | "secret" | "llm"
    console.log(entry.kind === "app" ? entry.provider : entry.name);
  }
}
```

**Returns** `AgentWithGrantsSummary[]`

#### `onecli.getEffectiveCredentials(agentId, options?)`

Which credentials the agent can actually use, and what each one can do under the
published policy. Read-only.

```ts
const { mode, secrets, connections } =
  await onecli.getEffectiveCredentials("agent-id");
```

Replaces the retired `GET /v1/agents/{id}/secrets` and `/connections` reads.
Those returned a stored assignment list; this returns the **effective** set, so a
credential granted by a policy rule appears here even though no assignment row
exists for it.

#### `onecli.getEffectiveAppPermissions({ provider, agentId? }, options?)`

What the project's published policy allows for an app, per tool. Omit `agentId`
for the all-agents baseline. The project-scope twin of
`onecli.org.getEffectiveAppPermissions`, and the replacement for the retired
`GET /v1/rules/permissions/{provider}`.

```ts
const { groups, variesByIdentity } = await onecli.getEffectiveAppPermissions({
  provider: "gmail",
});
```

#### `onecli.getConnectionAgentAccess(connectionId, options?)`

Which agents can reach a connection, and what each can do with it. Replaces the
retired `GET /v1/connections/{id}/agents`.

#### `onecli.listAppPermissionDefinitions(options?)`

Every provider's public tool catalog — the tool ids an `app`-target policy rule
can name.

#### `onecli.ensureAgent(input, options?)`

Ensure an agent exists. Creates it if missing, returns normally if it already exists.

```typescript
const result = await onecli.ensureAgent({
  name: "My Agent",
  identifier: "my-agent",
});

console.log(result.created); // true if newly created, false if already existed
```

Idempotent even at the agent cap: if the project is at its plan's agent limit but the target identifier already exists, the call still resolves with `created: false` instead of throwing a quota error.

**Returns** `{ name, identifier, created }`

---

### Agent grants

Which credentials an agent may use. An agent starts with **no grants** — the
gateway injects nothing for it until a connection or secret is attached. Grant
writes take effect immediately; there is no draft/publish step.

Grants are the writable **intent**. The read-only reflections
(`getEffectiveCredentials`, `getEffectiveAppPermissions`,
`getConnectionAgentAccess`) show the **effect** — what requests actually get
through once organization policy is applied on top. A tool a grant allows can
still be blocked (or forced to approval) by an org rule, so the reflections are
the view to trust when debugging a blocked request.

#### `onecli.getAgentGrants(agentId, options?)`

Everything granted to one agent.

```typescript
const grants = await onecli.getAgentGrants("agent-id");

for (const c of grants.connections) {
  // c.access: "full" (every tool) or "custom" (the allow/ask lists below)
  console.log(c.provider, c.label, c.access, c.allow, c.ask);
}
for (const s of grants.secrets) {
  console.log(s.name, s.type, s.scope);
}
```

**Returns** `AgentGrants` — `{ agentId, mode, connections, secrets }`.

#### `onecli.setConnectionGrant(agentId, connectionId, input, options?)`

Attach an app connection to an agent, or change what the agent may do with it.

```typescript
// Full access — every tool the app supports
await onecli.setConnectionGrant("agent-id", "connection-id", {
  access: "full",
});

// Custom — name the tools. `allow` runs freely; `ask` pauses for human approval.
await onecli.setConnectionGrant("agent-id", "connection-id", {
  access: "custom",
  allow: ["search_messages", "get_message"],
  ask: ["send_email"],
});
```

Two validation laws on custom grants (both a 422): the two lists together must
name at least one tool — to take everything away, detach instead — and a tool
can't be in both lists. Tool ids come from `listAppPermissionDefinitions()`.
The `ask` list requires a plan with manual approvals (403 otherwise).

**Returns** the agent's updated `AgentGrants`.

#### `onecli.removeConnectionGrant(agentId, connectionId, options?)`

Detach a connection from an agent. The gateway stops serving it to that agent
immediately.

**Returns** nothing (the server responds `204`).

#### `onecli.attachSecret(agentId, secretId, options?)`

Attach a secret (an API key or LLM key) to an agent. Secrets are
all-or-nothing — there are no per-tool lists.

**Returns** the agent's updated `AgentGrants`.

#### `onecli.detachSecret(agentId, secretId, options?)`

Detach a secret from an agent. **Returns** nothing (`204`).

#### `onecli.getConnectionGrants(connectionId, options?)`

The reverse view: which agents hold a grant for one connection.

```typescript
const { agents } = await onecli.getConnectionGrants("connection-id");
for (const a of agents) {
  console.log(a.agentId, a.access, a.allow, a.ask);
}
```

**Returns** `ConnectionGrants` — `{ connectionId, agents }`.

---

### Project provisioning

> **Cloud-only feature.** Calling `provisionProject()` against an OSS instance throws `OneCLIError`.

#### `onecli.provisionProject(input?, options?)`

Pre-create a user account with a project and API key. The API key works immediately. Requires admin or owner role.

```typescript
const result = await onecli.provisionProject({
  role: "member",
  skipOnboarding: true,
});

console.log(result.apiKey);    // oc_... (usable immediately)
console.log(result.claimUrl);  // https://app.onecli.sh/claim?token=...
console.log(result.projectId);
```

| Input            | Type                  | Default    | Description                                   |
| ---------------- | --------------------- | ---------- | --------------------------------------------- |
| `role`           | `"admin" \| "member"` | `"member"` | Role the provisioned user will have in the org |
| `skipOnboarding` | `boolean`             | `true`     | Whether the user skips the onboarding wizard   |

**Returns**

| Field       | Type     | Description                                              |
| ----------- | -------- | -------------------------------------------------------- |
| `id`        | `string` | Provision record ID                                      |
| `userId`    | `string` | Placeholder user ID (becomes the real user after claim)  |
| `projectId` | `string` | Pre-created project ID                                   |
| `apiKey`    | `string` | API key for the provisioned project (usable immediately) |
| `claimUrl`  | `string` | URL the user visits to claim the account                 |
| `expiresAt` | `string` | Expiration timestamp (ISO 8601)                          |

**Throws** `OneCLIError` if called against an OSS instance. **Throws** `OneCLIRequestError` with status 403 if the API key doesn't belong to an admin/owner.

---

### Manual approval

#### `onecli.configureManualApproval(callback, options?)`

Register a callback that's invoked whenever an agent request needs human approval. Starts background long-polling to the gateway. Returns a handle to stop polling.

```typescript
const handle = onecli.configureManualApproval(async (request) => {
  console.log(`${request.method} ${request.url}`);
  console.log(`Agent: ${request.agent.name}`);

  // `summary` is a structured, human-readable description of the request
  // (e.g. a Gmail send's base64 body decoded into To/Subject/Body).
  // `bodyPreview` is the same content flattened to text — safe to display directly.
  if (request.summary) {
    console.log(request.summary.action); // e.g. "Send email"
    for (const { label, value } of request.summary.details) {
      console.log(`${label}: ${value}`); // e.g. "To: a@b.com"
    }
  } else if (request.bodyPreview) {
    console.log(request.bodyPreview);
  }

  // Return 'approve' to forward the request, 'deny' to block it
  return "approve";
});

// Stop polling on shutdown
process.on("SIGTERM", () => handle.stop());
```

The callback is called once per pending approval. Multiple approvals are handled concurrently, and each callback runs independently. If the callback throws or the decision fails to submit, the same request is retried on the next poll cycle.

**Callback parameter: `ApprovalRequest`**

| Field            | Type                                                        | Description                                       |
| ---------------- | ----------------------------------------------------------- | ------------------------------------------------- |
| `id`             | `string`                                                    | Unique approval ID                                |
| `method`         | `string`                                                    | HTTP method (`GET`, `POST`, `DELETE`, etc.)        |
| `url`            | `string`                                                    | Full request URL                                  |
| `host`           | `string`                                                    | Hostname                                          |
| `path`           | `string`                                                    | Request path                                      |
| `headers`        | `Record<string, string>`                                    | Sanitized request headers (no credentials)        |
| `bodyPreview`    | `string \| null`                                            | Human-readable text rendering of the request, safe to display |
| `summary`        | `ApprovalSummary \| null` (optional)                        | Structured form of `bodyPreview` (see below); may be absent on older gateways |
| `agent`          | `{ id: string; name: string; externalId: string \| null }`  | The agent that made the request                   |
| `createdAt`      | `string`                                                    | When the request arrived (ISO 8601)               |
| `expiresAt`      | `string`                                                    | When the approval expires (ISO 8601)              |
| `timeoutSeconds` | `number`                                                    | Seconds until auto-deny (300)                     |

Where `ApprovalSummary` is:

```typescript
interface ApprovalSummary {
  action: string; // e.g. "Send email"
  details: { label: string; value: string }[]; // e.g. [{ label: "To", value: "a@b.com" }]
}
```

**Returns** `ManualApprovalHandle` with a `stop()` method to disconnect.

---

### Error classes

#### `OneCLIError`

General SDK error (e.g., missing API key).

```typescript
import { OneCLIError } from "@onecli-sh/sdk";
```

#### `OneCLIRequestError`

HTTP request error with `url` and `statusCode` properties.

```typescript
import { OneCLIRequestError } from "@onecli-sh/sdk";

try {
  await onecli.getContainerConfig();
} catch (error) {
  if (error instanceof OneCLIRequestError) {
    console.error(error.url);        // Request URL
    console.error(error.statusCode); // HTTP status code
  }
}
```

---

### Gateway errors & multiple accounts

Agent traffic doesn't go through this SDK — it rides the gateway proxy
transparently. When the gateway blocks or can't route a proxied request, the
response body is a typed JSON error (distinct from the management API's
`{ error: { message, type } }` envelope). The SDK ships those body types plus a
narrowing helper, so agent-side code can react without guessing:

```typescript
import {
  parseGatewayError,
  CONNECTION_ID_HEADER,
} from "@onecli-sh/sdk";

const send = async (headers: Record<string, string> = {}) =>
  fetch("https://gmail.googleapis.com/gmail/v1/users/me/messages", { headers });

let res = await send();
if (!res.ok) {
  const err = parseGatewayError(await res.json().catch(() => null));

  if (
    err?.error === "multiple_connections" ||
    err?.error === "multiple_providers"
  ) {
    // Two accounts could serve this request (e.g. two Gmail connections).
    // Retry the identical request naming one of them:
    const choice = err.connections[0];
    res = await send({ [CONNECTION_ID_HEADER]: choice.id });
  } else if (err) {
    // access_restricted, blocked_by_policy, credential_not_found, ... —
    // every arm carries a remediation URL or the blocking rule's name.
    console.error(err.error, err.message);
  }
}
```

All of these responses carry `x-should-retry: false`: retrying unchanged will
not succeed — the fix is the named remediation (add the header, grant the
connection to the agent, attach the credential). Successfully forwarded
responses advertise the available accounts in the `x-onecli-connections`
response header (a JSON array of `GatewayConnectionChoice`), so an agent can
learn the ids before ever hitting a 409.

| Body (`error`) | Status | Meaning |
| -------------- | ------ | ------- |
| `multiple_connections` | 409 | Several accounts of the same app match — retry with `x-onecli-connection-id` |
| `multiple_providers` | 409 | Accounts of different apps match — same retry protocol |
| `connection_not_found` | 404 | The `x-onecli-connection-id` you sent names no available connection — re-pick |
| `access_restricted` | upstream 401/403 | A credential exists, but this agent has no grant for it (`manage_url` opens the fix) |
| `blocked_by_policy` | 403 | A policy rule blocked the request (`rule_name` says which) |
| `blocked_by_default_policy` | 403 | Nothing allowed the request (deny-by-default) |
| `credential_not_found` | upstream 401/403 | No credential exists for this host at all (`secret_url` is a create link) |

---

### `onecli.org` — organization-level resources

Connections and rules shared by **every project** in the organization. Requests carry no `X-Project-Id`; authenticate with an organization API key (`oc_org_...`), and note every org operation requires the admin or owner role. Requires OneCLI Cloud or a self-hosted Enterprise instance (a 404 from servers without the org surface is mapped to a descriptive `OneCLIError`). `getAuthorizeUrl` is for server-side runtimes only (browser fetch hides redirect headers).

```typescript
const onecli = new OneCLI({ apiKey: "oc_org_your_org_key" });

// Connect an API-key app org-wide
await onecli.org.connectApp("fireflies", {
  fields: { apiKey: "ff-xxxx" },
  label: "shared",
});

// OAuth apps: get the authorize URL and open it in a browser
const url = await onecli.org.getAuthorizeUrl("google-drive");

// Manage org connections
const connections = await onecli.org.listConnections();
await onecli.org.renameConnection(connections[0].id, "prod");
await onecli.org.deleteConnection(connections[0].id);

// 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",
  action: "block",
  targets: [{ kind: "app", provider: "gmail", tools: ["send_email"] }],
});
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

// What the published policy actually allows for an app, per tool — read-only.
const effective = await onecli.org.getEffectiveAppPermissions({
  provider: "gmail",
});

// 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.
const handle = onecli.org.configureManualApproval(
  async (request) => {
    console.log(`[${request.projectId}] ${request.method} ${request.url}`);
    return "approve"; // or "deny"
  },
  { onError: (err) => console.error("approval poll failed", err) },
);
// handle.stop() when shutting down
```

Policy rules carry structured `targets` (app / connection / secret /
network) and `identities` (org rules take `user`/`group`; `agent`
identities exist at project scope only). 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).

Org rules are the organization-wide **ceiling**: they cap what any project
grant can allow. Per-agent access within a project is managed with
[agent grants](#agent-grants), not rules.

| Method | Endpoint | Returns |
|--------|----------|---------|
| `connectApp(provider, input)` | `POST /v1/org/apps/{provider}/connect` | `{ success: boolean }` |
| `getAuthorizeUrl(provider, options?)` | `GET /v1/org/apps/{provider}/authorize` | authorize URL (`string`) |
| `listConnections(provider?)` | `GET /v1/org/connections` | `OrgConnection[]` |
| `renameConnection(id, label)` | `PATCH /v1/org/connections/{id}` | `OrgConnection` |
| `deleteConnection(id)` | `DELETE /v1/org/connections/{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` |
| `getEffectiveAppPermissions({provider})` | `GET /v1/org/policy/effective-app-permissions` | `EffectiveAppPermissions` |
| `configureManualApproval(cb, options?)` | `GET /v1/org/approvals/pending` (long-poll) | `ManualApprovalHandle` |

---

### Types

Every request/response shape is exported. The full surface, by area:

```typescript
// Client + container configuration
import type {
  OneCLIOptions,
  RequestOptions,
  ContainerConfig,
  CredentialStub,
  GetContainerConfigOptions,
  ApplyContainerConfigOptions,
} from "@onecli-sh/sdk";

// Agents + grants
import type {
  Agent,
  CreateAgentInput,
  CreateAgentResponse,
  EnsureAgentResponse,
  AgentGrants,
  AgentGrantConnection,
  AgentGrantSecret,
  ConnectionGrantInput,
  ConnectionGrants,
  AgentGrantsSummary,
  GrantsSummaryEntry,
  AgentWithGrantsSummary,
} from "@onecli-sh/sdk";

// Read-only policy reflections (effective access)
import type {
  EffectiveCredentials,
  EffectiveCredential,
  CredentialAccessStatus,
  CredentialProvenance,
  ConnectionAgentAccess,
  ConnectionAgent,
  AgentAccessStatus,
  AgentCredentialStatus,
  AppPermissionDefinition,
  EffectiveAppPermissions,
  EffectiveToolGroup,
  EffectiveTool,
  EffectiveToolVerdict,
  EffectiveProvenance,
} from "@onecli-sh/sdk";

// Gateway proxied-error protocol (see "Gateway errors & multiple accounts")
import {
  parseGatewayError,
  CONNECTION_ID_HEADER,
  CONNECTIONS_HEADER,
} from "@onecli-sh/sdk";
import type {
  GatewayError,
  GatewayConnectionChoice,
  MultipleConnectionsError,
  MultipleProvidersError,
  ConnectionNotFoundError,
  AccessRestrictedError,
  BlockedByPolicyError,
  BlockedByDefaultPolicyError,
  CredentialNotFoundError,
} from "@onecli-sh/sdk";

// Manual approval
import type {
  ApprovalRequest,
  ApprovalSummary,
  ApprovalDetail,
  ManualApprovalCallback,
  ManualApprovalHandle,
  OrgApprovalRequest,
  OrgManualApprovalCallback,
  OrgManualApprovalOptions,
} from "@onecli-sh/sdk";

// Project provisioning
import type {
  ProvisionProjectInput,
  ProvisionProjectResponse,
} from "@onecli-sh/sdk";

// Organization surface (connections + org policy rules)
import type {
  ConnectOrgAppInput,
  GetOrgAuthorizeUrlOptions,
  OrgConnection,
  OrgPolicyRule,
  OrgRuleCondition,
  OrgRuleMethod,
  OrgRuleRateLimitWindow,
  PolicyRuleAction,
  PolicyRuleStatus,
  PolicyRuleIdentity,
  OrgPolicyRuleIdentityInput,
  PolicyRuleTarget,
  PolicyRuleTargetInput,
  PolicyRuleConditionsInput,
  CreateOrgPolicyRuleInput,
  UpdateOrgPolicyRuleInput,
  PolicyWriteOptions,
  PolicyWriteResult,
  PolicyPublishResult,
  PolicyLastPublish,
} from "@onecli-sh/sdk";
```

## How It Works

OneCLI runs on the host machine and acts as a gateway for containerized agents. When a container makes HTTPS requests to intercepted domains (e.g. `api.anthropic.com`), OneCLI:

1. Terminates TLS using a local CA certificate
2. Inspects the request and injects real credentials (replacing placeholder tokens)
3. Forwards the request to the upstream service
4. Returns the response to the container

**Containers never see real API keys.** They only have placeholder tokens that OneCLI swaps out transparently.

The SDK configures containers with the right environment variables (`HTTPS_PROXY`, `HTTP_PROXY`) and CA certificate mounts so this works automatically.

## Development

```bash
pnpm install       # Install dependencies
pnpm run build     # Build CJS + ESM
pnpm run typecheck # Type-check without emitting
pnpm run test      # Run tests
pnpm run dev       # Watch mode
```

## License

MIT
