AID-M-009.002: Authority Envelope & Action Risk Classification
Key question: is this requested action still inside the approved envelope? This lets policy engines fail closed when the answer is no.
Example:
support-agent-prod-001 may use search_kb and create_ticket on support data, but may not call issue_refund, export a full customer table, or delegate to an external billing agent.Pillars: app, infra
Lifecycle: scoping, operation
Scope boundary
Owns the machine-checkable authority envelope and action-risk classification for tools, data classes, environments, effect types, budgets, and delegation depth. It supplies policy facts but does not itself return the runtime allow or deny decision.
AID-M-009.001: AID-M-009.002 defines concrete authority limits and action risk inside the mission; AID-M-009.001 defines how independently the agent may Observe, Analyze, Decide, Execute, Delegate, or Persist. Authority and autonomy are orthogonal: permission to perform an action and permission to perform it without supervision are separate decisions.
AID-H-018.002: AID-M-009.002 defines the approved authority envelope and action-risk facts; AID-H-018.002 evaluates the exact runtime request against active policy and returns the authorization outcome. A valid envelope is policy input, not a pre-authorized action.
AID-M-009.006: AID-M-009.002 classifies action authority and risk; AID-M-009.006 binds the agent's aggregate capability and blast-radius tier to the required defensive control profile. Action classification does not prove that the safeguards required for an elevated tier are present.
Implementation guidance
- AID-M-009.002-G001: Define the authority envelope and classify tool actions by base risk and mandatory adjustment factors.
- AID-M-009.002-G002: Run envelope and tool-classification linting in CI to prevent drift between policy artifacts and runtime catalogs.
AID-M-009.002-G001
Concept:
The authority envelope defines hard boundaries on what an agent may access or affect, while tool classification provides the base risk inputs used later during per-action decisions. These artifacts should be versioned policy inputs, not informal documentation.
Step 1: Define the authority envelope
envelope:
tool_scope:
- search_kb
- create_ticket
- send_email
data_scope:
- public
- internal
env_scope:
- staging
- production
effect_scope:
- read
- reversible
- boundary
resource_budget:
max_api_calls_per_hour: 100
max_daily_spend_usd: 50
delegation_scope:
max_depth: 0
cross_trust_boundary: falseStep 2: Classify tools and define adjustment factors
# File: policy/tool_classification.yaml
tools:
send_email:
base_risk_class: boundary
function_type: execute
execute_sql_ddl:
base_risk_class: destructive
function_type: persist
irreversible: true
mandatory_risk_adjustments:
- crosses_org_boundary
- production_environment
- pii_in_payload
- large_blast_radius
- monetary_impact
- physical_world_effectAction: Fail closed when a requested tool, data class, environment, or effect type is missing from the envelope or when a tool lacks a maintained risk classification.
AID-M-009.002-G002
Concept
Lint the entire authority envelope and tool-classification contract, not only tool names. The CI gate must reject duplicate keys, unknown fields, stale classified tools, missing mandatory risk adjustments, invalid scope values, mismatched function types, and malformed resource or delegation limits.
Validate against a signed lint policy and the exact runtime catalog
The signed lint policy defines the organization's allowed scope values, risk classes, function types, required budget keys, mandatory risk-adjustment IDs, and expected version and digest of the applicable rule bundle. It also authorizes one generation by binding the exact authority-envelope, tool-classification, and runtime-catalog bytes, the catalog's HTTPS source origin, and its policy-selected freshness window; it does not supply the envelope's actual limits. The runtime catalog adapter must export that generation's complete deployed tool population, active bundle identity, and per-tool adjustment assignments using schema aidefend.runtime-tool-catalog.v1.
# File: governance/lint_authority_envelope.py
from __future__ import annotations
import hashlib
import json
import math
import os
import stat
from datetime import datetime, timezone
from pathlib import Path
import yaml
class StrictSafeLoader(yaml.SafeLoader):
pass
def unique_mapping(loader, node, deep=False):
loader.flatten_mapping(node)
result = {}
for key_node, value_node in node.value:
key = loader.construct_object(key_node, deep=deep)
if key in result:
raise ValueError(f"duplicate YAML key: {key}")
result[key] = loader.construct_object(value_node, deep=deep)
return result
StrictSafeLoader.add_constructor(
yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG,
unique_mapping,
)
def strict_yaml(raw: bytes):
loader = StrictSafeLoader(raw.decode("utf-8", errors="strict"))
try:
return loader.get_single_data()
finally:
loader.dispose()
def unique_object(pairs):
result = {}
for key, value in pairs:
if key in result:
raise ValueError(f"duplicate JSON key: {key}")
result[key] = value
return result
def regular_bytes(path: Path) -> bytes:
descriptor = os.open(
path,
os.O_RDONLY | getattr(os, "O_BINARY", 0)
| getattr(os, "O_CLOEXEC", 0) | getattr(os, "O_NOFOLLOW", 0),
)
try:
status = os.fstat(descriptor)
if not stat.S_ISREG(status.st_mode):
raise ValueError(f"lint input is not a regular file: {path}")
raw = os.read(descriptor, status.st_size + 1)
if len(raw) != status.st_size:
raise OSError(f"lint input changed during stable read: {path}")
finally:
os.close(descriptor)
if not raw:
raise ValueError(f"lint input is empty: {path}")
return raw
def sha256(raw: bytes) -> str:
return hashlib.sha256(raw).hexdigest()
def canonical_sha256(value) -> str:
return sha256(json.dumps(
value,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=False,
allow_nan=False,
).encode("utf-8"))
def string_population(value, name: str) -> list[str]:
if (
not isinstance(value, list)
or not value
or any(not isinstance(item, str) or not item for item in value)
or len(value) != len(set(value))
):
raise ValueError(f"{name} population is empty, untyped, or duplicated")
return value
envelope_raw = regular_bytes(Path(os.environ["AUTHORITY_ENVELOPE_PATH"]))
classification_raw = regular_bytes(Path(os.environ["TOOL_CLASSIFICATION_PATH"]))
catalog_raw = regular_bytes(Path(os.environ["RUNTIME_TOOL_CATALOG_PATH"]))
policy_raw = regular_bytes(Path(os.environ["AUTHORITY_LINT_POLICY"]))
envelope_doc = strict_yaml(envelope_raw)
classification_doc = strict_yaml(classification_raw)
catalog = json.loads(catalog_raw.decode("utf-8", errors="strict"), object_pairs_hook=unique_object)
policy = json.loads(policy_raw.decode("utf-8", errors="strict"), object_pairs_hook=unique_object)
policy_keys = {
"schema_version", "policy_version", "allowed_data_scopes",
"allowed_environment_scopes", "allowed_effect_scopes",
"allowed_risk_classes", "allowed_function_types",
"required_resource_budget_keys", "required_risk_adjustments",
"risk_adjustment_bundle_version", "risk_adjustment_bundle_sha256",
"authority_generation", "authority_envelope_sha256",
"tool_classification_sha256", "runtime_catalog_sha256",
"runtime_catalog_origin", "max_catalog_age_seconds",
"max_future_skew_seconds",
}
if not isinstance(policy, dict) or set(policy) != policy_keys:
raise ValueError("authority lint-policy fields differ")
if policy["schema_version"] != "aidefend.authority-envelope-lint-policy.v1":
raise ValueError("authority lint-policy schema differs")
for key in {
"allowed_data_scopes", "allowed_environment_scopes",
"allowed_effect_scopes", "allowed_risk_classes",
"allowed_function_types", "required_resource_budget_keys",
"required_risk_adjustments",
}:
string_population(policy[key], key)
if (
not isinstance(policy["policy_version"], str)
or not policy["policy_version"]
or not isinstance(policy["risk_adjustment_bundle_version"], str)
or not policy["risk_adjustment_bundle_version"]
or not isinstance(policy["risk_adjustment_bundle_sha256"], str)
or not isinstance(policy["authority_generation"], str)
or not policy["authority_generation"]
or any(
not isinstance(policy[field], str)
or len(policy[field]) != 64
or set(policy[field]) - set("0123456789abcdef")
for field in (
"authority_envelope_sha256", "tool_classification_sha256",
"runtime_catalog_sha256",
)
)
or not isinstance(policy["runtime_catalog_origin"], str)
or not policy["runtime_catalog_origin"].startswith("https://")
or isinstance(policy["max_catalog_age_seconds"], bool)
or not isinstance(policy["max_catalog_age_seconds"], (int, float))
or not math.isfinite(policy["max_catalog_age_seconds"])
or policy["max_catalog_age_seconds"] <= 0
or isinstance(policy["max_future_skew_seconds"], bool)
or not isinstance(policy["max_future_skew_seconds"], (int, float))
or not math.isfinite(policy["max_future_skew_seconds"])
or policy["max_future_skew_seconds"] < 0
):
raise ValueError("authority lint-policy identity is invalid")
if (
sha256(envelope_raw) != policy["authority_envelope_sha256"]
or sha256(classification_raw) != policy["tool_classification_sha256"]
or sha256(catalog_raw) != policy["runtime_catalog_sha256"]
):
raise ValueError("authority generation input bytes differ from signed policy")
if not isinstance(envelope_doc, dict) or set(envelope_doc) != {"envelope"}:
raise ValueError("authority envelope document schema differs")
envelope = envelope_doc["envelope"]
envelope_keys = {
"tool_scope", "data_scope", "env_scope", "effect_scope",
"resource_budget", "delegation_scope",
}
if not isinstance(envelope, dict) or set(envelope) != envelope_keys:
raise ValueError("authority envelope fields differ")
for field, allowed_field in (
("data_scope", "allowed_data_scopes"),
("env_scope", "allowed_environment_scopes"),
("effect_scope", "allowed_effect_scopes"),
):
values = string_population(envelope[field], field)
unknown = sorted(set(values) - set(policy[allowed_field]))
if unknown:
raise ValueError(f"authority envelope has unknown {field}: {unknown}")
envelope_tools = set(string_population(envelope["tool_scope"], "tool_scope"))
budget = envelope["resource_budget"]
if not isinstance(budget, dict) or set(budget) != set(policy["required_resource_budget_keys"]):
raise ValueError("resource-budget key population differs")
for name, value in budget.items():
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value) or value < 0:
raise ValueError(f"resource budget is invalid: {name}")
delegation = envelope["delegation_scope"]
if (
not isinstance(delegation, dict)
or set(delegation) != {"max_depth", "cross_trust_boundary"}
or isinstance(delegation["max_depth"], bool)
or not isinstance(delegation["max_depth"], int)
or delegation["max_depth"] < 0
or not isinstance(delegation["cross_trust_boundary"], bool)
):
raise ValueError("delegation scope is invalid")
if not isinstance(classification_doc, dict) or set(classification_doc) != {
"tools", "risk_adjustment_bundle",
}:
raise ValueError("tool-classification document schema differs")
bundle = classification_doc["risk_adjustment_bundle"]
if (
not isinstance(bundle, dict)
or set(bundle) != {"version", "rules"}
or bundle["version"] != policy["risk_adjustment_bundle_version"]
or not isinstance(bundle["rules"], dict)
or set(bundle["rules"]) != set(policy["required_risk_adjustments"])
):
raise ValueError("risk-adjustment bundle schema or population differs")
for adjustment_id, rule in bundle["rules"].items():
if (
not isinstance(rule, dict)
or set(rule) != {"applies_to_function_types", "required_for_irreversible"}
or not isinstance(rule["required_for_irreversible"], bool)
):
raise ValueError(f"risk-adjustment rule is invalid: {adjustment_id}")
function_types = string_population(
rule["applies_to_function_types"],
f"{adjustment_id}.applies_to_function_types",
)
if not set(function_types).issubset(policy["allowed_function_types"]):
raise ValueError(f"risk-adjustment rule has unknown function type: {adjustment_id}")
bundle_sha256 = canonical_sha256(bundle)
if bundle_sha256 != policy["risk_adjustment_bundle_sha256"]:
raise ValueError("risk-adjustment bundle digest differs from signed policy")
mandatory = tuple(sorted(bundle["rules"]))
classified = classification_doc["tools"]
if not isinstance(classified, dict) or not classified:
raise ValueError("classified tool population is empty")
if not isinstance(catalog, dict) or set(catalog) != {
"schema_version", "catalog_generation", "source_origin", "generated_at",
"risk_adjustment_bundle_version", "risk_adjustment_bundle_sha256", "tools",
}:
raise ValueError("runtime tool-catalog schema differs")
try:
generated_at = datetime.fromisoformat(
catalog["generated_at"].replace("Z", "+00:00")
).astimezone(timezone.utc)
except (AttributeError, TypeError, ValueError) as error:
raise ValueError("runtime tool-catalog timestamp is invalid") from error
catalog_age = (datetime.now(timezone.utc) - generated_at).total_seconds()
if (
catalog["schema_version"] != "aidefend.runtime-tool-catalog.v1"
or catalog["catalog_generation"] != policy["authority_generation"]
or catalog["source_origin"] != policy["runtime_catalog_origin"]
or catalog["risk_adjustment_bundle_version"] != bundle["version"]
or catalog["risk_adjustment_bundle_sha256"] != bundle_sha256
or catalog_age > policy["max_catalog_age_seconds"]
or catalog_age < -policy["max_future_skew_seconds"]
):
raise ValueError("runtime tool-catalog authority, freshness, or rule-bundle binding differs")
catalog_rows = catalog["tools"]
if not isinstance(catalog_rows, list) or not catalog_rows:
raise ValueError("runtime tool-catalog population is empty")
catalog_by_name = {}
for row in catalog_rows:
if (
not isinstance(row, dict)
or set(row) != {"name", "function_type", "risk_adjustment_ids"}
or not isinstance(row["name"], str)
or not row["name"]
or row["name"] in catalog_by_name
or row["function_type"] not in policy["allowed_function_types"]
or not isinstance(row["risk_adjustment_ids"], list)
or any(
not isinstance(item, str) or not item
for item in row["risk_adjustment_ids"]
)
or len(row["risk_adjustment_ids"]) != len(set(row["risk_adjustment_ids"]))
):
raise ValueError("runtime tool-catalog row is invalid or duplicated")
catalog_by_name[row["name"]] = row
if set(classified) != set(catalog_by_name):
raise ValueError("classified and runtime tool populations differ")
assignment_population = {}
for name, metadata in classified.items():
if (
not isinstance(metadata, dict)
or not {"base_risk_class", "function_type"}.issubset(metadata)
or not set(metadata).issubset({"base_risk_class", "function_type", "irreversible"})
or metadata["base_risk_class"] not in policy["allowed_risk_classes"]
or metadata["function_type"] not in policy["allowed_function_types"]
or metadata["function_type"] != catalog_by_name[name]["function_type"]
or ("irreversible" in metadata and not isinstance(metadata["irreversible"], bool))
):
raise ValueError(f"tool classification is invalid: {name}")
expected_adjustments = {
adjustment_id
for adjustment_id, rule in bundle["rules"].items()
if (
metadata["function_type"] in rule["applies_to_function_types"]
or (
metadata.get("irreversible", False)
and rule["required_for_irreversible"]
)
)
}
observed_adjustments = set(catalog_by_name[name]["risk_adjustment_ids"])
if observed_adjustments != expected_adjustments:
raise ValueError(f"runtime risk-adjustment assignment differs: {name}")
assignment_population[name] = sorted(observed_adjustments)
if not envelope_tools.issubset(catalog_by_name):
raise ValueError("authority envelope references an unknown runtime tool")
result = {
"schema_version": "aidefend.authority-envelope-lint-result.v1",
"status": "VALIDATED",
"authority_generation": policy["authority_generation"],
"runtime_catalog_origin": catalog["source_origin"],
"runtime_catalog_generated_at": catalog["generated_at"],
"policy_version": policy["policy_version"],
"policy_sha256": sha256(policy_raw),
"envelope_sha256": sha256(envelope_raw),
"classification_sha256": sha256(classification_raw),
"runtime_catalog_sha256": sha256(catalog_raw),
"risk_adjustment_bundle_version": bundle["version"],
"risk_adjustment_bundle_sha256": bundle_sha256,
"risk_adjustment_assignment_sha256": canonical_sha256(assignment_population),
"runtime_tool_count": len(catalog_by_name),
"envelope_tool_count": len(envelope_tools),
"mandatory_risk_adjustment_count": len(mandatory),
}
output = Path("evidence/authority-envelope-lint.json")
output.parent.mkdir(parents=True, exist_ok=True)
output.write_text(json.dumps(result, indent=2, sort_keys=True) + "\n", encoding="utf-8")
if json.loads(output.read_text(encoding="utf-8")) != result:
raise RuntimeError("authority-envelope lint readback differs")
Verify one immutable generation and run the release gate
set -euo pipefail
umask 077
root="$(mktemp -d)"
trap 'rm -rf -- "$root"' EXIT
install -m 0400 -- "$AUTHORITY_LINT_POLICY" "$root/lint-policy.json"
install -m 0400 -- "$AUTHORITY_LINT_POLICY_BUNDLE" "$root/lint-policy.sigstore.json"
install -m 0400 -- policy/authority_envelope.yaml "$root/authority-envelope.yaml"
install -m 0400 -- policy/tool_classification.yaml "$root/tool-classification.yaml"
install -m 0400 -- runtime/tool_catalog.json "$root/runtime-tool-catalog.json"
cosign verify-blob --key "$AUTHORITY_LINT_POLICY_VERIFY_KEY" \
--bundle "$root/lint-policy.sigstore.json" "$root/lint-policy.json" >/dev/null
export AUTHORITY_LINT_POLICY="$root/lint-policy.json"
export AUTHORITY_ENVELOPE_PATH="$root/authority-envelope.yaml"
export TOOL_CLASSIFICATION_PATH="$root/tool-classification.yaml"
export RUNTIME_TOOL_CATALOG_PATH="$root/runtime-tool-catalog.json"
python governance/lint_authority_envelope.py
python - <<'PY'
import json
from pathlib import Path
result = json.loads(Path("evidence/authority-envelope-lint.json").read_text(encoding="utf-8"))
if result.get("status") != "VALIDATED":
raise SystemExit("authority-envelope lint was not validated")
PY
Action: Treat any parser error, duplicate or extra key, input-byte or generation mismatch, unauthorized or stale catalog origin, missing catalog row, stale classification, invalid scope/budget/delegation value, function-type mismatch, signed rule-bundle mismatch, or per-tool risk-adjustment drift as a release failure. VALIDATED is a narrow lint result, not runtime authorization; the release controller admits only the exact signed-policy generation on a successful process exit and preserves this read-back result with the release.
Open-source tools
- Open Policy Agent (OPA)
- Cedar
- OpenFGA (authority-envelope relationship checks)
- Kyverno
Commercial / hosted tools
- Cerbos Hub
- Amazon Verified Permissions
- Permit.io
Threat mappings
MITRE ATLAS
- N/A
MAESTRO
- N/A
OWASP LLM Top 10 2026
- N/A
OWASP ML Top 10 2023
- N/A
OWASP Top 10 for Agentic Applications 2026
- N/A
NIST Adversarial Machine Learning 2025
- N/A
Cisco Integrated AI Security and Safety Framework
- AISubtech-5.2.1 Agent Profile Tampering (Rejects authority-envelope or tool-policy changes outside the signed release generation)
- AITech-5.2 Configuration Persistence (Rejects authority-envelope or tool-policy changes outside the signed release generation)
Google Secure AI Framework 2.0 - Risks
- N/A
Databricks AI Security Framework 3.0
- Agents - Tools MCP Server 13.22: Excessive Permissions and Scope Creep
- Agents - Tools MCP Client 13.31: Excessive Permission Granting