airs-cli-rs
airs-cli-rs is a native Rust port of the TypeScript Prisma AIRS CLI, airs-cli. It
is built on the published scm-sdk crate and runs side by side with the TypeScript
CLI: the executable is named airs-cli-rs, so both can be on PATH at once. From 0.3
it keeps its own tenant store (TOML files under ~/.config/airs-cli/) and creates,
edits, selects and deletes tenants itself. It no longer reads the TypeScript CLI's
registry; tenant import copies tenants from it once (see
Upgrading from 0.2).
docs/DESIGN.md is the behavioural contract. Its §12 lists every intentional difference from the TypeScript CLI.
Status
Version 0.9 covers milestones 1–3, the M3.5 foundations, M4t tenant writes, M4 runtime writes, M5 bulk scanning, M6a Red Team, M6b DLP, M6c Model Security and M6d Runtime extras (docs/MILESTONES.md):
| Milestone | Commands |
|---|---|
| M1 tenant loading and diagnostics | tenant list, tenant path, tenant read, tenant get, doctor |
| M2 read-only management | runtime profiles list/get, runtime topics list/get |
| M3 scanning | runtime scan |
| M3.5 foundations | pretty, table and json output with the TypeScript CLI's layouts and colours for every command; --debug (a redacted API log, status and headers only); Linux musl builds |
| M4t tenant writes | its own TOML tenant store; tenant create, set, unset, switch, delete and tenant import |
| M4 runtime writes | runtime topics create/update/delete/apply/revert/eval/sample, runtime profiles create/update/delete/cleanup |
| M5 bulk scanning | runtime bulk-scan, runtime resume-poll |
| M6a Red Team reads | the red team reads: redteam list/status/categories/languages, eula status/content, instances get, prompt-sets list/get/download, prompts list/get, properties list/values, targets list/get/profile/metadata/templates/error-logs, adapter list/get, network-broker channels list/get, network-broker stats |
| M6a Red Team writes and scans | the red team writes and the scan: redteam scan (with its bounded wait), abort, eula accept, registry-credentials, instances create/update/delete, devices create/update/delete, prompt-sets create/update/archive/upload, prompts add/update/delete, properties create/add-value, targets create/update/delete/probe/update-profile/validate-auth/init, adapter create/update/delete/validate, network-broker channels create/update |
| M6a Red Team reports and backups | redteam report|dashboard <jobId> (one scan's report), redteam report (the HTML environment report), targets backup, targets restore |
| M6b DLP | runtime dlp filtering-profiles list/get/replace, patterns list/get/create/replace/patch/delete, profiles list/get/create/replace/patch (and profiles delete, which explains why the API cannot delete a profile), dictionaries list/get/create/replace/patch/delete, runtime dlp backup, runtime dlp restore |
| M6c Model Security | model-security groups list/get/create/update/delete, rules list/get, rule-instances list/get/update, scans list/get/create/evaluations/evaluation/violations/violation/files, models list/get/versions/version/files, labels keys/values/add/set/delete, pypi-auth, install (uv only, with --fetch-uv) |
| M6d runtime extras | runtime api-keys list/create/regenerate/delete, customer-apps list/get/update/delete/consumption, deployment-profiles list, scan-logs query (the TypeScript stub), dashboard applications/application/application-violations/top-applications/violations-trend/apps-list, sessions chart/list/get/transaction/scan-content, runtime report (the HTML environment report), runtime profiles backup, runtime profiles restore |
markdown, csv and yaml output are dropped, and so is runtime dlp generate. Planned
next: gateway and supplychain (0.10–0.11).
Prebuilt, signed binaries are attached to each
Forgejo release.
Install
From crates.io (needs Rust 1.89 or newer). The crate is airs-cli; until 1.0 the
executable it installs is airs-cli-rs:
Prebuilt binaries for Apple Silicon macOS (signed and notarised) and static Linux x86_64 and arm64 are attached to each Forgejo release:
From source: the SDK, scm-sdk =0.1.1, comes from crates.io, so the first build
needs access to the crates.io index.
This produces target/release/airs-cli-rs. On Apple Silicon it is a native arm64
executable. Copy it onto your PATH, or install it with cargo install --path ..
Upgrading from 0.2
Up to 0.2, airs-cli-rs read the TypeScript CLI's tenant registry directly. From 0.3 it reads only its own store, so copy your tenants once:
- It reads the registry the TypeScript CLI uses (
PRISMA_AIRS_TENANTS_PATH, else$XDG_STATE_HOME/prisma-airs/tenants.json, else~/.local/state/prisma-airs/tenants.json) and the JSON configs it names. Those files are only read: never changed, locked or created. - The report (a table;
--output jsonfor scripts) has one row per tenant with the actionimport,replaceorskipand notes naming renamed, dropped and unknown keys. It never shows a value. - Keys are converted:
aiGw*becomegateway*,agentGuard*becomesupplychain*;dataDir, thetypesafe*keys, empty values, unknown keys and adefaultOutputofmarkdown,csvoryamlare dropped, each with a note. - The TypeScript selection is copied when airs-cli-rs has none yet.
- An existing airs-cli-rs tenant is never overwritten unless you pass
--replace. - A tenant whose config cannot be read, or whose
mgmtTsgIddiffers from its registration, is skipped; the command then exits 1 after copying the others. --from <registry>reads another registry,--name <name>copies one tenant, and--from-config <file> --name <name>copies a single JSON config file (it replaces the TypeScripttenant create --config).- The copy is independent: later changes made with the TypeScript CLI are not seen.
Run
airs-cli-rs tenant import --replaceagain to refresh.
Until you import, airs-cli-rs doctor fails its Tenant check with this hint, and the
"no tenant selected" messages say the same.
Usage
Commands use the selected tenant of the airs-cli-rs store. --tenant <name> selects
another registered tenant for one invocation and leaves the selection unchanged (it is
refused by the commands that change tenants, which take the name as an argument).
Tenants
|
|
- Where tenants live.
config.toml(the selection and each tenant's pinned TSG) andtenants/<name>.toml(that tenant's settings, secrets included) in$AIRS_CLI_CONFIG_DIR, else$XDG_CONFIG_HOME/airs-cli, else~/.config/airs-cli. Files are mode 0600 and directories 0700. The setting keys are the TypeScript ones, withaiGw*renamedgateway*andagentGuard*renamedsupplychain*;dataDirand thetypesafe*keys are gone. - Values are checked when you enter them. Numbers are decimal integers in range,
endpoints must be https (http only for loopback hosts) without credentials, a query
or a fragment,
mgmtTsgIdis 1–32 digits, anddefaultOutputispretty,tableorjson. An empty value is refused; usetenant unsetto remove a setting. A prompt asks again and says why. - Secrets never go on the command line.
tenant set <name> <secret key> <value>is refused; omit the value for a hidden prompt, or pipe it with--stdin. Prompts need stdin and stderr to be terminals; in scripts use the flags and--stdin. - Deleting removes the tenant's file and clears the selection if it was selected.
Without a terminal it needs
--force. - Outcome lines (
✓ …,ℹ …) go to stderr and--quiethides them; stdout stays empty. - Concurrent changes are safe. Every change takes one lock (
config.lock; a second change waits up to 5 s and then names the process holding it) and replaces files atomically, so a reader never sees a half-written file and a crash never leaves a stale lock.
Doctor
|
The doctor runs seven checks: tenant, config file, environment, scanner credentials, management credentials, scanner API and management OAuth. It exits 1 when any check fails. The TypeScript doctor's Node.js, Typesafe and AI Gateway checks are not ported (D19).
Profiles and topics
Without --output, these commands use the tenant's defaultOutput (pretty, table
or json), else pretty: a banner, then one entry per profile or topic (lists) or a
key/value block (gets). --output table prints a box-drawing table (ID │ Name │ … for
lists, Key │ Value for gets), and --output json a JSON array for lists and a JSON
object for gets. With --all-versions, a get prints the list layout of every matching
revision. A status line such as Showing 20 (more available) goes to stderr in every
format. A defaultOutput of markdown, csv or yaml makes the tenant config invalid
(D51).
Writing profiles and topics
- Every check runs before the tenant is read. Topic limits (UTF-8 bytes: name 100,
description 250, 2–5 examples of 250, 1,000 together), UUIDs,
--updated-bywith--force, a missing policy flag,--max-inline-latency(a whole number of seconds) and--dlp-profiles/--mask-data-inlinewithout--dlp-actionare usage errors (exit 2).--configfiles are read before the tenant too (at most 1 MiB, a UTF-8 JSON object);--configcannot be combined with other body flags. - A write is sent once. It is never re-sent after a timeout or a 5xx, so a create
can never be duplicated; only a 401 is replayed once with a fresh token. When the
service may have applied a failed write, the error says
Outcome unknown: the topic may have been created.; a reply that could not be read after a success says so too. Exit 1 in both cases. - Updates change only what you ask for.
profiles updatekeepsactiveunless--active/--no-activeis given, keeps the DLP profiles when only--dlp-actionchanges, and refuses an update with nothing to change.topics createandprofiles updateact on the highest revision of a name. - Confirmations (
deletewithout--force) need stdin and stderr to be terminals; the prompt is on stderr and onlyy/yesconfirms. In scripts pass--force(with--updated-by <email>, which selects the force endpoint). - Applying and reverting topics.
topics applyreads the topics, then the profile's highest revision, and writes that profile once with the topic's latest revision under its intent; other topics' entries stay exactly as they were, and nothing is written when the topic is already there. The service has no conditional write, so a change someone else makes to the profile between that read and the write is overwritten (the window is one round trip).topics revertremoves the topic from the profile (thetopic-guardrailsentry goes with its last topic), then deletes the topic with a plain delete. A topic that is still referenced (by another profile, or by an older revision of this one, since every profile update creates a new revision) is kept with a warning and the command still exits 0; runprofiles cleanup, thentopics delete. It asks after looking the topic up. - Output. Outcome lines (
✓ Topic created: …) go to stderr (--quiethides them); stdout carries only data: the topic record or the profile detail, inpretty,tableorjson(the tenant'sdefaultOutputis not used). The deletes print no data. The TypeScript CLI's hidden--formatstill works ontopics create,apply,revertandeval, with its deprecation warning.
Cleaning up old profile revisions
For every profile name with several revisions, the highest revision is kept and the
older ones are deleted, one at a time. A name whose revisions are missing a number or
an id, or whose highest revision is tied, is skipped with a warning. The listing must
be complete, or nothing is deleted. Without --updated-by the deletions are plain
deletes, so a revision a policy still references fails with its 409 and is kept;
--updated-by <email> uses the force endpoint, which detaches the revisions from
referencing policies (there is no git config fallback). The first deletion whose
outcome is unknown stops the run; the summary counts deleted, failed, unknown and not
attempted revisions, and anything but a clean run exits 1. The question needs stdin
and stderr to be terminals; after y the listing is read again, and nothing is
deleted if it changed. Without a terminal, pass --force or --dry-run.
Evaluating a topic
The CSV needs prompt, expected and intent columns (any order, header
case-insensitive), one intent (block or allow) for every row, and at least one row
the topic should and one it should not trigger on. The file and every prompt are checked
before anything is sent. Each row is one scan (with scanner credentials); --concurrency
(1–32, default 5) bounds the scans in flight and --rate the scans started per second.
A scan that does not complete (an error, a timeout, an unknown action or a failed
request) stops the run with no metrics, since counting it would inflate the results.
The output is the TypeScript layout: coverage, TPR, TNR, F1, the counts and the false
positives and negatives.
Scanning
Each scan sends exactly one request and never retries it. A scan that fails, times
out or returns an unknown action is shown as FAILED and exits 1; it is never shown as
allowed (D6). Response detections are listed as response.<key> (D7).
Bulk scanning
--file is a .csv (its prompt column) or a text file (one prompt per line), at most
16 MiB and 10,000 prompts, read before the tenant. Prompts are submitted through the
async API in chunks of up to 20, one --batch-size batch (default 25) at a time, and
polled until every row is resolved. The results go to --output-file (default
<profile>-bulk-scan.csv in the current directory) in the TypeScript CSV layout; an
existing file is never overwritten (D121). stdout shows the summary (--output json and
table work too, D130); a failed or timed-out row is written as failed and makes the
command exit 1 (fail closed).
Each job keeps a private state file in $XDG_STATE_HOME/airs-cli/bulk-scans/ (default
~/.local/state/airs-cli/bulk-scans/; 0700 directory, 0600 files) that is written durably
before every submission and before every poll (D118), with a per-job lock (D119). A
submission that may have reached AIRS (a server error, a dropped connection, Ctrl-C while it
is in flight) is recorded as ambiguous and never submitted again; a refused or rate-limited
one stays pending. Ctrl-C saves the job and names its state file (D129). State files keep
the prompts; delete a finished job's files when you no longer need them.
resume-poll continues a stopped job: it polls the receipts already accepted, submits
only the prompts whose submission was refused or never sent, and rewrites the job's CSV
with every result (the same bytes again on a finished job, with no request). It resumes
only a job in its state directory, under the tenant and scan endpoint that started it
(D127); a TypeScript airs-cli state is refused (D118). When a submission's outcome is
ambiguous, it first recovers the results of the accepted receipts, then stops and names
the lowest such prompt: nothing is ever resubmitted automatically, so review the session's
scans in the AIRS dashboard. --output-file moves the job's output to a new file; an
existing other file is never overwritten (D121). A second run on the same job is refused
at once, naming the process that holds it (D119).
Red Team (reads)
The red team commands use the selected tenant's redTeamDataEndpoint,
redTeamMgmtEndpoint and redTeamNetworkBrokerEndpoint (else the AIRS defaults) with its
management credentials. Every read prints pretty, table and json; the lists and gets
honour the tenant's defaultOutput. Ids, numbers and filters are checked before the tenant
is read (a bad UUID is a usage error, exit 2). Target, adapter and error-log documents never
show a credential: auth settings, secret headers, curl, provider keys, secret-named keys and
SECRET adapter variables are [REDACTED] in every format (D140). Lists are complete or an
error: prompt-sets list reads every page and an --all walk that meets an empty page before
the reported total fails (D147). prompt-sets download never overwrites: an existing path is
refused before any request, the template is written at mode 0600, and --output-file -
prints it (D142). See DESIGN §15 for the whole contract.
Red Team (writes and scans)
Every red team write is sent once: a 5xx, a timeout or a lost connection is never re-sent;
the error says the outcome is unknown, and for a scan start, an abort, the EULA and an upload
it names the command that shows what happened (D133). Only a 401 is replayed, once, with a
fresh token. scan waits with progress on stderr for at most --wait-timeout minutes
(default 1440); a scan that is still running when the wait ends, fails or is interrupted is
reported as started and never aborted; ABORTED exits 1 and PARTIALLY_COMPLETE exits 0
with a warning (D135, D136). abort, eula accept and the deletes ask on a terminal; scripts
pass --force (without a terminal and without --force they are usage errors, D137, D138).
devices delete refuses a --serial-numbers list that names no serial number, and
eula accept an empty EULA (D158, D159).
Outcome lines and hints go to stderr; stdout holds the data in the chosen format. The
registry token is shown only with --output json (D139); credentials in answers and the
validate-auth token preview are [REDACTED] (D140). Input files (--config, --goals,
--script-file, --variables-file, the upload CSV) are read before the tenant with size caps;
SECRET adapter variables are refused on the command line, so put them in a
--variables-file (D143, D154). targets init never overwrites and writes mode 0600 (D150).
Red Team (reports, backups and restores)
A scan job report prints the TypeScript layout, or --output json/table (D144). The
environment report reads seven sources (aggregates only, no names, ids or settings) and is
written as self-contained HTML only: --output is refused, and so is --debug (D155, D156);
it never overwrites a file and is mode 0600. targets backup writes one airs-cli-rs file per
run into an existing directory (default the current directory), mode 0600, never
overwritten; it holds every credential the service returns, so keep it private (D151). It
stores only the settings the service sent. A target whose credentials the service withholds
(masks) is stored as returned, marked credentialsWithheld and listed with ⚠; restore
refuses it, so recreate it or set its credentials by hand (D157). targets restore reads only such a file (a
TypeScript backup is refused with a message), checks that it comes from the selected tenant's
TSG (--allow-other-tenant overrides), refuses duplicate or ambiguous names and withheld
credentials before any write, offers --dry-run, asks before --overwrite on a terminal
(--force for scripts), sends each write once and stops at the first unknown outcome (D152).
DLP
The DLP commands use the selected tenant's dlpEndpoint (else
https://api.dlp.paloaltonetworks.com) with its management credentials. Every read prints
pretty, table and json and honours the tenant's defaultOutput. Pattern, profile and
dictionary listings show the tenant's own (custom) records by default, read from every page;
--include-predefined shows the service's pages as they are. Ids, numbers and --sort are
checked before the tenant is read (exit 2). JSON is the full record in camelCase, with the
keys of your tags and exclusion lists kept as they are (D171); keywords are fetched only with
--keywords. DLP records hold detection content (regexes, keywords): it is printed only by the
json and table reads and never in an error or the --debug log.
The writes build their body from flags, or take it whole from --body <json>, --body -
(piped stdin) or --body-file (never both; patches take --set key=value/--clear key or
--body-file). Every argument and body is checked before the tenant is read. Before a
replace, patch or delete the record is read: predefined (PANW-shipped), parent-managed and
unfamiliar-type records are refused, and nothing is sent. Deletes and replaces ask for
confirmation on a terminal and need --force otherwise; a replace names the fields it would
drop. Each write is sent once (a 401 is retried once with a fresh token): when its outcome is
unknown the error says so and, for a create, how to check before creating it again.
patterns replace keeps the pattern's type and technique unless you give them. The API has no
data-profile DELETE, so profiles delete explains that and sends nothing (exit 2). Outcome
lines go to stderr; --output json and table print the acknowledgement on stdout.
runtime dlp backup writes one airs-cli-rs file (mode 0600, never overwriting anything; the
path is checked before the tenant is read) holding your custom dictionaries with their
keywords, patterns and profiles, with every predefined record a profile references as a
reference only. It records what it could not store (skipped, failed, complete); a
dictionary whose keywords cannot be read is a failed row (exit 1). The file holds detection
content in clear, so keep it private. runtime dlp restore <file> reads only airs-cli-rs
backups (a TypeScript backup is refused), refuses a backup from another TSG unless you pass
--allow-other-tenant, plans every row against the destination before any write (predefined
references resolved by id or name, --pattern-map for the rest, --name-prefix for copies),
offers --dry-run, asks on a terminal (--force --expect-tsg <tsg> for scripts), only ever
creates (never updates or deletes), sends each create once and stops at the first failure,
naming a resource that may have been created. --on-conflict reconcile gives an
archived-name collision a deterministic suffix, so a re-run verifies that copy instead of
creating another. See DESIGN §16 for the whole contract.
Model Security (reads)
The model-security commands use the selected tenant's modelSecMgmtEndpoint (groups, rule
instances, rules) and modelSecDataEndpoint (scans, models, labels), else
https://api.apps.paloaltonetworks.com/aims/mgmt and …/aims/data, with its management
credentials. Every read prints pretty, table and json (the scan sub-reads and the label
lists too, which the TypeScript CLI prints in pretty only, D189); the group, rule,
rule-instance, scan and model reads honour the tenant's defaultOutput. UUIDs, the time
window, numbers and filters are checked before the tenant is read (exit 2, D184). Nothing the
service did not send is printed: a scan summary without counts shows no Rules line, and its
json counts are null (D180). Model URIs are printed with their credentials, query and
fragment [withheld] (D191). --all walks every page and fails rather than print a partial
list; every list honours --offset, --all and --max (D183); a page that does not show
everything says so on stderr. scans list --scan-origin filters exactly over every page
(the service has no such filter, D181). See DESIGN §17 for the whole contract.
Model Security (writes)
Every write is sent once: after a timeout, a 5xx or a dropped connection the CLI says the
outcome is unknown instead of re-sending (a 401 is retried once with a fresh token), so a
billed scans create is never sent twice (D179). groups delete, labels set (which
replaces every label and names the ones it drops) and labels delete ask on a terminal and
need --force elsewhere (D185). groups delete re-reads the group and reports deleted only
on a 404 or a tombstone; a group the service still shows gets a warning (D186). A group config
takes name, source_type, description and rule_configurations, a rule-instance config
state, field_values and security_group_uuid; other keys, a file that changes nothing and a
groups update without a flag are refused before the tenant is read, and no file or label
content is ever echoed in a message (D187, D188). --description '' clears a description. The
written record prints in the chosen --output on stdout, the ✓ line on stderr (D189, D190).
Model Security PyPI access and install
The PyPI index URL carries a short-lived access token. pypi-auth prints it with the credential
[withheld] in every format; the only way to get the full URL is --url-file <path>, a new file
created with mode 0600 that is never overwritten (D194). install uses uv only (0.12.0 or
later on PATH; TypeScript's python/pip fallback is gone, D195): it runs uv init --no-workspace -- <dir> and uv add --project <dir> --index airs-model-security=<index> -- 'model-security-client[<extras>]', handing the credential to uv add only through
UV_INDEX_AIRS_MODEL_SECURITY_USERNAME/_PASSWORD in its environment, never in its arguments,
and checks afterwards that pyproject.toml and uv.lock do not hold it (D196). --dir must not
exist yet and must not lie inside a uv workspace (D198). uv's output is scrubbed and goes to
stderr; stdout stays empty (D199). Ctrl-C is passed on to uv, which the CLI waits for (D200).
--dry-run prints the commands with the credential withheld, quoted for a POSIX shell (D197).
--fetch-uv downloads uv 0.12.17 for your platform into $XDG_DATA_HOME/airs-cli/tools
(else ~/.local/share/airs-cli/tools), verifies its pinned SHA-256 before extracting, and reuses
a copy that verifies. Later uv commands in the project need the credential again: set the two
variables from the URL pypi-auth --url-file saves.
Runtime extras (reads)
API keys, customer apps and deployment profiles use the tenant's mgmtEndpoint; the dashboard
and sessions commands, and consumption, use its mgmtDashboardEndpoint with the same OAuth
identity. An API key value that a read carries is never printed (D205); auth codes are printed on
stdout only. consumption looks the app up over the --time-interval window and refuses a name
two applications share (D211); without a name it prints one JSON document and exits 1 when an app
failed (D210), each app's reads having their own 60 s request budget however many apps the tenant
has. The dashboard and sessions commands print the response as JSON text in pretty
and json (control characters escaped) and also support table (D212); any failure after the
client is built is TypeScript's one Dashboard operation failed line. sessions list --all keeps
TypeScript's page checks and exits 1 after printing a partial list at its item or page cap.
sessions scan-content prints the stored prompt and response only with --show-content, or
writes them into a new 0600 --output-file, checked before any request (D225). scan-logs query
is TypeScript's stub: it sends nothing and exits 1 with a pointer to sessions list.
Runtime extras (writes and the report)
An API key value is shown by the service only once, so create and regenerate need
--key-file <path>: a new file that is created with mode 0600 and never overwritten, checked
before the tenant is read and reserved before the request. The key goes only into that file; the
command prints the key's id, name, …<last8> and the file's path (D204). If the answer's other
fields cannot be read, the key is still saved; if the path appeared while the request was in
flight, the key stays in the reserved .airs-*.tmp file next to it and the error names it.
regenerate, both deletes and update ask for confirmation (--force skips it; without a
terminal they need --force, D206). update replaces the whole record: its prompt names the
fields the file drops and a rename. Each write is sent once (a 401 is replayed once with a fresh
token); a timeout or 5xx says the outcome is unknown and what to check (D95). The writes print
their record in pretty, table or json (D223), and their outcome lines on stderr.
runtime report reads seven sources (application activity, security profiles, registered
applications, sessions, the session chart, top violations and the violation trend) and writes one
self-contained HTML file (strict Content Security Policy, no network access) that is new, 0600 and
never overwritten. The destination and its directory are checked before any request (D214). It
keeps aggregates and names only; a source that could not be read is shown as unavailable, never as
zero. It exits 1 when no source could be read, or with --strict when one is incomplete.
--output and --debug are refused (D155, D156).
Profile backup and restore
backup saves the latest revision of each selected profile and the exact topic revisions they
reference in one airs-cli-rs file (airs-cli-rs.runtime-profiles, JSON only): new, mode 0600,
never overwritten, its path and directory checked before the tenant is read. The records are
stored as the service sent them, with no invented defaults (D215). restore reads only such a
file (a TypeScript backup is refused, D216): a backup from another TSG needs
--allow-other-tenant, --expect-tsg must name the selected tenant's TSG, and without a
terminal the restore needs --force. It plans every profile and topic before the first write
(--on-conflict error|verify|skip|update, custom DLP through --dlp-map source=destination or an
explicit --on-missing-dlp basic), rechecks the destination, then sends each create or update
once and stops at the first failure; a write whose outcome is unknown is named, and a re-run with
--on-conflict verify resumes without duplicating anything (D217). An absent profile or topic
list in an answer is an error, never an empty inventory (D218). --debug is refused (D156).
Global flags
| Flag | Meaning |
|---|---|
--output <format> |
pretty, table or json, on every command. Accepted before or after the subcommand; the last one wins. Any other value (including the dropped markdown, csv and yaml) is a usage error, exit 2. tenant path prints the path whatever the (valid) format. |
--quiet |
Suppress status lines, banners and headings. Data (including the IDs in pretty lists) and errors still print. |
--debug |
Write redacted API diagnostics to a private JSONL file: one record per HTTP attempt (token requests, retries and replays included) with the method, the URL without its query, the status and allow-listed header values. Bodies, queries and credentials are never written. See Debug log. Accepted and ignored by tenant commands. |
--tenant <name> |
Use this registered tenant for one invocation (Rust-only, D33). |
-V, --version |
Print the version. |
-h, --help |
Print help. |
A positional prompt that starts with - needs -- before it. Option values may start
with - (--response -x is a value).
Colour follows the TypeScript CLI's styles, decided separately for stdout and stderr:
on for a terminal (with TERM set and not dumb), off for pipes and files.
NO_COLOR turns it off, CLICOLOR_FORCE turns it on even when piped, and CLICOLOR=0
turns it off. Tables, JSON and bare values are never coloured.
Behaviour
- Configuration comes only from tenant files. Credentials and endpoints are read
from the selected tenant's file in the airs-cli-rs store (see Tenants). The
TypeScript registry (
PRISMA_AIRS_TENANTS_PATH, else$XDG_STATE_HOME/prisma-airs/tenants.json, else~/.local/state/prisma-airs/tenants.json) is never used to select a tenant; when the store is empty but that registry exists, messages say to runairs-cli-rs tenant import. Credential and endpoint environment variables (PANW_*,PRISMA_AIRS_CONFIG_PATHand the other retired names) are ignored; the doctor's Environment check lists the ones that are set. - Two credential planes. Management commands authenticate with the tenant's
mgmtClientId,mgmtClientSecretandmgmtTsgId(OAuth). Scanning usesairsApiKeyorairsApiToken. Neither plane sends the other's credentials, and each only requires its own. - Wrong-tenant protection. A tenant file whose
mgmtTsgIddiffers from the TSG pinned inconfig.tomlis refused;tenant set <name> mgmtTsgId <tsg>changes both. - Store safety. Store files must be regular files (not symlinks) owned by you with
no group or other permissions; directories must not be writable by others. Only the
tenant write commands and
tenant importchange the store, and they never touch the TypeScript CLI's files. - Streams. stdout carries data only. stderr carries status lines (suppressed by
--quiet) and errors. A failing command leaves stdout empty, with two exceptions:doctorprints its report and exits 1 when a check fails, a scan whose verdict isfailedprints itsFAILEDresult and exits 1 (D6), andruntime bulk-scanandruntime resume-pollprint their summary and exit 1 when a row failed. - Secret safety. Output never contains secrets, tokens, raw response bodies or URL
queries.
tenant readandtenant getredact secret values and the userinfo, query and fragment of URLs (D45). - Endpoints. Every configured endpoint must use https; plain http is accepted only for loopback hosts. Endpoints with credentials, a query or a fragment are refused.
Debug log
--debug on doctor, the runtime profiles|topics commands (all but topics sample), runtime scan, runtime bulk-scan, runtime resume-poll and the redteam commands writes
debug-api-<epoch ms>-<8 hex>.jsonl to $XDG_STATE_HOME/airs-cli/debug/ (default
~/.local/state/airs-cli/debug/), not to the current directory as the TypeScript CLI
does (D61). The directory is created 0700 and the file 0600; only the newest 20 logs are
kept. stderr names the file ( Debug: API log → <path>, hidden by --quiet). Each line
is one attempt:
Header values outside a short allow-list (content-type, user-agent, date,
retry-after, x-request-id, …) are written as ***, which covers authorization,
x-pan-token and x-payload-hash. No request or response body and no URL query is ever
written, so prompts, profiles and tokens never reach the file (D64, D65). A log that
cannot be created fails the command before any request (exit 1, D68); a write that fails
later only prints ⚠ Debug: the API log could not be written; it is incomplete and
never changes the result (D67). Under --debug, error blocks omit the
Re-run with --debug … hint (D69).
Exit codes
| Code | Meaning |
|---|---|
| 0 | Success. A scan whose verdict is block also exits 0. |
| 1 | Operational failure: store, lock, tenant or tenant-file errors, missing credentials, HTTP, network or timeout errors, not found, failed scans, a failed doctor check, a debug log that cannot be created, a bulk-scan input, state, lock or output error, a bulk submission that was refused or whose outcome is unknown, a failed bulk-scan row, a resume-poll of a state it may not resume or of a job with an ambiguous submission. |
| 2 | Usage error, including every argument-parse error, the bulk-scan argument checks and an input file with no prompts, and every input error of a tenant write command (a bad name, key or value, a secret on the command line, no terminal for a prompt, delete without --force when not interactive). Usage is checked before the tenant store is read, so a usage error never makes a request or a change. |
| 130 | Interrupted by SIGINT, or a prompt cancelled with Ctrl-C or Ctrl-D (Cancelled; no configuration saved.). In-flight requests are cancelled first. |
| 143 | Terminated by SIGTERM. In-flight requests are cancelled first. |
The TypeScript CLI exits 1 on argument-parse errors (D1).
Documentation
- docs/DESIGN.md: the contract. Exact messages, exit codes, request
shapes, and in §12 every intentional difference from
airs-cli. - docs/COMPATIBILITY.md: each behaviour and each §12 row, with the tests that pin it.
- docs/BENCHMARKS.md: measured performance of both CLIs, on loopback and live.
- docs/MIGRATION.md: taking over the
airs-cliname, retiring the TypeScript CLI, and movingprisma-airs-harnessto Rust. - docs/TESTING.md: the test tiers, the TypeScript reference build, the live comparison and how to add tests.
- docs/MILESTONES.md: release scope and later milestones.
- AGENTS.md: invariants and the checks every change must pass.
- docs/research/: the subsystem research the design was derived from, with citations into both code bases.
Development
AIRS_REQUIRE_PARITY=1
See docs/TESTING.md for details. The live tier (a real tenant; the
maintainer runs it) has a read-only comparison, an M4 write flow on run-created
airs-rs-e2e-… resources with a ledger in target/live/ledger.jsonl, a sweeper for
leftovers older than an hour, an opt-in eval, and an opt-in bulk flow (at most 8 billed
prompts) that interrupts a bulk scan and proves resume-poll recovers it without a new
submission (TESTING §6).
License
Apache-2.0.