Directional security profiles
:::note Supported, for single-entry profiles
Directional profiles follow the TypeScript CLI 7.3.0 and scm-sdk 0.2.4. A live run against
a tenant (2026-10-22) accepted flag and --config creates, a protection edit of each
direction, shared-settings edits, a topic applied and reverted in each direction, and backup
and restore. Read the known limitations before relying on them.
:::
A security profile's policy holds AI entries (ai-security-profiles). Two layouts exist:
- A legacy entry keeps its protections (
model-protection,app-protection,agent-protection,data-protection) and its shared settings inmodel-configuration. Existing legacy profiles need no migration. - A directional entry has
content-type-mode: per_content_type(or acontent-type-configurationskey). Its protections live per direction incontent-type-configurations, underprompt,response,tool-callandtool-response; the service requires all four keys, though a direction may be empty. Itsmodel-configurationkeeps only the shared settings:latency,mask-data-in-storageandenable-full-conversation-inspection.
Creating
# A legacy profile, as before
airs runtime profiles create --name Legacy --prompt-injection block
# A directional profile with response protections
airs runtime profiles create --name Directional --direction response \
--toxic-content 'high:block, moderate:allow' --no-enable-full-conversation-inspection
# A complete JSON body: the file names the profile; --name and every other flag are refused
airs runtime profiles create --config ./directional.json --output json
Directional flag creation needs at least one protection flag. Shared settings alone
(--max-inline-latency, --inline-timeout-action, --[no-]mask-data-in-storage,
--[no-]enable-full-conversation-inspection) are refused before anything is sent (exit 2),
because the service refuses a profile with no protection in any direction (the TypeScript CLI
sends that create, and the service refuses it). The flags fill the --direction you name;
the other three directions are written empty, so a later update --direction can fill any of
them. A DLP block needs --dlp-action on a create.
Updating
# Edit only the response protections; every other direction and severity stays
airs runtime profiles update Directional --direction response \
--toxic-content 'high:block, moderate:block' --no-mask-data-inline
# Shared settings need no direction and keep every direction
airs runtime profiles update Directional --max-inline-latency 8 \
--no-mask-data-in-storage --no-enable-full-conversation-inspection
# A complete replacement (also how a direction is added, or a layout converted)
airs runtime profiles update Directional --config ./directional.json
| Rule | What happens |
|---|---|
| A protection edit on a directional profile | needs --direction, even with one direction |
| The selected direction is absent | refused; add it with a complete --config |
--direction on a legacy profile | refused; convert it with a complete --config |
| Several AI entries | --ai-profile-index <n> (zero-based) selects one, also for shared settings; the others stay |
An unknown content-type-mode | kept by shared edits; protection edits are refused |
| Detectors | merged by name: a match keeps its severities and topic lists; a name listed twice is refused |
--no-… flags | write an explicit false; of a pair, the last one given wins |
| A setting you do not pass | stays as stored, or absent; no default is written |
| An unknown direction key | kept with its value by reads, updates, backup and restore; no direction is added |
--dlp-profiles, --[no-]mask-data-inline | change only their fields on update; no --dlp-action needed |
These refusals happen after the profile is read; nothing is written and the exit code is 2.
--direction and --ai-profile-index values are checked before anything is read
(--ai-profile-index takes decimal digits only).
Topics
airs runtime topics apply --profile Directional --name Restricted --intent block --direction prompt
airs runtime topics revert --profile Directional --name Restricted --direction prompt --force
apply adds the topic to the selected direction and keeps every other topic and direction.
revert removes it from the selected direction and deletes the topic with a plain delete. It
refuses, with nothing written or deleted, when:
- the selected direction does not reference the topic;
- another direction, another AI entry or a retained legacy block of the profile still references it (detach those first).
A topic still referenced by another profile (or an older revision) is kept with a warning, as for legacy profiles.
Reading
airs runtime profiles getshowsProtection locations(AI[0] default / prompt, AI[0] default / response, …), andDLP tenant(JSONdlpTenantId, a string) only when the record carries one; in the live run nogetorlistreturned it. A value the service sends that the schema does not expect (anull, a number) is printed as sent; the TypeScript CLI refused such a read.- A
--configfile is checked before the tenant is read:dlp_tenant_idmust be a string,content-type-modea string, each direction an object andenable-full-conversation-inspectiona boolean, nevernull(exit 2). airs runtime reportadds a Protection configurations column: one entry per AI entry and direction, with the protection sections it holds.airs runtime profiles backupandrestorecarry every direction: DLP mappings, the Basic fallback and topic remapping work per direction, and--on-conflict verifycompares directions strictly. A backup keeps each record as the service sent it; a restore does not send the source tenant'sdlp_tenant_id.
Known limitations
Directional profiles are supported, for single-entry profiles: a live run (2026-10-22) accepted every direction for creates, edits, topics, backup and restore.
- Not verified live: profiles with several AI entries (
--ai-profile-index), DLP blocks inside a direction (--dlp-action,--dlp-profiles), and the runtime report'sProtection configurationscolumn. The offline tests cover them. - Service rules are not checked before sending, except one. Every directional entry needs
all four direction keys (
prompt,response,tool-call,tool-response; a flag create writes the ones it does not fill as{}, and a--configbody must list all four); theresponsedirection refusesprompt-injection; andtoxic-contenttakes onlyblockorallowfor each level. A write that breaks one fails with HTTP 400 (exit 1), and no response body is shown. The exception (D336): a directional flag create with no protection flag (none, or shared settings only) is refused before anything is sent (exit 2), because the service refuses a policy with no protection in any direction. A--configbody with four empty directions is still sent, and refused. dlp_tenant_idon writes. These writes do not senddlp_tenant_id: a flagprofiles update,topics applyandrevert,profiles restore(create and overwrite), andprofiles update --configwhen the file leaves the key out. The service's answer then reportsdlpTenantId: "none"; the effect on the profile's stored DLP tenant binding is unknown. Nogetorlistreturns the field, so the CLI cannot carry it over. This matches the TypeScript CLI 7.3.0 and affects legacy profiles too. A--configfile that holdsdlp_tenant_idsends it.
The behavioural contract is DESIGN §13.17 in the repository.