AID-H-018.007: Skill-Level Permission Manifest Validation & Runtime Enforcement
Pillars: app, infra
Lifecycle: building, validation, operation
Scope boundary
Owns the skill permission-manifest contract, schema validation, install-time permission checks, scoped credentials, and runtime interception of resource access after installation. It does not own skill content admission, enterprise inventory, agent-wide behavior contracts, or persistent-state repair.
AID-H-017.005: AID-H-018.007 constrains one installed skill; AID-H-017.005 defines and signs the authority and intended behavior of the agent as a whole. Skill permissions must remain within the agent contract but are independently declared and enforced.
AID-H-018.004: AID-H-018.007 enforces the installed skill's declared resource ceiling; AID-H-018.004 narrows the active tool set and action budget for the current request or session. Runtime authority is the intersection of skill permission and dynamic scope.
AID-H-018.005: AID-H-018.007 controls which resource classes a skill may access; AID-H-018.005 controls whether a particular runtime value may flow to a particular sink. Resource permission does not authorize every data flow through that resource.
AID-H-030: AID-H-018.007 owns the permission schema and runtime enforcement after installation; AID-H-030 owns security analysis and the admission decision before a skill is allowed. An admitted skill still requires least-privilege runtime permissions.
AID-I-004.006: AID-H-018.007 denies undeclared writes to protected identity and state resources; AID-I-004.006 protects the persistence surface through baseline, diff, rollback, and file-level integrity mechanisms. Permission enforcement and state-surface recovery are distinct layers.
AID-M-001.003: AID-H-018.007 enforces permissions per installed skill; AID-M-001.003 maintains enterprise-wide skill identity, version, owner, deployment, approval, and lifecycle inventory. Local enforcement requires but does not replace centralized visibility and revocation coordination.
AID-H-018.009: AID-H-018.007 defines and intercepts the installed skill's declared file-resource ceiling; AID-H-018.009 supplies the generic descriptor- or handle-safe filesystem broker for skills and non-skill adapters. A manifest PASS does not prove that each opened or affected object remained inside the declared root.
AID-H-021.005: AID-H-018.007 enforces a skill's declared file, network, shell, tool, and protected-resource permissions after installation and activation; AID-H-021.005 decides whether the exact repository-controlled activation manifest may leave restricted mode. Workspace trust does not grant runtime skill permissions, and a valid skill permission manifest does not authorize workspace activation.
Implementation guidance
- AID-H-018.007-G001: Define and enforce a structured Skill Permission Manifest schema; reject skills without a valid manifest at install time.
- AID-H-018.007-G002: Implement a runtime manifest enforcement interceptor that blocks skill resource access beyond declared scope.
- AID-H-018.007-G003: Issue per-skill scoped credentials with short TTLs; prohibit shared agent-global API keys.
AID-H-018.007-G001
Concept
Every skill entering the enterprise - whether from an external registry or internal development - must ship a machine-readable manifest declaring its complete resource scope. The manifest must be treated as enforceable policy, not optional documentation. Reject skills with no manifest or with a manifest that fails structural or semantic validation.
Required manifest sections
files: includesread,write, anddeny_write. Entries must be either (a) exact canonical absolute paths, or (b) approved reserved logical resource IDs for protected agent identity/state resources. Broad wildcards such as*,~/*, or root-level unrestricted paths are prohibited.network: includes an outbound allowlist. Default deny all other egress. Enterprises should define whether controlled wildcards are permitted; direct IP-literal egress should be denied by default.shell: boolean, defaultfalse.tools: allowlist of invocable tool names.risk_tier: declared risk level used for cross-checks against requested scope.identity_files_write_justification: required when a skill requests write access to a protected identity/state resource.
Reserved logical resource IDs for protected identity/state resources:
protected_resources:
- agent.identity.soul # commonly SOUL.md
- agent.memory.primary # commonly MEMORY.md
- agent.identity.agents_config # commonly AGENTS.md
Example install-time validator
# File: skill_admission/manifest_validator.py
from __future__ import annotations
import ipaddress
import json
from pathlib import Path
import jsonschema
SCHEMA_PATH = "schemas/skill_manifest.json"
# Reserved logical IDs for protected identity/state resources.
# Common platform examples are shown in comments for operator clarity.
PROTECTED_LOGICAL_IDS = {
"agent.identity.soul", # commonly SOUL.md
"agent.memory.primary", # commonly MEMORY.md
"agent.identity.agents_config", # commonly AGENTS.md
}
ALLOWED_RISK_TIERS = {"L0", "L1", "L2", "L3"}
def is_ip_literal(host: str) -> bool:
try:
ipaddress.ip_address(host)
return True
except ValueError:
return False
def validate_manifest(manifest: dict) -> list[str]:
errors: list[str] = []
# 1. Schema compliance
with open(SCHEMA_PATH, "r", encoding="utf-8") as f:
schema = json.load(f)
try:
jsonschema.validate(manifest, schema)
except jsonschema.ValidationError as e:
return [f"Schema violation: {e.message}"]
files = manifest.get("files", {})
write_targets = set(files.get("write", []))
deny_write_targets = set(files.get("deny_write", []))
risk_tier = manifest.get("risk_tier", "L1")
justification = manifest.get("identity_files_write_justification")
network_allow = set(manifest.get("network", {}).get("allow", []))
# 2. Protected identity/state resource override must be explicit + justified
protected_write_targets = write_targets.intersection(PROTECTED_LOGICAL_IDS)
if protected_write_targets:
if not justification:
errors.append("Protected identity/state write requested without identity_files_write_justification")
if deny_write_targets.intersection(protected_write_targets):
errors.append("Same protected resource appears in both write and deny_write")
# 3. Risk-tier cross-checks
if risk_tier not in ALLOWED_RISK_TIERS:
errors.append(f"Invalid risk_tier: {risk_tier}")
if risk_tier == "L0" and manifest.get("shell", False):
errors.append("risk_tier L0 incompatible with shell: true")
if risk_tier in {"L0", "L1"} and protected_write_targets:
errors.append("Low-risk skill cannot request protected identity/state write access")
# 4. Network semantics checks
for host in network_allow:
if is_ip_literal(host):
errors.append(f"Direct IP egress is not allowed in network.allow: {host}")
if host in {"*", "0.0.0.0/0", "::/0"}:
errors.append(f"Over-broad outbound destination is not allowed: {host}")
return errors
Action: Integrate this validator into your skill admission pipeline and private skill registry. Skills that fail validation must not be installable. Skills that request protected identity/state writes or fail risk-tier consistency checks must enter elevated human review.
Keep the action safely bounded
Reconcile the complete population of every packaged file and every requested permission/destination/tool/resource for each submitted skill version; reject expired, revoked, or stale policy and asset bindings under the policy-defined freshness window.
Verify safely
A separately credentialed verifier independently replays artifact-to-manifest binding and schema/semantic decisions with missing, extra, wildcard, traversal, protected-write, risk-tier, signature, and package-mutation fixtures, reads back effective state from the enforcement system, exercises positive, negative, boundary, stale-evidence, and dependency-failure fixtures, and reconciles every eligible item.
AID-H-018.007-G002
Concept
Use the skill manifest as a fail-closed capability ceiling before dispatching any file, network, shell, or tool request. For files, this guidance authorizes only the declared operation and logical root; AID-H-018.009 remains the sole owner of platform path confinement and must return an already-opened descriptor/handle plus an opened-object receipt. Never reopen a model-supplied name after that broker returns.
Important runtime semantics: protected identity/state resources use reserved logical IDs; outbound destination checks define controlled wildcard semantics, deny direct IP literals by default, and re-validate the final destination after redirects.
Example runtime interceptor
# File: skill_runtime/manifest_enforcer.py
from __future__ import annotations
from dataclasses import dataclass
import ipaddress
import logging
from typing import BinaryIO, Protocol
from urllib.parse import urlparse
logger = logging.getLogger("manifest_enforcer")
PROTECTED_LOGICAL_IDS = {
"agent.identity.soul", # commonly SOUL.md
"agent.memory.primary", # commonly MEMORY.md
"agent.identity.agents_config", # commonly AGENTS.md
}
@dataclass(frozen=True)
class ManifestPolicy:
read_root_ids: set[str] # platform-owned H-018.009 roots
write_root_ids: set[str] # platform-owned H-018.009 roots
protected_resource_writes: set[str] # reserved logical IDs
network_allow: set[str] # exact FQDNs or controlled wildcards
shell_allowed: bool
tools_allowed: set[str]
@dataclass(frozen=True)
class FileAccessRequest:
root_id: str
relative_path: str
mode: str
protected_resource_id: str | None = None
class DescriptorHandleBroker(Protocol):
def open_verified(
self, *, root_id: str, relative_path: str, operation: str
) -> tuple[BinaryIO, dict]:
"""H-018.009 returns the already-opened object and its receipt."""
raise RuntimeError("protocol-only method")
def is_ip_literal(host: str) -> bool:
try:
ipaddress.ip_address(host)
return True
except ValueError:
return False
def fqdn_allowed(host: str, allowlist: set[str]) -> bool:
host = host.lower().rstrip(".")
if is_ip_literal(host):
return False
for rule in allowlist:
rule = rule.lower().rstrip(".")
if rule.startswith("*."):
suffix = rule[2:]
if host.endswith("." + suffix) and host != suffix:
return True
elif host == rule:
return True
return False
def open_manifest_bounded(
policy: ManifestPolicy,
request: FileAccessRequest,
broker: DescriptorHandleBroker,
) -> tuple[BinaryIO, dict]:
allowed_modes = {
"r", "rb", "r+", "rb+", "r+b", "w", "wb", "w+", "wb+", "w+b",
"a", "ab", "a+", "ab+", "a+b", "x", "xb", "x+", "xb+", "x+b",
}
if request.mode not in allowed_modes:
raise PermissionError("unsupported file mode")
if (
not request.root_id
or not request.relative_path
or "\x00" in request.relative_path
or request.relative_path.startswith(("/", "\\"))
):
raise PermissionError("file request must use a logical root and relative path")
needs_read = request.mode.startswith("r") or "+" in request.mode
needs_write = request.mode[0] in {"w", "a", "x"} or "+" in request.mode
if needs_read and request.root_id not in policy.read_root_ids:
raise PermissionError("skill manifest denies file read root")
if needs_write and request.root_id not in policy.write_root_ids:
raise PermissionError("skill manifest denies file write root")
if (
needs_write
and request.protected_resource_id in PROTECTED_LOGICAL_IDS
and request.protected_resource_id not in policy.protected_resource_writes
):
raise PermissionError("skill manifest denies protected-resource write")
operation = (
"read_write" if needs_read and needs_write
else "write" if needs_write else "read"
)
opened, receipt = broker.open_verified(
root_id=request.root_id,
relative_path=request.relative_path,
operation=operation,
)
expected = {
"control_id": "AID-H-018.009",
"root_id": request.root_id,
"relative_path": request.relative_path,
"operation": operation,
}
if (
not hasattr(opened, "close")
or not isinstance(receipt, dict)
or any(receipt.get(key) != value for key, value in expected.items())
or not isinstance(receipt.get("opened_object_id"), str)
or not receipt["opened_object_id"]
):
try:
opened.close()
finally:
raise RuntimeError("filesystem broker returned a misbound object receipt")
return opened, receipt
def check_network(policy: ManifestPolicy, url: str, redirect_chain: list[str] | None = None) -> bool:
if redirect_chain is not None and (not isinstance(redirect_chain, list) or len(redirect_chain) > 10):
return False
candidate_urls = [url, *(redirect_chain or [])]
for candidate in candidate_urls:
host = (urlparse(candidate).hostname or "").lower()
if not fqdn_allowed(host, policy.network_allow):
logger.warning("BLOCKED network egress to %s", candidate)
return False
return True
def check_tool(policy: ManifestPolicy, tool_name: str) -> bool:
if tool_name not in policy.tools_allowed:
logger.warning("BLOCKED tool invocation: %s", tool_name)
return False
return True
def check_shell(policy: ManifestPolicy) -> bool:
if not policy.shell_allowed:
logger.warning("BLOCKED shell execution")
return False
return True
Action: Run the manifest ceiling in-process or in a trusted sidecar. File requests carry only the approved operation, logical root ID, and relative path into the non-bypassable AID-H-018.009 broker; downstream code consumes its returned descriptor/handle and receipt and never reopens relative_path by name. Emit structured block events with skill ID, operation, logical target, and reason. Pair network checks with platform egress policy where applicable.
Keep the action safely bounded
Reconcile the complete population of every file, protected-resource, network, shell, and tool access attempted by each skill instance; reject expired, revoked, or stale policy and asset bindings under the policy-defined freshness window.
Verify safely
Test manifest denial, unsupported modes, wrong logical roots, protected-resource aliases, DNS rebinding, redirects, IP literals, alternate ports, shell, tool, and interceptor-bypass fixtures. Confirm each allowed file operation consumes exactly one current AID-H-018.009 opened-object receipt and the returned descriptor/handle. Path traversal, link, replacement-race, and opened-object identity tests are scored only under AID-H-018.009; this guidance must not claim a second filesystem-confinement PASS.
AID-H-018.007-G003
Concept
Each approved skill needs its own non-human identity and short-lived credentials whose scope mirrors the approved manifest. This prevents one compromised skill from inheriting the full authority of the agent host and gives responders a concrete token accessor or workload identity to revoke.
Step 1: Derive credential scope from the approved manifest
Resolve the current approved manifest through a trusted registry into policy objects dedicated to that exact skill and manifest digest. Generic category-wide policy names or token metadata alone do not enforce per-skill isolation.
# File: identity/skill_token_issuer.py
from __future__ import annotations
import os
from dataclasses import dataclass
import re
from typing import Protocol
import hvac
VAULT_ADDR = os.environ["VAULT_ADDR"]
VAULT_TOKEN = os.environ["VAULT_TOKEN"]
@dataclass(frozen=True)
class SkillIdentityRequest:
skill_id: str
manifest_hash: str
approved_scope: dict
class VerifiedSkillPolicyRegistry(Protocol):
def resolve_current(
self, *, skill_id: str, manifest_hash: str, approved_scope: dict
) -> tuple[str, tuple[str, ...]]:
"""Return a current revision and policies dedicated to this skill."""
raise RuntimeError("protocol-only method")
def issue_skill_token(
req: SkillIdentityRequest, registry: VerifiedSkillPolicyRegistry
) -> dict:
client = hvac.Client(url=VAULT_ADDR, token=VAULT_TOKEN)
if not client.is_authenticated():
raise RuntimeError("Vault authentication failed")
if (
not isinstance(req.skill_id, str)
or not req.skill_id
or not re.fullmatch(r"[a-f0-9]{64}", req.manifest_hash)
or not isinstance(req.approved_scope, dict)
):
raise ValueError("skill identity request is malformed")
policy_revision, policies = registry.resolve_current(
skill_id=req.skill_id,
manifest_hash=req.manifest_hash,
approved_scope=req.approved_scope,
)
if (
not isinstance(policy_revision, str)
or not policy_revision
or not policies
or len(policies) != len(set(policies))
or any(not isinstance(name, str) or not name for name in policies)
or any(req.skill_id not in name for name in policies)
):
raise RuntimeError("registry did not return skill-dedicated policies")
response = client.auth.token.create(
policies=list(policies),
ttl="30m",
explicit_max_ttl="30m",
renewable=False,
display_name=f"skill-{req.skill_id}",
num_uses=500,
meta={
"skill_id": req.skill_id,
"manifest_hash": req.manifest_hash,
"policy_revision": policy_revision,
},
)
return {
"client_token": response["auth"]["client_token"],
"accessor": response["auth"]["accessor"],
"lease_duration": response["auth"]["lease_duration"],
"policy_revision": policy_revision,
"policy_names": list(policies),
}
Step 2: Hand the skill only its own short-lived token
Inject the token into the isolated skill runtime, never into a shared global environment. Persist the token accessor and manifest hash with the skill inventory record so you can revoke or investigate the skill independently.
# File: identity/runtime_injection.py
from identity.skill_token_issuer import SkillIdentityRequest, issue_skill_token
def provision_skill_runtime(
skill_id: str, manifest_hash: str, approved_scope: dict, runtime, policy_registry
):
token_bundle = issue_skill_token(
SkillIdentityRequest(
skill_id=skill_id,
manifest_hash=manifest_hash,
approved_scope=approved_scope,
),
policy_registry,
)
runtime.set_env("VAULT_TOKEN", token_bundle["client_token"])
runtime.set_metadata("vault_accessor", token_bundle["accessor"])
runtime.set_metadata("manifest_hash", manifest_hash)
runtime.set_metadata("policy_revision", token_bundle["policy_revision"])
runtime.set_metadata("policy_names", token_bundle["policy_names"])
Step 3: Verify revocation works
In staging, revoke the issued accessor and confirm the skill can no longer reach protected backends while sibling skills continue to work.
test -n "$VAULT_ACCESSOR"
vault token revoke -accessor "$VAULT_ACCESSOR"
Action: Replace shared agent-global secrets with Vault-issued per-skill tokens or an equivalent workload identity system. Keep TTLs short, record the token accessor with the skill approval record, and prove you can revoke one skill without rotating every other credential in the environment.
Keep the action safely bounded
Reconcile the complete population of every credential issuance, renewal, use, expiry, and revocation for each skill instance; reject expired, revoked, or stale policy and asset bindings under the policy-defined freshness window.
Open-source tools
- Open Policy Agent (OPA)
- python-jsonschema
- Pydantic
- Falco
- Cilium Tetragon
- SPIRE
- OpenBao (open-source short-lived per-skill token issuance)
Commercial / hosted tools
- HashiCorp Vault Enterprise
- HCP Vault Dedicated
- Calico Enterprise
- Isovalent Enterprise Platform
- Palo Alto Networks Prisma Cloud
- Aqua Platform (CNAPP)
- CyberArk Secrets Manager SaaS
Threat mappings
MITRE ATLAS
- AML.T0011 User Execution
- AML.T0110 AI Agent Tool Poisoning
- AML.T0115 Publish Poisoned AI Artifacts
- AML.T0115.002 Publish Poisoned AI Artifacts: AI Agent Tools
- AML.T0110.001 AI Agent Tool Poisoning: Implementation
- AML.T0011.002 User Execution: Poisoned AI Agent Tool
- AML.T0053 AI Agent Tool Invocation (manifest enforcement constrains tool invocation scope per skill)
- AML.T0086 Exfiltration via AI Agent Tool Invocation (network allowlist enforcement blocks unauthorized exfiltration)
- AML.T0081 Modify AI Agent Configuration (deny-write-by-default on protected identity/state resources prevents skill-driven config tampering)
- AML.T0084 Discover AI Agent Configuration (file-read restrictions prevent skills from reading sensitive agent configuration outside declared scope)
- AML.T0098 AI Agent Tool Credential Harvesting (per-skill scoped credentials reduce value of shared-secret harvesting)
- AML.T0108 AI Agent
- AML.T0072 Cyber Communication Channel (runtime enforcement of declared network destinations denies undeclared skill callbacks)
MAESTRO
- Agent Tool Misuse (L7) (skill-level manifests constrain multi-tool workflow abuse)
- Lateral Movement (Cross-Layer) (scoped credentials and restricted egress reduce post-compromise movement)
- Privilege Escalation (Cross-Layer) (least-privilege manifests reduce aggregate authority available to a compromised skill)
OWASP LLM Top 10 2026
- LLM01:2026 Prompt Injection (per-skill scoped credentials and runtime manifest enforcement bound the capabilities available after a successful injection)
- LLM02:2026 Sensitive Information Disclosure
- LLM03:2026 Excessive Agency
OWASP ML Top 10 2023
- N/A
OWASP Top 10 for Agentic Applications 2026
- ASI02:2026 Tool Misuse and Exploitation
- ASI03:2026 Identity and Privilege Abuse
- ASI05:2026 Unexpected Code Execution (RCE) (manifest controls reduce unauthorized shell/code paths available to a skill)
- ASI01:2026 Agent Goal Hijack (skill boundaries constrain what hijacked agent goals can accomplish through installed skills)
NIST Adversarial Machine Learning 2025
- NISTAML.018 Prompt Injection (runtime manifest enforcement reduces the effect of prompt-induced out-of-scope actions)
- NISTAML.039 Compromising connected resources
Cisco Integrated AI Security and Safety Framework
- AISubtech-12.1.3 Unsafe System / Browser / File Execution
- AISubtech-14.1.2 Insufficient Access Controls
- AISubtech-14.2.1 Permission Escalation via Delegation
- AISubtech-2.1.2 Capability and Permission Overreach
- AISubtech-5.1.1 Long-term / Short-term Memory Injection
- AISubtech-5.2.1 Agent Profile Tampering
- AISubtech-8.2.3 Data Exfiltration via Agent Tooling
- AISubtech-9.1.1 Code Execution
- AISubtech-9.1.2 Unauthorized or Unsolicited System Access
- AISubtech-9.1.3 Unauthorized or Unsolicited Network Access
- AITech-12.1 Tool Exploitation
- AITech-14.2 Abuse of Delegated Authority
- AITech-2.1 Excessive Agency
- AITech-5.1 Memory System Persistence
- AITech-5.2 Configuration Persistence
- AITech-8.2 Data Exfiltration / Exposure
- AITech-9.1 Model or Agentic System Manipulation
Google Secure AI Framework 2.0 - Risks
- RA: Rogue Actions (manifest enforcement constrains skill actions to declared scope)
- IIC: Insecure Integrated Component (permission manifests secure integrated skill components)
- SDD: Sensitive Data Disclosure (file-read and network restrictions reduce sensitive-data exposure through skills)
Databricks AI Security Framework 3.0
- Agents - Core 13.2: Tool Misuse (manifest enforcement constrains skill-level tool misuse)
- Agents - Core 13.3: Privilege Compromise (per-skill scoped credentials reduce privilege compromise from shared global keys)
- Agents - Tools MCP Server 13.19: Credential and Token Exposure (per-skill short-lived tokens reduce shared credential exposure)
- Platform 12.4: Unauthorized privileged access (manifest-driven least-privilege enforcement reduces unauthorized access)