Command-Line Tools (CLIs)
Expose a vendor's own command-line tool to agents with defineCli, governed at the network request rather than the command line.
An extension can ship a vendor's command-line tool — gh, az — as a
single agent tool, with no hand-written tool per command. Scrydon does not try
to understand the command line. It governs the HTTPS requests the CLI makes:
every request is intercepted, classified as a read, a write or a deletion,
checked against what the workspace allows, and sent with your organization's
credential stamped on by the platform. The CLI itself never holds a real
credential.
Only reviewed Scrydon releases may declare a CLI in v1.0.0. Intercepting
and stamping a CLI's traffic gives an extension more reach than its explicit tools
get, so a CLI declared by an organization-installed extension is refused
before it runs, as CAPABILITY_NOT_CONFIGURED
(Tool execution errors).
This page documents the contract those releases follow, and what administrators
control.
When to ship a CLI instead of tools
Ship a CLI when all of these hold:
- The vendor maintains a complete CLI, and hand-writing a tool per command would mean chasing its surface forever.
- The CLI talks to a REST or GraphQL API over HTTPS/1.1 on port 443.
- The CLI reads its token from an environment variable, or signs in with OAuth client credentials against a token URL.
- It runs non-interactively: no TTY, no browser sign-in, closed standard input.
Prefer explicit tools when you need a narrow, typed surface, or when the vendor has no CLI. The two are additive — an extension can ship both.
The three facts, using gh
A CLI that reads a token from an environment variable needs three facts and nothing else:
import { defineCli } from "@scrydon/sdk-authoring/extensions/sdk/define-cli";
import ghHelpTree from "./cli/gh.help.json" with { type: "json" };
// inside defineExtension({ … provides: { … } })
provides: {
toolkits: [githubToolkit],
clis: [
defineCli({
id: "gh", // 1. the CLI's id
binary: { image: "gh", entrypoint: "gh" }, // 2. which image, which program
credentialEnv: ["GH_TOKEN"], // 3. where it expects a token
env: { GH_PROMPT_DISABLED: "1", GH_TELEMETRY: "false", NO_COLOR: "1" },
helpTree: ghHelpTree,
}),
],
},| Field | Meaning |
|---|---|
id | Lowercase, 2–32 characters (a-z, digits, -, starting with a letter). Unique within the extension. |
binary.image | A catalog key for a CLI image the platform operator pinned by digest — never an image reference. An extension cannot introduce its own image. |
binary.entrypoint | The program to run inside that image ([A-Za-z0-9._-], no path). |
credentialEnv | Up to 8 environment variable names. Each receives an inert, per-run placeholder so the CLI believes it is signed in. The real credential is added to the request by the platform, only on hosts bound to it. |
helpTree | The CLI's own --help, crawled offline from the pinned image and committed beside the extension. It becomes the tool description. At most 512 commands, and at most 3 levels deep. |
helpCrawl.maxDepth | Optional, 1–3. How many levels of subcommands the help crawl walks for this CLI. Set it when the full tree does not fit 512 commands — see Large help trees. |
endpointEnv | Optional. The environment variable that receives the host of the organization's configured endpoint — GH_HOST for gh. Only the name is authored; the platform fills the value from the organization's endpoint, never from the manifest or the call. See endpointEnv. |
toolkit | Optional. The id of the toolkit whose connection the CLI runs with and whose enablement lists it. Leave it out when exactly one toolkit uses auth.default; set it when two or more do — see Which toolkit owns a CLI. |
The hosts a CLI may reach are the extension's declared egress hosts — the same list its explicit tools use. A CLI adds no host of its own.
credentialEnv is optional. A generic HTTP client such as curl never reads a
token from its environment, and it does not need to: the platform adds the
credential to each request on the wire either way. The Atlassian extension
ships curl with an id, a binary, the one host its credential is for, and a
help tree:
defineCli({
id: "curl",
binary: { image: "curl", entrypoint: "curl" },
env: {},
// Jira and Confluence both run on the Atlassian Cloud sign-in, so the CLI
// names the toolkit it belongs to.
toolkit: "jira",
// The Atlassian Cloud sign-in is valid on the Jira/Confluence gateway only;
// the extension also reaches api.bitbucket.org, a separate sign-in.
credentialHosts: [{ hosts: ["api.atlassian.com"] }],
helpTree: curlHelpTree,
})Large help trees
The help crawl walks up to 3 levels of subcommands and keeps at most 512
commands. A CLI with more than that — az has about 1,600 — declares a smaller
depth instead of being cut off part-way through a level:
defineCli({
id: "az",
// …
helpCrawl: { maxDepth: 1 },
helpTree: azHelpTree,
})The crawl reads maxDepth from the manifest, stops at that depth, and records
max_depth as the reason the tree is partial. That reason is accepted because
the same CLI version always produces the same tree at the same depth. A tree
cut short any other way — by the crawl's time, command or size budget — can
differ from one run to the next and is refused when the extension loads. So is
a tree deeper than the declared maxDepth. The tool description tells the
model the list is partial, and <command> --help still answers for every
command.
An Authorization header the agent passes on the command line is removed
before the platform's credential is added, so it can neither replace nor leak
alongside the real one.
An extension may declare up to 8 CLIs, and an extension whose only surface is a CLI is valid.
Declarative extras, using az
Some CLIs cannot be handed a token: az insists on signing in itself. Three
optional fields cover this without any az-specific code:
defineCli({
id: "az",
binary: { image: "az", entrypoint: "az" },
bootstrap: {
tokenEndpoint: "credential.tokenUrl",
preamble: [
"login", "--service-principal",
"-u", "{clientId}", "-p", "{dummy}", "--tenant", "{tenantId}",
"--allow-no-subscriptions",
],
},
credentialHosts: [
{ hosts: ["management.azure.com"], scopes: ["https://management.azure.com/.default"] },
{ hosts: ["graph.microsoft.com"], scopes: ["https://graph.microsoft.com/.default"] },
],
env: {
AZURE_CORE_COLLECT_TELEMETRY: "no",
AZURE_CORE_ONLY_SHOW_ERRORS: "yes",
AZURE_CORE_DISABLE_CONFIRM_PROMPT: "yes",
},
helpTree: azHelpTree,
})bootstrap
tokenEndpoint: "credential.tokenUrl"— the platform answers the CLI's sign-in request to the credential's own token URL itself, with a placeholder access token. The real token is minted by the platform and never enters the sandbox, so no command —az account get-access-tokenincluded — can print it. Discovery requests to the sign-in host pass through unauthenticated.preamble— 1 to 32 arguments run as a separate invocation of the same program before the agent's command, in the same sandbox. It is invisible to the model. Each argument is refused if it contains shell metacharacters, looks like a secret, or uses a placeholder other than{clientId},{tenantId}or{dummy}.
A CLI that signs in this way needs the connection to use an application identity (for Azure, an Entra service principal).
credentialHosts
credentialHosts says which hosts receive a credential, and which one. Each
entry takes one of two forms:
- Hosts only —
{ hosts: ["api.atlassian.com"] }. The connection's own credential is added on exactly these hosts, and on no other host the extension reaches. Nothing is minted, so any connection type works. - Hosts and scopes — one connection can need a different token per host:
Azure issues one audience per token. The platform mints a token for the
entry's scopes and adds it on the entry's hosts. This form needs an
application identity, like
bootstrap. A connection counts as one only when it was connected with the OAuth client-credentials grant. That is read from the stored connection, never from the manifest or the request. Any other connection is refused withIDENTITY_NOT_CONFIGURED. The platform mints each token itself, only for scopes the extension's manifest declares, and the application's secret never leaves the platform. Withbootstrap, the CLI signs in with that application's client id and tenant.
The rules for both:
- up to 16 entries, each with 1–16 hosts and, when present, 1–8 scopes;
- hosts are exact, lowercase DNS names — no wildcard, no IP literal, and no host may appear in two entries;
- every host must be one the extension is allowed to reach.
A wildcard is refused on purpose: suffixes like *.core.windows.net are shared
by every customer of the vendor, and a credential bound to one would follow a
request to any of them.
When the organization configured its own endpoint for the extension (a
GitHub Enterprise Server host, for example), the connection's credential is
added on that endpoint only, and never on the extension's public default
host. This applies both without credentialHosts and to entries that list
hosts only; entries with scopes still get a token for the audience they name.
Without credentialHosts, and with no organization endpoint, the
connection's credential is added on the extension's one host. When the extension's
own egress list has more than one host, the platform does not guess which of
them the credential belongs to: the call
fails with TOOL_VENDOR_CLI_CREDENTIAL_HOSTS_REQUIRED until the CLI declares
its hosts. The Atlassian extension is the example: it also reaches
api.bitbucket.org, which uses a separate Bitbucket sign-in, so its curl
names api.atlassian.com alone and a request to Bitbucket carries no
credential.
env
Static, non-secret configuration, up to 1,024 characters per value. The
platform owns — and refuses — proxy variables, certificate-trust variables,
PATH, HOME, TMPDIR and loader variables. It also refuses variables that
change execution or certificate trust (BASH_ENV, NODE_OPTIONS, PAGER,
EDITOR, BROWSER, *_NO_VERIFY-style switches and similar), secret-shaped
values, and any name also listed in credentialEnv.
endpointEnv
Some CLIs are pointed at a self-hosted instance by an environment variable:
gh reads GH_HOST to talk to a GitHub Enterprise Server instead of
github.com. endpointEnv names that variable, and the platform sets it to the
host of the endpoint the organization configured for the extension (a
connection setting of kind endpoint):
defineCli({
id: "gh",
binary: { image: "gh", entrypoint: "gh" },
// gh reads GH_ENTERPRISE_TOKEN for a GHES host, GH_TOKEN for github.com
credentialEnv: ["GH_TOKEN", "GH_ENTERPRISE_TOKEN"],
endpointEnv: "GH_HOST",
// …
});| The organization configured | The CLI sees |
|---|---|
| No endpoint | No such variable. The CLI keeps its public default, and the credential goes to the extension's own host. |
| One endpoint | The variable, set to that endpoint's host (for example ghes.example.com). The credential is added on that host only. |
| More than one endpoint | Nothing — the call fails with TOOL_VENDOR_CLI_NO_BINDABLE_CREDENTIAL. The platform does not guess which endpoint the CLI should target. |
The value comes only from the organization's configuration. The call cannot
set it: the agent passes arguments and standard input, never environment
variables. The name follows the same rules as env, and must not also be a
credentialEnv name or a key of env.
List every token variable the CLI reads for the self-hosted host in
credentialEnv too — here GH_ENTERPRISE_TOKEN — so the CLI finds a
placeholder and sends the request, and the platform adds the real credential.
The first-party GitHub extension declares endpointEnv: "GH_HOST", but its
connection does not offer an endpoint setting, so its gh always targets
github.com. GitHub Enterprise Server is not supported in v1.0.0.
Wire policy
Every request the CLI makes is classified before it leaves the platform. With no configuration at all:
| Request | Class |
|---|---|
GET, HEAD | read |
POST, PUT, PATCH | write |
DELETE | destructive |
A _method override field in the query string, form, JSON or multipart body | the higher of the real method's class and the overridden one — an override never lowers the class |
| Anything the classifier cannot read | write — never read |
Method-override headers (X-HTTP-Method-Override and its variants) are
removed before the request is sent, so they can neither hide a deletion nor be
honoured by the vendor.
The GraphQL rule. Method and path say nothing about a GraphQL call, so any
POST or GET whose last path segment is graphql is parsed:
- a
queryisread; - a
mutationorsubscriptioniswrite; - a batch takes the highest class of its operations;
- a body that does not parse is at least
write.
wirePolicy.overrides
Some APIs read with POST, or delete with something other than DELETE.
Overrides re-class specific requests, up to 256 per CLI:
wirePolicy: {
overrides: [
// Jira search is a POST under a per-site id; it only reads.
{
match: { method: "POST", pathPattern: "/ex/jira/*/rest/api/3/search/jql" },
wireClass: "read",
},
// A GraphQL root field that deletes.
{ match: { graphqlOperation: "deleteRepository" }, wireClass: "destructive" },
],
},| Match field | Meaning |
|---|---|
host | Exact host. |
method | GET, HEAD, POST, PUT, PATCH or DELETE. |
pathPrefix | The path starts with this (canonical form, starting with /). |
pathPattern | The whole path, where * matches exactly one segment. No regular expressions. |
graphqlOperation | A GraphQL root field (such as deleteRepository) — never the client-chosen operation name. |
Each override constrains at least one field. Rules that keep a loosening override from becoming an escape hatch:
- A tightening override (one that raises the class) applies if any GraphQL root field in the request matches.
- A loosening override applies only if every GraphQL root field matches.
- Every override is evaluated. The highest matching tightening override wins, and a loosening override applies only when no tightening override matched.
- A loosening override never applies to a request already classified
destructive, or to a GraphQL body that did not parse.
The default trusts HTTP method semantics. An API with side-effecting GETs is
under-classified until an override tightens it.
What administrators control
Three settings decide whether a CLI may write. The effective tier is the lowest of the three, and it is resolved once per call:
| Setting | Where | Default |
|---|---|---|
| Organization ceiling | Organization security settings, CLI write ceiling | No ceiling. Set it to read-only to cap every CLI in the organization. If the setting cannot be read, every CLI is read-only until it is fixed. |
| Assignment tier | The account's assignment to a workspace | Read only. An admin can grant Read and write on an explicit assignment. |
| Block toggle | The agent block's Allow writes switch, per CLI tool | Off. A call that does not ask for write is a read call. |
- Access a person has through their own linked identity, rather than through an explicit assignment, is always read-only.
- Token-credential CLIs cannot reach
writetoday. Connections that use an API key, a bot token or basic authentication have no assignable account to carry a write grant, so every CLI running on one is read-only. destructiveis never a standing grant. Even atwrite, each deletion needs a person to approve that exact request. A run with no person to approve — a scheduled or unattended run — cannot delete.- Every call's tier and its reason are in the Audit Log. They appear as
cliTierandcliTierReasonon that call'sextension.execution.succeededorextension.execution.failedevent. - Every request a CLI makes is in the Audit Log. Each one writes an
egress.allowedoregress.blockedevent. The event records the method, a redacted host and path, the request's class and the broker's decision. An allowed request also recordsstatus, the HTTP status the vendor answered with. Each redirect hop gets its own event. A refused request has nostatus, because it never reached the vendor. - A change to an assignment's tier is audited. Raising it writes
extension.cli_tier.raisedand lowering it writesextension.cli_tier.lowered. Each event records who made the change and both the previous and the new tier. If the event cannot be written, the change is not saved and the request fails.
Requesting write from the Platform API
The block toggle is the cliTier field on a tool execution request:
await client.action.tools.execute({
toolId: "github:cli:gh",
input: { args: ["issue", "create", "--repo", "acme/app", "--title", "Flaky test"] },
cliTier: "write",
});cliTier accepts "read" or "write"; it is ignored for tools that are not
CLIs. Omitting it means read. Asking for write never grants more than the
organization ceiling and the assignment allow.
Adding a CLI tool to an agent block
When an organization installs your extension, each CLI you declare becomes a
tool named <id> CLI (for example gh CLI). It appears only inside an
agent block: the agent block's tool picker offers it next to your toolkits'
tools. It is never a block of its own, so it is not in the workflow toolbar or
the automations tool picker. In settings, the extension's Includes tab shows
it as a gh CLI chip on the row of the toolkit that owns it; the chip lists
the hosts it may reach and the settings it needs before it can write. You add it
the same way you add any other tool, and you add it on its own. Selecting
All operations on a toolkit never includes a CLI.
The CLI runs with the connection of the toolkit that owns it. It is listed only when all of these hold:
- the extension is installed and the owning toolkit is enabled;
- the installed release is a reviewed Scrydon release.
A CLI that the platform would refuse to run is not offered.
Which toolkit owns a CLI
If exactly one of your toolkits uses the extension's default credential
(auth.default), that toolkit owns the CLI and you need not say so. If two or
more do, as Jira and Confluence both do in the Atlassian extension, name the
owner with toolkit:
defineCli({ id: "curl", toolkit: "jira", /* … */ })The platform dispatches a CLI with the default credential, so its owner must
use it. defineExtension throws, and publishing refuses the manifest, when:
toolkitnames a toolkit the extension does not declare;toolkitnames a toolkit bound to a credential other thanauth.default;- there is no
toolkit, and no toolkit usesauth.default; or - there is no
toolkit, and two or more toolkits useauth.default.
Nothing picks an owner for you, and a CLI is never published without one.
An extension whose only surface is a CLI still declares a toolkit to own it, with no tools of its own. The Azure extension does exactly this:
const azureToolkit = defineToolkit({ id: "azure", name: "Azure", tools: [] });
defineExtension({
// …
provides: {
toolkits: [azureToolkit],
clis: [defineCli({ id: "az", toolkit: azureToolkit.id, /* … */ })],
},
});That toolkit is what an administrator enables and connects in settings, and the CLI is then listed in the agent block's tool picker.
Adding the tool grants nothing by itself. Every call still starts at read,
and the tier, the broker's wire policy and the approval rules below govern
every request it makes.
The Allow writes switch in an agent block
Each CLI tool you add to an agent block has its own Allow writes switch. It
is off when you add the tool. Turning it on stores write for that one tool;
turning it off, or switching the tool to a different operation, removes it.
The switch is the block author's choice. An agent cannot turn it on: a tool
call whose arguments contain cliTier or approvedRequestDigests is refused
with INVALID_TOOL_INPUT and nothing is sent.
Approving a deletion
Only an Agent step in a workflow can ask a person to approve a deletion.
When a CLI called from an Agent step makes a destructive request, the run
pauses and asks a person to approve it, in the same place as any other agent
action that needs confirmation. The reviewer sees:
- the method, host and path of the request, and the GraphQL operation name when there is one;
- a preview of the request body;
- the tool and the exact arguments the agent called it with.
If the request cannot be shown to the reviewer, the platform does not ask: the
Agent step fails with CLI_CONFIRMATION_UNREVIEWABLE and nothing is sent. No
one is ever asked to approve a request they cannot see.
Approve re-runs the whole CLI command — the same tool call, with the same arguments, from the beginning — once, with that one request approved. The CLI starts over, so any requests it made before the destructive one are sent again on the re-run. A command that creates something and then deletes something else creates it twice. Keep the destructive step in a command of its own.
So the platform does not offer an approval that would send a write twice. If
an earlier request in the same command was already sent as a write or a
destructive request, nobody is asked: the call fails with
CLI_CONFIRMATION_AFTER_WRITE, and the agent is told to run the destructive
command on its own, as a separate call. That separate call asks for approval
as usual. The earlier write was sent, and its outcome is recorded as unknown,
so check its result with a read before repeating it.
The approval covers that one request and nothing else: if the re-run makes a different destructive request, the run pauses again for a new approval.
Reject declines that call and the agent carries on. Nothing runs again:
the command is not re-run and the refused request is never sent. The agent
gets the call's result as EXEC_ACTION_CONFIRMATION_DECLINED, which says a
person declined it, and continues the same step. It can tell the user, or try
something else. A declined call cannot be approved afterwards. If the agent
still needs the deletion, it has to make a new call, and that call asks for
approval again.
Deletions need a person, so they only work where a person can approve them.
Anywhere else — chat, the Platform API, a workflow's tool block, a scheduled or
unattended run — the call fails with CLI_CONFIRMATION_REQUIRED and nothing is
deleted. In an Agent step the CLI's reads and writes run without asking; only
a deletion stops for approval. A step whose plan never asks for confirmation
cannot delete: the call fails with CLI_CONFIRMATION_NOT_APPROVABLE.
What agents see
- One tool per CLI, named
<id>_cli(tool id<extension>:cli:<id>). Its description is built from the committed help tree: the CLI's summary and its top-level commands. - Input is an argv array, up to 256 arguments of up to 32 KiB each, plus
optional standard input up to 1 MiB. There is no shell: pipes, globs,
redirection and
$(...)do not work. One command per call. --helpis answered from the crawl.--help,-horhelp <command>on a command that has subcommands is answered from the committed help tree, without starting a sandbox. Help for a leaf command runs the real CLI.- The result is data. A successful run returns
{ exitCode, stdout, stderr }; a non-zero exit is part of the result, not a failure — unless the platform refused one of the CLI's requests, in which case the refusal wins (see the error codes below). - Each call runs in a fresh, isolated sandbox with a deadline of at most 270 seconds,
up to 128 outbound requests, request bodies up to 8 MiB and a 2 MiB result.
Nothing carries over between calls. For long-running vendor operations,
start the operation (for example with
--no-wait) and check on it with a separate read in a later call.
Conformance
Every CLI image is checked before a release ships it: the CLI runs against a mock vendor, through the same interception, and either passes with no CLI-specific code or is refused with one named reason. The checks run in this order and the first failure is the verdict:
| Failure | Meaning | What the author does |
|---|---|---|
NEEDS_TTY | The CLI would not run without a terminal or open standard input. | Set the CLI's non-interactive switches in env, or the CLI cannot be shipped. |
PROXY_IGNORED | The CLI did not send its traffic through the platform's proxy. | The CLI cannot be shipped. In production such a CLI simply fails closed. |
HOST_NOT_ALLOWED | The CLI tried to reach a host outside the granted set. | Declare the host the command needs, or accept that the command is out of scope. |
CA_UNTRUSTED | The CLI did not trust the per-run certificate authority. | The CLI pins certificates or ignores every standard trust-store variable; it cannot be shipped. |
DUMMY_ON_WIRE | The placeholder credential appeared somewhere other than a credential header (URL, body). | The CLI places tokens where the platform cannot replace them safely; it cannot be shipped as is. |
DIGEST_UNSTABLE | Two identical runs produced different requests, so a person's approval could not be bound to one. | Find the non-deterministic input (a timestamp, a random id) and pin it with a flag or env. |
RUN_FAILED | The run touched the network correctly but did not complete cleanly. | Fix the invocation; a connection alone is never a pass. |
Out of scope in v1.0.0
These fail closed today rather than degrade:
- Request signing (AWS SigV4-style), so
awsandgcloudare not supported. - Mutual TLS client certificates.
- Credential files instead of environment variables (a kubeconfig, for example).
- HTTP/2-only and gRPC CLIs — the platform speaks HTTP/1.1 only.
- WebSockets and other streaming upgrades.
- Endpoints on a port other than 443, or plain HTTP. A self-hosted service on
:8080or:8443cannot be driven through a CLI. - Binaries delivered by the extension. A CLI always comes from an image the platform operator pinned.
- Data-plane hosts on shared wildcard domains (for
az: Storage, Key Vault, Container Registry). A credential is never bound to a wildcard host. - Uploads above the request body cap (
CLI_UPLOAD_TOO_LARGE).
az also checks for its own updates on every run, at
azcliprod.blob.core.windows.net. The Azure extension allows that one exact
host and no credentialHosts entry names it, so the check succeeds and never
carries a credential. A CLI whose update host its extension does not allow
still runs; expect one blocked-egress event per call.
Error codes
Two codes are policy decisions. They reach the caller as
POLICY_BLOCKED (HTTP 422) with the code in details.policyCode:
| Code | Meaning | What to do |
|---|---|---|
CLI_REQUEST_TIER_DENIED | The CLI made a write (or destructive) request and the effective tier for this call is read. | Check all three settings in What administrators control. A person approving the request cannot lift a tier. |
CLI_CONFIRMATION_REQUIRED | The CLI made a destructive request. The details carry the method, host, path and a preview of the body. | Run the command from an Agent step. A person approves that exact request; the whole command is then re-run once with that approval. A different request needs its own approval. |
In an Agent step, the model sees a tier denial as a failed tool call with code
POLICY_BLOCKED, and its message starts with CLI_REQUEST_TIER_DENIED. The
run records the call's outcome as unknown, so the step never retries it on its
own.
The platform returns these directly to the CLI as an HTTP error, so the CLI exits non-zero and the call's result shows the refusal:
| Code | HTTP | Meaning | What to do |
|---|---|---|---|
CLI_UPLOAD_TOO_LARGE | 413 | The request body exceeded the 8 MiB cap. | Send less; large uploads are out of scope. |
CLI_MISDIRECTED_REQUEST | 421 | The request's Host did not match the host the CLI connected to. | Report it — a conforming CLI never does this. |
CLI_METHOD_NOT_ALLOWED | 405 | A method other than GET, HEAD, POST, PUT, PATCH, DELETE. | The command is not supported. |
CLI_INVALID_REQUEST_TARGET | 400 | The request target was not a plain path. | Report it — a conforming CLI never does this. |
These come back as the tool call's failure code:
| Code | Meaning | What to do |
|---|---|---|
CLI_TIMEOUT_OUTCOME_UNKNOWN | The CLI was stopped at its deadline. The vendor may already have accepted the request, and the operation may still be running. | Check the resource's state with a read-only command before retrying. |
CLI_BOOTSTRAP_FAILED | The bootstrap.preamble sign-in exited non-zero, so the agent's command never ran. | Check the connection's application identity (client id, tenant) and that the extension's token URL is reachable. |
CLI_CA_UNAVAILABLE | The sandbox did not receive the per-run certificate authority. | Platform fault — retry; report it if it persists. |
CLI_PROXY_UNAVAILABLE | The sandbox's connection to the platform proxy was not provisioned. | Platform fault — retry; report it if it persists. |
CLI_ENTRYPOINT_NOT_FOUND | binary.entrypoint is not installed in the pinned image. | Fix the manifest's entrypoint, or the image mapping. |
CLI_ARGS_ALTERED_BY_DLP | Data-loss prevention masked part of an argument, which would have run a different command. The command did not run. | Remove the sensitive value from the command, or amend the DLP policy. |
CLI_CONFIRMATION_NOT_APPROVABLE | The CLI made a destructive request from an Agent step whose tool never asks for confirmation. Nothing was sent. | Change the tool's confirmation setting, or do the deletion another way. |
CLI_CONFIRMATION_AFTER_WRITE | The CLI made a destructive request after an earlier request in the same command had already been sent as a write. Approving would re-run the command and send that write again, so no one was asked and the destructive request was not sent. | Run the destructive command on its own, as a separate call; that call asks for approval. Check the earlier write's result with a read before repeating it. |
CLI_CONFIRMATION_UNAVAILABLE | The CLI made a destructive request and the approval could not be recorded. Nothing was sent. | Retry the step; report it if it persists. |
EXEC_ACTION_CONFIRMATION_DECLINED | A person declined the destructive request this call needed approval for. The call was not run again and the request was never sent. | Tell the user, or take a different approach. Repeating the call asks for approval again. |
Manifest and configuration problems — an undeclared CLI id, a missing
application identity, a credential that binds to none of the CLI's hosts —
surface as TOOL_VENDOR_CLI_* codes before the CLI runs; see
Tool execution errors.