AID-H-018.007: Skill-Level Permission Manifest Validation & Runtime Enforcement

Version 1.20261015 · Actionable sub-technique

Require every agentic AI skill to declare a structured, machine-readable permission manifest that enumerates the files, network destinations, shell capabilities, invocable tools, risk tier, and protected identity or state resources it needs. Enforce the declared boundary at install-time validation and runtime execution. The skill layer addresses scope composition in which several individually authorized tools combine into a workflow with greater authority. Protected identity and state resources use reserved logical resource IDs and require explicit elevated declarations for write access.

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.
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

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

Implement a runtime manifest enforcement interceptor that blocks skill resource access beyond declared scope.
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

Issue per-skill scoped credentials with short TTLs; prohibit shared agent-global API keys.
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

Commercial / hosted tools

Threat mappings

MITRE ATLAS

MAESTRO

OWASP LLM Top 10 2026

OWASP ML Top 10 2023

OWASP Top 10 for Agentic Applications 2026

NIST Adversarial Machine Learning 2025

Cisco Integrated AI Security and Safety Framework

Google Secure AI Framework 2.0 - Risks

Databricks AI Security Framework 3.0

AID-H-018.007: Skill-Level Permission Manifest Validation & Runtime Enforcement | AIDEFEND

Loading interactive view…

A Practical Knowledge Base for AI Security Defenses

Version