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 .changeset/0000-sdk-pin-0-17-0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cdot65/prisma-airs-cli": patch
---

Bump `@cdot65/prisma-airs-sdk` to `^0.17.0`. No behavior change — the CLI does not yet touch any surface changed between 0.13.2 and 0.17.0; this unlocks upcoming AI Gateway and Red Team adapter commands.
5 changes: 5 additions & 0 deletions .changeset/0001-aigateway-workspace-read.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cdot65/prisma-airs-cli": minor
---

Add the `airs aigateway` command group (AI Gateway, new SDK 0.14+ domain): `workspace list` (with `--plane data|admin`, `--status active|archived`, and `--all` to merge tenant-wide active + archived reads) and `workspace get <uuid|slug>`. New optional config fields `aiGwDataEndpoint`/`aiGwAdminEndpoint`/`aiGwTokenEndpoint` (`PANW_AI_GW_*` env vars; credentials shared with `PANW_MGMT_*`). `airs doctor` now reports AI Gateway reachability, and 403s explain which SCM grant is missing (workspace-scope vs tenant-root admin).
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,10 @@ PANW_DLP_ENDPOINT=
# PANW_MODEL_SEC_DATA_ENDPOINT=
# PANW_MODEL_SEC_MGMT_ENDPOINT=
# PANW_MODEL_SEC_TOKEN_ENDPOINT=

# ── AI Gateway API (endpoint overrides; creds shared with PANW_MGMT_*) ─
# Data plane (/ai_gw/v2) — scoped workspace reads + telemetry
# PANW_AI_GW_DATA_ENDPOINT=
# Admin plane (/ai_gw/admin/v2) — tenant-wide reads + all writes
# PANW_AI_GW_ADMIN_ENDPOINT=
# PANW_AI_GW_TOKEN_ENDPOINT=
5 changes: 5 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ src/
│ │ ├── completion.ts # airs completion <shell> — shell completion scripts
│ │ ├── runtime.ts # Runtime scanning + config management + topics (profiles)
│ │ ├── redteam.ts # Red team operations (scan, targets CRUD + backup/restore, prompt-sets CRUD, prompts CRUD, properties)
│ │ ├── aigateway.ts # AI Gateway operations (workspace list/get; two-plane routing, --all merge)
│ │ └── modelsecurity.ts # Model security operations (groups, rules, rule-instances, scans, labels, pypi-auth)
│ ├── bulk-scan-state.ts # Validated item-centric v2 bulk state; atomic 0600 checkpoints for safe resume
│ ├── parse-input.ts # Input file parsing — CSV (prompt column) or plain text (line-per-prompt)
Expand Down Expand Up @@ -130,6 +131,7 @@ src/
│ ├── promptsets.ts # SdkPromptSetService — custom prompt set CRUD via RedTeamClient
│ ├── dlp/ # DLP namespace: filtering-profiles, patterns, profiles, dictionaries SDK service wrappers
│ ├── redteam.ts # SdkRedTeamService — red team scan CRUD, polling, reports
│ ├── aigateway.ts # SdkAiGatewayService — AI Gateway workspace reads + 403 grant hints
│ ├── modelsecurity.ts # SdkModelSecurityService — security groups, rules, scans, labels
│ └── types.ts # ScanResult, ProfileTopic, ScanService, ManagementService, PromptSetService, RedTeamService, ModelSecurityService
├── backup/
Expand Down Expand Up @@ -322,6 +324,9 @@ See `.env.example` for the full list. Config priority: CLI flags > env vars > `~
|----------|---------|---------|
| `PANW_MGMT_ENDPOINT` | SDK default | Management API endpoint |
| `PANW_MGMT_TOKEN_ENDPOINT` | SDK default | Management API token endpoint |
| `PANW_AI_GW_DATA_ENDPOINT` | SDK default | AI Gateway data-plane endpoint (`/ai_gw/v2`) |
| `PANW_AI_GW_ADMIN_ENDPOINT` | SDK default | AI Gateway admin-plane endpoint (`/ai_gw/admin/v2`) |
| `PANW_AI_GW_TOKEN_ENDPOINT` | mgmt token endpoint | AI Gateway token endpoint override |
| `SCAN_CONCURRENCY` | `5` | Concurrent AIRS scans (1-20) |
| `DATA_DIR` | `~/.prisma-airs/runs` | Run state persistence directory |

Expand Down
4 changes: 4 additions & 0 deletions docs-site/docs/cli/aigateway/_category_.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"label": "AI Gateway",
"position": 4
}
97 changes: 97 additions & 0 deletions docs-site/docs/cli/aigateway/workspaces.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
---
sidebar_label: workspace
---

# aigateway workspace

Manage **AI Gateway workspaces** — the unit almost every other AI Gateway
resource is keyed by. Credentials are shared with the management API
(`PANW_MGMT_*`); only the endpoints are AI Gateway-specific
(`PANW_AI_GW_DATA_ENDPOINT`, `PANW_AI_GW_ADMIN_ENDPOINT`, with
`PANW_AI_GW_TOKEN_ENDPOINT` falling back to `PANW_MGMT_TOKEN_ENDPOINT`).

:::warning Two planes, and the default hides rows

The AI Gateway spans two planes with **different SCM role scopes**:

- **data** (`/ai_gw/v2`) — returns only workspaces your service account holds a
workspace-scope grant on. Needs `view_only_admin` or higher on the
`main_airs_workspace_<TSG>` scope.
- **admin** (`/ai_gw/admin/v2`) — returns every workspace in the tenant. Needs
an admin role at tenant-root scope.

A bare `list` is therefore **not** "all workspaces": it shows only *active*
workspaces *you are scoped to*. Use `--plane admin` for the whole tenant and
`--all` to also include archived rows.

On a 403 the CLI tells you which grant is missing: `errorCode AB03` means the
workspace-scope grant (data plane); otherwise the tenant-root admin grant. Note
SCM's Access Management UI *edits* the existing role row by default — click
**Add Role** so the account ends up with both role rows.

:::

### aigateway workspace list

List workspaces.

```text
airs aigateway workspace list [options]
```

#### Options

| Flag | Required | Default | Description |
|------|:--------:|---------|-------------|
| `--plane <plane>` | No | `data` | Plane to read from: `data` (scoped) or `admin` (whole tenant) |
| `--status <status>` | No | `active` | Filter by lifecycle state: `active` or `archived` |
| `--all` | No | — | Merge active + archived admin-plane reads (whole tenant, both states) |
| `--output <format>` | No | `pretty` | Output format: pretty, table, csv, json, yaml |

Without `--status`, archived workspaces are omitted entirely — there is no
single call returning both states, so `--all` merges two admin-plane reads.

#### Examples

```bash
airs aigateway workspace list
airs aigateway workspace list --plane admin
airs aigateway workspace list --plane admin --status archived
airs aigateway workspace list --all --output json
```

### aigateway workspace get

Get one workspace by UUID **or** slug, including the settings blocks list rows
do not carry.

```text
airs aigateway workspace get <ref> [options]
```

#### Options

| Flag | Required | Default | Description |
|------|:--------:|---------|-------------|
| `--plane <plane>` | No | `data` | Plane to read from: `data` (scoped) or `admin` (whole tenant) |
| `--output <format>` | No | `pretty` | Output format: pretty, json, yaml |

A workspace outside your workspace scope answers `403 AB03` on the data plane
(not 404) — re-read it with `--plane admin`. An **archived** workspace answers
`404 AB08` for both its UUID and slug on either plane; inspect archived rows
via `list --status archived` instead.

:::note Status can disagree between endpoints

`list` reports `active` for workspaces whose `get` reports `null`. The CLI
renders a null status as `unknown` — treat it as unknown, never as inactive,
and prefer the list value.

:::

#### Examples

```bash
airs aigateway workspace get ws-main-a-349e0e
airs aigateway workspace get 16f7e90d-382a-4e78-b577-1b01eb5f8297 --plane admin --output json
```
1 change: 1 addition & 0 deletions docs-site/docs/cli/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ slug: /cli/

Auto-generated from the `airs` command tree. Every command below lists its synopsis, options, and at least one input/output example.

- [`airs aigateway workspace`](aigateway/workspaces.md)
- [`airs model-security groups`](model-security/groups.md)
- [`airs model-security install`](model-security/install.md)
- [`airs model-security labels`](model-security/labels.md)
Expand Down
6 changes: 3 additions & 3 deletions docs-site/docs/developers/api/functions/loadConfig.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Function: loadConfig()

> **loadConfig**(`cliOverrides?`, `configFilePath?`): `Promise`\<\{ `airsApiEndpoint?`: `string`; `airsApiKey?`: `string`; `airsApiToken?`: `string`; `airsNumRetries?`: `number`; `dataDir`: `string`; `dlpEndpoint?`: `string`; `mgmtClientId?`: `string`; `mgmtClientSecret?`: `string`; `mgmtEndpoint?`: `string`; `mgmtTokenEndpoint?`: `string`; `mgmtTsgId?`: `string`; `modelSecDataEndpoint?`: `string`; `modelSecMgmtEndpoint?`: `string`; `modelSecTokenEndpoint?`: `string`; `redTeamDataEndpoint?`: `string`; `redTeamMgmtEndpoint?`: `string`; `redTeamNetworkBrokerEndpoint?`: `string`; `redTeamTokenEndpoint?`: `string`; `scanConcurrency`: `number`; \}\>
> **loadConfig**(`cliOverrides?`, `configFilePath?`): `Promise`\<\{ `aiGwAdminEndpoint?`: `string`; `aiGwDataEndpoint?`: `string`; `aiGwTokenEndpoint?`: `string`; `airsApiEndpoint?`: `string`; `airsApiKey?`: `string`; `airsApiToken?`: `string`; `airsNumRetries?`: `number`; `dataDir`: `string`; `dlpEndpoint?`: `string`; `mgmtClientId?`: `string`; `mgmtClientSecret?`: `string`; `mgmtEndpoint?`: `string`; `mgmtTokenEndpoint?`: `string`; `mgmtTsgId?`: `string`; `modelSecDataEndpoint?`: `string`; `modelSecMgmtEndpoint?`: `string`; `modelSecTokenEndpoint?`: `string`; `redTeamDataEndpoint?`: `string`; `redTeamMgmtEndpoint?`: `string`; `redTeamNetworkBrokerEndpoint?`: `string`; `redTeamTokenEndpoint?`: `string`; `scanConcurrency`: `number`; \}\>

Defined in: [src/config/loader.ts:59](https://github.com/cdot65/prisma-airs-cli/blob/main/src/config/loader.ts#L59)
Defined in: [src/config/loader.ts:62](https://github.com/cdot65/prisma-airs-cli/blob/main/src/config/loader.ts#L62)

## Parameters

Expand All @@ -16,4 +16,4 @@ Defined in: [src/config/loader.ts:59](https://github.com/cdot65/prisma-airs-cli/

## Returns

`Promise`\<\{ `airsApiEndpoint?`: `string`; `airsApiKey?`: `string`; `airsApiToken?`: `string`; `airsNumRetries?`: `number`; `dataDir`: `string`; `dlpEndpoint?`: `string`; `mgmtClientId?`: `string`; `mgmtClientSecret?`: `string`; `mgmtEndpoint?`: `string`; `mgmtTokenEndpoint?`: `string`; `mgmtTsgId?`: `string`; `modelSecDataEndpoint?`: `string`; `modelSecMgmtEndpoint?`: `string`; `modelSecTokenEndpoint?`: `string`; `redTeamDataEndpoint?`: `string`; `redTeamMgmtEndpoint?`: `string`; `redTeamNetworkBrokerEndpoint?`: `string`; `redTeamTokenEndpoint?`: `string`; `scanConcurrency`: `number`; \}\>
`Promise`\<\{ `aiGwAdminEndpoint?`: `string`; `aiGwDataEndpoint?`: `string`; `aiGwTokenEndpoint?`: `string`; `airsApiEndpoint?`: `string`; `airsApiKey?`: `string`; `airsApiToken?`: `string`; `airsNumRetries?`: `number`; `dataDir`: `string`; `dlpEndpoint?`: `string`; `mgmtClientId?`: `string`; `mgmtClientSecret?`: `string`; `mgmtEndpoint?`: `string`; `mgmtTokenEndpoint?`: `string`; `mgmtTsgId?`: `string`; `modelSecDataEndpoint?`: `string`; `modelSecMgmtEndpoint?`: `string`; `modelSecTokenEndpoint?`: `string`; `redTeamDataEndpoint?`: `string`; `redTeamMgmtEndpoint?`: `string`; `redTeamNetworkBrokerEndpoint?`: `string`; `redTeamTokenEndpoint?`: `string`; `scanConcurrency`: `number`; \}\>
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
},
"license": "MIT",
"dependencies": {
"@cdot65/prisma-airs-sdk": "^0.13.2",
"@cdot65/prisma-airs-sdk": "^0.17.0",
"@inquirer/prompts": "^8.3.0",
"chalk": "^5.6.2",
"commander": "^14.0.3",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

109 changes: 109 additions & 0 deletions src/airs/aigateway.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { AIGatewayClient, type AIGatewayClientOptions } from '@cdot65/prisma-airs-sdk';
import type {
AiGatewayService,
AiGatewayWorkspace,
AiGatewayWorkspaceDetail,
AiGatewayWorkspaceGetOptions,
AiGatewayWorkspaceListOptions,
} from './types.js';

/**
* `usage_limits`/`rate_limits` are `array | record | null` on the wire — the
* array of policy objects is canonical, but the legacy single-object form is
* still accepted upstream. Normalize everything to an array.
*/
function toLimitArray(value: unknown): Array<Record<string, unknown>> {
if (Array.isArray(value)) return value as Array<Record<string, unknown>>;
if (value !== null && typeof value === 'object') return [value as Record<string, unknown>];
return [];
}

/** Normalize an SDK workspace list row into an AiGatewayWorkspace. */
function normalizeWorkspace(raw: Record<string, unknown>): AiGatewayWorkspace {
return {
id: raw.id as string,
slug: raw.slug as string,
name: raw.name as string,
icon: raw.icon as string | null | undefined,
description: raw.description as string | null | undefined,
createdAt: raw.created_at as string | undefined,
lastUpdatedAt: raw.last_updated_at as string | undefined,
isDefault: Boolean(raw.is_default),
status: raw.status as string | null | undefined,
scopeName: raw.scope_name as string | undefined,
};
}

/** Normalize an SDK workspace detail into an AiGatewayWorkspaceDetail. */
function normalizeWorkspaceDetail(raw: Record<string, unknown>): AiGatewayWorkspaceDetail {
return {
...normalizeWorkspace(raw),
defaults: raw.defaults as Record<string, unknown> | null | undefined,
usageLimits: toLimitArray(raw.usage_limits),
rateLimits: toLimitArray(raw.rate_limits),
securitySettings: raw.security_settings as Record<string, boolean> | undefined,
dataPlaneSecuritySettings: raw.data_plane_security_settings as
| Record<string, unknown>
| undefined,
settings: raw.settings as Record<string, unknown> | undefined,
};
}

/**
* A 403 from the AI Gateway is a grant problem, and which grant depends on the
* plane: `errorCode AB03` means the workspace-scope grant is missing (data
* plane); otherwise the tenant-root admin grant is (admin plane). Returns a
* user-facing hint, or undefined for non-403 errors.
*/
export function aiGatewayGrantHint(err: unknown): string | undefined {
const status =
(err as { status?: number; statusCode?: number })?.status ??
(err as { statusCode?: number })?.statusCode;
if (status !== 403) return undefined;
const message = err instanceof Error ? err.message : String(err);
const grant = message.includes('AB03')
? 'the service account is missing a workspace-scope grant (data plane, /ai_gw/v2)'
: 'the service account is missing a tenant-root admin grant (admin plane, /ai_gw/admin/v2)';
return (
`${grant}. SCM Access Management edits the existing role row by default — ` +
`use "Add Role" so the account ends up with both role rows, not one row moved.`
);
}

/** Service wrapper over the SDK AIGatewayClient. */
export class SdkAiGatewayService implements AiGatewayService {
private client: AIGatewayClient;

constructor(opts?: AIGatewayClientOptions) {
this.client = new AIGatewayClient(opts);
}

async listWorkspaces(options?: AiGatewayWorkspaceListOptions): Promise<AiGatewayWorkspace[]> {
const response = await this.client.workspaces.list(options);
return (response.data as Array<Record<string, unknown>>).map(normalizeWorkspace);
}

async listAllWorkspaces(): Promise<AiGatewayWorkspace[]> {
// No single call returns both states: the API filters to active unless
// asked otherwise, and only the admin plane sees the whole tenant.
const [active, archived] = await Promise.all([
this.client.workspaces.list({ plane: 'admin' }),
this.client.workspaces.list({ plane: 'admin', status: 'archived' }),
]);
return [
...(active.data as Array<Record<string, unknown>>),
...(archived.data as Array<Record<string, unknown>>),
].map(normalizeWorkspace);
}

async getWorkspace(
workspaceRef: string,
options?: AiGatewayWorkspaceGetOptions,
): Promise<AiGatewayWorkspaceDetail> {
const raw = (await this.client.workspaces.get(workspaceRef, options)) as Record<
string,
unknown
>;
return normalizeWorkspaceDetail(raw);
}
}
63 changes: 63 additions & 0 deletions src/airs/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1275,3 +1275,66 @@ export interface ManagementService {
// Scan logs
queryScanLogs(opts: ScanLogQueryOptions): Promise<ScanLogQueryResult>;
}

// ---------------------------------------------------------------------------
// AI Gateway
// ---------------------------------------------------------------------------

/**
* Which plane to route an AI Gateway workspace read through.
* `data` returns only workspaces the service account holds a workspace-scope
* grant on; `admin` returns every workspace in the tenant.
*/
export type AiGatewayPlane = 'data' | 'admin';

/** Normalized AI Gateway workspace list row. */
export interface AiGatewayWorkspace {
id: string;
slug: string;
name: string;
icon?: string | null;
description?: string | null;
createdAt?: string;
lastUpdatedAt?: string;
isDefault: boolean;
/**
* Lifecycle state. `get` can report `null` for a workspace `list` calls
* `active` — treat `null` as "unknown", never "inactive".
*/
status?: string | null;
/** SCM role scope granting data-plane access to this workspace. */
scopeName?: string;
}

/** Normalized AI Gateway workspace detail (list row + settings blocks). */
export interface AiGatewayWorkspaceDetail extends AiGatewayWorkspace {
defaults?: Record<string, unknown> | null;
/** Usage-limit policies, always an array (legacy single-object form is wrapped). */
usageLimits: Array<Record<string, unknown>>;
/** Rate-limit policies, always an array (legacy single-object form is wrapped). */
rateLimits: Array<Record<string, unknown>>;
securitySettings?: Record<string, boolean>;
dataPlaneSecuritySettings?: Record<string, unknown>;
settings?: Record<string, unknown>;
}

export interface AiGatewayWorkspaceListOptions {
plane?: AiGatewayPlane;
/** Omitting this returns active workspaces only — archived rows are hidden. */
status?: 'active' | 'archived';
}

export interface AiGatewayWorkspaceGetOptions {
plane?: AiGatewayPlane;
}

/** Service interface for AI Gateway operations used by the CLI. */
export interface AiGatewayService {
listWorkspaces(opts?: AiGatewayWorkspaceListOptions): Promise<AiGatewayWorkspace[]>;
/** Merge an active and an archived admin-plane read — no single call returns both. */
listAllWorkspaces(): Promise<AiGatewayWorkspace[]>;
getWorkspace(
workspaceRef: string,
opts?: AiGatewayWorkspaceGetOptions,
): Promise<AiGatewayWorkspaceDetail>;
}
Loading
Loading